Files
cmautobuy/docs/task/18-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

8.3 KiB
Raw Blame History

18 Admin PDD 商品页面

  • 类型:需求(界面)
  • 父级大工单:#14
  • 所属 MVP / 版本:#15 / MVP
  • 状态:验收通过
  • 日期:2026-08-07
  • Gitea 工单:#18

背景与目标

#16 把 PDD 商品做成了独立的 pdd_products 表,但没有任何界面能操作它—— MarkCollecting 和 SoftDeletePddProduct 写完之后一直是死代码。

这个页面是打通 Client 和 Admin 闭环的那一环:操作员在这里录入 PDD 链接、 发起采集任务,客户端领走去采,采回来的规格和价格显示在这里, 后面的规格匹配和采购任务才有数据可用。

最终方案

三段式布局,与其他页面一致:工具条 / 表格 / 底部状态条。

链接解析必须严格

ErrBadPddURL 不做容错兜底。goods_id 上有 UNIQUE 约束,防重全靠它—— 解析不出来就直接报错让操作员重新复制链接。兜底猜一个的话,同一个商品会存成 好几行,采好几遍,规格映射还说不清指向哪一行。

  • 认 yangkeduo.com 和 pinduoduo.com(含子域名)
  • 认 goods_id 和 _x_goods_id 两个参数名
  • 短链一律拒绝,提示操作员回 App 里「分享 → 复制链接」拿完整链接
  • 卡域名是为了拦「粘错链接」——粘了淘宝链接却存进 PDD 表, 要等客户端跑到手机上才会发现

创建采集任务:先占状态再建任务

ok, err := repository.MarkCollecting(tx, goodsID)  // 只在 pending/failed 时成功
if !ok { skipped++; continue }
repository.InsertCollectTask(tx, newCollectTaskID(), p.GoodsID, p.URL)

MarkCollecting 同时起两个作用:占状态,以及判断「这个商品有没有人已经在采」。 两者在同一个事务里,所以并发点多次只会建出一个任务。

采集任务不指定客户端(assigned_client = NULL + status = 'pending'), 靠 #17 的无主任务领取机制。

跳过了几个必须在提示里说出来,否则操作员会以为都建上了,等半天没动静。

规格显示需要 dimensions

submit.go 的 collectedData 增加了 Dimensions 字段。没有它就只能按 Go 的 map 遍历,而 map 是无序的——同一个商品每次刷新页面「颜色 / 尺码」的先后都可能变。

工具条用一个表单 + formaction

工单画的是两个独立按钮,实现改成一个表单、按钮上用 formaction 区分动作。 两套勾选框迟早不同步。

与建单方案的差异

实施中有四处超出工单字面:

  1. INSERT INTO tasks 从 service 层提到 repository 层。原先写在 service/pdd.go 里,违反 admin/AGENTS.md:71「只有 repository 能写 SQL」。 改为 repository.InsertCollectTask。
  2. 工具条用一个表单 + formaction,理由见上。
  3. 修了 #16 的一个缺陷:EnsurePddProduct 复活分支清空了 skus_json 却漏了 title,导致复活后状态显示「未采集」但标题还留着旧值。 #16 验收要求「采集结果被清空」,title 是 SetCollectResult 写的, 属于采集结果,所以一并清。
  4. **弹窗底部的「重新采集」**走同一条创建路径,只针对当前商品。工单未提。

改了哪些

  • admin/service/pdd.go:新建。链接解析、列表查询组装、详情、 CreatePddCollectTasks、状态文案、价格格式化、维度排序。
  • admin/handler/web/pdd.go:新建。六个路由的处理。
  • admin/templates/pdd/list.html / edit_modal.html:新建。
  • admin/repository/pdd.go:pddColumns 常量 + scanPddProduct 统一扫描; 复活分支补清 title;列表查询与筛选。
  • admin/repository/task.go:InsertCollectTask。
  • admin/service/submit.go:collectedData 增加 Dimensions。
  • admin/handler/web/web.go:六个路由注册。
  • admin/static/css/app.css / js/app.js:弹窗、截断、下拉样式与交互。
  • admin/templates/partials/header.html:导航加「PDD 商品」。
  • admin/service/pdd_page_test.go:新建,32 个用例。
  • docs/admin/01-requirements.md / 05-ui-specification.md:同步。

