TMS Database to WeCom: A Parcel Tracking Bot
Last month, a friend who runs a logistics company asked me: can I send a tracking number in WeCom and get the parcel’s logistics status back?
They have a TMS system that’s been running for years, powered by PostgreSQL with 200+ tables. No API docs, no in-house dev team — just a read-only database account.
Customer service checks parcels every day. They already have a query system, but it’s a separate link — an extra step. The goal was to query directly in WeCom.
Database Research
Connected to the PostgreSQL TMS database. 200+ tables — the first step was finding the ones that matter.
After walking through the business process with them, I narrowed it down to three core tables:
business_express— the main waybill table, one row per tracking numbercustomerservice_track— tracking events, one row per scan or status updatebusiness_shipperconsignee— sender and receiver information
The relationship: one row in business_express maps to many rows in customerservice_track and one row in business_shipperconsignee.
SQL Validation
With the core tables identified, I wrote a validation query. Goal: input a tracking number → get the latest tracking info.
SELECT
e.business_customerinvoicecode,
COALESCE(st.order_statusname, e.business_status),
e.consignee_country,
s.consignee_name,
t.track_date,
t.track_location,
t.track_content
FROM business_express e
JOIN business_shipperconsignee s ON e.business_id = s.business_id
JOIN customerservice_track t ON e.business_id = t.business_id
LEFT JOIN basic_orderstatus st ON e.business_status = st.order_status
WHERE LOWER(e.business_customerinvoicecode) = LOWER($1)
ORDER BY t.track_date DESC
LIMIT 20
Query works. Read-only, no writes, no locks.
Technical Approach
With the data source confirmed, I built the WeCom parcel tracking bot.
I prototyped in Python first. But the target server was Windows Server 2012 — Go’s single-binary deployment was simpler than maintaining a Python environment, so I went with Go:
WeCom @bot → Go Web Service → PostgreSQL TMS (read-only)
- Go (chi router + pgx driver): compiles to a single
.exefile - NSSM Windows service: auto-start on boot, restart on crash
- DeepSeek API: non-tracking messages fall back to AI chat
- Read-only account:
GRANT SELECT, no write operations
Lessons Learned
Working Without API Docs
No API documentation. Since there wasn’t any, they gave me a read-only database account directly — connect to the database, understand the schema, and get to work.
Schema Discovery Takes Time
200+ tables, but only 3 are core. The hard part wasn’t writing SQL — it was finding the right tables. That required going back and forth with the business team to confirm the workflow. Table names alone aren’t enough.
Go vs Python
I started with Python 3.8 for prototyping. The target server was Windows Server 2012, which already had Python 3.8 installed. Python would have worked.
But I chose Go anyway:
| Aspect | Python | Go 1.20 Single Binary |
|---|---|---|
| Runtime | Python 3.8 already installed | None needed, compiled |
| Process management | Wrap Python with winsw/NSSM | Register .exe directly |
| Dependencies | pip install + venv maintenance | go build, one file |
| Crash recovery | Extra handling needed | NSSM restarts automatically |
| OS support | Works on Server 2012 | Go 1.20 is the last version supporting Server 2012 (Go 1.21+ requires Windows 10) |
Server 2012 is over a decade old, limiting both Python and Go. Go 1.20 was sufficient, and single-binary deployment was far simpler than managing a Python environment. The deployment environment dictates tech choices, not popularity.
WeCom KF Messages: Pick the Latest by send_time
Unlike internal group chat bots, WeCom’s customer service (KF) message callback doesn’t send the message content directly. It sends a kf_msg_or_event event notification, then you call the SyncKFMsg API to pull the actual messages.
sync_msg returns a list of all unread messages for that KF account. This list can include:
- Multiple messages from the user (you should only process the newest one)
- Messages you’ve already handled (duplicate delivery)
- Non-text messages (images, system messages, etc.)
So you can’t just loop through and process every message. Instead, iterate the list and pick the latest by send_time:
var newest *wework.KFMsgItem
for i := range items {
if items[i].MsgType != "text" || items[i].Origin != 3 {
continue
}
if newest == nil || items[i].SendTime > newest.SendTime {
newest = &items[i]
}
}
The filters: MsgType == "text" (text messages only), Origin == 3 (customer messages only, excluding agent replies). After finding the newest message, also deduplicate by MsgID to prevent duplicate replies.
Current Status
Works.
Summary
- The real challenge in legacy system integration isn’t technical — it’s understanding the business schema
- No API docs? A read-only database account is enough to get started
- Deployment drives tech choices: Windows Server 2012 → Go 1.20 single binary + NSSM
- From database access to a working SQL query: one afternoon
This approach works for any scenario with an old database, no API documentation, and a need to query data inside an IM app.