Files
cmautobuy/docs/task/17-admin-claim-支持领取无主任务.md
T
chengmaandClaude Opus 5 fab20cfd2f docs: 归档任务 #16 #17
按 AGENTS.md §9 补上两份归档。内容是审查时实跑的结果,不是复述报告。

#16 PDD 商品数据独立成表
- 记录三处超出工单但必要的决定:TaskExists → GetTaskInfo、
  采错商品做成整体回滚的硬拒绝、复活时清空旧采集结果
- 记录审查时补验的 HTTP 层端到端:422 拒绝后幂等记录 0 条
  (若残留会导致客户端重试永远拿到缓存的失败响应)
- 三项未验证:界面未实现、MarkCollecting/SoftDeletePddProduct 无生产调用方、
  artifact_ref 未规范化

#17 claim 支持领取无主任务
- 记录契约变更:§5.1 从「只分配给指定 Client」改为两种并存
- 记录优先级测试的构造方式(无主任务 created_at 更早,
  没有优先级规则时会红)
- 遗留:tasks 表缺「需要真实下单」标记,本次把无主任务开放后风险被放大,
  开启真实下单前必须补

两个工单保持 open,等用户验收。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:02:21 +08:00

147 lines
5.5 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.
# 17 Admin claim 支持领取无主任务
- 类型:需求(含接口契约变更)
- 父级大工单:#14
- 所属 MVP / 版本:#15 / MVP
- 状态:待验收
- 日期:2026-08-07
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/17
## 背景与目标
PDD 商品页要能「创建采集任务」而**不指定客户端**——采集只是浏览商品页,
没有副作用,哪台设备采都一样,没必要每次都挑一台。
但原来 `claim` 的查询是:
```sql
WHERE assigned_client = ? AND status = 'assigned'
```
**无主任务(`assigned_client IS NULL` + `status='pending'`)永远不会被任何客户端领到**,
建出来就是死的。
本工单**推翻了一条已定案的规则**:`docs/client/04-admin-api-contract.md` §5.1
原写「Admin 只把任务分配给指定的 Client」。
## 最终方案
### 查询同时覆盖两种任务
```sql
WHERE ( (assigned_client = ? AND status = 'assigned') -- 指定给本机的
OR (assigned_client IS NULL AND status = 'pending') ) -- 无主的
ORDER BY (assigned_client IS NULL), priority DESC, created_at
```
**指定给本机的优先于无主的**。`(assigned_client IS NULL)` 求值为 0/1,0 排前面。
理由:显式分配是人为决定,应当先兑现;无主任务谁抢都一样,可以等。
### 原子抢占合成一条语句
```sql
UPDATE tasks
SET status = 'claimed', assigned_client = ?, claimed_at = ?, updated_at = ?
WHERE task_id = ?
AND ( (status = 'assigned' AND assigned_client = ?)
OR (status = 'pending' AND assigned_client IS NULL) )
```
对「指定给我的」那种,写 `assigned_client` 是写同一个值,无副作用;
对无主的,这一步就是**「谁领到就标记谁」**。检查影响行数防并发,
为 0 说明被抢先了,换下一条候选。
### 分配策略
| 任务类型 | 分配 | 理由 |
|---|---|---|
| 采集 | **不指定** | 只是浏览商品页,无副作用,哪台设备采都一样 |
| 采购 | **可指定,允许留空** | 涉及钱和账号——不同设备可能登着不同的拼多多账号,需要指定时能指定;留空即接受「谁先抢到谁去下单」 |
Client 侧对这两种**没有任何区别**:调 `claim`,拿到任务就做,做完提交,
不需要知道任务原来有没有主。
### 未改动
表结构不用动——`assigned_client` 本来可空,`status` 的 `pending` 也已存在。
## 与建单方案的差异
无。按工单实施。
## 改了哪些
- `admin/repository/task.go`:`ClaimNextTask` 的查询条件、排序和原子更新。
- `admin/service/claim_unassigned_test.go`:新建,7 个测试。
- `docs/client/04-admin-api-contract.md`:§5.1 改写为两种任务并存并说明各自适用场景;
§10「已定案」那条同步。
- `docs/admin/04-client-api.md`:§2 处理步骤、SQL 示例、§9 实现清单。
## 验收结果
| 验收标准 | 结果 |
|---|---|
| 无主的采集任务能被领到 | 通过 |
| 领到后 `assigned_client` 被写成领取者编号 | 通过 |
| `status` 变 `claimed`,`claimed_at` 有值 | 通过 |
| **指定给本机的优先于无主的** | 通过 |
| 仍然领不到指定给别的客户端的任务 | 通过 |
| `supported_types` 过滤仍生效 | 通过 |
| **并发:多客户端抢同一无主任务,只有一个拿到** | 通过 |
| 领取后 `task_claims` 有对应记录 | 通过 |
| 两侧契约文档已更新,不再声称「只分配给指定 Client」 | 通过(全库无残留) |
| `go vet` / `gofmt` / `go test` 全过 | 通过 |
## 测试
执行的命令(Go 1.23.0,`admin/` 目录):
```
go vet ./... 无输出
gofmt -l . 无输出
go test ./... -count=1 ok,62 个测试全 PASS(新增 7 个)
go test ./service/ -run '并发抢无主' -count=20 ok
```
新增的 7 个测试:
```
TestClaim_无主任务能被领到并标记领取者
TestClaim_无主任务只能被领一次
TestClaim_指定给本机的优先于无主的
TestClaim_指定给别人的仍然领不到
TestClaim_无主任务也受supported_types约束
TestClaim_并发抢无主任务只有一个拿到
TestClaim_无主任务领取后也记领取历史
```
优先级那条特意构造成「无主任务的 `created_at` 更早」——没有优先级规则的话,
按 `created_at` 排序会先拿到无主的,测试就会红。
并发那条断言:8 个客户端抢同一条无主任务,**正好 1 个拿到**,
且库里 `assigned_client` 记的就是那个赢家。重复 20 次稳定。
**未验证到的部分:**
- 未做 HTTP 层端到端验证(只验证了 service 层)。`claim` 接口本身未改,
改动全在 repository 的查询里。
- 未与真实 Client 联调——Client 的 `HttpAdminGateway` 目前只实现了
`register_client`,`claim_next` 尚未实现。
## 遗留问题
**`tasks` 表没有字段标记「该任务需要真实下单」。**
契约要求「不向只声明 `dry_run` 的 Client 分配需要真实下单的任务」,
但没有这个字段就无法执行。本工单把无主任务开放给所有客户端后,
这条风险被放大:一台声明 `dry_run` 的客户端可能抢到本该真实下单的任务。
当前真实下单开关默认关闭、全部走演练模式,不会出问题。
**开启真实下单前必须补该字段**,与采购安全门禁(`docs/admin/06` §3)一并处理。
用户已确认暂不为此单独建工单,留在此处与 #14 Epic 的全局风险中记录。
## 相关提交
- `dd387e3` feat: claim 支持领取无主任务 (#17)