--- id: T-208 title: 三闸门提交围栏与结果调和 API phase: 2 deps: [T-207, T-210] status: TODO created: 2026-08-04 vikunja_task_id: 47 context_ref: cd47d0c work_branch: task/t-208-submission-fence needs_device: false needs_human_review: false write_paths: - docs/tasks/T-208.md - admin/migrations/00008_submission_fence.sql - admin/internal/migrations/migrations_test.go - admin/internal/domain/submission.go - admin/internal/domain/submission_test.go - admin/internal/submission/** - admin/internal/taskclaim/** - admin/internal/taskdetail/** - admin/internal/server/submission_fence.go - admin/internal/server/submission_fence_test.go - admin/internal/server/router.go - admin/internal/transport/webui/webui.go - admin/internal/transport/webui/templates/task-detail.html - admin/cmd/server/** - admin/README.md - docs/api.md - docs/04-architecture.md - docs/routes.md - docs/current-state.md --- ## 问题 / 背景 T-205 将保存同一 attempt 的 append-only 三闸门事件和执行摘要,T-210 将开放 Gate1/Gate2/Gate3 三种固定证据资产,T-207 只允许围栏前人工恢复。真实点击“提交订单”前仍缺少服务端唯一提交围栏、首次一次性许可、围栏响应不明处理、点击后一次性结果以及同一 submission 的人工调和。T-208 只实现采购服务端围栏与调和;不连接真机、不点击提交订单、更不付款。 ## 关联需求与交互 - 功能:F-006、F-007、F-011、F-017。 - 用户故事:US-004、US-005、US-008。 - 交互:IX-009、IX-011;围栏由后台申请,Web 只显示和调和同一 submission。 - 依赖:T-207、T-210;显式保留 T-210 作为三证据类型安全门禁。T-207 传递 T-205/T-302,T-210 传递 T-107/T-204/T-302。 - API:POST /api/v1/purchase-attempts/{aid}/submission-fence、POST /api/v1/order-submissions/{sid}/result、POST /order-submissions/{sid}/reconcile。 - 后续消费者:T-401 单趟真实提交。 ## 方案 1. 新增 `00008_submission_fence.sql`,保存 fence request 幂等事实、唯一 order_submission proof、三个 gate evidence 外键、一次性 result request、append-only 人工调和记录,以及冻结的 device/session/claim generation/claim token hash 归属。authorization 与 attempt 各最多一个 submission;降级遇到新事实原子拒绝。 2. 设备认证必须在读取 body 前完成;严格 JSON 请求绑定 task/version、authorization、attempt、session、generation、claim token、最后事件序号、规格、两次单价、数量、确认页金额、精确 submit_control_text/match_count 和三个 evidence asset id,不接受未知字段、自由页面文本、XML、路径、selector、坐标或动作脚本。 3. `operation=CREATE_ONCE` 才可能创建围栏。事务必须先取得 SQLite 写线性化位置并再次确认设备 ACTIVE,再复用 T-302 的 current claim validator:设备/session/task/authorization/attempt/generation/token 全部一致,claim lease 与 authorization 严格未过期,task/attempt 为 ORDERING,authorization 为 CLAIMED。当前 validator 不得为结果上报放宽。 4. 围栏事务逐字段比对请求、T-205 append-only 事件、purchase attempt summary 与 T-210 evidence assets。三个资产必须精确为 SKU_PANEL_GATE_1、SKU_PANEL_GATE_2、ORDER_CONFIRM_GATE_3,属于同一 task/attempt/device;服务端只验证 kind/归属/hash 事实,不解析图片内容。 5. 服务端重新执行三闸门:颜色/尺码精确等于授权;Gate1/Gate2 为规范十进制正金额且完全相等;Gate1 × 授权数量不超 total_price_cap;数量精确相等;Gate3 规格/数量一致且确认页总额不超上限;submit_control_text 精确等于“提交订单”,可点击祖先计数精确为一。任一缺失、歧义、顺序错误或冲突都零围栏、零许可。 6. 首次 CREATE_ONCE 在一个事务中创建唯一 submission,并把 authorization CLAIMED→FENCED、attempt ORDERING→FENCED,使 task 永久不可重领。只有这次首次明确成功响应可含 `click_permitted=true`;许可不是数据库中可重取的开关,也不能由业务代码手工构造。 7. 同 fence_key 同规范 proof 的任何重放都返回同一 submission_id,但必须 `click_permitted=false`、`reconciliation_required=true`;同 key 异 proof、不同 key 命中同 authorization/attempt、并发失败或服务重启都绝不再次许可。 8. `operation=LOOKUP_ONLY` 只用于围栏响应不明后的同键调和:永远不创建 submission、永远不许可点击。找到原围栏时返回同一 submission 并保持/转入调和;找不到时明确无围栏事实,但恢复路径仍不得重新申请首次点击许可。 9. 围栏后的 result 不再要求活跃租约,也不得放宽 current claim validator;新增独立 fenced ownership validator,只按 order_submission 冻结的 device/session/generation/token hash 验证原设备归属。它不能创建、续租、释放或转领 claim。 10. 每个 submission 只接受一条一次性 result 事实。SUBMITTED 转 WAITING_PAYMENT 并永久消费授权;EXTERNAL_PAYMENT_HANDOFF、SECURITY_CHALLENGE、UNKNOWN 转 RECONCILIATION_REQUIRED 并保持围栏。相同 result_key 同载荷稳定重放,不同 key/observation 不得覆盖。没有 retry observation。 11. 本任务从 result wire 移除旧 `evidence_asset_id`。首次真实提交后的页面尚未经本项目真机取证,不新增 SUBMISSION_RESULT kind;UNKNOWN 允许无截图。绝不把 Gate3 图冒充结果图,绝不上传外部支付页、支付凭据或完整 XML。后续结果截图必须另立取证和 evidence kind 任务。 12. 管理调和只处理指定 submission,结论固定为 CONFIRMED_CREATED、CONFIRMED_NOT_CREATED_OR_CANNOT_COMPLETE、STILL_UNKNOWN;记录 reviewer、固定结论、服务端时间和幂等键,不触发设备动作。前两种形成最终人工结论后 authorization 永久 CONSUMED;STILL_UNKNOWN 保持围栏和待调和。 13. T-206 详情显示同一 submission 的 proof、首次许可事实、result 和调和记录。任何围栏后页面都没有重置、取消、重新授权、重新领取、再次提交或普通重试入口;WAITING_PAYMENT 仍明确系统尚未付款。 ## 验收要点 - 两设备、同设备、同/异 key 并发中最多一个首次响应拥有 `click_permitted=true`;事务回滚时零 submission/状态变化。 - 覆盖首次成功、同键重放、响应截断、服务重启、异载荷/异 key 冲突;所有非首次路径 click_permitted 都为 false。 - LOOKUP_ONLY 找到/未找到、并发和重启都零创建、零许可;围栏响应不明不通过新 key 或恢复模式获得许可。 - 覆盖错误/缺失/跨 task/attempt/device 的三类 evidence、事件缺口/乱序、summary 冲突,以及规格、数量、金额、上限、提交文本和 match count 全部负例。 - 覆盖 current claim validator 的 lease/authorization 边界;静态和运行测试证明 fenced ownership validator 只验证冻结归属,不能被用于领取、续租、围栏创建或释放。 - 覆盖 fence 与 T-207 reset/cancel 并发;任一 submission 一旦存在,所有 authorization 状态和人工结论下都永久拒绝释放。 - 覆盖四种 result、同键重放、异 observation 冲突、UNKNOWN 无截图,以及三种人工调和结论;任何结果均不开放第二次点击。 - Migration 覆盖升级/重开、唯一/FK/trigger、已有安全事实、降级拒绝和 foreign_key_check。 - 静态检查没有 client/PDD selector、通用点击、submit_order_once、结果截图 kind、支付、免密支付、先用后付或扣款能力。 - `go test ./...`、`go test -race ./...`、`go vet ./...`、`go build ./...`、完整 init、上下文、Vikunja export 与 diff-check 通过。 ## 执行记录 ### 2026-08-06T02:58:55Z · ila 2026-08-06T02:58:54.857Z · Codex 2026-08-06 合并式最终提交面板契约:submission-fence 请求使用 gate3_submit_amount、完整 submit_control_text、match_count 与 nearest_clickable_ancestor_unique;不得继续使用 confirm_page_amount/confirm_amount。服务端要求 Gate3 严格等于 Gate2 且不超上限。 ## 边界 - 2026-08-06 起 Gate3 是合并式最终提交面板唯一结构化最终控件金额;围栏请求/存储必须使用 `gate3_submit_amount`、完整控件文本、匹配数和最近可点击祖先唯一布尔值。旧 `confirm_page_amount` / `confirm_amount` 不得继续消费;T-210 迁移未完成时禁止建立围栏。 - `CREATE_ONCE` 建立围栏时必须复用 T-302 的 **current claim validator**,严格要求当前设备、session、 task、authorization、attempt、generation、claim token、未过期 lease/authorization 与当前状态 全部一致。不得为了让围栏或结果流程跑通而放宽、复活或绕过该 validator。 - 围栏后的 result 必须使用独立 **fenced ownership validator**,只验证 order_submission 已冻结的 device/session/generation/token hash 归属。它不得被用于领取、续租、创建第二个围栏、释放、转领, 也不得反向削弱 current claim validator。 - 只有首次 `CREATE_ONCE` 原子创建唯一 submission 的明确成功响应可以包含 `click_permitted=true`。同 key 重放、异 key 冲突、并发输家、服务重启、响应不明恢复及任何 `LOOKUP_ONLY` 都必须 `click_permitted=false`;许可不得作为可重取数据库开关或手工构造对象。 - `LOOKUP_ONLY` 永远零创建、零许可。围栏响应不明时不得换 key、再次申请首次许可或凭页面猜测点击; 找到原围栏只能调和同一 submission,找不到也只能按围栏前安全失败处置。 - 围栏事务必须逐字段比对请求、T-205 append-only events/summary 和 T-210 三类 evidence 资产, 并在服务端重算三闸门与精确唯一“提交订单”控件。缺失、歧义、跨 task/attempt/device、价格/规格/ 数量冲突或事件乱序均零围栏、零许可。 - 本任务不新增 `SUBMISSION_RESULT` 或其他结果截图 kind,并从 result wire 移除旧 `evidence_asset_id`。首次真实提交后的页面尚未完成本项目真机取证;`UNKNOWN` 允许无截图,不得把 Gate3 图片冒充结果图,外部支付页、支付凭据和完整 XML 永不上传。 - 任一 submission 建立后,authorization、attempt 和 submission 永久不可释放、重置、取消、重新 授权、重新领取或再次点击。result 或人工调和只能收紧到 `WAITING_PAYMENT` / `FAILED` / 保持 `RECONCILIATION_REQUIRED`,绝不能回到围栏前状态。 - 本任务只实现服务端围栏、一次性结果和同一 submission 调和;不修改 client/PDD 页面判据,不连接 真机,不实现 `submit_order_once()`,不点击“提交订单”,不编写支付、免密支付、先用后付或扣款能力。