Files
cmautobuy/docs/task/17-admin-claim-支持领取无主任务.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

5.5 KiB
Raw Blame History

17 Admin claim 支持领取无主任务

  • 类型:需求(含接口契约变更)
  • 父级大工单:#14
  • 所属 MVP / 版本:#15 / MVP
  • 状态:验收通过
  • 日期:2026-08-07
  • Gitea 工单:#17

背景与目标

PDD 商品页要能「创建采集任务」而不指定客户端——采集只是浏览商品页, 没有副作用,哪台设备采都一样,没必要每次都挑一台。

但原来 claim 的查询是:

WHERE assigned_client = ? AND status = 'assigned'

无主任务(assigned_client IS NULL + status='pending')永远不会被任何客户端领到, 建出来就是死的。

本工单推翻了一条已定案的规则:docs/client/04-admin-api-contract.md §5.1 原写「Admin 只把任务分配给指定的 Client」。

最终方案

查询同时覆盖两种任务

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 排前面。 理由:显式分配是人为决定,应当先兑现;无主任务谁抢都一样,可以等。

原子抢占合成一条语句

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 全过 通过

测试

执行的命令(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)