借款还款同金额回单须经独立复核确认归属后方可核销(09-17-company-loan-principal-repayment)

- 同一出借人同金额的其他批次曾导出或上传回单(含确认失败后已取消的)时,
  最终核销入口要求已复核的归属确认;OCR 匹配与人工回单复核均不能绕过,
  证据不足保持待对账,本金与占用不变;存在候选时失败回单不能释放占用
- 新表 loan_receipt_attributions(迁移 20261006010000):绑定交易指纹、
  回单核验与人工录入证据、当时候选集合;依据类型不含时间吻合;复核人须
  当前有资格且独立于登记人、批次创建人、提交人、导出人、回单上传人、
  人工录入人与出借员工;失效记录在重新登记时标为 superseded,历史只增不改
- 借款回单上传取出借人锁,与核销候选检查串行;串行只保证按当时已知候选
  判断,核销之后才出现的新证据留待晚到结果对账流程处理
- 付款就绪检查反映归属要求;页面登记/复核/失效重登,写明同额分期每期需确认
- 测试:两个错误核销复现改为回归断言;证据绑定、确定性并发两种顺序、
  PC 浏览器完整登记复核重登流程;变异测试均变红

注意:借款功能尚不具备真实还款验收条件(失败回单与对账流程、离职后确认
失败、真实回单样本、手机端弹窗、独立复核待完成);未推送、未部署。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
2026-10-06 20:20:51 +08:00
共同撰写人 Claude Opus 5.5
父节点 b639422211
当前提交 b88790d396
共修改 13 个文件,包含 718 行新增和 31 行删除
@@ -149,7 +149,7 @@ async function claimVerifiedReceipts(tx, config, batch, { readOnly = false } = {
});
requireFact(existingAliases.every((alias) => alias.claim_fingerprint === match.fingerprint && alias.key_id === keyId),
"receipt_transaction_reused");
claims.push({ match, exists: existing.length > 0, existingAliases });
claims.push({ match, options, exists: existing.length > 0, existingAliases });
}
requireFact(new Set(claims.map(({ match }) => match.fingerprint)).size === claims.length,
"receipt_transaction_reused");
@@ -160,7 +160,9 @@ async function claimVerifiedReceipts(tx, config, batch, { readOnly = false } = {
aliasOwners.set(alias, match.item_id);
}
}
if (readOnly) return;
if (readOnly) return claims.map(({ match, options }) => ({ itemId: match.item_id, fingerprint: match.fingerprint, keyId: match.key_id,
verificationIds: [...new Set(options.map((option) => option.verification_id))],
manualProposalIds: [...new Set(options.map((option) => option.manual_proposal_id).filter(Boolean))] }));
for (const { match, exists, existingAliases } of claims) {
if (!exists) {
await tx.paymentReceiptClaim.create({ data: {