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>
This commit is contained in:
@@ -90,6 +90,31 @@ go run .
|
||||
|
||||
**这个命令是安全的**:Admin 只管理数据,不会连手机、不会下单。放心随便跑。
|
||||
|
||||
### 配置顺运宝账号(同步货运单需要,其余四个模块不需要)
|
||||
|
||||
顺运宝数据页的「同步」需要读 `admin/config.yaml`。第一次用要自己建这个文件
|
||||
(已在 `.gitignore` 里,不会被提交):
|
||||
|
||||
```powershell
|
||||
cd D:\chengma\cmautobuy\admin
|
||||
copy config.example.yaml config.yaml
|
||||
```
|
||||
|
||||
用编辑器打开 `config.yaml`,把 `username` / `password` 改成真实的顺运宝账号密码。
|
||||
|
||||
`[必须]` 密码要加引号,纯数字密码不加引号会被 YAML 解析成整数,前导 0 也会丢:
|
||||
|
||||
```yaml
|
||||
password: "0012345" # ✓ 正确
|
||||
password: 0012345 # ✗ 解析成整数 12345
|
||||
```
|
||||
|
||||
没有这个文件时,点「同步」会提示"没有找到配置文件……请复制
|
||||
config.example.yaml",不是一句读不出原因的报错。
|
||||
|
||||
接口细节和这几个配置项各自的含义见
|
||||
[08 顺运宝接口](08-顺运宝接口.md) §8。
|
||||
|
||||
## 4. 你应该看到什么
|
||||
|
||||
左边(或顶部)是五个模块的导航:
|
||||
@@ -98,7 +123,7 @@ go run .
|
||||
|---|---|
|
||||
| 蝦皮数据 | 导入蝦皮商品报表,填 PDD 链接,发起采集 |
|
||||
| PDD 商品 | 维护拼多多商品档案,发起采集,查看采回来的规格价格。这个页面不依赖蝦皮和顺运宝的任何数据,单独就能跑通"建商品 → 建采集任务 → 领走执行 → 提交结果 → 显示已采集"这条闭环,见 [05 界面规范](05-ui-specification.md) §5 |
|
||||
| 顺运宝数据 | 同步货运单,匹配规格,生成采购任务 |
|
||||
| 顺运宝数据 | 同步货运单(需要先配置 `config.yaml`,见上一节)。规格匹配和生成采购任务是后续工单的范围,本页暂不提供 |
|
||||
| 采集采购 | 看采集和采购任务执行到哪一步了 |
|
||||
| 客户端列表 | 看哪些客户端在干活 |
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ SQLite 同一时刻只允许一个写事务,连接放太开会互相抢锁、
|
||||
| v2 | 新增 `task_claims`(领取历史,见 §8)。 |
|
||||
| v3 | 把 PDD 采集数据从 `shopee_products` 拆到独立的 `pdd_products`(本文档 §4 描述的最终结构);重建 `shopee_products`,去掉已经搬走的四个字段;重建 `sku_mappings`,主键改成 `(shopee_sku_id, pdd_goods_id)`(§6.1 的理由)。 |
|
||||
| v4 | `pdd_products` 增加可空的 `shop_name`;老数据保持 `NULL`。 |
|
||||
| v5 | 顺运宝货运单同步(工单 #46):新增 `syb_session`(会话缓存)、`syb_sync_state`(同步进度)两张表;`syb_orders` 增加可空的 `product_spec`(规格原文)。三条都是新增,v1–v4 一个字节没改。 |
|
||||
|
||||
**v3 为什么丢弃旧 `sku_mappings` 数据(见 #20):** 新主键需要 `pdd_option_key`,
|
||||
这是 Go 的 `service.OptionKey()` 用 `json.Marshal` 算出来的规范化键,SQL 语句
|
||||
@@ -419,11 +420,12 @@ UPDATE pdd_products
|
||||
|
||||
```sql
|
||||
CREATE TABLE syb_orders (
|
||||
syb_id TEXT PRIMARY KEY, -- 货运单 ID
|
||||
order_no TEXT NOT NULL, -- 订单号
|
||||
syb_id TEXT PRIMARY KEY, -- 货运单**明细行** ID(顺运宝 details[].id)
|
||||
order_no TEXT NOT NULL, -- 订单号(顺运宝外层 code,不是 orderCode)
|
||||
title TEXT, -- 商品标题
|
||||
shopee_goods_id TEXT, -- 蝦皮商品 ID
|
||||
shopee_sku_id TEXT, -- 蝦皮规格 ID
|
||||
product_spec TEXT, -- 规格原文,v5 新增,对应 shopee_skus.spec_raw
|
||||
shopee_goods_id TEXT, -- 蝦皮商品 ID(顺运宝 productId,11 位)
|
||||
shopee_sku_id TEXT, -- 蝦皮规格 ID,人工/自动匹配的结果
|
||||
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
||||
price_twd_cent INTEGER CHECK (price_twd_cent IS NULL OR price_twd_cent >= 0),
|
||||
image_url TEXT, -- 存 URL,不存图片本身
|
||||
@@ -450,7 +452,49 @@ CREATE INDEX idx_syb_orders_list ON syb_orders(updated_at DESC, syb_id DESC);
|
||||
|
||||
**"编号能对上"和"本地一定查得到"是两回事,别混。**
|
||||
|
||||
**"匹配状态"是派生的,不存字段**:`sku_mappings` 里有对应记录就是"已匹配"。
|
||||
**"匹配状态"是派生的,不存字段**:`shopee_sku_id` 非空就是"已匹配"(本工单未做
|
||||
`sku_mappings` 联查复用,规格匹配是后续工单的范围)。
|
||||
|
||||
`[必须]` **`shopee_sku_id` 顺运宝同步绝不能覆盖**(工单 #46)。它是规格匹配的
|
||||
结果(人工确认或自动匹配产生),顺运宝那边根本没有这个值(顺运宝只给商品级 `productId`,
|
||||
不含蝦皮規格ID,见 §5.1 下方接口对照)。`repository.UpsertSybOrder` 的
|
||||
`ON CONFLICT DO UPDATE SET` 里不出现这一列,新建行时才会写它(此时通常是空值)。
|
||||
|
||||
`[必须]` 一行对应顺运宝一张货运单的**一个商品明细**(`details[]` 的一项),
|
||||
不是一张货运单——一张货运单可以有多个商品,各占一行,`syb_id` 用的是
|
||||
`details[].id`,不是货运单本身的 `id`。
|
||||
|
||||
`[必须]` `price_twd_cent` 一律取 `detail/listByStock` 接口的值(元)自己 ×100
|
||||
转分、先四舍五入再转整数;不要用 `/am/stock/list` 列表接口的金额字段——
|
||||
同一响应里不同金额字段的单位不统一,见 [08 顺运宝接口](08-顺运宝接口.md) §5.1。
|
||||
|
||||
`[建议]` 收件人姓名/电话/地址不入库,`syb_data` 落库前已剔除。
|
||||
|
||||
### 5.1 `syb_session` 顺运宝会话缓存、`syb_sync_state` 同步进度(v5)
|
||||
|
||||
```sql
|
||||
CREATE TABLE syb_session (
|
||||
username TEXT PRIMARY KEY,
|
||||
cookies TEXT NOT NULL, -- JSON 数组,Cookie 名/值/路径
|
||||
expires_at TEXT NOT NULL, -- min(JWT exp, 24h)
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE syb_sync_state (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1), -- 只允许一行
|
||||
last_synced_at TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
- 只存 Cookie,不存登录 JWT——认证完全靠 Cookie,JWT 从不参与后续请求,
|
||||
见 [08 顺运宝接口](08-顺运宝接口.md) §3.1。
|
||||
- `syb_sync_state` 只允许一行(`CHECK (id = 1)`),是全局的"上次同步到哪"。
|
||||
- `[必须]` 增量同步从 `last_synced_at` 对应的**日期当天**重新拉,不是第二天——
|
||||
`created` 筛选粒度是日期,`last_synced_at` 精确到秒,从第二天拉会漏掉当天
|
||||
晚些时候创建的单,且不会报错。宁可重复拉(靠 upsert 幂等)也不能漏。
|
||||
- `[必须]` 只有一次同步**全部成功**才更新 `last_synced_at`;中途失败不更新,
|
||||
否则下次同步会跳过这段区间,漏掉的单永远补不回来。
|
||||
|
||||
## 6. `sku_mappings` 规格映射
|
||||
|
||||
|
||||
@@ -410,20 +410,49 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
||||
[同步] [创建采购任务] 订单号 [___] [搜索] [删除]
|
||||
```
|
||||
|
||||
`[待定]` MVP 阶段**同步按钮只做占位**:点击提示"同步功能待接入",不发请求。
|
||||
`[必须]` 「同步」按工单 #46 实现:
|
||||
|
||||
```text
|
||||
点「同步」
|
||||
├─ 本地缓存的会话未过期 ──→ 后台开始同步,立即跳回列表页,
|
||||
│ 状态条显示"同步已开始,请稍后刷新页面查看结果"
|
||||
└─ 没有缓存会话/已过期 ───→ 弹出登录弹窗(不是报错):
|
||||
账号(config.yaml 带出,只读,不显示密码)
|
||||
验证码图片 [点图/换一张 可刷新]
|
||||
验证码文字输入框
|
||||
[登录并同步]
|
||||
```
|
||||
|
||||
同步是长任务,不阻塞 HTTP 请求线程——点完立即跳转,结果异步写进内存态的
|
||||
"最近一次同步报告",下次刷新页面时状态条会显示:
|
||||
|
||||
```text
|
||||
同步完成:日期范围 2026-08-09 ~ 2026-08-09,货运单 12 张,商品明细 27 条
|
||||
(新增 20,更新 7,跳过 0)
|
||||
```
|
||||
|
||||
有跳过/失败会在后面列出具体原因,不是只给个数字。同步中再次点「同步」会提示
|
||||
"已经有一个同步任务在跑",不会并发跑两个。
|
||||
|
||||
`[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。
|
||||
|
||||
### 6.2 表格列
|
||||
|
||||
☐ / 货运单ID / 订单号 / 商品标题 / 蝦皮商品ID / 规格SKU / 数量 /
|
||||
☐ / 货运单明细ID / 订单号 / 商品标题 / 规格 / 蝦皮商品ID / 规格SKU / 数量 /
|
||||
价格(台币)/ 图片 / **匹配状态** / 更新时间
|
||||
|
||||
- 图片显示小缩略图,点击看大图。`[必须]` 存 URL,不要把图片塞进数据库。
|
||||
- 匹配状态是**算出来的**(`sku_mappings` 里有没有记录),不是存的字段。
|
||||
- 完整货运单 JSON 不作为列显示,在详情里看。
|
||||
- 一行对应顺运宝一张货运单的**一个商品明细**,不是一张货运单——一张货运单
|
||||
可以有多个商品,各占一行。
|
||||
- 图片显示小缩略图。`[必须]` 存 URL,不要把图片塞进数据库。
|
||||
- 匹配状态是**算出来的**(`shopee_sku_id` 是否非空),不是存的字段。
|
||||
本工单(#46)不做规格匹配功能,待匹配的行整行标黄,但不提供匹配入口。
|
||||
- 完整货运单 JSON 不作为列显示,落在 `syb_data` 里,供后续排查用。
|
||||
|
||||
### 6.3 规格匹配弹窗(双击行打开)
|
||||
### 6.3 规格匹配弹窗(双击行打开)—— 未实现,见下方说明
|
||||
|
||||
`[不做]` 工单 #46(顺运宝货运单同步)明确不做这一节描述的匹配弹窗,
|
||||
`shopee_sku_id` 由同步留空,本节描述的是**规格匹配**这个后续工单要实现的目标
|
||||
界面,先记录在这里,不代表当前已经能用。
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────┐
|
||||
|
||||
@@ -243,6 +243,34 @@ amtOrder 612.0
|
||||
差 66,应该是优惠。`[必须]` **不要用「明细合计 == amtOrder」做校验**,
|
||||
会误报。
|
||||
|
||||
### 5.3 `created` 是 UTC+8,不是 UTC —— 日期范围查询最容易算错的地方
|
||||
|
||||
`[必须]` 实测 `raw_data/shunyunbaoerp_stock_query.har`:
|
||||
|
||||
```text
|
||||
HAR 记录的抓包时刻 startedDateTime 2026-07-28T03:31:45Z (= 11:31:45 UTC+8)
|
||||
同一次请求响应里的 created 2026-07-28 10:37:59
|
||||
```
|
||||
|
||||
`10:37:59` 作为 **UTC+8** 讲得通(比抓包时刻早 54 分钟,正常)。
|
||||
若把它当成 **UTC**,换算成 UTC+8 就是 18:37,比抓包时刻**晚 7 小时**——
|
||||
订单创建于尚未发生的未来,不成立。所以 `created` 是 UTC+8,不是 UTC。
|
||||
|
||||
`[必须]` §4.2「按日期范围」的 `dvalue` 筛的就是这个 `created`,
|
||||
所以**换算"今天是哪一天"也必须用 UTC+8**,不能用 UTC 或本机系统时区
|
||||
(本机系统时区不一定是 UTC+8,取决于部署环境)。用 UTC 算的话,
|
||||
在 UTC+8 的 00:00–08:00 这段时间会把"今天"算成昨天,当天早晨创建的单
|
||||
这一轮同步拉不到——虽然下一轮的起始日期仍是"上次同步日",范围会覆盖
|
||||
回来、不会永久丢单,但操作员当场点同步会以为同步坏了。
|
||||
|
||||
`[必须]` 代码里固定用 `time.FixedZone("UTC+8", 8*60*60)`,不要用
|
||||
`time.LoadLocation("Asia/Shanghai")`——那个要读系统 tzdata,Windows 上
|
||||
默认没有,打包成 exe 后会在运行时报错。
|
||||
|
||||
`[待定]` 只有一个样本(一次抓包)支撑这个结论,且没有拿到顺运宝官方
|
||||
文档确认。以后如果日期范围附近出现"该有的单没同步到",先来这里核对
|
||||
这条结论是否仍然成立。
|
||||
|
||||
---
|
||||
|
||||
## 6. 货运明细
|
||||
@@ -385,6 +413,8 @@ password: "0012345" # ✓
|
||||
- [ ] 会话失效时服务端返回的**确切**形态(HTTP 码 / `code` / `msg` 文案)
|
||||
- [ ] 同一账号多处登录是否互踢
|
||||
- [ ] 验证码错误、密码错误分别返回什么,能否区分
|
||||
- [ ] `created` 是 UTC+8 这一条(见 §5.3)只有一次抓包支撑,
|
||||
没有官方文档确认,也没有跨夏令时/时区配置的验证
|
||||
|
||||
`[必须]` 最后两条影响错误提示的准确性:分不清「密码错」和「验证码错」的话,
|
||||
操作员会一直重输密码。
|
||||
|
||||
Reference in New Issue
Block a user