# 005 规范转账事件与双边归并技术决策 对应 Issue:B-43/B-83/B-114(`docs/issues/006-p1-canonical-transfer-matching.md`)。 ## 分层模型 - 银行源行是不可变观察;匹配层从不修改 `source_rows`。 - 新增“不可变决定日志 + 可重建当前投影”的规范事件层: - `transfer_match_decisions` 等证据表只增不改(UPDATE/DELETE 被触发器拒绝)。 - `current_transfer_decisions` 与 `transfer_observation_claims` 是可更新、可重建的 当前投影,`source_row_id` 主键保证一条观察不可能同时属于两个当前事件。 - B-44 余额计算只读 `eligible_intercompany_events` 视图,只含 `active + current + classification='intercompany'`,且单边事件必须 `locked = 1`(管理员按证据确认)才可计算;paired 事件不受锁定状态限制; 单边内部、待审、同公司调拨、外部事件全部排除。 ## 已确认业务口径(B-114) 1. 单边流水即使双方公司都能由批准账号唯一确认,也先进入未决金额,不进入 B-44 已确认往来余额;只有完成双边归并(`intercompany` + `paired`)或管理员按证据确认 (`intercompany` + locked 单边)后才可计算。 2. 跨日事件经济日期取付款方 outgoing 银行入账时间,与导入顺序无关。 3. 自动窗口 v1:非空参考号相同且账号镜像 <= 3 自然日(M1);无参考号精确账号镜像 <= 1 自然日(M2);别名/个人映射镜像 <= 1 自然日且参考号或摘要相等(M3)。 超窗或同层多候选全部进入人工审核,禁止按行号、导入顺序或名字破平局。 4. 个人过账映射绑定具体账号、生效区间、允许方向和代表公司,必须管理员批准; 姓名只作辅助证据,不能单独定案。 ## 参与方解析 - 本方:源行 `own_account` 精确命中生效期内已批准账户(`own_exact`);缺失时才用 上传时持久化的批准上传账户(`upload_account`),且必须与批次公司一致。冲突进入 审核,不以 `company_id` 或户名反推。 - 对方:严格按账号层解析——精确账号 -> 已审核账号别名 -> 已批准且在窗口内、方向 允许的个人过账映射;每层唯一最佳结果才继续。户名/户名别名只用于一致性/冲突证据, 冲突降级到审核。 - 方向:`expense > 0 且 income = 0` 为 outgoing;`income > 0 且 expense = 0` 为 incoming;双正/双零/负数/缺币种进入 `unresolved`。金额用 `Decimal` 精确相等, 不设手续费容差。 ## 匹配与状态机 - 候选硬门槛:双方工作表 confirmed、方向相反、金额与币种相等、公司端点互为反向。 - M1/M2/M3 唯一候选自动 paired;R1(端点可确认但镜像/参考号证据不足)与 R2(同层 多候选)进入 `needs_review`;待审行不能被其他自动匹配抢占。 - 状态:`unresolved -> internal_single -> matched`、`needs_review -> matched | same_company_transfer | external`;单边确认两端同公司立即为同公司调拨。 - 幂等:rule_version + 当前观察集合 + 证据未变时零写入;locked 人工决定自动重跑 跳过;纠错只能通过 `reverse`(`mode='reversal'` 新记录)+ 原因,之后允许重跑。 ## 事务、并发与回滚 - “确认工作表 + 为新增确认行建立/更新事件 + audit”在同一个服务事务,任一失败整体 回滚,不存在 confirmed 但未匹配的半成品。 - 独立重跑使用 `BEGIN IMMEDIATE`,按 `source_row_id` 升序写,SQLite 30 秒 busy timeout 保留;投影可从只增日志重建(`rebuild_current_projection`)。 - migration down 只用于测试/上线前回退;生产产生决定后默认保留 migration 5 与审计 数据,不执行破坏性 down。 ## Decimal 金额全程以规范十进制字符串存储和比较,任何路径都不转 `float`;`100.0` 与 `100.00` 视为相等,`100.00` 与 `100.01` 不匹配。