按 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>
147 lines
5.5 KiB
Markdown
147 lines
5.5 KiB
Markdown
# 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)
|