Admin:claim 支持领取无主任务 #17

Closed
opened 2026-08-07 10:48:53 +08:00 by ila · 2 comments
Owner

基本信息

  • 类型:需求(含接口契约变更)
  • 父级大工单:#14
  • 所属 MVP / 版本:#15 / MVP
  • 阶段:2. 客户端接入

要解决什么

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」。

做什么 / 不做什么

做:

  1. ClaimNextTask 支持两种任务:指定给本机的 + 无主的
  2. 领到无主任务时写入 assigned_client("谁领到就标记谁")
  3. 更新两侧契约文档
  4. 补测试

不做:

  • 不改表结构(assigned_client 本来可空,status 已有 pending)
  • 不做 PDD 页面(另一个工单)
  • 不实现采购任务创建

怎么做

查询

WHERE task_type IN (...)
  AND (  (assigned_client = ? AND status = 'assigned')      -- 指定给我的
      OR (assigned_client IS NULL AND status = 'pending') ) -- 无主的
ORDER BY (assigned_client IS NULL), priority DESC, created_at

[必须] 指定给我的优先于无主的(ORDER BY (assigned_client IS NULL) 让 0 排在 1 前面)。
理由:显式分配是人为决定,应当先兑现;无主任务谁抢都一样,可以等。

原子抢占

两种情况可以合成一条语句——对"指定给我的"那种,写入 assigned_client 是写同一个值,无副作用:

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) )

[必须] 检查影响行数,为 0 说明被别人抢先了,换下一条候选。这是防并发重复领取的唯一手段,
SQLite 没有 SELECT ... FOR UPDATE。

[必须] 领取成功后仍要写 task_claims——提交结果时的权限判断依赖它
(只有从没领过的才返回 403,见 docs/admin/04-client-api.md §4.1)。

分配策略

任务类型 分配 理由
采集 不分配 只是浏览商品页,无副作用,哪台设备采都一样
采购 可分配,允许留空 涉及钱和账号,需要指定时能指定;不指定就谁抢到算谁的

预计修改文件

文件 改什么
admin/repository/task.go ClaimNextTask 查询与原子更新
admin/service/client_test.go 补测试
docs/client/04-admin-api-contract.md §5.1 改写;§10「已定案」那条同步
docs/admin/04-client-api.md §2 领取任务的处理步骤

验收标准

  • 无主的采集任务能被领到
  • 领到无主任务后,assigned_client 被写成领取者的编号
  • status 变为 claimed,claimed_at 有值
  • 指定给本机的任务优先于无主任务(同优先级同创建时间下)
  • 仍然不会领到指定给别的客户端的任务
  • supported_types 过滤仍然生效(只声明 collect 的不会拿到 purchase)
  • 并发:多个客户端同时抢同一个无主任务,只有一个拿到
  • 领取后 task_claims 有对应记录
  • 两侧契约文档已更新,不再声称"只分配给指定 Client"
  • go vet / gofmt -l . / go test ./... 全过

怎么验证

cd D:\chengma\cmautobuy\admin
go vet ./...
gofmt -l .
go test ./... -count=1

不需要真机,不需要 Client。

风险和回退

风险 应对
无主的采购任务会被任意客户端抢走 涉及账号归属时必须显式分配。已写进契约文档
dry_run 客户端可能抢到需要真实下单的任务 tasks 表目前没有字段标记任务是否需要真实下单。真实下单开关默认关闭时不出问题,开启前必须补该字段——记入后续工单
并发抢占测试不好写 起多个 goroutine 抢同一条,断言只有一个成功

回退:改动集中在 admin/repository/task.go,git revert 即可,无表结构变更。

## 基本信息 - 类型:需求(含接口契约变更) - 父级大工单:#14 - 所属 MVP / 版本:#15 / MVP - 阶段:2. 客户端接入 ## 要解决什么 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」。 ## 做什么 / 不做什么 做: 1. `ClaimNextTask` 支持两种任务:指定给本机的 + 无主的 2. 领到无主任务时写入 `assigned_client`("谁领到就标记谁") 3. 更新两侧契约文档 4. 补测试 不做: - 不改表结构(`assigned_client` 本来可空,`status` 已有 `pending`) - 不做 PDD 页面(另一个工单) - 不实现采购任务创建 ## 怎么做 ### 查询 ```sql WHERE task_type IN (...) AND ( (assigned_client = ? AND status = 'assigned') -- 指定给我的 OR (assigned_client IS NULL AND status = 'pending') ) -- 无主的 ORDER BY (assigned_client IS NULL), priority DESC, created_at ``` `[必须]` **指定给我的优先于无主的**(`ORDER BY (assigned_client IS NULL)` 让 0 排在 1 前面)。 理由:显式分配是人为决定,应当先兑现;无主任务谁抢都一样,可以等。 ### 原子抢占 两种情况可以合成一条语句——对"指定给我的"那种,写入 `assigned_client` 是写同一个值,无副作用: ```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) ) ``` `[必须]` 检查影响行数,为 0 说明被别人抢先了,换下一条候选。这是防并发重复领取的唯一手段, SQLite 没有 `SELECT ... FOR UPDATE`。 `[必须]` 领取成功后仍要写 `task_claims`——提交结果时的权限判断依赖它 (只有从没领过的才返回 403,见 `docs/admin/04-client-api.md` §4.1)。 ### 分配策略 | 任务类型 | 分配 | 理由 | |---|---|---| | 采集 | **不分配** | 只是浏览商品页,无副作用,哪台设备采都一样 | | 采购 | **可分配,允许留空** | 涉及钱和账号,需要指定时能指定;不指定就谁抢到算谁的 | ## 预计修改文件 | 文件 | 改什么 | |---|---| | `admin/repository/task.go` | `ClaimNextTask` 查询与原子更新 | | `admin/service/client_test.go` | 补测试 | | `docs/client/04-admin-api-contract.md` | §5.1 改写;§10「已定案」那条同步 | | `docs/admin/04-client-api.md` | §2 领取任务的处理步骤 | ## 验收标准 - [ ] 无主的采集任务能被领到 - [ ] 领到无主任务后,`assigned_client` 被写成领取者的编号 - [ ] `status` 变为 `claimed`,`claimed_at` 有值 - [ ] **指定给本机的任务优先于无主任务**(同优先级同创建时间下) - [ ] 仍然不会领到指定给别的客户端的任务 - [ ] `supported_types` 过滤仍然生效(只声明 collect 的不会拿到 purchase) - [ ] **并发:多个客户端同时抢同一个无主任务,只有一个拿到** - [ ] 领取后 `task_claims` 有对应记录 - [ ] 两侧契约文档已更新,不再声称"只分配给指定 Client" - [ ] `go vet` / `gofmt -l .` / `go test ./...` 全过 ## 怎么验证 ```powershell cd D:\chengma\cmautobuy\admin go vet ./... gofmt -l . go test ./... -count=1 ``` 不需要真机,不需要 Client。 ## 风险和回退 | 风险 | 应对 | |---|---| | **无主的采购任务会被任意客户端抢走** | 涉及账号归属时必须显式分配。已写进契约文档 | | dry_run 客户端可能抢到需要真实下单的任务 | `tasks` 表目前**没有字段标记任务是否需要真实下单**。真实下单开关默认关闭时不出问题,**开启前必须补该字段**——记入后续工单 | | 并发抢占测试不好写 | 起多个 goroutine 抢同一条,断言只有一个成功 | 回退:改动集中在 `admin/repository/task.go`,`git revert` 即可,无表结构变更。
Author
Owner

