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

183 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 18 Admin PDD 商品页面
- 类型:需求(界面)
- 父级大工单:#14
- 所属 MVP / 版本:#15 / MVP
- 状态:验收通过
- 日期:2026-08-07
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/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 表,
要等客户端跑到手机上才会发现
### 创建采集任务:先占状态再建任务
```go
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](23-admin-pdd-页工具条文案与宽度调整.md)(工具条文案与宽度)。