docs: 归档任务 #18
记录审查时实跑的结果:4 并发建采集任务只产生 1 条、商品标题 XSS 在 列表页和弹窗均已转义、13 规格弹窗渲染正常。 如实记录打回过程:架构角色先按 §7.3"单处笔误"例外自改 §4.3, 发现该例外不成立(全库 5 处),剩余 3 处按 §7.2 打回。 未验证四项:浏览器实机交互、主题与缩放、20+ 规格弹窗滚动、artifact_ref。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# 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 |
|
||||
|
||||
## 测试
|
||||
|
||||
架构角色亲自执行(Go 1.23.0,`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. 浏览器实机交互和主题 / 缩放需人工过一遍。
|
||||
|
||||
## 相关提交
|
||||
|
||||
- 见 `docs/task/` 同批提交
|
||||
Reference in New Issue
Block a user