--- id: T-216 title: 设备下单命令投递、确认与恢复 phase: 2 deps: - T-215 status: DONE created: 2026-07-28 context_ref: 827afc7 work_branch: null write_paths: - docs/tasks/T-216.md - docs/00-ai-start-here.md - docs/02-requirements.md - docs/04-architecture.md - docs/06-tasks.md - docs/08-interaction-checklist.md - docs/api.md - docs/current-state.md - docs/routes.md - backend-api/migrations/** - backend-api/internal/domain/** - backend-api/internal/usecase/** - backend-api/internal/repository/sqlite/** - backend-api/internal/transport/httpapi/** - android-buyer/app/src/main/** - android-buyer/app/src/test/** --- ## 问题 / 背景 T-215 只生成 `PENDING_DELIVERY` 下单授权。Android 仍停留在旧的手机人工选择流程, 也没有可靠方式知道 Admin 已授权哪个 candidate。直接在 Admin 浏览器推送或收到 HTTP 响应后立即执行都无法处理 Android 后台限制、断网、进程重启和响应丢失。 本任务把授权作为同一 execution 的可重放设备命令,由 Roubao 的现有前台服务主动 拉取。App 必须先把完整命令写入 Keystore-backed 加密状态,再确认收妥。T-216 不 执行拼多多页面动作。 ## 冻结合约 ### 拉取而非服务端推送 1. App 在非终态 execution 的 30 秒 heartbeat 后,以及用户点击“立即同步”时调用 `POST /api/v1/tasks/{task_id}/commands/next`。不增加 WebSocket、FCM 或浏览器到 手机的直连通道。 2. 请求必须携带 BUYER bearer、`X-Claim-Token`,并提交当前 `device_id`、 `execution_id`、`claim_generation`。服务端校验 task、user、device、execution、 generation、token、有效租约、`WAITING_CONFIRMATION` 和未请求取消。 3. 没有命令返回 `204`。当前 active authorization 为 `PENDING_DELIVERY/DELIVERED/ACKNOWLEDGED` 时返回同一命令;一个 execution 不会 返回第二条 active 命令。 ### 命令内容与持久身份 命令 `schema_version=1`、`type=CREATE_PENDING_ORDER`,只包含服务端快照和受控 observation,不包含第三方 URL: ```json { "id": "authorization-uuid", "schema_version": 1, "type": "CREATE_PENDING_ORDER", "authorization_version": 1, "task_id": "uuid", "execution_id": "uuid", "task_content_sha256": "64-char-hex", "original_sku": "雾霾蓝 / L", "quantity": 2, "candidate": { "candidate_key": "64-char-hex", "observed_ordinal": 1, "title": "实际观测标题", "sku_text": "雾霾蓝 / L", "price_text": "39.80", "card_signature": "64-char-hex", "detail_signature": "64-char-hex", "detail_evidence_sha256": "64-char-hex", "specification_evidence_sha256": "64-char-hex" }, "command_sha256": "64-char-hex", "authorization_status": "DELIVERED" } ``` 1. `observed_ordinal` 只用于重新定位提示,不能直接作为点击索引;T-217 必须重新核对 card/detail/SKU/证据。 2. `command_sha256` 是 schema v1 除自身和投递状态外的 canonical JSON SHA-256。 首次投递在事务内写入该 hash、`delivered_at` 和 `ORDER_AUTHORIZATION_DELIVERED` 事件;重投不新增事件。 3. 已 `ACKNOWLEDGED` 但 App 本地状态丢失时仍允许同一身份重新拉取,以便安全恢复; 不允许因此改写 hash 或创建新命令。 ### App 先持久化后确认 1. 新增加密 `PendingOrderCommand`,保存完整快照、command hash、ACK 幂等键和确认 状态。先验证 schema/type、task/execution/content hash、数量和所有指纹,再一次 `store.save`;保存失败不得发送 ACK。 2. 保存成功后调用 `POST /api/v1/tasks/{task_id}/commands/{command_id}/ack`,请求包含 execution、 generation 和 command hash,并带 claim token 与 `Idempotency-Key`。 3. ACK 在事务内把 `DELIVERED -> ACKNOWLEDGED`,写 `acknowledged_at`、幂等记录和 `ORDER_AUTHORIZATION_ACKNOWLEDGED` 事件。相同 key/body 或同 command/hash 重试 返回相同结果;hash、归属、状态或身份不匹配返回冲突。 4. HTTP ACK 响应丢失时 App 保留同一 ACK key 重试。只有后端确认后本地 `acknowledged=true`,UI 显示“后台已授权,等待订单核验”;进程重启继续 ACK, 不能重新采集或领取其他任务。 ### 旧手机人工确认收口 1. 后台任务非空候选上传成功后,App 进入 `WAITING_ADMIN_CONFIRMATION`,不再显示 本地接受/拒绝候选按钮,也不提交旧的 `HUMAN_REVIEW + COMPLETE`。 2. 空候选仍可按现有安全终态回传;独立本地探针不受后台任务模式影响。 3. 前台服务在等待 Admin 时继续 heartbeat、同步 outbox 和拉取命令;租约到期、 取消、认证失效或命令校验失败时保持安全停止。 ## 数据模型 v9: - `order_authorizations` 增加不可变 `command_sha256` 和投递计数/最后投递时间; - `device_order_command_ack_requests` 保存 device、幂等键、请求 hash、command 和时间; - `task_events` 增加 `ORDER_AUTHORIZATION_DELIVERED` 与 `ORDER_AUTHORIZATION_ACKNOWLEDGED`。 存在命令投递/ACK 数据或新事件时 destructive down 必须失败。 ## 验收要点 - [x] 只有原 user/device/execution/generation/token 且租约有效时能拉取命令。 - [x] 首次拉取原子进入 `DELIVERED`;网络重放返回相同 command/hash 且不重复事件。 - [x] App 严格校验并先加密持久化,存储失败不 ACK,重启可恢复同一命令。 - [x] ACK 幂等进入 `ACKNOWLEDGED`,错误 hash/command/设备和并发请求被拒绝。 - [x] 后台候选流程等待 Admin,不再允许手机本地选择并提前完成任务。 - [x] App 等待/已授权状态可见,但不打开拼多多、不选择 SKU/数量、不提交订单。 - [x] v9 migration、Go test/race/vet、Android test/Debug/Release 和根验证通过。 ## 边界 - 不重新打开图片搜索或定位 Admin 选择的商品;属于 T-217。 - 不选择 SKU/数量、进入确认订单页或比较最终金额;属于 T-217。 - 不点击确认购买、读取订单号或处理付款;属于 T-218/T-219。 - 不新增 FCM/WebSocket,不把第三方 URL 当命令,不授权支付。 ## 执行记录 - 2026-07-28:T-215 实现提交 `827afc7` 后领取。复用现有 30 秒前台 heartbeat 和 Keystore-backed 状态,选择 pull + durable-before-ACK,避免 Android 后台推送和 HTTP 不确定结果造成命令丢失。 - 2026-07-28:新增 v9、命令 pull/ACK API、同一授权重放和投递/确认审计事件。 HTTP 集成覆盖错误 hash、响应丢失后的相同 key 重试、ACK 后恢复拉取、事件不重复 及带数据禁止降级。 - 2026-07-28:Android `1.4.13 (18)` 增加命令 canonical hash 校验、加密持久化、 固定 ACK key、重启恢复和等待/已授权 UI。后台任务有候选后不再出现手机接受/ 拒绝入口,也禁止再次采集;空候选仍保留安全终态。 - 2026-07-28:Go/Kotlin 固定哈希向量一致;Android 测试证明存储失败时 ACK 调用数 为 0。`go test ./...`、`go test -race ./...`、`go vet ./...`、Android `test`、 Debug/Release assemble 和根 `.\init.ps1` 全部通过。APK 已构建,未在本任务中 覆盖安装或执行拼多多页面动作。