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

6.4 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-216 设备下单命令投递、确认与恢复 2
T-215
DOING 2026-07-28 827afc7 null
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:

{
  "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 不确定结果造成命令丢失。