Files
cmroubao/docs/tasks/T-205.md
T

9.5 KiB
Raw Blame History

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
T-203
T-204
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 和取消安全停止。

已定合约

  1. 所有设备接口只接受有效 BUYER Bearer token;认证上下文中的 user_id/device_id 是权威身份。请求中的 device_id 必须与上下文一致,不能代表其他设备。
  2. App 在发起 claim 前生成独立的 256 bit Raw URL X-Claim-Token 并先安全保存, 同时生成独立 Idempotency-Key。服务只保存 claim token 的 SHA-256,响应不回显 原值;网络结果不确定时必须复用同一组值。
  3. claim 幂等记录按用户、设备、操作和 key 隔离,请求 hash 包含 claim token hash。 每次成功领取递增 claim_generation。同 key 同请求在同一 generation 的租约仍 属于该设备时返回同一任务;同 key 不同请求返回 409;原领取已释放、取消或被 过期回收时返回稳定 409 CLAIM_REPLAY_EXPIRED。无任务的首次结果记录为 NO_TASK,同 key 重放仍返回 204。
  4. 设备 heartbeat 记录服务端时间、App/Android/拼多多版本和两个确定性就绪位: 无障碍已连接、拼多多已安装。客户端上报的活跃任务只用于一致性检查,服务端任务 归属是权威。heartbeat 超过 2 分钟或任一就绪位为 false 时禁止领取。
  5. 领取在单个 SQLite immediate transaction 内完成。若当前设备自己仍有一条已过期 CLAIMED,先回收该任务,避免设备唯一归属冲突;否则在 PENDING 或已过期 CLAIMED 中按创建时间最早、ID 最小选择。一个任务只能归属一台设备;同一设备 只允许一个未过期 CLAIMED 或任意 RUNNING/WAITING_CONFIRMATION 任务。
  6. 初始 claim 租约默认 10 分钟。只有当前用户、当前设备、匹配 token hash 且租约 未过期时可以 start、release、heartbeat 或确认取消。所有到期计算只使用服务端 UTC 时钟,不信任 client_time。
  7. start 只允许 CLAIMED -> RUNNING,在同一事务创建唯一 execution attempt, 并把运行租约改为默认 90 秒;请求携带 claim 响应的 expected_version, Idempotency-Key 同请求重放返回同一 execution。
  8. 运行 heartbeat 每 30 秒计划调用,只有 RUNNING/WAITING_CONFIRMATION 且 execution/generation 匹配时才更新 step、last_seen_at 和 90 秒运行租约。 过期运行租约拒绝续租和后续自动操作,任务保持当前非终态且绝不自动回到队列, 防止失联设备与新设备重复执行;恢复/失败归档后置。
  9. release 只允许未过期 CLAIMED -> PENDING,请求必须匹配 expected_version, 并清除用户、设备、token 和租约。已开始任务不能 release。过期 CLAIMED 可由 下一次 claim 原子回收。
  10. 管理取消 PENDING/CLAIMED 时立即进入 CANCELED;RUNNING 或 WAITING_CONFIRMATION 只设置取消请求,不伪装为已停止。任务 heartbeat 返回 cancel_requested=true,App 在安全检查点以 expected_version 调用 cancel-ack 后才进入 CANCELED 并结束 execution。
  11. 每次状态迁移递增 version 并追加带 actor 的任务事件;heartbeat 只更新租约和 当前 step,不追加高频事件。claim/start/heartbeat/transition 响应都返回当前 version、claim_generation、服务端时间和租约到期时间。终态不可回退,非法 状态、错误设备、错误 token、execution 不匹配和过期租约使用彼此稳定但不泄露 其他任务内容的错误码。
  12. 三个时长通过配置提供安全默认值和上下界:claim lease 10 分钟、运行 lease 90 秒、设备 heartbeat TTL 2 分钟。T-205 不启动后台定时清理器。
  13. 参考图不暴露资产存储路径。App 只能通过 claim 响应中的 task-scoped URL, 携带当前 BUYER token、设备身份、X-Claim-Token 和 claim_generation 读取; 服务端同时校验当前状态和未过期租约,并返回 private, no-store JPEG。

方案

  1. 新增 00004_claims_and_lifecycle.sql,扩展任务 claim/cancel 字段、设备就绪字段, 增加 execution 与生命周期幂等表,并扩展任务事件枚举;迁移必须可空兼容历史数据。
  2. 在 domain 增加 claim、execution、状态迁移和就绪实体;在独立 LifecycleService 中完成输入校验、token hash、服务端时钟、错误映射和响应模型。
  3. SQLite repository 使用显式事务完成 claim/reclaim、start、release、heartbeat、 取消请求/确认和幂等重放;所有条件更新同时校验状态、归属、token、租约和版本。
  4. Gin 增加独立 BUYER 设备路由组,复用 T-204 Bearer middleware;handler 只解析 header/body、读取认证 principal、调用 usecase 和映射稳定响应。
  5. 管理取消从仅 PENDING 扩展为上述安全语义;管理详情返回 claim、execution 和 取消请求的非秘密摘要,永不返回 claim token/hash。
  6. claim 响应只给出 task-scoped 参考图 URL;图片 handler 在读取本地资产前重新 授权当前 claim,避免长期资产 URL 或跨任务枚举。

验收要点

  • migration 可 up/down/up,历史任务保持可读,数据库不存在原始 claim token。
  • heartbeat 身份、版本长度、就绪位和新鲜度校验正确,客户端不能冒充设备。
  • 多 goroutine/多连接并发 claim 同一任务时精确一个成功,不重复分配。
  • 同一设备不能领取第二条活跃任务;不同设备可以领取不同任务。
  • claim 首次响应丢失后用同 key/token 重放得到同一任务;冲突/过期重放被拒绝。
  • 无任务的幂等重放保持 204,不因稍后新增任务改变旧请求结果。
  • start 精确创建一个 execution;重复 start 同 key 返回同一 execution。
  • 错误设备、错误 token、过期 CLAIMED 租约、非法状态均不能 start/release。
  • 当前 claim 才能读取参考图,错误 token/generation/设备或过期租约均被拒绝。
  • RUNNING heartbeat 续租并返回取消标志;过期运行租约不会回队列或被其他设备领取。
  • 管理取消活跃任务只请求停止;App 安全确认后才进入 CANCELED。
  • 事件 actor、version、execution attempt 和状态迁移可审计,响应/日志不泄露秘密。
  • Go test/race/vet/gofmt、迁移/HTTP 并发 smoke、根脚本全部通过。

边界

  • 不实现 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 严重度问题。