id, title, phase, deps, status, created, context_ref, work_branch, write_paths
| id |
title |
phase |
deps |
status |
created |
context_ref |
work_branch |
write_paths |
| T-205 |
实现原子 claim、租约和状态机 |
2 |
|
DONE |
2026-07-26 |
49db5b8 |
main |
| README.md |
| backend-api/** |
| docs/00-ai-start-here.md |
| docs/02-requirements.md |
| docs/03-tech-stack.md |
| docs/04-architecture.md |
| docs/05-coding-rules.md |
| docs/07-user-stories.md |
| docs/08-interaction-checklist.md |
| docs/api.md |
| docs/current-state.md |
| docs/routes.md |
| docs/tasks/T-205.md |
| progress.md |
|
问题 / 背景
T-203 已能创建 PENDING 任务,T-204 已建立 BUYER + 预授权设备联合身份,但 App
还不能安全领取任务。现有正式文档只给出接口草图,未解决 claim token 只返回一次时
如何在响应丢失后安全重放,也没有固定设备就绪新鲜度、运行中租约过期、取消确认和
并发领取的精确语义。T-206 在这些合约固定前不能接入 HTTP TaskSource。
关联需求与交互
- 功能:F-003、F-007。
- 用户故事:US-003、US-006。
- 交互:IX-005、IX-008;本任务只实现后端,Android 页面接入属于 T-206。
- 架构/API:任务状态机、
devices/heartbeat、claim-next、start、任务 heartbeat、
reference-image、release 和取消安全停止。
已定合约
- 所有设备接口只接受有效 BUYER Bearer token;认证上下文中的
user_id/device_id
是权威身份。请求中的 device_id 必须与上下文一致,不能代表其他设备。
- App 在发起 claim 前生成独立的 256 bit Raw URL
X-Claim-Token 并先安全保存,
同时生成独立 Idempotency-Key。服务只保存 claim token 的 SHA-256,响应不回显
原值;网络结果不确定时必须复用同一组值。
- claim 幂等记录按用户、设备、操作和 key 隔离,请求 hash 包含 claim token hash。
每次成功领取递增
claim_generation。同 key 同请求在同一 generation 的租约仍
属于该设备时返回同一任务;同 key 不同请求返回 409;原领取已释放、取消或被
过期回收时返回稳定 409 CLAIM_REPLAY_EXPIRED。无任务的首次结果记录为
NO_TASK,同 key 重放仍返回 204。
- 设备 heartbeat 记录服务端时间、App/Android/拼多多版本和两个确定性就绪位:
无障碍已连接、拼多多已安装。客户端上报的活跃任务只用于一致性检查,服务端任务
归属是权威。heartbeat 超过 2 分钟或任一就绪位为 false 时禁止领取。
- 领取在单个 SQLite immediate transaction 内完成。若当前设备自己仍有一条已过期
CLAIMED,先回收该任务,避免设备唯一归属冲突;否则在 PENDING 或已过期
CLAIMED 中按创建时间最早、ID 最小选择。一个任务只能归属一台设备;同一设备
只允许一个未过期 CLAIMED 或任意 RUNNING/WAITING_CONFIRMATION 任务。
- 初始 claim 租约默认 10 分钟。只有当前用户、当前设备、匹配 token hash 且租约
未过期时可以 start、release、heartbeat 或确认取消。所有到期计算只使用服务端
UTC 时钟,不信任
client_time。
start 只允许 CLAIMED -> RUNNING,在同一事务创建唯一 execution attempt,
并把运行租约改为默认 90 秒;请求携带 claim 响应的 expected_version,
Idempotency-Key 同请求重放返回同一 execution。
- 运行 heartbeat 每 30 秒计划调用,只有
RUNNING/WAITING_CONFIRMATION 且
execution/generation 匹配时才更新 step、last_seen_at 和 90 秒运行租约。
过期运行租约拒绝续租和后续自动操作,任务保持当前非终态且绝不自动回到队列,
防止失联设备与新设备重复执行;恢复/失败归档后置。
release 只允许未过期 CLAIMED -> PENDING,请求必须匹配 expected_version,
并清除用户、设备、token 和租约。已开始任务不能 release。过期 CLAIMED 可由
下一次 claim 原子回收。
- 管理取消
PENDING/CLAIMED 时立即进入 CANCELED;RUNNING 或
WAITING_CONFIRMATION 只设置取消请求,不伪装为已停止。任务 heartbeat 返回
cancel_requested=true,App 在安全检查点以 expected_version 调用
cancel-ack 后才进入 CANCELED 并结束 execution。
- 每次状态迁移递增
version 并追加带 actor 的任务事件;heartbeat 只更新租约和
当前 step,不追加高频事件。claim/start/heartbeat/transition 响应都返回当前
version、claim_generation、服务端时间和租约到期时间。终态不可回退,非法
状态、错误设备、错误 token、execution 不匹配和过期租约使用彼此稳定但不泄露
其他任务内容的错误码。
- 三个时长通过配置提供安全默认值和上下界:claim lease 10 分钟、运行 lease
90 秒、设备 heartbeat TTL 2 分钟。T-205 不启动后台定时清理器。
- 参考图不暴露资产存储路径。App 只能通过 claim 响应中的 task-scoped URL,
携带当前 BUYER token、设备身份、
X-Claim-Token 和 claim_generation 读取;
服务端同时校验当前状态和未过期租约,并返回 private, no-store JPEG。
方案
- 新增
00004_claims_and_lifecycle.sql,扩展任务 claim/cancel 字段、设备就绪字段,
增加 execution 与生命周期幂等表,并扩展任务事件枚举;迁移必须可空兼容历史数据。
- 在 domain 增加 claim、execution、状态迁移和就绪实体;在独立
LifecycleService 中完成输入校验、token hash、服务端时钟、错误映射和响应模型。
- SQLite repository 使用显式事务完成 claim/reclaim、start、release、heartbeat、
取消请求/确认和幂等重放;所有条件更新同时校验状态、归属、token、租约和版本。
- Gin 增加独立 BUYER 设备路由组,复用 T-204 Bearer middleware;handler 只解析
header/body、读取认证 principal、调用 usecase 和映射稳定响应。
- 管理取消从仅 PENDING 扩展为上述安全语义;管理详情返回 claim、execution 和
取消请求的非秘密摘要,永不返回 claim token/hash。
- claim 响应只给出 task-scoped 参考图 URL;图片 handler 在读取本地资产前重新
授权当前 claim,避免长期资产 URL 或跨任务枚举。
验收要点
边界
- 不实现 Android HTTP TaskSource、token 安全存储、前台服务或页面;属于 T-206。
- 不实现候选、执行证据、批量事件、完成/失败结果归档;属于 T-207。
- 不实现后台通知、WebSocket、跨实例锁、自动重试或多设备调度运营页面。
- 不把过期
RUNNING 自动回到 PENDING,也不允许任何路径提交订单或支付。
- 不提交运行数据库、claim/access token、真实设备心跳或私有订单样本。
执行记录
2026-07-26:任务开始
- 基于提交
49db5b8 开始,工作区干净;T-203/T-204 依赖均已完成。
- codebase-memory MCP 本轮仍未暴露 graph 工具,按项目规则回退到
rg 和定点读取。
- 先固定 client-generated claim token、安全幂等重放、就绪 TTL、租约过期与取消确认
语义,避免在迁移和 handler 写完后再改变跨端协议。
2026-07-26:实现与验证完成
- 新增 migration 00004、生命周期领域/用例/SQLite 仓储和 BUYER Gin 路由;管理端
取消扩展为
PENDING/CLAIMED 立即取消、RUNNING/WAITING_CONFIRMATION
请求安全停止,并在任务详情展示非秘密 claim/execution 摘要。
go test -count=1 ./... 共 192 个测试通过;全包 go test -race -count=1 ./...、
go vet ./...、gofmt -d .、三入口 Windows 构建和根 init.ps1 均通过。
- migration CLI 完成
status -> up -> up -> down -> up,第二次 up 无重复应用;
两个独立数据库连接和真实 TCP 并发 claim 都验证同设备精确一个成功。
- Playwright 连接真实 Gin/SQLite 流程验证任务详情在 1440x900、390x844、360x800
无横向溢出、破图或弹窗重叠;运行中“请求安全停止”分支由 SSR 测试覆盖。
- 独立审查发现并修复 start 后 claim 幂等重放被误判过期的问题;同设备优先恢复
自己过期 CLAIMED 的亲和策略已补充文档和测试。未发现 High 严重度问题。