feat(tasks): implement atomic claims and leases

This commit is contained in:
QiuSW
2026-07-26 16:16:36 +08:00
parent 49db5b8305
commit ce875af889
50 changed files with 7589 additions and 207 deletions
+50 -13
View File
@@ -292,10 +292,16 @@ CLAIMED/RUNNING/WAITING_CONFIRMATION
规则:
- `SUCCEEDED` 是验证结果成功,不代表已下单;`order_submitted=false`。
- `CLAIMED` 的默认租约计划为 10 分钟,运行时每 30 秒心跳续租;数值进入配置。
- `CLAIMED` 默认租约为 10 分钟;`RUNNING/WAITING_CONFIRMATION` 默认租约为
90 秒,App 计划每 30 秒 heartbeat 续租。三个时长均由有上下界的环境变量配置。
- 只有当前设备和有效 claim token 能启动、续租、上报或结束任务。
- 终态不可回退;重试创建新的 execution attempt,不篡改历史证据。
- 取消正在自动化的任务时,App 应在下一个安全检查点停止。
- App 在 claim 前生成并安全保存 256 bit Raw URL token;后端只存 SHA-256,
token 与 `Idempotency-Key` 相互独立。
- 过期 `CLAIMED` 可以原子回收;过期 `RUNNING/WAITING_CONFIRMATION` 保持原状态,
不自动回队列。
- 管理取消 `PENDING/CLAIMED` 立即终态;执行中只设置停止请求,App 在下一个安全
检查点停止并调用 `cancel-ack` 后才进入 `CANCELED`。
### 4.2 Android 工作流状态
@@ -348,6 +354,9 @@ IDLE
| `android_version` | nullable | Android 系统版本 |
| `pdd_version` | nullable | 已验证拼多多版本 |
| `last_seen_at` | nullable | 最近心跳 |
| `readiness_reported_at` | nullable | 最近一次就绪上报的服务端时间 |
| `accessibility_enabled` | NOT NULL | 无障碍连接就绪位 |
| `pdd_installed` | NOT NULL | 拼多多安装就绪位 |
| `is_enabled` | NOT NULL | 后端开关 |
设备必须先由本地管理命令预授权。首次 BUYER 联合登录可以把 `bound_user_id` 为空的
@@ -378,17 +387,28 @@ IDLE
| `quantity` | `> 0` | 权威数量 |
| `max_budget` | `> 0`, nullable | 全部数量的权威最高商品总预算,币种为 CNY |
| `status` | NOT NULL | 任务状态枚举 |
| `created_by` | FK | 创建人 |
| `claimed_by_user_id` | FK, nullable | 当前采购员 |
| `claimed_by_device_id` | FK, nullable | 当前设备 |
| `claim_generation` | `>= 0` | 每次领取递增,释放后保留用于审计 |
| `claim_token_hash` | nullable | 64 位小写 SHA-256;永不保存原 token |
| `claim_issued_at` | nullable | 当前 claim 签发时间 |
| `claim_expires_at` | nullable | 租约到期时间 |
| `cancel_reason` | nullable | 管理取消原因 |
| `cancel_requested_at` | nullable | 执行中停止请求时间 |
| `cancel_requested_by_user_id` | FK, nullable | 请求停止的 ADMIN |
| `canceled_at` | nullable | 取消终态时间 |
| `version` | NOT NULL | 乐观锁/状态并发控制 |
| `created_at/updated_at` | NOT NULL | 审计时间 |
### `task_events`
- 事件只追加;T-204 已实现 `TASK_CREATED`、`TASK_CANCELED`。
- 事件只追加;T-205 已实现 `TASK_CREATED`、`TASK_CLAIMED`、
`TASK_RECLAIMED`、`TASK_RELEASED`、`TASK_STARTED`、
`TASK_CANCEL_REQUESTED`、`TASK_CANCELED`。
- `actor_user_id` 是指向 `users` 的可空外键。新管理操作必须写入真实 ADMIN,
T-203 历史事件保持为空。
- `actor_device_id` 是指向 `devices` 的可空外键;App 状态迁移同时记录用户和设备。
- 设备/任务 heartbeat 不写事件,避免高频审计膨胀。
- 管理任务详情 API 可返回 actor;密码、session、设备 secret 和 access token 永不
进入事件。
@@ -399,17 +419,28 @@ IDLE
| `id` | PK | 一次执行尝试 |
| `task_id` | FK | 所属任务 |
| `attempt_no` | UNIQUE(task, no) | 尝试序号 |
| `claim_generation` | UNIQUE(task, generation) | 对应的 claim 代次 |
| `device_id/user_id` | FK | 执行设备和人员 |
| `extracted_requirements` | JSON | 已校验的模型结果 |
| `candidate_result` | JSON, nullable | 候选及匹配理由 |
| `outcome` | nullable | `CANDIDATE_ACCEPTED`、`CANDIDATE_REJECTED`、`NO_MATCH` 或 `MANUAL_REQUIRED` |
| `current_step` | 1-64 bytes | 最近 heartbeat 的执行步骤 |
| `last_heartbeat_at` | NOT NULL | 最近运行 heartbeat |
| `order_submitted` | NOT NULL, false | MVP 数据库约束必须为 false |
| `error_code/error_message` | nullable | 结构化失败 |
| `started_at/finished_at` | nullable | 执行耗时 |
| `started_at/finished_at` | started 必填 | 执行耗时和安全结束 |
### `execution_events` 与 `assets`
候选、模型派生结果、outcome、结构化失败和 execution evidence 属于 T-207,不在
T-205 的最小 execution 表中提前伪造。
- `execution_events` 只追加,记录 step、事件类型、可读消息和时间;不保存密码或 token。
### `lifecycle_requests`
- 主键为 `user_id + device_id + operation + idempotency_key`。
- operation 为 `CLAIM_NEXT`、`START`、`RELEASE`、`CANCEL_ACK`;保存请求 SHA-256
与 task/generation/execution 结果引用。
- `NO_TASK` 也是稳定结果;同 key 重放不会因后来新增任务而改变。
- claim 原 token 不进入该表,请求 hash 只包含 token hash。
### `assets` 与后续 `execution_events`
- T-207 的 `execution_events` 只追加,记录 step、事件类型、可读消息和时间;不保存
密码或 token。
- `assets` 保存文件相对路径、媒体类型、大小、哈希、创建者和保留时间。
- 原图和截图必须通过鉴权接口读取,文件名不能直接作为公开 URL。
- 任务参考图上传可接受 JPEG/PNG/WebP,但后端必须先真实解码、限制字节与像素,再
@@ -419,10 +450,16 @@ IDLE
- 合约以 [`api.md`](api.md) 为准。
- `claim-next` 必须由 usecase 调用 repository,在一个数据库事务内完成
“选取 + 校验设备空闲 + 更新状态”。
- 创建、领取和完成接口支持 `Idempotency-Key`。
“幂等检查 + 就绪/单活校验 + FIFO 选取/过期回收 + 更新 + 事件”。
- 当前设备若有自己的过期 `CLAIMED`,优先原子回收该任务;没有时才在全局候选中
按 `created_at ASC, id ASC` 选择。该例外保持设备唯一归属并避免静默清理审计状态。
- SQLite DSN 使用 `_txlock=immediate`、WAL 和 busy timeout;跨两个独立数据库连接
的并发测试必须精确得到一个领取者。
- claim/start/release/cancel-ack 支持 `Idempotency-Key`;heartbeat 不需要幂等表。
- App 对网络超时不能假定失败;必须用任务详情确认服务端最终状态。
- 后端拒绝非法状态迁移,即使客户端 UI 隐藏了对应按钮。
- App 参考图通过 task-scoped URL 读取;每次校验当前 user/device/generation/token/
租约,不允许用可猜测 asset ID 越权读取其他任务。
- SQLite MVP 使用单后端进程;切换多实例前先迁移 PostgreSQL 并验证领取竞争。
- Gin handler 只做鉴权上下文、binding、调用 usecase 和响应映射。