-- ============================================================================ -- CMA 司库(资金)体系 · 阶段一 MVP —— DDL(阶段 1 落盘版) -- ============================================================================ -- 状态 : 落地版(2026-09-15 阶段 1)。G1 = 放行(有条件),见 -- wecom-project/pending-tasks/_verification/fullstack-202609151735-cma-treasury-g1-review.md -- 评审稿出处 : wecom-fullstack/pending-tasks/_reports/treasury-stage1-ddl-draft-20260915.sql -- (md5 3aa4d944691b49cfda9b1f08f6bea549 —— 归档件,不回改) -- 本版修正 : P2-6 data_status 默认 fail-safe / D-8 submitter 语义 / 调整2 detail 结构化 -- / P2-8 金额纪律注释 / 无物理外键的应用层覆盖声明(§F) -- 依据 : PRD v1.1 §3.2/§3.3(md5 cc9ca839b7fd6f633f4066ba385819be) -- 财务口径签认 v1(md5 74b43cfdf25abf5b7654eba54e82d099) -- 财务侧前置交付 F1/F2/F3(md5 a49ae4340c05dfa20aba7daad25cc25f) -- 目标环境 : MySQL 8.0(InnoDB),charset/collation 以只读探针实测为准 -- 范围 : 6 张新表 + cash_plans 加 4 列(向后兼容,只加列不改列) -- 阶段三的 4 张表(pools/pool_members/credits/internal_transfers)不建 -- 幂等契约 : 同一脚本重复执行结果不变(见 §E) -- 命名约定 : 约束名与 PRD §3.2 字面一致(唯一键 uq_* / 普通索引 ix_*) -- 执行方式 : 仅由 backend/scripts/treasury_migrate.py --apply 执行(本文件不含裸 ALTER 执行逻辑) -- ============================================================================ -- ============================================================================ -- §A. 建表(6 张,CREATE TABLE IF NOT EXISTS 天然幂等) -- 应用层软链(不建物理外键,G1 §3-2 裁定)覆盖声明见 §F -- ============================================================================ -- A1. 资金账户主数据(统一管理:台账唯一入口) -- tier 口径(财务签认 口径1 / PRD M1):按【账户性质】判定,禁止按 org_nodes.level 派生 -- hq : 法人银行户 / 收款通道(1002xx、微信、支付宝、友赞商户) -- grassroots: 现金 / 备用金 / 备用卡 / 中间账户(100101/100104/100105/100106/100107) -- platform : 分公司独立法人账户(当前两实体均无,禁止造虚层级) CREATE TABLE IF NOT EXISTS treasury_accounts ( id INT AUTO_INCREMENT PRIMARY KEY, entity_id INT NOT NULL COMMENT '企业ID(多租户铁律,必带)', account_no VARCHAR(64) NOT NULL COMMENT '账号/虚拟账户号(掩码)', account_name VARCHAR(100) NOT NULL COMMENT '账户名(如 建设银行(货款账户))', account_type VARCHAR(20) NOT NULL COMMENT 'cash现金/bank银行/e_wallet微信支付宝/other', bank_name VARCHAR(100) NULL, subject_code VARCHAR(20) NULL COMMENT '对应科目(1001xx/1002xx)— 流水/头寸归集用', currency VARCHAR(10) DEFAULT 'CNY', tier VARCHAR(20) NOT NULL COMMENT 'hq总部/platform平台/grassroots基层(按账户性质判定)', tier_basis VARCHAR(20) DEFAULT 'nature' COMMENT '[D-1] tier 判定依据:nature账户性质/level派生/manual人工;口径1 要求 accounts 一律 nature', owner_node_id INT NULL COMMENT '使用/保管部门标注 org_nodes.id(不参与 tier 派生、不作授权依据)— 软链,应用层校验见 §F', open_balance FLOAT NULL COMMENT '期初余额(万元);F2 到位前留 NULL。金额纪律见 §F-1', open_period VARCHAR(20) NULL COMMENT '期初期间 YYYY-MM', is_pool_member TINYINT DEFAULT 0 COMMENT '阶段三资金池用', status VARCHAR(20) DEFAULT 'active' COMMENT 'active/inactive/closed', remark VARCHAR(500) NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_tacct_entity_no (entity_id, account_no), KEY ix_tacct_entity (entity_id, status), KEY ix_tacct_subject (entity_id, subject_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='司库·资金账户主数据'; -- A2. 账户头寸(日/期末) -- 铁律:余额 = 期初 + 流水(期初缺失时 data_status='estimated'); -- 禁止把 voucher_details 的"流水净额累计"当余额(PRD §2.2-1 实证) CREATE TABLE IF NOT EXISTS treasury_balances ( id INT AUTO_INCREMENT PRIMARY KEY, entity_id INT NOT NULL, account_id INT NOT NULL COMMENT 'treasury_accounts.id(软链,应用层校验见 §F)', period VARCHAR(20) NOT NULL COMMENT 'YYYY-MM-DD 日头寸 / YYYY-MM 月末头寸', balance FLOAT NOT NULL COMMENT '账面余额(万元)。金额纪律见 §F-1', available FLOAT NULL COMMENT '可用余额(万元)(NULL=未采集,勿以 balance 冒充)', frozen FLOAT DEFAULT 0 COMMENT '冻结/在途(万元)', source VARCHAR(30) NOT NULL COMMENT 'ledger期初+流水/import导入/manual手工/pool归集', data_status VARCHAR(20) DEFAULT 'estimated' COMMENT 'verified/estimated —— [P2-6] 默认 estimated(fail-safe):verified 必须显式写入(仅期初已落定/对账通过);estimated 头寸禁止 block 规则生效', batch VARCHAR(100) NULL COMMENT '导入批次指纹(幂等用,对齐 cash_vouchers_json._content_fingerprint 范式)', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_tbal_acct_period (account_id, period), KEY ix_tbal_entity_period (entity_id, period), KEY ix_tbal_entity_status (entity_id, data_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='司库·账户头寸(日/期末)'; -- A3. 资金内控规则库(P0 核心) -- params JSON 结构与口径映射: -- NP-001 budget_required {"judge":"budget_plans","entity1":"block","entity2":"warn", -- "occupancy":"paid+approved+pending_approval+this", -- "unmatched":"specify_budget_flow"} -- AU-001 authorization {"tiers":[{"from":0,"to":3,"required_approvals":0},...], -- "daily_cum_limit":9,"max_strict":true,"bounds":"[from,to)"} -- BL-001 dual_sign {"min_amount":30,"distinct_natural_person":true} -- LQ-001 liquidity {"target":26,"warn":12,"block":6,"block_enabled":false, -- "skip_when":"data_status=estimated"} CREATE TABLE IF NOT EXISTS treasury_control_rules ( id INT AUTO_INCREMENT PRIMARY KEY, entity_id INT NOT NULL, rule_code VARCHAR(20) NOT NULL COMMENT 'NP-001/AU-001/BL-001/LQ-001/LQ-002/TF-001/AC-001', rule_name VARCHAR(100) NOT NULL, rule_type VARCHAR(30) NOT NULL COMMENT 'budget_required/authorization/dual_sign/liquidity/two_funds/account_valid', severity VARCHAR(10) NOT NULL COMMENT 'block拦截/warn预警', params JSON NULL COMMENT '阈值参数(金额段/下限/比例等,不硬编码)', rule_version VARCHAR(20) DEFAULT 'v1' COMMENT '[调整2] 规则版本,写入 control_hits.detail.rule_version(审计可自证)', enabled TINYINT DEFAULT 1, remark VARCHAR(500) NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_tcr_entity_code (entity_id, rule_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='司库·资金内控规则库'; -- A4. 支付单(内控闸门载体) -- 拆单累计穿透(口径2/M4)所需索引:ix_tpay_daysplit(同提交人×同收款方×同自然日累计) -- [D-8] submitter / created_by 语义(G1 复算发现的未披露增补,追认采纳): -- submitter = 支付单提交人 —— AU-001 拆单累计穿透的"同一提交人"判据(业务语义字段) -- created_by = 建单人 —— 历史兼容列(与既有表 created_by 命名一致) -- 首版两者同值(提交即建单);保留双列以便后续"审批链与建单分离"时语义不破坏 CREATE TABLE IF NOT EXISTS treasury_payments ( id INT AUTO_INCREMENT PRIMARY KEY, entity_id INT NOT NULL, pay_no VARCHAR(50) NOT NULL COMMENT '支付单号', from_account_id INT NOT NULL COMMENT '付款账户(必须已登记,禁未入台账账户)——软链,应用层校验见 §F', to_name VARCHAR(200) NOT NULL COMMENT '收款方', to_account_no VARCHAR(64) NULL, amount FLOAT NOT NULL COMMENT '金额(万元)。金额纪律见 §F-1', currency VARCHAR(10) DEFAULT 'CNY', purpose VARCHAR(500) NOT NULL COMMENT '用途(必填,内控留痕)', pay_type VARCHAR(30) NULL COMMENT '货款/费用/税费/薪酬/融资/内部划转', pay_date DATE NULL COMMENT '业务付款日(拆单累计穿透按此自然日归集;口径2)', budget_plan_id INT NULL COMMENT '关联预算计划(NP-001 授权性判据首选)——软链', kpi_id INT NULL, related_plan_id INT NULL COMMENT '关联 cash_plans.id(不新建表,复用收付计划)——软链', status VARCHAR(20) DEFAULT 'draft' COMMENT 'draft/precheck/pending_approval/approved/rejected/paid/cancelled', check_result VARCHAR(20) NULL COMMENT 'pass/blocked/warn', check_detail JSON NULL COMMENT '[{rule_code,verdict,detail,matched_scope}] 命中档位写在 matched_scope(口径2-③)', required_approvals TINYINT NULL COMMENT '[D-2] 本单实际所需审批级数(取最严档结果,落库便于审计复现)', day_cum_amount FLOAT NULL COMMENT '[D-3] 判定时 同提交人×同收款方×同自然日 累计金额(万元,含本单)', approvals JSON NULL COMMENT '[{account,role,person_name,at,comment}] — 双签须记账号+角色+自然人姓名(口径4)', dual_sign_effective TINYINT NULL COMMENT 'BL-001:两账号是否不同自然人;0=形式双签(放行无效,审计标红)', is_waived TINYINT DEFAULT 0 COMMENT '[D-4] 是否经豁免通道放行(豁免人/事由落 treasury_control_hits)', submitter VARCHAR(100) NOT NULL COMMENT '[D-8] 提交人(AU-001 拆单累计穿透"同一提交人"判据)', submitted_at DATETIME NULL COMMENT '[D-5] 提交时间(口径2 日累计/占用口径需要审批链时间锚)', approved_at DATETIME NULL COMMENT '[D-5] 放行时间', paid_at DATETIME NULL, created_by VARCHAR(100) NOT NULL COMMENT '[D-8] 建单人(历史兼容列;首版与 submitter 同值)', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_tpay_entity_no (entity_id, pay_no), KEY ix_tpay_entity_status (entity_id, status), KEY ix_tpay_daysplit (entity_id, pay_date, to_name, submitter), KEY ix_tpay_acct (entity_id, from_account_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='司库·支付单(资金内控闸门)'; -- A5. 规则命中留痕(审计,append-only,不可省) -- [调整2 / D-6] detail 固定结构化字段(规则引擎按此写入,审计追问要能自证): -- {"rule_code":..,"rule_version":..,"matched_scope":..,"threshold":.., -- "cumulative":..,"verdict_basis":".."} CREATE TABLE IF NOT EXISTS treasury_control_hits ( id INT AUTO_INCREMENT PRIMARY KEY, entity_id INT NOT NULL, target_type VARCHAR(20) NOT NULL COMMENT 'payment/plan/internal_transfer', target_id INT NOT NULL, rule_code VARCHAR(20) NOT NULL, verdict VARCHAR(20) NOT NULL COMMENT 'pass/blocked/waived/warn', detail JSON NULL COMMENT '[调整2] 结构化判据快照,固定字段 {rule_code, rule_version, matched_scope, threshold, cumulative, verdict_basis}', waived_by VARCHAR(100) NULL COMMENT '豁免审批人(豁免级别不得低于该笔所需审批级别,口径4)', waived_reason VARCHAR(500) NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY ix_tch_target (target_type, target_id), KEY ix_tch_entity_rule (entity_id, rule_code, created_at), KEY ix_tch_waived (entity_id, verdict, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='司库·内控规则命中留痕(append-only 审计)'; -- A6. 分级授权矩阵(不硬编码;A/B 数值生效前需任总过目) -- 边界口径(口径2-④ 左闭右开):amount_from 含、amount_to 不含;amount_to IS NULL = 无上限 CREATE TABLE IF NOT EXISTS treasury_limits ( id INT AUTO_INCREMENT PRIMARY KEY, entity_id INT NOT NULL, scope_type VARCHAR(20) NOT NULL COMMENT 'role/account/tier', scope_value VARCHAR(50) NOT NULL COMMENT 'finance/ceo/…/account_id/tier', amount_from FLOAT NOT NULL COMMENT '段下限(万元,含)。金额纪律见 §F-1', amount_to FLOAT NULL COMMENT '段上限(万元,不含),NULL=无上限', required_approvals TINYINT DEFAULT 1 COMMENT '所需审批级数(≥2=双签)', approver_roles JSON NOT NULL COMMENT '["ceo","finance"](顺序=先复核后批准,口径4)', require_budget TINYINT DEFAULT 1, daily_cum_limit FLOAT NULL COMMENT '自主档日累计上限(=3×A;超限升一级审批,口径2-②)', status VARCHAR(20) DEFAULT 'active', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '[D-9] 审计必需(PRD 该表无此列,G1 追认采纳)', UNIQUE KEY uq_tlim_scope (entity_id, scope_type, scope_value, amount_from), KEY ix_tlim_entity (entity_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='司库·分级授权矩阵'; -- ============================================================================ -- §B. 扩展既有表(只加列、不改列、默认 NULL —— 存量数据零影响) -- 注意:MySQL 8.0 不支持 ADD COLUMN IF NOT EXISTS, -- 裸 ALTER 重复执行会报 1060 Duplicate column name。 -- 故 §B 由 migrate 脚本按 information_schema 预检结果条件执行(见 §E-②)。 -- ============================================================================ -- B0. 幂等预检(migrate 脚本执行前必跑;输出缺失列清单,全 0 即跳过) -- SELECT COLUMN_NAME FROM information_schema.COLUMNS -- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cash_plans' -- AND COLUMN_NAME IN ('account_id','tier','pool_id','is_locked'); ALTER TABLE cash_plans ADD COLUMN account_id INT NULL COMMENT '资金来源/去向账户 treasury_accounts.id(软链,无物理外键)', ADD COLUMN tier VARCHAR(20) NULL COMMENT 'hq/platform/grassroots(三位一体视角;可由 org_nodes.level 派生)', ADD COLUMN pool_id INT NULL COMMENT '归属资金池(阶段三启用,本阶段恒 NULL)', ADD COLUMN is_locked TINYINT DEFAULT 0 COMMENT '是否已被预算/内控锁定'; -- B1. 可选索引 [D-7](G1 已随 D-1~D-7 全部采纳;与 §B 同批执行) ALTER TABLE cash_plans ADD KEY ix_cashplans_entity_acct (entity_id, account_id); -- ============================================================================ -- §C. 数据种子(首版数值 = 财务建议值;任总拍板后只改数据行、不改代码) -- 幂等:INSERT ... ON DUPLICATE KEY UPDATE(唯一键 = uq_tcr_entity_code / uq_tlim_scope) -- 行数契约:规则 8 行(e1 4 + e2 4)+ 授权矩阵 6 行 -- ============================================================================ -- C1. 三条 block 规则 + LQ-001(entity1 / entity2 各一套) -- NP-001:entity1=block,entity2=warn(分期上线,过渡期 1 个月后切 block) -- AU-001:tiers 采用签认首版 A/B(酣客 3/30 万;博海 5/50 万)—— 待任总过目 -- BL-001:distinct_natural_person=true(两账号须属不同自然人) -- LQ-001:首版一律 warn、block_enabled=false(阶段二接预警) INSERT INTO treasury_control_rules (entity_id, rule_code, rule_name, rule_type, severity, params, rule_version, enabled) VALUES (1,'NP-001','无预算不开支(授权性判据)','budget_required','block', JSON_OBJECT('judge','budget_plans','severity','block', 'occupancy','paid+approved+pending_approval+this', 'unmatched','specify_budget_flow','transition_until','2026-10-15'),'v1',1), (1,'AU-001','超授权拦截(含拆单累计穿透+日累计升级)','authorization','block', JSON_OBJECT('tiers',JSON_ARRAY(JSON_OBJECT('from',0,'to',3,'required_approvals',0), JSON_OBJECT('from',3,'to',30,'required_approvals',1), JSON_OBJECT('from',30,'to',NULL,'required_approvals',2)), 'daily_cum_limit',9,'merge_same_payee_same_day',true,'max_strict',true,'bounds','[from,to)'),'v1',1), (1,'BL-001','大额支付双签(须不同自然人)','dual_sign','block', JSON_OBJECT('min_amount',30,'required_approvals',2,'distinct_natural_person',true, 'record_fields',JSON_ARRAY('account','role','person_name'),'order','review_then_approve'),'v1',1), (1,'LQ-001','流动性下限(首版 warn-only 页面并列对照)','liquidity','warn', JSON_OBJECT('target',26,'warn',12,'block',6,'block_enabled',false, 'skip_when','data_status=estimated'),'v1',1), (2,'NP-001','无预算不开支(博海首版 warn+强制补录)','budget_required','warn', JSON_OBJECT('judge','budget_plans','severity','warn','occupancy','paid+approved+pending_approval+this', 'unmatched','specify_budget_flow','transition_until','2026-10-15'),'v1',1), (2,'AU-001','超授权拦截(含拆单累计穿透+日累计升级)','authorization','block', JSON_OBJECT('tiers',JSON_ARRAY(JSON_OBJECT('from',0,'to',5,'required_approvals',0), JSON_OBJECT('from',5,'to',50,'required_approvals',1), JSON_OBJECT('from',50,'to',NULL,'required_approvals',2)), 'daily_cum_limit',15,'merge_same_payee_same_day',true,'max_strict',true,'bounds','[from,to)'),'v1',1), (2,'BL-001','大额支付双签(须不同自然人)','dual_sign','block', JSON_OBJECT('min_amount',50,'required_approvals',2,'distinct_natural_person',true, 'record_fields',JSON_ARRAY('account','role','person_name'),'order','review_then_approve'),'v1',1), (2,'LQ-001','流动性下限(博海首版默认 warn/关闭)','liquidity','warn', JSON_OBJECT('target',50,'warn',10,'block',5,'block_enabled',false, 'skip_when','data_status=estimated'),'v1',0) ON DUPLICATE KEY UPDATE rule_name=VALUES(rule_name), rule_type=VALUES(rule_type), severity=VALUES(severity), params=VALUES(params), rule_version=VALUES(rule_version); -- C2. 授权矩阵(scope_type='role',按 entity 生效;双签档 approver_roles 顺序=先复核后批准) INSERT INTO treasury_limits (entity_id, scope_type, scope_value, amount_from, amount_to, required_approvals, approver_roles, require_budget, daily_cum_limit) VALUES (1,'role','finance',0, 3, 0, JSON_ARRAY(), 1, 9), (1,'role','ceo', 3, 30, 1, JSON_ARRAY('ceo'), 1, NULL), (1,'role','ceo', 30, NULL,2, JSON_ARRAY('finance','ceo'),1, NULL), (2,'role','finance',0, 5, 0, JSON_ARRAY(), 1, 15), (2,'role','ceo', 5, 50, 1, JSON_ARRAY('ceo'), 1, NULL), (2,'role','ceo', 50, NULL,2, JSON_ARRAY('finance','ceo'),1, NULL) ON DUPLICATE KEY UPDATE amount_to=VALUES(amount_to), required_approvals=VALUES(required_approvals), approver_roles=VALUES(approver_roles), daily_cum_limit=VALUES(daily_cum_limit); -- ============================================================================ -- §D. 回滚 / DOWN(由 treasury_migrate.py --down 执行;顺序与 UP 相反) -- ============================================================================ -- D1. 回滚 cash_plans 扩展列:[P1-3] 按列存在性**逐列** DROP(部分态不再 1091) -- 回滚前 [P1-4] 必先 mysqldump cash_plans(写路径账号 + 临时 defaults-file 注入口令, -- 不硬编码、不回显;dump 失败 fail-closed —— 先 dump 成功才允许删列) -- D2. 回滚新表(先删留痕/引用方,再删主数据):DROP TABLE IF EXISTS 逐张 -- D3. 回滚安全性:6 张新表首版为空表(DROP 无损);一旦有业务数据, -- 回滚须经人工确认(--yes),不提供 --force 免确认开关 -- D4. 回滚不触碰 cash.py 既有表结构,11 端点契约不受影响 -- ============================================================================ -- §E. 幂等与验证(由 treasury_migrate.py 逐条实现) -- ============================================================================ -- E1. 幂等三件套 + 计数口径([P0-2] 修正) -- ① CREATE TABLE IF NOT EXISTS(表级幂等) -- ② §B 列扩展按 information_schema 预检(列级幂等) -- ③ §C 种子走 ON DUPLICATE KEY UPDATE(数据级幂等) -- ⇒ 计数口径 = **新增对象数**(apply 前后 information_schema diff),不是"执行语句数": -- 首跑:created_new=6 / added_cols=4 / 种子 8 规则行 + 6 授权行 -- 二跑:created_new=0 / added_cols=0 / 种子行数不变 -- ④ up() 结尾断言 check()["missing_tables"] == [],否则非零退出(禁静默成功) -- ⑤ 指纹行写入 import_logs(batch=treasury-ddl-),[P2-7] entity_id=1 为约定: -- 迁移指纹属实体无关元数据,import_logs.entity_id NOT NULL 约束下以 1 占位; -- 幂等按 (import_type,batch) 先查后写,不依赖表上是否有唯一键 -- E2. 表级自检(落库后) -- SHOW CREATE TABLE treasury_accounts; -- 逐表核对 entity_id/唯一键/引擎字符集 -- SELECT TABLE_NAME FROM information_schema.TABLES -- WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME LIKE 'treasury%'; -- 期望 6 行 -- SELECT TABLE_NAME, COLUMN_NAME, IS_NULLABLE FROM information_schema.COLUMNS -- WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME LIKE 'treasury%' -- AND COLUMN_NAME='entity_id'; -- 期望 6 行且全 NOT NULL -- E3. 存量零影响证明 -- SELECT COUNT(*) FROM cash_plans; -- 迁移前后一致(基线以探针实测为准) -- ============================================================================ -- §F. 应用层约束声明(G1 裁定"不建物理外键"的附条件覆盖说明 + 金额纪律) -- ============================================================================ -- F-1 金额纪律([P2-8] / G1 §3-3 附条件): -- * 全部金额列为 FLOAT(万元)—— 与 cash_plans.amount / budget_plans.budget_value 同口径; -- FLOAT 非精确货币类型(技术债登记:迁 DECIMAL(18,4) 须与 cash_plans/budget_plans 同批) -- * 写库前一律 round(x, 4);比较一律用容差 TOL=1e-4,禁止浮点等值比较(引擎实现见 treasury.py) -- F-2 软链的应用层校验覆盖(逐表说明,G1 §3-2 附条件): -- ① treasury_balances.account_id → 写入/查询前校验 account 存在 且 account.entity_id == 传入 entity_id(防跨实体误挂) -- ② treasury_payments.from_account_id → AC-001:必须命中 treasury_accounts 且 status='active'(账外账户拦截) -- ③ treasury_payments.budget_plan_id → NP-001:命中 budget_plans 且 entity_id 一致;未命中走"指定/补录预算"流程 -- ④ treasury_payments.related_plan_id → 校验 cash_plans 同 entity_id(只读复用,不改 cash.py 契约) -- ⑤ treasury_payments.kpi_id → 校验 kpis 同 entity_id(NP-001 兜底匹配用) -- ⑥ treasury_control_hits.target_id → target_type 限定取值域(payment/plan/internal_transfer), -- 写入前校验目标单据存在且同 entity_id -- ⑦ treasury_accounts.owner_node_id → 校验 org_nodes 同 entity_id(不存在则留空,禁造虚层级) -- * 唯一键 + 应用层校验 + AC-001 覆盖主要一致性风险;跨实体写入在应用层一律 403/400 拒绝 -- ============================================================================