11 KiB
id, title, phase, deps, status, created, vikunja_task_id, context_ref, work_branch, needs_device, needs_human_review, write_paths
| id | title | phase | deps | status | created | vikunja_task_id | context_ref | work_branch | needs_device | needs_human_review | write_paths | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| T-208 | 三闸门提交围栏与结果调和 API | 2 |
|
TODO | 2026-08-04 | 47 | cd47d0c |
task/t-208-submission-fence | false | false |
|
问题 / 背景
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 单趟真实提交。
方案
- 新增
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;降级遇到新事实原子拒绝。 - 设备认证必须在读取 body 前完成;严格 JSON 请求绑定 task/version、authorization、attempt、session、generation、claim token、最后事件序号、规格、两次单价、数量、确认页金额、精确 submit_control_text/match_count 和三个 evidence asset id,不接受未知字段、自由页面文本、XML、路径、selector、坐标或动作脚本。
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 不得为结果上报放宽。- 围栏事务逐字段比对请求、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 事实,不解析图片内容。
- 服务端重新执行三闸门:颜色/尺码精确等于授权;Gate1/Gate2 为规范十进制正金额且完全相等;Gate1 × 授权数量不超 total_price_cap;数量精确相等;Gate3 规格/数量一致且确认页总额不超上限;submit_control_text 精确等于“提交订单”,可点击祖先计数精确为一。任一缺失、歧义、顺序错误或冲突都零围栏、零许可。
- 首次 CREATE_ONCE 在一个事务中创建唯一 submission,并把 authorization CLAIMED→FENCED、attempt ORDERING→FENCED,使 task 永久不可重领。只有这次首次明确成功响应可含
click_permitted=true;许可不是数据库中可重取的开关,也不能由业务代码手工构造。 - 同 fence_key 同规范 proof 的任何重放都返回同一 submission_id,但必须
click_permitted=false、reconciliation_required=true;同 key 异 proof、不同 key 命中同 authorization/attempt、并发失败或服务重启都绝不再次许可。 operation=LOOKUP_ONLY只用于围栏响应不明后的同键调和:永远不创建 submission、永远不许可点击。找到原围栏时返回同一 submission 并保持/转入调和;找不到时明确无围栏事实,但恢复路径仍不得重新申请首次点击许可。- 围栏后的 result 不再要求活跃租约,也不得放宽 current claim validator;新增独立 fenced ownership validator,只按 order_submission 冻结的 device/session/generation/token hash 验证原设备归属。它不能创建、续租、释放或转领 claim。
- 每个 submission 只接受一条一次性 result 事实。SUBMITTED 转 WAITING_PAYMENT 并永久消费授权;EXTERNAL_PAYMENT_HANDOFF、SECURITY_CHALLENGE、UNKNOWN 转 RECONCILIATION_REQUIRED 并保持围栏。相同 result_key 同载荷稳定重放,不同 key/observation 不得覆盖。没有 retry observation。
- 本任务从 result wire 移除旧
evidence_asset_id。首次真实提交后的页面尚未经本项目真机取证,不新增 SUBMISSION_RESULT kind;UNKNOWN 允许无截图。绝不把 Gate3 图冒充结果图,绝不上传外部支付页、支付凭据或完整 XML。后续结果截图必须另立取证和 evidence kind 任务。 - 管理调和只处理指定 submission,结论固定为 CONFIRMED_CREATED、CONFIRMED_NOT_CREATED_OR_CANNOT_COMPLETE、STILL_UNKNOWN;记录 reviewer、固定结论、服务端时间和幂等键,不触发设备动作。前两种形成最终人工结论后 authorization 永久 CONSUMED;STILL_UNKNOWN 保持围栏和待调和。
- 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(),不点击“提交订单”,不编写支付、免密支付、先用后付或扣款能力。