借款对账 S3:银行终态确认与释放(09-17-company-loan-principal-repayment)

迁移 20261008010000
- 银行终态证据单独成表(不进 OCR、不改付款状态、不计回单),只能作废一次,
  作废即令依赖事实失效;复核人可凭理由查看(审计、SHA-256 校验)
- 每次银行提交一条终态事实:书面终态证据,或流水号等于指令号的严格失败回单;
  同一指令的终态可了结该指令的处理中回单,其他提交不能代替
- 新终态 confirmed_not_paid;释放须清单已复核未变、每次提交有成立的终态事实、
  导出均关联提交、回单均有解释、等待期满、无历史汇总
- 最终取消在同一事务内重验(与新提交、晚到回单串行);两条释放路径都检查等待期
- 防挪用:指令号跨批次唯一(含撤回)、失败回单绑定本批次指令、一笔失败交易
  只支撑一个批次、同一书面证据不能用于两个批次、登记提交即离开“未发送”

验证:借款 HTTP 57 组、浏览器 7 组、77 迁移;server 4472;四轮同模型只读复核
阻断项均已修复,资金守卫变异测试均变红。

注意:界面未完成,不能单独部署;真实回单待验;补证后的恢复缺口尚未设计。
未推送、未部署。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
2026-10-07 16:50:17 +08:00
共同撰写人 Claude Opus 5.5
父节点 db28706177
当前提交 14aee82d5c
共修改 12 个文件,包含 1299 行新增和 81 行删除
@@ -1,3 +1,67 @@
## 2026-10-07 S3 本地实施记录(用户批准本地实施、针对性验证与只读复核,并要求纳入三条补充要求;未提交、未推送、未部署、未查询生产)
迁移 `20261008010000_loan_bank_terminal_facts`:
- `loan_bank_terminal_evidences`:银行终态证据(银行书面回复、网银指令查询官方 PDF 等)**单独成表**,与付款回单分开。它不进入 OCR,上传不改变付款状态,不计入回单核验数量和作废回单判定。证据只能作废一次;作废后,依赖它的终态事实在同一事务内被取代。付款回单无法改成这一类型,因此不能借更换类型来解除成功或处理中回单的阻断。
- `loan_bank_terminal_facts`:每次银行提交至多一条待复核或已通过的终态事实,分两类——
- `termination_evidence`:终态为已拒绝、已撤销或已失败;
- `failure_receipt`:严格判定的失败回单,且回单流水号必须等于该提交登记的指令号。
`resolves_attachment_ids` 用于列出由这条终态事实了结的处理中回单。这些回单必须是本批次付款的单页处理中回单,且流水号等于该提交的指令号;其他提交的终态事实不能代替。终态事实只增不改,复核人须独立。
- 还款状态新增 `confirmed_not_paid`:终态,必须带依据,可取消释放。完整性函数和历史保护函数原样重建,只加入这个状态。
规则:
- 银行指令号登记时规范化(NFKC、去空白、大写)保存,且同一指令号不能同时登记在两个借款批次。
- 释放条件 `assertNotPaidReleasable`,以下全部满足才可释放:
- 无历史汇总;
- 提交清单已复核且至今未变;
- 每次有效提交都有已复核、当前仍成立的终态事实;
- 每条导出记录都被某次提交关联;
- 本批次每份付款回单要么被终态事实覆盖,要么是能证明为明确失败或他笔付款的作废回单;
- 距最后一次导出已满内部等待期(默认 3 天,可配置)。
- 经独立复核人确认后,状态转为 `confirmed_not_paid`。
- 内部等待期适用于全部释放路径:确认未付款、单次失败确认,并在最终取消时重验(2026-10-07 复核补:单次失败路径原先漏了这一项)。
- **最终取消时在同一事务内重验**:`confirmed_not_paid` 重跑上述全部条件,`confirmed_failed` 重跑单次失败路径的全部条件。取消、上传回单、登记提交都要先锁批次,因此三者串行执行。
- 在失败或未付款结论之后上传的晚到回单,仍会被记录(状态不变),并因此阻断释放;释放之后才出现的新证据,留给晚到结果对账流程处理。
- 单次失败路径同时补上导出关联:有导出记录时,提交必须关联该导出(`loan.export_unlinked`)。
- 新增 8 个原因码,三处都已登记;新增接口包括证据的上传与作废、终态事实的登记与复核、确认未付款(复核人操作)。
页面:仍未实施,留待 S8。**不得单独部署启用。**
验证与复核:
- 首轮针对性验证:HTTP 49 组、浏览器 7 组、77 迁移;11 个资金守卫变异均变红;server 4471 等通过。
- 同模型只读复核:BLOCK,三项(前两项经复核人副本探针实测),均已修复:
- B1 已登记银行提交但状态仍为 `not_sent` 时,可单人直接取消释放:登记提交时同一事务内转为 `unknown`;取消路径的 `not_sent` 分支另加防线,只要存在任何提交(含已撤回)、历史汇总或终态证据就拒绝。
- B2 `failure_receipt` 终态事实绕过同金额失败归属、未做交易唯一性检查,同一笔失败交易可能分别让两个批次释放:
- 失败回单分支复用单次失败路径的检查(不得被认领为付款、密钥一致);
- 存在同金额候选时,须有已复核、只覆盖该次提交的失败归属。归属新增 `failure_scope`(batch/submission),唯一性改为“批次+类型+覆盖提交”;
- 新增只增不改的 `loan_failure_evidence_uses`:一笔失败交易只能作为一个批次的依据,单次失败路径和终态事实路径都在此登记,复用返回 `loan.failure_evidence_reused`。
- B3 终态证据无法查看:新增 `GET …/loan-terminal-evidences/:id/file`,财务或合格的独立复核人可读,须填写理由并写审计,返回前校验 SHA-256,不缓存。
- 同轮非阻断修复:
- 结论已定或批次已释放后,导出、执行由 500 改为明确的 409;
- 释放后不得作废证据;
- 同一批次内指令号也须唯一。
- 第二轮同模型只读复核:BLOCK 一项(副本探针实测),B1、B3 修复成立。
- **N1**:单次失败路径不比对失败回单流水号与本批次指令号;只登记了提交的批次也不算同额候选,于是 A 可凭 B 指令的失败回单确认失败并释放。已修:
- 单次失败路径(确认与最终取消均调用 `assertFailureFacts`)及批次级失败归属,都要求失败回单流水号等于唯一已确认提交的指令号(指令号跨批次唯一);
- 同额候选新增“存在任何提交登记(含撤回)”。
由于有了指令号绑定,跨批次复用失败回单在归属阶段就被拦下,`loan_failure_evidence_uses` 登记表退为纵深防御(测试断言 A 已登记用量)。
- 同轮修复的非阻断项:
- 归属唯一索引与查重加入 `failure_scope`,批次级与提交级可并存,并加 CHECK:提交级必须是失败类、只覆盖一次提交;
- 单次失败路径按覆盖提交精确查找批次级归属;
- uses 表纳入回单密钥一致性检查;
- 证据查看理由改由请求头 `x-access-reason`(URL 编码)传递,不进入访问日志。
- 新增反例:其他批次指令的失败回单走单次路径被拒,且“只登记提交”的批次算作候选;批次级与提交级失败归属并存。
- 第三轮同模型只读复核(N1 修复):PASS_WITH_NOTES,无阻断;四条路径(单次失败、批次级与提交级失败归属、failure_receipt 终态事实)绑定一致。按备注补两处纵深:
- 指令号跨批次唯一改为不论状态:撤回后也不能在其他批次重新登记;
- 同一份银行书面证据(按 SHA-256)不能上传到两个批次。
记录:
- 候选范围扩大(只登记提交也算)的代价是同额历史批次让后续批次持续需要归属确认,方向安全,不放宽;
- **回单“流水号/凭证号”与网银指令号是否同一编号、两者如何对应,须用真实建行回单在 S0 核实**。若不同,受影响的是需要绑定的路径——失败回单(单次失败路径、失败归属、`failure_receipt` 终态事实)以及处理中回单的了结,这些路径会被阻断(安全但不可用);只凭银行书面终态证据(`termination_evidence`)、本批次又没有需要了结的处理中回单时,不要求两个编号相等,不受影响。归属不能靠财务照抄回单号来证明;绑定只是机器校验,实际证明仍依赖银行证据、同额候选归属和独立复核。
- 部署前注意(存量):生产若已有在 S1 之前确认失败、尚未取消的借款批次,按新规则取消时会重跑 S1 条件(单次已复核提交、指令号绑定等),可能需要补登提交并复核;生产是否存在此类数据待 S0 核实。
- 记录在案(未改):
- `confirmed_not_paid` 之后若证据作废、出现晚到回单或新提交,批次保持占用(方向安全)。其中“钱确实没付、只需补证后重新复核”的情况,会被“只有 `unknown` 状态才能登记终态事实”拦住;S6 只处理晚到成功,覆盖不了这种情况。**这是尚未设计的恢复缺口**,需另行设计恢复路径,不由 S6 负责;
- 等待期默认 3 天,可通过配置 `loanReleaseWaitDays` 调整,尚未接入环境变量;只有手工提交、没有任何导出时,等待期不生效(按设计“距最后一次导出”计算)。
## 2026-10-07 S1、S2 本地实施记录(用户 2026-10-06 批准本地实施、针对性验证与只读复核;未提交、未推送、未部署;未做 S0 生产查询)
S1(迁移 `20261007010000_loan_bank_submissions`):
@@ -57,7 +57,7 @@
- [ ] AC8:迁移与回退影响、独立只读复核、恢复风险处置和发布前条件完整;本地合成通过不替代真实银行接受性或部署,真实录入/付款另批。
- [ ] AC9(对账,design 3.1/3.3):制单导出与银行提交尝试分别落表并关联;提交清单须经独立复核;确认失败只计未作废附件,但作废的成功或处理中回单仍按事实阻断;确认失败按批次冻结快照核对,出借员工离职不阻断;一条指令的终态不能推定其他提交已结束。
- [ ] AC10(失败归属,3.1):存在同金额候选时,确认失败须有已复核、写明覆盖哪些指令的失败归属;未确认时保持占用。
- [ ] AC11(银行终态释放,3.2):每次已登记提交均有能唯一定位指令、经独立复核的终态证据,且无未解决的成功或处理中事实,才可转 `confirmed_not_paid` 并释放;对账单、截图、时间依据与等待期均不能单独放行;释放后禁止再导出。
- [ ] AC11(银行终态释放,3.2;2026-10-07 补充:同一指令的终态事实可了结该指令此前的处理中回单,其他提交不能代替;最终取消释放的事务须重验全部条件,并与新提交、晚到回单串行;终态证据与付款回单分开存放,作废或被替换后不能继续作为依据):每次已登记提交均有能唯一定位指令、经独立复核的终态证据,且无未解决的成功或处理中事实,才可转 `confirmed_not_paid` 并释放;对账单、截图、时间依据与等待期均不能单独放行;释放后禁止再导出。
- [ ] AC12(人工确认未提交,3.2a,用户 2026-10-06 确认支持):全部证据加两名不同的独立复核人,转 `confirmed_not_submitted`,记录与页面标为人工确认;人员不足或材料缺失时继续占用。
- [ ] AC13(冻结,3.5):冻结按原因记录;拦截新申请及其他在途申请的复核与导出,不拦事实登记;误报经独立复核关闭不补记;证据不确定继续冻结;全部原因关闭才解冻。
- [ ] AC14(晚到成功,3.4):只增不改并标记原结论被推翻;按冻结快照与历史审批核对;覆盖已释放与已确认失败未释放;逐笔先处理占用冲突再按差额结清,余额列待追回;补记不生成可导出指令,整笔超额也完整记录;同一交易不能两次使用。