Files
cmautobuy/admin/syb/columns.go
T
chengmaandClaude Opus 5 5e426cacf6 feat: 顺运宝货运单同步 (#46)
顺运宝模块此前是骨架,「同步」点了提示"待接入"。5195 个蝦皮商品已经
进系统,但货运单(真实订单)一条都没有,后面的规格匹配无从谈起。

按接口契约(docs/admin/08,从 4 份 HAR 还原)实现:配置、登录(界面
手工输验证码)、会话缓存到 SQLite、按日期范围增量同步、落 syb_orders。

shopee_sku_id 绝不被同步覆盖。它是规格匹配的结果,顺运宝那边根本没有
这个值(只给 11 位商品ID,蝦皮規格ID 是 12 位)。同步写进去就是写空,
把人工攒的匹配成果洗掉且不报错。它只出现在 INSERT 列清单里,不在
DO UPDATE SET 里;repository 层和 service 端到端各有一个测试守着。

增量从「上次同步日期当天」重拉,不是第二天。created 筛选粒度是日期而
last_synced_at 精确到秒,从第二天拉会漏掉当天晚些时候创建的单且不报错。
宁可重复拉(upsert 幂等)也不能漏。中途失败不更新 last_synced_at,
否则下次跳过这段区间,漏的单永远补不回来。

日期运算用 UTC+8,不是 UTC。审查时从 HAR 确认 created 是当地时间:
抓包于 2026-07-28T03:31:45Z(= 11:31 UTC+8),同一响应里 created 是
"2026-07-28 10:37:59";若它是 UTC 则等于 18:37 UTC+8,比抓包晚 7 小时,
订单创建于未来,不成立。用 UTC 算会在本地 00:00-08:00 把"今天"算成昨天,
当天早晨的单这轮拉不到。用 time.FixedZone 写死,不用 LoadLocation——
那要读系统 tzdata,Windows 默认没有,打包成 exe 会失败。

金额一律取 detail/listByStock 的值:08 §5.1 实测同一响应里 amtOrder
在列表接口是分、escrowAmount 却不是,单位不统一,取错差 100 倍。

迁移 v5 纯追加(syb_session、syb_sync_state、syb_orders.product_spec),
v1-v4 逐字未动,CheckSchema 覆盖新表新列。

会话有效性判断把「网络故障」和「明确未登录」的分类集中在 Client.do()
一处——网络抖一下就判定登出的话,验证码会弹个不停,还会丢掉有效会话。

测试全部用 httptest 假服务端,不打真实站点。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 11:49:13 +08:00

110 lines
4.7 KiB
Go

package syb
// columnSpec 是货运单列表请求体里 columns 数组的一项:声明服务端
// 要返回哪一列。
//
// `[必须]` 完整照抄 raw_data/shunyunbaoerp_single.py 的 COLUMN_SPECS
// (72 项),不要自己删减——docs/admin/08-顺运宝接口.md §4.1 明确说
// "服务端可能依赖这批列做联表",删了不知道会不会漏别的字段。
type columnSpec struct {
TableName string
ColName string
FieldName string
HasAlias int
TableAlias string
}
// stockColumnSpecs 是货运单列表查询用到的 72 个列定义,
// 逐项抄自 raw_data/shunyunbaoerp_single.py 的 COLUMN_SPECS。
var stockColumnSpecs = []columnSpec{
{"t_stock", "created", "created", 0, "t"},
{"t_stock", "order_code", "orderCode", 0, "t"},
{"t_stock", "printer", "printer", 0, "t"},
{"t_stock", "weight_time", "weightTime", 0, "t"},
{"t_stock", "weight_inputer", "weightInputer", 0, "t"},
{"t_stock", "pkg_time", "pkgTime", 0, "t"},
{"t_stock", "code", "code", 0, "t"},
{"t_stock", "status", "status", 0, "t"},
{"t_stock", "order_status", "orderStatus", 0, "t"},
{"t_stock", "purchase_status", "purchaseStatus", 0, "t"},
{"t_stock", "exp_code", "expCode", 0, "t"},
{"t_stock", "exp_page_code", "expPageCode", 0, "t"},
{"t_stock", "exp_allow_print", "expAllowPrint", 0, "t"},
{"t_stock", "exp_page_status", "expPageStatus", 0, "t"},
{"t_stock", "page_id", "pageId", 0, "t"},
{"t_stock", "upload_time", "uploadTime", 0, "t"},
{"t_stock", "pay_time", "payTime", 0, "t"},
{"t_stock", "shop_day_to_ship", "shopDayToShip", 0, "t"},
{"t_stock", "remark9", "tsremark9", 1, "t"},
{"t_stock", "remark9", "remark9", 0, "t"},
{"t_stock", "detail_qty", "detailQty", 0, "t"},
{"t_stock", "order_qty", "orderQty", 0, "t"},
{"t_stock_detail", "inner_exp_code", "innerExpCode", 0, "t7"},
{"t_stock_detail", "shelf_code", "shelfCode", 0, "t7"},
{"t_stock", "shelf_code", "tsshelfCode", 1, "t"},
{"t_stock", "store_type", "storeType", 0, "t"},
{"t_stock", "order_bag_code", "orderBagCode", 0, "t"},
{"t_stock", "weight_cust_pkg", "weightCustPkg", 0, "t"},
{"t_stock", "weight_consign", "weightConsign", 0, "t"},
{"t_stock", "amt_order", "amtOrder", 0, "t"},
{"t_stock_append", "offline_amount", "offlineAmount", 0, "t8"},
{"t_stock_append", "escrow_amount", "escrowAmount", 0, "t8"},
{"t_stock", "exp_cod", "expCod", 0, "t"},
{"t_stock", "exp_company", "expCompany", 0, "t"},
{"t_stock", "transport", "transport", 0, "t"},
{"t_stock", "order_origin", "orderOrigin", 0, "t"},
{"t_stock", "exp_out_type", "expOutType", 0, "t"},
{"t_stock", "order_platform", "orderPlatform", 0, "t"},
{"t_stock", "exp_pkg_type", "expPkgType", 0, "t"},
{"t_stock", "exp_pkg_code", "expPkgCode", 0, "t"},
{"t_stock", "exp_batch", "expBatch", 0, "t"},
{"t_stock", "exp_ti_huo", "expTiHuo", 0, "t"},
{"t_stock", "print_time", "printTime", 0, "t"},
{"t_stock", "receiver", "receiver", 0, "t"},
{"t_stock", "receiver_tel", "receiverTel", 0, "t"},
{"t_stock", "receiver_addr", "receiverAddr", 0, "t"},
{"t_stock", "receiver_shop_name", "receiverShopName", 0, "t"},
{"t_stock", "receiver_shop_code", "receiverShopCode", 0, "t"},
{"t_stock", "product_name", "productName", 0, "t"},
{"t_stock", "is_cancel", "isCancel", 0, "t"},
{"t_stock", "err_msg", "errMsg", 0, "t"},
{"t_stock", "remark2", "remark2", 0, "t"},
{"t_stock", "remark1", "remark1", 0, "t"},
{"t_stock", "note", "note", 0, "t"},
{"t_store", "name", "name", 0, "t1"},
{"sys_user", "fullname", "sufullname", 1, "t3"},
{"sys_user", "dept_label_path", "deptLabelPath", 0, "t3"},
{"t_stock", "shop_name", "shopName", 0, "t"},
{"t_shop", "shop_id", "shopId", 0, "t6"},
{"t_stock", "remark4", "remark4", 0, "t"},
{"t_stock", "remark7", "remark7", 0, "t"},
{"t_stock", "package_time", "packageTime", 0, "t"},
{"t_stock", "packer", "packer", 0, "t"},
{"t_stock", "pack_type", "packType", 0, "t"},
{"t_stock", "track_status", "trackStatus", 0, "t"},
{"t_stock", "track_desc", "trackDesc", 0, "t"},
{"t_stock", "err_status", "errStatus", 0, "t"},
{"t_stock", "err_time", "errTime", 0, "t"},
{"t_stock", "err_msg", "tserrMsg", 1, "t"},
{"t_stock", "remark2", "tsremark2", 1, "t"},
{"t_stock", "remark1", "tsremark1", 1, "t"},
{"t_stock", "product_volume_str", "productVolumeStr", 0, "t"},
}
// columnsPayload 把 stockColumnSpecs 转成请求体要的 JSON 形状:
//
// {"tableName":"t_stock","colName":"created","fieldName":"created","hasAlias":0,"tableAlias":"t"}
func columnsPayload() []map[string]any {
out := make([]map[string]any, 0, len(stockColumnSpecs))
for _, c := range stockColumnSpecs {
out = append(out, map[string]any{
"tableName": c.TableName,
"colName": c.ColName,
"fieldName": c.FieldName,
"hasAlias": c.HasAlias,
"tableAlias": c.TableAlias,
})
}
return out
}