审查打回后另修:

  • docs/admin/01-requirements.md / admin/handler/web/shopee.go / admin/templates/shopee/list.html:清理「PDD 链接唯一的录入口」的过期说法。
  • admin/handler/web/shopee.go:no_link 的过期 TODO。

验收结果

全部 12 项通过。架构角色独立复跑的重点项:

检查 结果
链接解析:短链拒绝 400,且不写库
链接解析:非 PDD 域名拒绝 400,且不写库
并发:4 个请求同时对同一商品建采集任务 只产生 1 条,商品置 collecting
CSRF:无 token 的 POST 403
端到端:建商品 → 建任务 → 领取 → 提交 → 刷新 状态流转正确,规格与价格显示正常
商品标题 XSS 列表页与弹窗均已转义,原样输出 0 次
13 个规格的弹窗 14 行、3808 字节,price_cent: null 显示「未采到」
软删除后列表消失,重建同链接复活原行 id 不变,title / skus_json 已清空
五个页面回归 全部 200

测试

架构角色亲自执行(admin/ 目录):

go vet ./...              无输出
gofmt -l .                无输出
go test ./... -count=1    ok  cmautobuy/admin/service  0.945s

本工单新增 32 个用例。

审查时补测了工单和实现报告都未覆盖的两项:

① 并发建采集任务
   4 个 curl 同时 POST /pdd/collect,同一个 goods_id
   → tasks 表里正好 1 条 COL-64cd722f5fb938ab
   → pdd_products.collect_status = collecting
   (MarkCollecting 在事务里同时充当"有没有人在采"的判断,
     单线程读代码看不出它扛不扛并发,跑一次才知道)

② 商品标题 XSS
   Client 提交 title = "<script>alert('XSS')</script>正常标题"
   规格里含 "<img src=x onerror=alert(1)>"
   → 列表页原样 <script> 出现 0 次,转义形态 2 次
   → 弹窗原样 onerror 出现 0 次,转义形态 1 次
   (标题来自拼多多,是外部不可信数据;工单未要求,但必须查)

未验证到的部分:

  • 浏览器里的真实交互:双击开弹窗、Esc 关闭、全选联动、删除二次确认, 只验证了服务端产出的 HTML 和约 90 行原生 JS 的源码,没有在真实浏览器里点过。
  • 深色 / 浅色主题、1366×768、显示缩放:CSS 是手写新增的,没有实机看过。
  • 20 个以上规格的弹窗滚动表现:测到 13 个,更多时是否需要滚动容器没试过。
  • artifact_ref 仍未规范化(#16 的遗留,本工单未涉及)。

审查过程

打回一次。

打回原因: docs/admin/05 §4.3 仍写着蝦皮弹窗是「PDD 链接唯一的录入口, 也是采集的唯一发起点」,与新增的 PDD 页自相矛盾。而实际情况是反过来的—— 蝦皮页的 ShopeeCollect 是骨架(501),真能用的只有 PDD 页。

实现报告把它列为「已知待收敛点」,方向对但范围少了:全库共 5 处,不是 1 处。

架构角色先按 CLAUDE.md §7.3「单处笔误」例外自己改了 §4.3,发现该例外不成立, 剩余 3 处按 §7.2 打回。同时发现 #16 的遗留:shopee.go 的 TODO 写着 「空 → no_link」,而 no_link 已在 #16 从 CHECK 约束删除,照写会直接撞约束。

返工结果: 3 处说法已对齐,no_link TODO 已改并加了警告。 「不要放到表格每一行,N 个 SKU 行会建出 N 个重复任务」这条理由四处全部保留, 只把适用范围从「全局唯一」收成「本页只在弹窗里」。行为未动, ShopeeCollect / ShopeeSave 仍返回 501。

遗留问题

  1. 蝦皮页与 PDD 页两个采集入口并存,蝦皮页那个是骨架。合并与否 归「蝦皮↔PDD 关联入口」工单决定,两处文档都已标注。
  2. 采集任务在界面上看不见——tasks 页只查 purchase。已建 #19 处理。
  3. 浏览器实机交互和主题 / 缩放需人工过一遍。

相关提交

  • eb8d357 feat: PDD 商品页面 (#18)
  • 4975c68 docs: 归档任务 #18

后续调整见 #23(工具条文案与宽度)。