#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>
6.8 KiB
6.8 KiB
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,按 PDDgoods_id定位(不是蝦皮的)。 - 新增两条校验:Client 返回的
goods_id与请求不符 → 整体回滚拒绝;skus为空数组 → 置failed而非collected。 - migrations 直接改 v1(当时无业务代码依赖),不加新迁移。
与建单方案的差异
实施中有三处超出工单字面,均已确认必要:
TaskExists重构为GetTaskInfo。 原函数只返回蝦皮goods_id, 而采集结果要按 PDDgoods_id落库,不改取不到正确的键。- 「采错商品」校验做成整体回滚的硬拒绝,并新增
ErrCollectMismatch(HTTP 422)。 工单只说「校验」,实现选了最严的做法:不拦的话会把 B 的规格价格存到 A 名下, 之后按它下单就是买错东西。 - 复活时一并清空旧采集结果(
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结构。
遗留问题
artifact_ref的格式需要与 Client 侧一起定,否则两边各存各的。MarkCollecting/SoftDeletePddProduct在 #18 完成前是死代码。
相关提交
998c06afeat: PDD 商品数据独立成表 (#16)