实施完成,待验收

验证结果(Go 1.23.0)

go vet / gofmt -l . 无输出;go test ./... -count=1 62 个测试全 PASS(新增 7 个)。

并发用例重复 20 次稳定:8 个客户端抢同一条无主任务,正好 1 个拿到,
且库里 assigned_client 记的就是那个赢家。

优先级那条测试特意构造成「无主任务的 created_at 更早」——
没有优先级规则的话按 created_at 排序会先拿到无主的,测试就会红。

10 条验收标准全部通过。

契约变更

docs/client/04-admin-api-contract.md §5.1 从「Admin 只把任务分配给指定的 Client」
改为两种并存:采集任务不指定,采购任务可指定可留空。全库已无「只分配给指定 Client」的残留说法。

未验证到的部分

  • 未做 HTTP 层端到端(claim 接口本身未改,改动全在 repository 查询里)
  • 未与真实 Client 联调——Client 的 HttpAdminGateway 目前只实现了 register_client

遗留

tasks 表缺「该任务需要真实下单」的标记字段,本次把无主任务开放给所有客户端后,
这条风险被放大:声明 dry_run 的客户端可能抢到本该真实下单的任务。
当前真实下单开关默认关闭、全部演练模式,不出问题;开启前必须补该字段。
经确认暂不单独建工单。

## 实施完成,待验收 - **实现提交:** `dd387e3` feat: claim 支持领取无主任务 (#17) - **归档提交:** `fab20cf` docs: 归档任务 #16 #17 - **归档文档:** `docs/task/17-admin-claim-支持领取无主任务.md` ### 验证结果(Go 1.23.0) `go vet` / `gofmt -l .` 无输出;`go test ./... -count=1` **62 个测试全 PASS**(新增 7 个)。 并发用例重复 20 次稳定:8 个客户端抢同一条无主任务,**正好 1 个拿到**, 且库里 `assigned_client` 记的就是那个赢家。 优先级那条测试特意构造成「无主任务的 `created_at` 更早」—— 没有优先级规则的话按 `created_at` 排序会先拿到无主的,测试就会红。 10 条验收标准全部通过。 ### 契约变更 `docs/client/04-admin-api-contract.md` §5.1 从「Admin 只把任务分配给指定的 Client」 改为两种并存:采集任务不指定,采购任务可指定可留空。全库已无「只分配给指定 Client」的残留说法。 ### 未验证到的部分 - 未做 HTTP 层端到端(`claim` 接口本身未改,改动全在 repository 查询里) - 未与真实 Client 联调——Client 的 `HttpAdminGateway` 目前只实现了 `register_client` ### 遗留 `tasks` 表缺「该任务需要真实下单」的标记字段,本次把无主任务开放给所有客户端后, 这条风险被放大:声明 `dry_run` 的客户端可能抢到本该真实下单的任务。 当前真实下单开关默认关闭、全部演练模式,不出问题;**开启前必须补该字段**。 经确认暂不单独建工单。
Author
Owner

验收通过,关闭

用户确认验收通过。归档状态已更新(3fdab7b)。

  • 实现提交: dd387e3
  • 归档: docs/task/17-admin-claim-支持领取无主任务.md

遗留:tasks 表缺「该任务需要真实下单」的标记字段。当前真实下单开关默认关闭、全部演练模式,不出问题;开启前必须补该字段。

## 验收通过,关闭 用户确认验收通过。归档状态已更新(`3fdab7b`)。 - **实现提交:** `dd387e3` - **归档:** `docs/task/17-admin-claim-支持领取无主任务.md` 遗留:`tasks` 表缺「该任务需要真实下单」的标记字段。当前真实下单开关默认关闭、全部演练模式,不出问题;**开启前必须补该字段**。
ila closed this issue 2026-08-07 17:18:36 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: chengma/cmautobuy#17