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

156 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: T-218
title: 单次订单提交与订单回读
phase: 2
deps:
- T-217
status: DONE
created: 2026-07-28
context_ref: 1e87b62
work_branch: null
write_paths:
- docs/tasks/T-218.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
- 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-217 已在手机和后端保存同一授权的 READY dry-run,并停在拼多多确认订单页。本任务
消费该 READY 一次:确认地址和付款边界安全后最多点击一次“提交订单”,随后只读回到
订单列表,以订单号和下单时间完成对账。
“HTTP 请求失败”“App 进程退出”和“页面没有及时跳转”都不能被解释为未提交。服务端
一旦发出单次提交围栏,App 在任何恢复路径都只能对账或转人工,不能再次点击。系统
只创建待付款订单,不选择、确认或执行支付。
## 关联需求与交互
- `FR-21`:Admin 授权绑定不可变任务、candidate 和人工 review。
- `FR-22`:一个授权最多创建一个拼多多订单,结果不确定时先对账。
- `US-010` / `IX-011`:设备只提交一次、回读订单号和时间,Admin 付款提醒由
T-219 完成。
## 冻结合约
### 提交前置条件
1. App 本地加密 READY 与后端 `order_dry_runs.status=READY` 必须引用同一 task、
execution、generation、authorization、command hash、SKU、数量、单价、总额和
evidence hash;租约有效、任务未取消且后端在线。
2. 每次不可逆动作前重新读取当前确认订单页。标题锚点、颜色、尺码、数量、商品金额/
应付总额必须继续满足 T-217;页面必须能确定性看到已配置的收货地址。出现
“添加收货地址”“请选择地址”、地址缺失或地址不可判定时停止。
3. App 不修改地址、优惠券、运费、服务或付款方式。最终动作必须是唯一、可见、启用
且文本精确为“提交订单”的无障碍节点;出现“立即支付”“确认支付”“免密支付”
“先用后付”或其他自动扣款含义时停止。OCR 只读结果不能产生点击。
4. App 先用幂等请求从后端创建 `order_submission`。服务端事务内把该 authorization
永久置为可能已消费并返回唯一 submission ID。响应不确定时只查询/重放同一请求,
不生成新的 ID。
5. App 收到 submission ID 后,先在 Keystore-backed 状态保存
`CLICK_ASSUMED`,再调用唯一的 `submitOrderOnce` accessibility 动作。保存成功即
认为点击可能已发生;即使实际点击尚未发出就崩溃,恢复后也只能对账或转人工。
### 单次动作与恢复
1. 一个 authorization、dry-run 和 submission 都有数据库唯一约束;提交 start
的同 key/body 重放返回原记录,不同 body 或第二个 submission 返回冲突。
2. accessibility 层不得暴露通用文本点击或坐标点击给订单提交状态机。
`submitOrderOnce` 只接受当前 submission fence,并在进程内原子消费;它不能点击
支付、验证码、风控、地址或未知页面。
3. 点击前、点击返回后、HTTP 超时、网络断开、进程重启或页面未知均不允许第二次
`submitOrderOnce`。本地状态只有
`READY -> CLICK_ASSUMED -> RECONCILING -> RECONCILED/MANUAL_REVIEW` 单向迁移。
4. 点击后若进入收银台/支付页,只允许系统返回或只读导航到订单列表;不点击任何付款
控件。验证码、风控或系统权限页立即转人工。
### 订单回读与唯一对账
1. App 在受控预算内进入当前拼多多账号的订单列表/待付款列表,只读取最近订单,不
滚动无界历史。对每个可见项读取订单编号、平台显示的下单时间、状态、标题、SKU、
数量和金额;WebView 语义树折叠时可用端上 OCR 只读降级。
2. 可归属订单必须同时满足:待付款状态、标题有界锚点一致、颜色/尺码唯一匹配、数量
和总额精确一致、下单时间位于服务端 `fenced_at` 前后有界窗口。订单编号必须来自
明确“订单编号”标签,不能把物流号、商品 ID 或页面其他数字当成订单号。
3. 只有一个订单满足全部条件时,App 上传受控截图并幂等回传订单号、平台下单时间、
订单字段和 evidence。后端将 submission 置为 `RECONCILED`、authorization 置为
`CONSUMED`、execution 的 `order_submitted=true`,并写审计事件。
4. 零个或多个可能订单、字段缺失/冲突、时间无法解析、订单状态不是待付款或证据上传
失败时进入 `MANUAL_REVIEW`/`RECONCILING`。这不是可重试提交错误;后续只能重新
读取订单列表或由人员对账。
5. 订单号属于受限业务数据:只保存到鉴权后端和 App 加密状态,不写普通日志、不发给
VLM、不进入截图文件名。T-219 才在 Admin 详情显示待付款提醒。
## 数据模型与 API
v11:
- `order_submissions`:每个 authorization/dry-run 唯一一行,保存 submission ID、
command/dry-run hash、`fenced_at`、状态、预期 SKU/数量/金额、回读订单号/时间、
evidence 和错误码;
- `device_order_submission_requests`:按 device + operation + idempotency key 保存
请求 hash 和稳定结果;
- task events 增加 `ORDER_SUBMISSION_FENCED`、`ORDER_SUBMISSION_RECONCILED` 和
`ORDER_SUBMISSION_MANUAL_REVIEW`;
- `order_authorizations` 增加不可逆消费中/已消费状态;`execution_outcomes` 仅在
唯一对账成功后允许 `order_submitted=true`。
设备接口:
- `POST /api/v1/tasks/{task_id}/order-submissions/start`
- `POST /api/v1/tasks/{task_id}/order-submissions/{submission_id}/reconcile`
- `POST /api/v1/tasks/{task_id}/order-submissions/{submission_id}/manual-review`
所有写接口都要求 BUYER bearer、原 claim token、execution/generation、command 和
dry-run 身份及 `Idempotency-Key`。start 只接受 READY;reconcile/manual-review
只接受同一已经围栏的 submission。
## 验收要点
- [x] start 只有 READY、有效租约、未取消和完全一致的命令/金额可成功。
- [x] 同一授权最多一个 submission;响应丢失、重启和重复请求不能产生第二次点击。
- [x] 缺少地址、自动扣款语义、非精确“提交订单”或页面不确定时 fail closed。
- [x] 点击前持久化 `CLICK_ASSUMED`;恢复路径只有订单回读,没有重新提交。
- [x] 订单列表只读回读必须唯一匹配订单号、下单时间、SKU、数量、金额和待付款状态。
- [x] 零个/多个匹配转人工且仍禁止重新提交;证据和事件可审计。
- [x] 代码路径不包含支付点击,订单号不进日志/VLM。
- [x] v11 migration、Go test/race/vet、Android test/Debug/Release 和根验证通过。
- [x] 真机至少证明缺地址/危险付款语义会阻塞;只有具备受控地址和明确待付款语义时
才允许执行真实单次提交 smoke。
## 边界
- 不自动选择或修改收货地址、优惠券、运费、服务和付款方式。
- 不点击“立即支付/确认支付/免密支付/先用后付”,不保存支付凭证。
- 不用 VLM、OCR、旧 ordinal、坐标或模糊文本决定不可逆点击。
- 不因未找到订单而重按提交;人工付款提醒和 Admin 展示属于 T-219。
## 执行记录
- 2026-07-28:在 T-217 实现提交 `1e87b62` 后领取。冻结服务端一次性提交围栏、
点击前 `CLICK_ASSUMED` 持久化、恢复只对账、订单列表唯一匹配和自动付款禁区。
- 2026-07-28:合约提交 `682cc73`。实现 v11 submission/request 表、三条设备接口、
服务端唯一围栏和幂等对账;Android 加入确认页安全检查、加密恢复状态、单次点击
资格、待付款订单解析和有限唯一匹配。
- 2026-07-28:补强崩溃窗口:只有本次调用新建的 intent 可在收到围栏后获得点击
资格;恢复出的旧 intent、`CLICK_ASSUMED` 及其后状态永远不能重新点击。围栏已在
后端建立时,租约到期只允许继续订单列表对账。
- 2026-07-28:`go test ./...`、`go test -race ./...`、`go vet ./...`,
Android Debug/Release 单元测试和 APK 构建、根 `.\init.ps1` 全部通过。
- 2026-07-28:Debug `1.4.15 (20)` 真机阻断 smoke 通过。拼多多 8.17 确认页显示
“手动添加收货地址”“立即支付”和“先用后付”,肉包明确报告未创建提交围栏;
触发前后生产库 `order_submissions=0`,未点击下单或付款。因没有受控收货地址,
未执行真实订单提交。