diff --git a/T-218.-.md b/T-218.-.md new file mode 100644 index 0000000..0f8f931 --- /dev/null +++ b/T-218.-.md @@ -0,0 +1,158 @@ + +> 同步来源:[`docs/tasks/T-218.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-218.md) · commit `afc651f75a3a` + +--- +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`,未点击下单或付款。因没有受控收货地址, + 未执行真实订单提交。