借款对账 S1、S2:导出与银行提交分开登记、确认失败收紧及失败回单识别口径(09-17-company-loan-principal-repayment)

S1(迁移 20261007010000)
- 借款导出逐次记录;存量批次保留已知导出次数为历史汇总,不视为真实导出,存在即拦截
- 银行提交登记(只能撤回一次)与提交清单确认;独立复核,复核后任何登记、撤回、导出即失效
- 确认失败只计未作废附件;作废附件无法证明为明确失败或确属他笔付款即阻断
- S3 前临时拦截:多次导出、多次提交、清单未复核或历史汇总均不能确认失败
- 确认失败改按付款后规则;确认人不得兼任导出、提交登记、清单确认

S2(迁移 20261007020000)
- 存在同金额候选时须有已复核、覆盖提交清单的失败归属;结算与失败归属分槽

失败回单识别口径(用户同意并入)
- 新增原始状态措辞字段(多字段原文不一致即为空);失败事实严格判定:单页完整、
  仅“交易失败/转账失败/付款失败/支付失败”、唯一问题为交易失败、字段齐全;
  已撤销、已冲正、已退汇不接受;顶层识别状态与成功匹配不变
- 测试改走真实识别与归一化链,仅替换云端 OCR 响应

验证:借款 HTTP 41 组、浏览器 7 组、76 迁移;server 4463;共享回单测试 237;
三轮同模型只读复核的阻断项均已修复。

注意:界面未完成,不能单独部署;真实回单待验。未推送、未部署。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
2026-10-07 11:47:24 +08:00
共同撰写人 Claude Opus 5.5
父节点 3fb33fe93c
当前提交 db28706177
共修改 16 个文件,包含 986 行新增和 56 行删除
+88
查看文件
@@ -737,6 +737,10 @@ model Employee {
loan_receipt_attributions_proposed LoanReceiptAttribution[] @relation("LoanReceiptAttributionProposer")
loan_receipt_attributions_reviewed LoanReceiptAttribution[] @relation("LoanReceiptAttributionReviewer")
payment_order_exports_made PaymentOrderExport[]
loan_bank_submissions_registered LoanBankSubmission[]
loan_submission_attestations_proposed LoanBankSubmissionAttestation[] @relation("LoanSubmissionAttestationProposer")
loan_submission_attestations_reviewed LoanBankSubmissionAttestation[] @relation("LoanSubmissionAttestationReviewer")
@@map("employees")
}
@@ -1977,6 +1981,10 @@ model PaymentBatch {
@@index([payment_account_id], map: "idx_payment_batches_payment_account")
@@index([payment_order_last_exported_by], map: "idx_payment_batches_order_exported_by")
loan_receipt_attributions LoanReceiptAttribution[]
payment_order_exports PaymentOrderExport[]
loan_legacy_export_summary LoanLegacyExportSummary?
loan_bank_submissions LoanBankSubmission[]
loan_bank_submission_attestations LoanBankSubmissionAttestation[]
@@map("payment_batches")
}
@@ -3994,6 +4002,8 @@ model LoanReceiptAttribution {
key_id String @db.VarChar(64)
verification_ids Json
manual_proposal_ids Json
kind String @default("settlement") @db.VarChar(16)
covered_submission_ids Json @default("[]")
competitor_batch_ids Json
basis_kind String @db.VarChar(32)
basis_note String @db.VarChar(1000)
@@ -4013,3 +4023,81 @@ model LoanReceiptAttribution {
@@index([payment_batch_id, fingerprint], map: "loan_receipt_attributions_batch_fingerprint_idx")
@@map("loan_receipt_attributions")
}
// Loan bank-result reconciliation S1 (migration 20261007010000_loan_bank_submissions).
// Append-only/one-way history triggers and partial unique index live in the migration.
model PaymentOrderExport {
id String @id @default(uuid()) @db.Uuid
payment_batch_id String @db.Uuid
sequence Int
exported_at DateTime @default(now())
exported_by String @db.Uuid
file_sha256 String @db.VarChar(64)
customer_serials Json
total_amount Decimal @db.Decimal(14, 2)
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict)
exporter Employee @relation(fields: [exported_by], references: [id], onDelete: Restrict)
submissions LoanBankSubmission[]
@@unique([payment_batch_id, sequence], map: "payment_order_exports_batch_sequence_key")
@@map("payment_order_exports")
}
model LoanLegacyExportSummary {
payment_batch_id String @id @db.Uuid
known_export_count Int
last_exported_at DateTime?
last_exported_by String? @db.Uuid
unknown_facts String @default("per_export_detail,file_digest,bank_submission_mapping") @db.VarChar(200)
created_at DateTime @default(now())
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict)
@@map("loan_legacy_export_summaries")
}
model LoanBankSubmission {
id String @id @default(uuid()) @db.Uuid
payment_batch_id String @db.Uuid
export_id String? @db.Uuid
channel String @db.VarChar(32)
submitted_at DateTime
bank_instruction_ref String @db.VarChar(120)
customer_serial String? @db.VarChar(64)
note String @db.VarChar(1000)
status String @default("active") @db.VarChar(16)
withdrawn_reason String? @db.VarChar(500)
withdrawn_at DateTime?
registered_by String @db.Uuid
registered_at DateTime @default(now())
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict)
export PaymentOrderExport? @relation(fields: [export_id], references: [id], onDelete: Restrict)
registrar Employee @relation(fields: [registered_by], references: [id], onDelete: Restrict)
@@index([payment_batch_id, status], map: "loan_bank_submissions_batch_idx")
@@map("loan_bank_submissions")
}
model LoanBankSubmissionAttestation {
id String @id @default(uuid()) @db.Uuid
payment_batch_id String @db.Uuid
submission_ids Json
all_submission_ids Json
export_ids Json
note String @db.VarChar(1000)
status String @default("proposed") @db.VarChar(16)
proposed_by String @db.Uuid
proposed_at DateTime @default(now())
reviewed_by String? @db.Uuid
reviewed_at DateTime?
review_note String? @db.VarChar(500)
superseded_at DateTime?
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict)
proposer Employee @relation("LoanSubmissionAttestationProposer", fields: [proposed_by], references: [id], onDelete: Restrict)
reviewer Employee? @relation("LoanSubmissionAttestationReviewer", fields: [reviewed_by], references: [id], onDelete: Restrict)
@@map("loan_bank_submission_attestations")
}