152 lines
9.5 KiB
Markdown
152 lines
9.5 KiB
Markdown
---
|
||||
|
|
id: T-205
|
|||
|
|
title: 实现原子 claim、租约和状态机
|
|||
|
|
phase: 2
|
|||
|
|
deps:
|
|||
|
|
- T-203
|
|||
|
|
- T-204
|
|||
|
|
status: DONE
|
|||
|
|
created: 2026-07-26
|
|||
|
|
context_ref: 49db5b8
|
|||
|
|
work_branch: main
|
|||
|
|
write_paths:
|
|||
|
|
- 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 或跨任务枚举。
|
|||
|
|
|
|||
|
|
## 验收要点
|
|||
|
|
|
|||
|
|
- [x] migration 可 up/down/up,历史任务保持可读,数据库不存在原始 claim token。
|
|||
|
|
- [x] heartbeat 身份、版本长度、就绪位和新鲜度校验正确,客户端不能冒充设备。
|
|||
|
|
- [x] 多 goroutine/多连接并发 claim 同一任务时精确一个成功,不重复分配。
|
|||
|
|
- [x] 同一设备不能领取第二条活跃任务;不同设备可以领取不同任务。
|
|||
|
|
- [x] claim 首次响应丢失后用同 key/token 重放得到同一任务;冲突/过期重放被拒绝。
|
|||
|
|
- [x] 无任务的幂等重放保持 204,不因稍后新增任务改变旧请求结果。
|
|||
|
|
- [x] start 精确创建一个 execution;重复 start 同 key 返回同一 execution。
|
|||
|
|
- [x] 错误设备、错误 token、过期 CLAIMED 租约、非法状态均不能 start/release。
|
|||
|
|
- [x] 当前 claim 才能读取参考图,错误 token/generation/设备或过期租约均被拒绝。
|
|||
|
|
- [x] RUNNING heartbeat 续租并返回取消标志;过期运行租约不会回队列或被其他设备领取。
|
|||
|
|
- [x] 管理取消活跃任务只请求停止;App 安全确认后才进入 CANCELED。
|
|||
|
|
- [x] 事件 actor、version、execution attempt 和状态迁移可审计,响应/日志不泄露秘密。
|
|||
|
|
- [x] 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 严重度问题。
|