2026-08-07 11:02:21 +08:00
|
|
|
|
# 16 Admin PDD 商品数据独立成表
|
|
|
|
|
|
|
|
|
|
|
|
- 类型:重构(数据模型)
|
|
|
|
|
|
- 父级大工单:#14
|
|
|
|
|
|
- 所属 MVP / 版本:#15 / MVP
|
2026-08-07 17:18:32 +08:00
|
|
|
|
- 状态:验收通过
|
2026-08-07 11:02:21 +08:00
|
|
|
|
- 日期:2026-08-07
|
|
|
|
|
|
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/16
|
|
|
|
|
|
|
|
|
|
|
|
## 背景与目标
|
|
|
|
|
|
|
|
|
|
|
|
原来 `pdd_data` 是 `shopee_products` 上的一个 JSON 字段,两个蝦皮商品指向同一个
|
|
|
|
|
|
PDD 链接时会各存一份、各采一次;`collect_status` 描述的是 PDD 商品的状态,
|
|
|
|
|
|
却挂在蝦皮商品上,两份可能不一致。
|
|
|
|
|
|
|
|
|
|
|
|
更要紧的是 **PDD 商品变动频繁**(A 下架就得换成 B),而 `sku_mappings` 只按
|
|
|
|
|
|
`shopee_sku_id` 做键,换商品后旧映射还在:
|
|
|
|
|
|
|
|
|
|
|
|
- B 没有同名规格 → 建任务报错,尚且安全
|
|
|
|
|
|
- **B 恰好有同名规格但完全是另一件货 → 静默买错,事后查不出来**
|
|
|
|
|
|
|
|
|
|
|
|
目标是把 PDD 商品变成独立实体,并让规格映射按「蝦皮 SKU + PDD 商品」隔离。
|
|
|
|
|
|
|
|
|
|
|
|
## 最终方案
|
|
|
|
|
|
|
|
|
|
|
|
- 新增 `pdd_products` 表:`id` 主键 + `goods_id UNIQUE` + `skus_json` +
|
|
|
|
|
|
`collect_status` / `collect_msg` / `artifact_ref` / `collected_at` + `deleted_at`。
|
|
|
|
|
|
- `collect_status` 只有 4 个值(`pending` / `collecting` / `collected` / `failed`)。
|
|
|
|
|
|
去掉 `no_link`——这张表里有这一行就说明链接填了;「未填链接」改由
|
|
|
|
|
|
`shopee_products.pdd_goods_id IS NULL` 表达。
|
|
|
|
|
|
- 软删除可复活:按 `goods_id` 查(含已删除的),查到已删除的就清 `deleted_at`、
|
|
|
|
|
|
状态回 `pending`,不新增行(`UNIQUE` 会冲突)。
|
|
|
|
|
|
- `shopee_products` 去掉 `pdd_data` / `collect_status` / `collect_error` /
|
|
|
|
|
|
`collected_at`,`pdd_goods_id` 改为指向 `pdd_products`。
|
|
|
|
|
|
- `sku_mappings` 主键改为 `(shopee_sku_id, pdd_goods_id)`,新增 `pdd_option_key`。
|
|
|
|
|
|
查映射永远带上当前 PDD 商品,换商品后天然查不到旧映射,**不需要删数据**;
|
|
|
|
|
|
换回原商品时旧映射直接复用。
|
|
|
|
|
|
- 新增 `OptionKey()`:用 `json.Marshal` 实现——Go 序列化 map 时按键名排序,
|
|
|
|
|
|
天然规范化,不用自己拼字符串(规格文字里可能含 `=` 或 `;`)。
|
|
|
|
|
|
**存映射和查 SKU 必须用同一个函数**,各写一遍会静默算出不同结果。
|
|
|
|
|
|
- 采集结果改落 `pdd_products`,按 **PDD** `goods_id` 定位(不是蝦皮的)。
|
|
|
|
|
|
- 新增两条校验:Client 返回的 `goods_id` 与请求不符 → 整体回滚拒绝;
|
|
|
|
|
|
`skus` 为空数组 → 置 `failed` 而非 `collected`。
|
|
|
|
|
|
- migrations 直接改 v1(当时无业务代码依赖),不加新迁移。
|
|
|
|
|
|
|
|
|
|
|
|
## 与建单方案的差异
|
|
|
|
|
|
|
|
|
|
|
|
实施中有三处超出工单字面,均已确认必要:
|
|
|
|
|
|
|
|
|
|
|
|
1. **`TaskExists` 重构为 `GetTaskInfo`。** 原函数只返回蝦皮 `goods_id`,
|
|
|
|
|
|
而采集结果要按 PDD `goods_id` 落库,不改取不到正确的键。
|
|
|
|
|
|
2. **「采错商品」校验做成整体回滚的硬拒绝**,并新增 `ErrCollectMismatch`(HTTP 422)。
|
|
|
|
|
|
工单只说「校验」,实现选了最严的做法:不拦的话会把 B 的规格价格存到 A 名下,
|
|
|
|
|
|
之后按它下单就是买错东西。
|
|
|
|
|
|
3. **复活时一并清空旧采集结果**(`skus_json` / `collect_msg` / `collected_at`)。
|
|
|
|
|
|
工单只说清 `deleted_at`。记录被删过一次,旧数据不该再当有效的用,
|
|
|
|
|
|
否则复活后会显示「已采集」但数据是删除前的。
|
|
|
|
|
|
|
|
|
|
|
|
另删除 `admin/repository/shopee.go`——两个函数签名全变且都迁到了 `pdd.go`,留着是死代码。
|
|
|
|
|
|
|
|
|
|
|
|
## 改了哪些
|
|
|
|
|
|
|
|
|
|
|
|
- `admin/repository/db.go`:migrations v1 重写(`pdd_products` 新增,
|
|
|
|
|
|
`shopee_products` / `sku_mappings` 调整)。
|
|
|
|
|
|
- `admin/repository/pdd.go`:新建。PDD 商品读写、`EnsurePddProduct` 三分支
|
|
|
|
|
|
(新建 / 直接用 / 复活)、`MarkCollecting`、`SoftDeletePddProduct`、
|
|
|
|
|
|
采集结果与失败落库。
|
|
|
|
|
|
- `admin/repository/shopee.go`:删除。
|
|
|
|
|
|
- `admin/repository/task.go`:`TaskExists` → `GetTaskInfo`,返回 `TaskInfo`。
|
|
|
|
|
|
- `admin/service/optionkey.go`:新建。`OptionKey()` 单一实现。
|
|
|
|
|
|
- `admin/service/submit.go`:采集分支改写新表,增加两条校验。
|
|
|
|
|
|
- `admin/model/model.go`:`PddProduct` 结构,`CollectStatus` 减为 4 个值。
|
|
|
|
|
|
- `admin/handler/api/client_api.go`:`ErrCollectMismatch` → 422 映射。
|
|
|
|
|
|
- `admin/service/pdd_test.go`:新建,14 个测试。
|
|
|
|
|
|
- `admin/service/submit_test.go`:两个采集测试改为针对 `pdd_products`。
|
|
|
|
|
|
- `docs/admin/00-glossary.md` / `01-requirements.md` / `03-data-model.md` /
|
|
|
|
|
|
`04-client-api.md` / `05-ui-specification.md`:同步。
|
|
|
|
|
|
|
|
|
|
|
|
## 验收结果
|
|
|
|
|
|
|
|
|
|
|
|
| 验收标准 | 结果 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `pdd_products` 建表,`goods_id` 有 UNIQUE | 通过 |
|
|
|
|
|
|
| `collect_status` 只接受 4 个值 | 通过 |
|
|
|
|
|
|
| `shopee_products` 已去掉四个字段 | 通过(现剩 8 列) |
|
|
|
|
|
|
| `sku_mappings` 主键为 `(shopee_sku_id, pdd_goods_id)` | 通过 |
|
|
|
|
|
|
| OptionKey:键顺序不影响结果 | 通过 |
|
|
|
|
|
|
| OptionKey:特殊字符不影响 | 通过 |
|
|
|
|
|
|
| OptionKey:空组合有明确行为不 panic | 通过 |
|
|
|
|
|
|
| 软删除后默认查询查不到 | 通过 |
|
|
|
|
|
|
| 软删除后同 `goods_id` 再保存复活原行 | 通过 |
|
|
|
|
|
|
| 采集成功写入 `skus_json`,状态 `collected` | 通过 |
|
|
|
|
|
|
| `goods_id` 与请求不符 → 拒绝,不写 `skus_json` | 通过 |
|
|
|
|
|
|
| `skus` 为空 → 置 `failed` | 通过 |
|
|
|
|
|
|
| **换 PDD 商品后查 B 的映射查不到 A 的** | 通过 |
|
|
|
|
|
|
| **换回 A 后 A 的映射仍可用** | 通过 |
|
|
|
|
|
|
| `go vet` / `gofmt` / `go test` 全过 | 通过 |
|
|
|
|
|
|
| 文档同步,链接与章节引用校验 | 通过(各 0 处失效) |
|
|
|
|
|
|
|
|
|
|
|
|
## 测试
|
|
|
|
|
|
|
2026-08-08 09:59:34 +08:00
|
|
|
|
执行的命令(`admin/` 目录):
|
2026-08-07 11:02:21 +08:00
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
go vet ./... 无输出
|
|
|
|
|
|
gofmt -l . 无输出
|
|
|
|
|
|
go test ./... -count=1 ok,55 个测试全 PASS
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
审查时另行补验了工单未覆盖的 HTTP 层端到端:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
① 采回的 goods_id 与请求不符
|
|
|
|
|
|
→ 422 COLLECT_GOODS_MISMATCH
|
|
|
|
|
|
→ 消息「请求采集的是商品 PDD-111,客户端返回的却是 PDD-999」
|
|
|
|
|
|
→ 拒绝后:skus_json 空、状态仍 collecting、任务仍 claimed、幂等记录 0 条
|
|
|
|
|
|
(整体回滚干净,无半写状态;幂等记录若残留会导致客户端重试永远拿到缓存的失败响应)
|
|
|
|
|
|
|
|
|
|
|
|
② 采到 0 个规格
|
|
|
|
|
|
→ 200,但 collect_status = failed
|
|
|
|
|
|
→ msg「未采集到任何规格,请检查商品是否已下架」
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**未验证到的部分:**
|
|
|
|
|
|
|
|
|
|
|
|
- 界面上「采集状态来自两张表」的判断逻辑未实现(工单明确排除界面),只改了文档说明。
|
|
|
|
|
|
- `MarkCollecting` / `SoftDeletePddProduct` 目前只有测试在调,**无生产调用方**,
|
|
|
|
|
|
等 #18 的界面接上。
|
|
|
|
|
|
- `artifact_ref` 目前直接存 `diagnostics` 的原始 JSON,**未按
|
|
|
|
|
|
`client-001:artifacts/...` 格式规范化**,因为 Client 侧尚未定义 `diagnostics` 结构。
|
|
|
|
|
|
|
|
|
|
|
|
## 遗留问题
|
|
|
|
|
|
|
|
|
|
|
|
1. `artifact_ref` 的格式需要与 Client 侧一起定,否则两边各存各的。
|
|
|
|
|
|
2. `MarkCollecting` / `SoftDeletePddProduct` 在 #18 完成前是死代码。
|
|
|
|
|
|
|
|
|
|
|
|
## 相关提交
|
|
|
|
|
|
|
|
|
|
|
|
- `998c06a` feat: PDD 商品数据独立成表 (#16)
|