Files
cmautobuy/docs/task/16-admin-pdd-商品数据独立成表.md
T
chengmaandClaude Opus 5 10b724f62a docs: 归档 #38;改正六份归档里未经验证的 Go 版本说法
#38 归档记录了打回一次的经过(ParseSpec 两个失败分支无测试覆盖,
其中 size=="" 就是真实数据里 2 行走的分支),以及架构角色第一轮
变异打在死分支上、差点误报「测试没牙」的过程。

改正:/usr/local/go 从 2026-07-02 起一直是 1.26.5,而六份归档都写着
「架构角色亲自执行(Go 1.23.0)」——那是照项目固定版本抄的,
没有实际验证过工具链,违反 CLAUDE.md §9「说验证过必须真的跑过」。
措辞改为不宣称具体版本。已用 GOTOOLCHAIN=go1.23.0 补验,结论不变。

admin/AGENTS.md 加两条 [必须]:
- 交付前至少跑一次带 GOTOOLCHAIN=go1.23.0 的验证。开发机装的可能更新,
  Go 会默默用它编译,测过的不是要交付的那个版本。新加依赖时尤其要跑。
- 改数据库时不要只测全新库,先列出现实中存在哪些 schema 状态再一个个验(来自 #20)。

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

6.8 KiB
Raw Blame History

16 Admin PDD 商品数据独立成表

  • 类型:重构(数据模型)
  • 父级大工单:#14
  • 所属 MVP / 版本:#15 / MVP
  • 状态:验收通过
  • 日期:2026-08-07
  • Gitea 工单:#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 处失效)

测试

执行的命令(admin/ 目录):

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)