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 number
  • customerservice_track — tracking events, one row per scan or status update
  • business_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 .exe file
  • 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.