150 lines
6.4 KiB
Markdown
150 lines
6.4 KiB
Markdown
---
|
||||
|
|
id: T-216
|
|||
|
|
title: 设备下单命令投递、确认与恢复
|
|||
|
|
phase: 2
|
|||
|
|
deps:
|
|||
|
|
- T-215
|
|||
|
|
status: DOING
|
|||
|
|
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 必须失败。
|
|||
|
|
|
|||
|
|
## 验收要点
|
|||
|
|
|
|||
|
|
- [ ] 只有原 user/device/execution/generation/token 且租约有效时能拉取命令。
|
|||
|
|
- [ ] 首次拉取原子进入 `DELIVERED`;网络重放返回相同 command/hash 且不重复事件。
|
|||
|
|
- [ ] App 严格校验并先加密持久化,存储失败不 ACK,重启可恢复同一命令。
|
|||
|
|
- [ ] ACK 幂等进入 `ACKNOWLEDGED`,错误 hash/command/设备和并发请求被拒绝。
|
|||
|
|
- [ ] 后台候选流程等待 Admin,不再允许手机本地选择并提前完成任务。
|
|||
|
|
- [ ] App 等待/已授权状态可见,但不打开拼多多、不选择 SKU/数量、不提交订单。
|
|||
|
|
- [ ] 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 不确定结果造成命令丢失。
|