# 架构设计说明书 · 基金转换(convert)交易 > 版本:**v2.1(精简最终态 · T+1 受理/确认分离模型)** · 2026-09-11 · **已通过独立 AI 联网审查(0 阻断级),已定稿(2026-09-11 用户批准)** > 上游:`docs/PRD/PRD-基金转换交易.md`(v1.1) > 分支:`risk-control-agent` · 关联:`docs/项目框架设计/架构设计-风控模块.md` > 技术选型不变:FastAPI + SQLAlchemy Core(text SQL) + MySQL 双库 + Redis + LangGraph + DeepSeek。 > 本文定义**目录划分、模块职责、核心时序、技术决策、事务与并发、测试策略、任务映射(第 4 步输入)**。 --- ## 0. 合规与真实业务基准 > 来源:证监会公告〔2025〕22 号《公开募集证券投资基金销售费用管理规定》(2026-01-01 施行)+ 汇添富/中欧/鹏华/国投瑞银/东海/华商/金鹰/财通/中银/东方证券资管 10 家转换业务公告。清单见 §0.5。 ### 0.1 法规硬约束 | 项 | 22 号文规定 | 本项目取值 | 落点 | | --- | --- | --- | --- | | 申购费率上限 | 主动偏股 ≤**0.8%** / 其他混合 ≤**0.5%** / **指数·债券 ≤0.3%** / 货基 0(22号文 §8) | 种子 **0.0080 / 0.0050 / 0.0030 / 0** | PRD §4.3 | | 赎回费下限 | <7 日 ≥**1.5%** / 7–30 日 ≥**1.0%** / 30–180 日 ≥**0.5%**(22号文 §10) | 档表见 §7(+180–365 0.25% / ≥365 0,合同约定) | PRD §2.1.3 | | 赎回费归属 | **全额计入基金财产**(22号文 §10,旧规 75%/50%/25% 分级已废止) | `core_fee_rule.to_fund_ratio` = **1.0**(D16,留痕、不参与计算) | PRD §4.1 | > **豁免不启用**:22 号文 §10 允许个人持满 7 日的指数/债基、机构持满 30 日的债基另约赎回费。本项目不走豁免,保留完整费率梯度以便演示与测试。 ### 0.2 行业惯例 | 项 | 真实做法(多份公告一致) | 本项目 | | --- | --- | --- | | 份额精度 | 转入份额四舍五入保留 2 位(主流);**个别差异**:易方达 ETF 场外整数位、南方截断 | 主流 2 位 HALF_UP;**按产品 `core_share_rule`(product_id+business_type) 配置**(D27) | | 金额精度 | 转出金额/赎回费/补差费 2 位四舍五入(主流);**个别差异**:东方赎回费 2 位后截断 | 主流 HALF_UP;**按产品 `rounding_mode` 配置**(D27) | | T+1 确认 | T 申请(未知价法)→ T+1 权益登记 → T+2 可用;持有期自确认日起算 | 转入新批次 `confirmed_at = T+1`(交易日历推算) | | 先进先出 / 逐批计费 | 注册日期在前先转出;不同持有期分别计费 | FIFO + 逐批计费 | | 单笔计算法 | 当日多笔转换单笔计费,不合并 | 逐笔 | | 未知价法 | 以申请受理当日净值为基准 | 确认时取 T 日净值 | | 转出可赎回 / 转入可申购 | 双方状态校验 | FR-C9a | | 同机构限制 | 同一销售机构 + 同一管理人 + 同一 TA | `CROSS_ENTITY_NOT_SUPPORTED` | | 冻结份额 / 撤单 | 冻结不可转(司法冻结为人工柜台);T 日结束前可撤 | 在途占用由受理单推导;撤单已实现 | | **赎回申报方向** | **份额赎回**——投资者以份额申报,登记机构按 T 日净值算金额 | **redeem 份额申报**(D26) | | **巨额赎回** | 转出与赎回同优先级、可全额或部分转出;真实条款为**部分延期赎回**(未确认部分顺延至下一开放日) | **投资者层部分成交**(可用份额不足时实际=min(申请,可用))+ 占用释放(**不顺延**,属本项目投资者层建模取舍,与基金层顺延不同)(D28) | | **资金流** | 赎回款经**注册登记账户**中转,一次转换一次资金划转 | Core 侧建 `core_cash_flow` 中转建模(D29) | | **A/C 份额互转** | 同一基金 A/C 类可互转(由管理人公告决定,非所有基金支持) | `core_product` 加 `share_class` + per-product 开关 `allow_ac_convert`(D30) | ### 0.3 已拍板的多口径决策 | 项 | 拍板结论 | 落点 | | --- | --- | --- | | 补差费口径 | **B(价外法两端差)为默认**;A 以 `convert_diff_fee_mode='rate_diff'` 保留 | D13 / §7 / §11 | | 最低持有余额处置 | **两种都做**:`min_hold_action ∈ {force_transfer, force_redeem}` | D15 / §7 | | P1 普通申赎无批次 | **不跳过**(批次表覆盖全部交易类型;遇历史持仓无批次**兜底补建**) | D8 / §7 | | P2 转出归零 | **保留(`remain_qty`/`qty` = 0),不删行** | D9 / §7 | | P3 Core 侧明细 | **仅补偿,不对外查询** | D6 / §7 | ### 0.4 与真实业务全量对齐(D26~D30) > 所有与真实业务不符的简化本期全部收敛:redeem 份额申报、按产品舍入、投资者层部分成交、资金流中转、A/C 互转。唯一硬限制 = 模拟库不是真实登记机构底座;但以下差异在模拟层内均可建模对齐真实语义。 | # | 原简化 | 真实对齐落地 | 决策 | | --- | --- | --- | --- | | ① | redeem 金额反算份额 | 份额申报;`赎回金额 = qty × T日净值 − 赎回费` | D26 | | ② | redeem 折算份额估算 | 随①收敛(份额来自申报,T+1 取 T 日真实净值) | D26 | | ③ | 赎回费统一 HALF_UP | 按产品 `rounding_mode`(东方截断等) | D27 | | ④ | 转入份额统一 2 位 | 按产品 `share_digits`(易方达整数/南方截断等) | D27 | | ⑤ | 巨额赎回不做部分成交 | 确认段部分成交(投资者可用份额不足时实际=min(申请,可用))+ 占用释放(不顺延,投资者层取舍;基金层巨额赎回为顺延) | D28 | | ⑥ | 资金流不建模中转 | Core 侧 `core_cash_flow` 中转建模 | D29 | | ⑦ | 无 `share_class` | `core_product` 加 `share_class` + seed 标注,放开 A/C 互转 | D30 | | ⑧ | 赎回费归属不参与 | 22 号文全额计入(`to_fund_ratio=1.0`)数值已对齐 | D16 | ### 0.5 来源清单 | # | 来源 | 关键结论 | | --- | --- | --- | | 1 | 证监会公告〔2025〕22 号(2026-01-01 施行) | 申购费上限 22号文 §8、赎回费下限与全额计入基金财产 22号文 §10 | | 2 | 上交所 501048 号公告(汇添富) | 口径 A 补差费公式 + 完整算例;0.1 份阈值强制全转 | | 3 | 中国证券报 2015-11-24(中欧) | 口径 A + T+1 权益登记、T+2 可查询 | | 4 | 中国证券报 2026-08-19(鹏华) | 口径 B 外扣法 + 同销售机构 + 持有期自确认日重算 | | 5 | 中国证券报 2026-07-08(国投瑞银) | 口径 B 价外法两端差 + 单笔计算法 + 逐批计费 | | 6 | 中国金融信息网(泰信/东海) | 未知价法、先进先出、2 位四舍五入、误差在基金资产列支 | | 7 | 上海证券报 2026-07-30 | 巨额赎回时转出与赎回同优先级;余额不足最低持有 → 强制赎回 | | 8 | 中国证券报 2026-09-01(中银) | 申请份额精确到 2 位、单笔 ≥1000 份、余额低于 1000 份须全部转出 | | 9 | 深交所 43da8375(东方证券资管) | 口径 A + 赎回费部分计入基金财产(旧规,已被 22 号文取代) | | 10 | 睿远 §65 / 华泰保兴 §69 / 东方 §57 / 国投瑞银 §33 | 赎回 = 份额赎回(投资者以份额申报) | | 11 | 农银汇理 / 泰信 / 华宝兴业「巨额赎回」条款 | 转出与赎回同优先级、可全额或部分转出、未确认部分顺延至下一开放日 | | 12 | 东方 §59 | 个别管理人赎回费 2 位后截断(非四舍五入) | --- ## 1. 设计原则 **继承既有(不可违反):** 1. 分层 `api(薄)→ service(业务)→ repository/tool`;唯一例外 `app/gateway/`(模拟外部 Core 交易系统,仅此层可写 `jinrong_core`) 2. 规则即纯函数(`rules.py`),便于单测与阈值调整 3. 双库无跨库事务(`jinrong_core` / `jinrong_agent` 各自 engine) 4. 审计表**只 INSERT**;trace_id 用 contextvars 贯通 5. 不新增 ORM(`app/model/entities.py` 不动),沿用 **text SQL + 返回 dict** **本期新增:** 6. **计算与编排分离**:折算/费率/舍入做成**无 IO 纯函数**(`service/convert/calc.py`),编排层只取数、调纯函数、管事务 7. **受理/确认/撤单三段编排**:受理段(Core 单库落单)、确认段(Core 单库批量串行扣减)、撤单段(Core 单库),各自独立事务 8. **批次表必存 + 兜底补建**:`core_share_lot` 覆盖全部交易类型;普通申赎有批次则 FIFO 扣/增,无批次则**兜底补建**(按 `core_holding.as_of` 建初始批次,`lot_bootstrap` 单点) 9. **锁原语**:统一 `locks.run_locked`(等待 + 降级双层锁) 10. **`Decimal` 一律显式 `quantize`,禁止裸浮点**:金额/份额 2 位 `ROUND_HALF_UP`(默认),按产品 `core_share_rule` 的 `rounding_mode`/`share_digits` 可覆盖(D27);舍入误差在基金资产列支,响应可选回传 `rounding_diff` - ⚠️ Python `Decimal.quantize()` 默认 `ROUND_HALF_EVEN`(银行家舍入),**必须显式传舍入方式** 11. **响应金额全部 `str()` 化 + 单点量化**:库内一律 `DECIMAL(18,4)`,对外必经 `convert_service._q()` 单点量化(金额/份额 2 位、净值/费率/尾差 4 位),响应、审计 summary、日志**共用同一出口** 12. **单次转换最多跨 `convert_batch_max_lots` 个批次**(默认 200):超限返回 **HTTP** 400 `TOO_MANY_LOTS`,防事务膨胀 13. **Core 读写账号物理分离**:`jinrong_core` 拆只读账号与最小写权限账号;`get_engine(database, role)` 缓存键由「库名」改为「(库名, 角色)」(D20) 14. **全差异按真实业务走**:所有与真实不符的简化本期全部收敛(①~⑧,§0.4 / D26~D30) --- ## 2. 目录与文件划分 ```text app/ ├── api/ │ ├── simulate.py # 【改】TradeRequest 按 trade_type 分支校验; │ │ # subscribe=金额申购 / redeem=份额申报(D26) / convert=份额申报; │ │ # convert 调 convert_service;错误码映射 ApiError │ └── convert_admin.py # 【新增】运维接口:确认触发 FR-C23 / 受理单查询 / 撤单 │ ├── gateway/ # 【core 库写侧唯一出口】 │ ├── trade_gateway.py # 【改】convert 分支:校验 → 受理(落 core_convert_request,202),折算移到确认段; │ │ # redeem 分支:份额申报 qty 入参,赎回金额=qty×净值−费; │ │ # subscribe/redeem 分支:增补批次维护(FR-C16)+ T+2 可赎回校验(FR-C25) │ ├── gateway_repository.py # 【改】insert_trade 增 qty / convert_group_id / cash_flow 两列写入 │ └── convert_core_repository.py # 【改】仅 confirm_convert(确认事务:扣批次+写流水+lot_detail+holding+中转+cash_flow+置confirmed); │ # accept/cancel 归 convert_request_repository(受理单权威状态机) │ ├── service/ │ ├── convert/ # 【新增包】 │ │ ├── types.py # 【类型】Lot / FeeRule / PlanResult / ShareRule,@dataclass(frozen=True) │ │ ├── calc.py # 【纯函数】FIFO 分配 / 逐批计费 / 补差费(A/B)/ 份额折算 / 舍入(按产品) │ │ ├── nav.py # 【纯函数】净值口径:确认时取 T 日净值(不回退) │ │ ├── fee.py # 【纯函数】持有期→赎回费率查表(输入规则行,不查库) │ │ ├── lot_bootstrap.py # 【纯函数】批次补建规则(D8):gateway 兜底补建 与 rebuild_lots.py 同源 │ │ ├── errors.py # 【新增】convert 专属异常(继承 ApiError,§8.3) │ │ ├── convert_service.py # 【编排】受理段 + 确认段(含部分确认 D28)+ 撤单段 + 幂等 + 执行权 + 补偿日志 │ │ └── confirm_service.py # 【新增】T+1 批处理确认编排(串行捞受理单 → 逐单 confirm_convert) │ └── risk/ │ ├── rules.py # 【改】_amount_view();RISK-002/005 走金额视图 │ ├── engine.py # 【改】process_convert_event(out, in),确认事务提交后调用 │ ├── alert_service.py # 【改】record_trade_alerts 增 events 可选参数 │ └── locks.py # 【改】仅 run_locked(双层锁) │ ├── repository/ │ ├── core_ro.py # 【改】增 get_nav_as_of / get_redeem_fee_rules / list_share_lots / │ │ # sum_remain_qty / get_holding / get_trade_calendar / get_share_rule │ ├── share_lot_repository.py # 【新增】core_share_lot 读:FIFO 选批 + 汇总 │ ├── convert_request_repository.py # 【新增】core_convert_request 权威状态机仓储:accept_convert(受理事务 INSERT)/ │ │ # cancel_convert(撤单事务置 cancelled)/ query_request(状态查询) │ ├── convert_repository.py # 【改】agent 库 risk_convert_detail:仅写「agent 侧详情写入进度」 │ └── risk_repository.py # 【不改】复用 insert_audit_log │ ├── tool/ │ └── core_tools.py # 【改】query_recent_trades 汇总走 _amount_view;持仓查询过滤 qty <= 0 │ ├── config/ │ └── settings.py # 【改】§11 全部 convert_* 配置项 + §11.1 四个 DB 账号(D20) └── utils/ └── db.py # 【改】get_engine(database, role="rw")(D20) scripts/ ├── core/ │ ├── 00-grant.sql # 【新增】3 应用账号 + 最小权限(D20) │ ├── 01-ddl.sql # 【改】core_fee_rule / core_share_lot / core_convert_lot_detail / │ │ # core_convert_request(新增) / core_trade_calendar(新增) / core_cash_flow(新增 D29) │ │ # + core_product 加 share_class(D30) / rounding_mode / share_digits(D27) 等 8 列 │ ├── 07-seed-fee-rule.sql # 【新增】赎回费持有期分档(5 档) │ ├── 08-seed-share-lot.sql # 【新增】按持仓反推批次 │ ├── 09-seed-org.sql # 【新增】fund_company / ta_code / subscribe_fee_rate │ ├── 11-seed-share-rule.sql # 【新增】真实产品舍入/位数规则,逐产品列 (share_digits, rounding_mode) 组合(D27) │ ├── rebuild_lots.py # 【新增】按 core_holding 重建批次(快照重建,非交易回滚) │ └── reset.ps1 # 【改】$files 追加 07/08/09/10/11 ├── agent/ │ └── cleanup_pending_convert.py # 【改】扫描 core_convert_request 超 SLA 的非终态单(accepted/nav_pending)置 expired ├── dev/ │ └── calc_convert_demo.py # 【改】与生产同一套 calc.py 实算主示例/同费率对照,回填 PRD §5.3 与验收断言 └── demo/ └── rebuild_alerts.py # 【改】支持 --convert-group 按 Core 侧数据补写详情(D6) ``` **不新增**:ORM 实体、`app/service/risk/` 新规则(RISK-001~006 口径不变,仅金额视图去重)。 --- ## 3. 核心时序(受理段 + 确认段 + 撤单段) ```text POST /api/simulate/trade {trade_type: "convert", from_product_id, to_product_id, qty, client_request_id?} │ ├─ ① 参数与产品校验(不落库):from != to · can_redeem / can_subscribe · fund_company+ta_code 同机构(含 share_class 同基金判定 D30) ├─ ② 受理日判定(15:00 截点 + 交易日历 → T) ├─ ③ 可用份额校验(不落库):Σ remain_qty − Σ(未终态受理单 requested_qty) + 最低份额(含全额豁免) ├─ ④ 适当性校验(转入端,受理时校验一次留痕含测评版本号) ├─ ⑤ 受理事务(Core 单库):INSERT core_convert_request(status='accepted',含 accept_date/confirm_date/available_date) │ 撞 uk_idem → 返回首次受理回执(幂等);其他失败 → 整体回滚 └─ ⑥ 返回 202 + 受理回执(不含任何金额/净值/折算字段) POST /api/simulate/trade/convert/{gid}/cancel (T 日 15:00 前) │ ├─ ① 校验受理单存在 + 状态 accepted/nav_pending ├─ ② 当前时间 ≤ T 日 convert_cutoff_time(15:00) ├─ ③ 撤单事务(Core 单库):置 cancelled + cancelled_at;占用自然释放 └─ ④ 落审计 decision='convert_cancelled' POST /api/simulate/trade/convert/confirm?accept_date=T (运维/批处理 FR-C23,指定业务日) │ ├─ run_locked("convert:confirm:{T}") # 批处理串行锁(D22) ├─ 捞 core_convert_request WHERE accept_date=T AND status∈(accepted, nav_pending) ORDER BY accepted_at └─ 逐单 confirm_convert(见下) confirm_convert(单笔确认事务,Core 单库): ├─ 1 取两端 T 日净值;缺失 → 置 nav_pending,本轮跳过(不 reject、不降级) ├─ 2 复核产品可申赎状态 + 适当性(转入端,D25)→ 不满足 → rejected + 释放占用 ├─ 3 plan_lots(FIFO + 最低持有处置)+ 折算(calc.py,口径 B) ├─ 4 【可用份额不足 D28】若申请份额 > 可用份额(部分成交场景):实际成交 = min(申请, 可用); │ 未确认部分占用释放(不顺延至后续批处理,属投资者层建模取舍;基金层巨额赎回为顺延);记录 partial 标记 │ (部分成交 = 受理段③占用校验之外的二次兜底,覆盖 T 日~T+1 间其它在途单抢占致可用份额变少的场景) ├─ 5 确认事务(Core 单库):扣批次(条件 UPDATE 哨兵)+ 写两条流水 + core_convert_lot_detail │ + 更新两端 core_holding + 写 core_cash_flow(中转,D29)+ 置 confirmed │ 失败 → 整体回滚 → 保持 accepted,下轮重试(幂等) ├─ 6 确认后同步跑规则引擎(一次):process_convert_event(out, in);异常落 engine_error + 本地日志,不阻断 └─ 7 agent 侧写详情 + 主审计(附加写入):失败不回滚 Core,落待补偿(SLA 24h) ``` > **引擎时机**:受理时无金额(净值未公告),规则引擎金额类规则无输入;确认后跑,读到已更新持仓,集中度规则(RISK-006)连带效应仍被覆盖。引擎只调用一次,传入两条流水。 --- ## 4. 关键技术决策(D1~D30) | # | 决策点 | 选型 | 备选 | 理由 | | --- | --- | --- | --- | --- | | D1 | 折算计算放哪 | `service/convert/calc.py` 纯函数 | 内联在 service | 金额逻辑最易错,必须可穷举单测 | | D2 | 批次读写分工 | 读 → `share_lot_repository`;写 → `convert_core_repository`(确认段同事务) | 全塞一个 repository | 写必须与两条流水同事务;读需在事务外预检 | | D3 | 两段事务实现 | 受理事务 `convert_request_repository.accept_convert` + 确认事务 `convert_core_repository.confirm_convert`,各 `with engine.begin()` 单库单方法 | 单事务实时扣减 | 受理单下沉 Core 后两段各自同库,单事务闭合(PRD §7.1) | | D4 | 锁原语 | 统一 `run_locked`(双层锁,抢不到返回 202+已有 group_id) | 一次性 try_lock | 语义与「抢不到立即 202」一致 | | D5 | 引擎只跑一次 | `process_convert_event(out, in)`,确认后调用 | 给现有函数加参数 | 语义清晰 | | D6 | 补偿数据来源 | `core_convert_lot_detail` 含 nav/nav_date,rebuild_alerts 仅凭 Core 侧补写 | 靠 agent 库残留 | 附加写入失败 agent 库可能为空,Core 侧必须自包含 | | D7 | 金额去重 | `rules._amount_view()`:同 convert_group_id 组内保留转出端 | 删行/求和 | 组内无 redeem 时取第一条(防御) | | D8 | 普通申赎批次维护 | **兜底补建**:无批次 → 按 core_holding.as_of 补建 | 跳过/报错 | P1 已拍板 | | D9 | `core_holding` 同步 | UPSERT;转出后 qty=0 保留行 | 删除行 | P2 已拍板 | | D10 | 净值查询 | `core_ro.get_nav_as_of(pid, trade_date)`(nav_date <= :d 降序取 1) | 改 get_latest_nav | 现有方法无条件取最新,改它动既有调用方 | | D11 | 费率来源 | 运行时读 `core_fee_rule` + `core_product.subscribe_fee_rate` | 硬编码 | 数据驱动 | | D12 | 请求模型 | 扩展 `TradeRequest`(trade_type 分支校验) | 新增 ConvertRequest | 端点契约不变 | | D13 | 补差费公式 | **口径 B(价外法两端差)为默认**,A 配置保留 | 只用 A | 已拍板 | | D14 | T+1 确认 | `confirmed_at` = T+1 **交易日**(core_trade_calendar 推算),不用自然日近似 | 自然日近似 | 真实业务 T+1 为工作日 | | D15 | 最低持有余额处置 | 两种都实现(force_transfer/force_redeem),默认 force_transfer | 只做强制全转 | 已拍板 | | D16 | 赎回费归属 | `to_fund_ratio` = 1.0(22 号文 §10 全额计入),不参与计算 | 不建模 | 字段留痕 | | D17 | 规则引擎时机 | **确认事务提交后**同步跑(受理时无金额) | 受理后跑 | 受理段无金额,引擎无输入(§3) | | D18 | 批次补建规则复用 | 纯函数 `lot_bootstrap.bootstrap_lots()`,gateway 与 rebuild_lots 同调 | 两处各写 | 防漂移 | | D19 | 引擎异常钩子预留 | `process_convert_event(..., on_error_hook=None)` | 写死 | 二期可插拔 | | D20 | Core 读写账号分离 | `xh_core_ro`/`xh_core_rw`/`xh_agent_rw` + `get_engine(db, role)` | 单 root | 主架构 C1 已拍板 | | **D21** | 受理单落 Core 库 | `core_convert_request`(权威状态机)落 jinrong_core | 落 agent 库 | 在途占用须与 core_share_lot 同库单 SQL 闭环;幂等锚点 uk_idem 与流水同库消除「重跑第二组流水」 | | **D22** | 批处理串行锁 | `run_locked("convert:confirm:{accept_date}")` | 无锁/各单并发 | 防两个批处理同时跑同一受理日;锁 + 状态二次校验双保险 | | **D23** | 全量重写 | 删「T 日实时扣减」路径,无双模型开关 | 双模型并存 | 用户拍板:全量重写 T+1,口径统一无漂移 | | **D24** | 确认触发 | 运维接口 FR-C23 + 脚本(手动指定业务日),语义批量串行;**不引 apscheduler**(模拟库无真实时钟,二期可接) | 引入调度器 | 用户拍板「按真实业务」:登记机构语义=批量串行自动确认;落地为手动接口+脚本模拟批处理 | | **D25** | 适当性 T+1 复核 | 确认段第 2 步加 `suitability_check(转入端)`,不通过 → rejected(份额不变) | 不复核 | 用户拍板「T+1 复核」 | | **D26** | redeem 份额申报 | redeem 入参改 `qty`(份额);`赎回金额 = qty × T日净值 − 赎回费`;加 T+2 可赎回校验(FR-C25) | 金额反算份额(与真实相反) | 用户拍板「按真实业务走」:真实=份额赎回(睿远 §65 等) | | **D27** | 按产品舍入/位数 | `core_share_rule`(product_id + business_type) 配置(易方达 FOF 转换转入 2 位、ETF 场外整数位,须产品级区分);`rounding_mode ∈ {HALF_UP, TRUNCATE(=ROUND_DOWN)}`(东方/南方截断=ROUND_DOWN,须与种子一致);**11-seed-share-rule.sql 逐产品列 `(share_digits, rounding_mode)` 组合,禁错配** | 统一 HALF_UP/2 位 | 用户拍板「都按真实业务走」:真实不统一,主流口径不算「对齐真实」 | | **D28** | 可用份额不足部分确认 | 确认段支持部分成交(实际=min(申请,可用))+ 未确认部分占用释放(不顺延至后续批处理,属投资者层建模取舍;真实基金层巨额赎回为「部分延期赎回/顺延」,不可混淆) | 不做(无份额池) | 用户拍板:真实规则(可用份额不足部分成交)不可因模拟简化「就算了」;基金层同比例确认需另引现金头寸,本期不做 | | **D29** | 资金流中转建模 | Core 侧建 `core_cash_flow`(注册登记账户中转),一次转换 1 笔资金划转 | 不建模(core_trade 记两份金额) | 用户拍板:真实资金流经中转账户,模拟层须还原语义 | | **D30** | A/C 份额互转 | `core_product` 加 `share_class` 列 + `allow_ac_convert TINYINT DEFAULT 0` + seed 真实产品标注;放开同基金 A/C 互转(须 `allow_ac_convert=1`,易方达 FOF 等公告暂不开通) | 无 share_class 不做 | 用户拍板:真实产品有 A/C 份额,是 seed 未建列,非真实业务没有 | --- ## 5. 事务、并发与补偿 ### 5.1 三段 + 引擎 + SQL 清单 | 段 | 库 | 入口 | SQL / 动作 | | --- | --- | --- | --- | | 受理 | core | `convert_request_repository.accept_convert` | 1×`INSERT core_convert_request`(`uk_idem` 兜底) | | 确认 | core | `convert_core_repository.confirm_convert`(**单事务**) | 扣批次(条件 UPDATE)+ 2×`INSERT core_trade` + `core_convert_lot_detail` + 2×`UPSERT core_holding` + `core_cash_flow`(中转)+ `SET confirmed` | | 撤单 | core | `convert_request_repository.cancel_convert` | 1×`UPDATE core_convert_request SET cancelled` | | 引擎 | — | `engine.process_convert_event(out, in)` | 确认事务提交后;异常落 `engine_error`,不阻断 | 受理/确认/撤单任一 SQL 失败 → 整体回滚(`with engine.begin()` 异常即 rollback)。 ### 5.2 重试判定(仅在取得执行权后) ```sql -- 受理段:查受理单状态 SELECT status FROM core_convert_request WHERE convert_group_id = :gid; -- 确认段:查是否已有流水(幂等,不产生第二组) SELECT 1 FROM core_trade WHERE convert_group_id = :gid LIMIT 1; ``` 走 `core_ro`(只读)。依赖索引(`idx_confirm_queue` / `core_trade.idx_convert_group`),否则全表扫。 ### 5.3 锁原语(统一 run_locked) ```python run_locked(f"convert:avail:{cid}:{out_pid}", lambda acquired: ...) # 受理段可用份额短临界区 run_locked(f"convert:idem:{client_request_id}", lambda acquired: ...) # 幂等执行权(抢不到返回 202) run_locked(f"convert:confirm:{accept_date}", lambda acquired: ...) # 批处理串行 run_locked(f"convert:rerun:{gid}", lambda acquired: ...) # agent 详情补写串行 ``` > **确认段扣批次哨兵(并发安全)**:必须用**条件 UPDATE + rowcount 断言**,禁止先 SELECT 后 UPDATE: > ```sql > UPDATE core_share_lot SET remain_qty = remain_qty - :q > WHERE lot_id = :id AND remain_qty >= :q; -- rowcount == 1 才成功;否则 LOT_CONFLICT(409) 让路,不重扣 > ``` ### 5.4 补偿(一期:监控告警 + 人工脚本,SLA 24h) | 项 | 口径 | | --- | --- | | 触发者 | `risk_convert_detail.status='failed'` 或 `core_convert_request` 超 SLA 非终态的巡检/告警 | | 补偿工具 | `scripts/demo/rebuild_alerts.py --convert-group`(Core 侧自包含补写) | | **SLA** | `convert_confirm_sla_days=2`(交易日,T+1 必确认 + 净值延迟宽容) | | 兜底清理 | `cleanup_pending_convert.py` 把超 SLA 的 `accepted`/`nav_pending` 置 `expired`(标记不硬删,释放占用) | > **幂等锚点**:convert 补偿以转出端 `out_trade_id` 为锚点(复用现有 `find_alerts_by_trade`)。 --- ## 6. 规则引擎改造(FR-C11 / FR-C15) ### 6.1 `_amount_view` ```python def _amount_view(trades: list[dict]) -> list[dict]: """同 convert_group_id 组内只保留转出端(trade_type='redeem')。无 gid 原样通过。""" ``` `run_rules` 内部:`eligible` 供 RISK-001/003/004;`amount_view(eligible)` 供 RISK-002/005。RISK-006 读持仓快照(确认后已更新)。 ### 6.2 一次转换 = 一条预警事件 ```python def process_convert_event(out_trade, in_trade, *, core_ro=None, risk_repo=None, thresholds=None, on_error_hook=None) -> dict: ``` - 确认事务提交后调用,传入两条流水 - `record_trade_alerts(primary=out_trade, hits, events=[event_of(out), event_of(in)])` → 一张单、payload.events 两条 --- ## 7. 计算口径落地(纯函数签名) > **类型定义**:`Lot` / `FeeRule` / `PlanResult` / `ShareRule` 统一放 `service/convert/types.py`,`@dataclass(frozen=True)`。 ```python # service/convert/calc.py def plan_lots(lots, requested_qty, min_hold_qty, min_hold_action) -> PlanResult: ... def lot_amount(qty, nav) -> Decimal: # 2 位(默认,可由 share_digits 覆盖) def lot_fee(amount, rate, rounding_mode="HALF_UP") -> Decimal: # 按产品 rounding_mode(D27) def pick_fee_rate(rules, hold_days) -> Decimal: # 分档匹配(左闭右开) def convert_amount(out_amount, redeem_fee) -> Decimal: ... def diff_fee(conv_amount, out_rate, in_rate, mode="amount_diff") -> Decimal: # 口径 B 默认(D13) def in_qty(in_amount, in_nav, share_digits=2) -> Decimal: # 按产品 share_digits(D27) def hold_days(redeem_confirm_date, confirmed_at) -> int: ... # 持有期按自然日计(赎回确认日−申购确认日),非交易日; # 第一参须传本次转换受理单的确认日 T+1(即赎回 leg 被确认之日),严禁传受理日 T,亦严禁传被赎批次的 # 申购 confirmed_at——否则边界少算 1 天(如真实满 7 日算成 6 日→误收 1.5%);口径详见 PRD §2.1.3; # 单测须覆盖满 7/30/180 日边界 def rounding_diff(in_amount, in_nav, actual_in_qty, places=4) -> Decimal: ... def ensure_batch_limit(plan, max_lots) -> None: ... def redeem_amount(qty, nav, fee_rate, rounding_mode="HALF_UP") -> Decimal: # D26:份额赎回金额 def partial_confirm(requested, available) -> tuple[Decimal, bool]: # D28:部分确认 ``` **赎回费分档(区间左闭右开 `[min_hold_days, max_hold_days)`)** | 档 | min | max | rate | 依据 | | --- | --- | --- | --- | --- | | < 7 日 | 0 | 7 | **0.0150** | 22 号文 §10 下限 | | 7–30 日 | 7 | 30 | **0.0100** | 22 号文 §10 下限 | | 30–180 日 | 30 | 180 | **0.0050** | 22 号文 §10 下限 | | 180–365 日 | 180 | 365 | 0.0025 | 合同约定 | | ≥ 365 日 | 365 | NULL | 0.0000 | 合同约定 | > **费率档说明**:<7/7–30/30–180 三档为 **22 号文 §10 下限**(法定最低),真实产品可高于下限;本 seed 取下限演示(`to_fund_ratio=1.0` 全额计入)。180 日以上为合同约定档。 **`core_holding` 更新口径(PRD FR-C13)** | 端 | qty | cost_amount | market_value | as_of | pnl_pct | | --- | --- | --- | --- | --- | --- | | 转出 | `qty − actual_qty` | `cost × (1 − actual/原qty)` | `新qty × out_nav` | nav_date | `(mv−cost)/cost` | | 转入 | `qty + in_qty` | `cost + in_amount` | `新qty × in_nav` | nav_date | 同上 | **FIFO 选批 SQL(确定性 tiebreaker)** ```sql SELECT lot_id, confirmed_at, remain_qty FROM core_share_lot WHERE customer_id = :cid AND product_id = :pid AND remain_qty > 0 ORDER BY confirmed_at ASC, lot_id ASC LIMIT :max_lots; ``` --- ## 8. 接口契约落地 ### 8.1 请求模型(`api/simulate.py`) ```python class TradeRequest(BaseModel): customer_id: str product_id: str | None = None # subscribe 必填 trade_type: str # subscribe | redeem | convert amount: Decimal | None = None # subscribe 必填(金额申购) from_product_id: str | None = None # convert 必填 to_product_id: str | None = None # convert 必填 qty: Decimal | None = None # convert 必填(份额申报);redeem 必填(份额赎回,D26) client_request_id: str | None = None # 幂等键(选填) @model_validator(mode="after") ``` > subscribe = 金额申购;redeem = 份额申报(D26);convert = 份额申报。 ### 8.2 响应 `convert` 受理成功返回 **HTTP 202 Accepted**(受理回执,不含金额/净值);确认结果 T+1 后由查询接口返回。`redeem` 成功返回确认结果(份额申报,金额由 `qty × 净值 − 费` 算出)。 ### 8.3 错误码映射 | 异常 | HTTP | error_code | | --- | --- | --- | | `ProductNotRedeemable` | 400 | `PRODUCT_NOT_REDEEMABLE` | | `ProductNotSubscribable` | 400 | `PRODUCT_NOT_SUBSCRIBABLE` | | `InsufficientShares` | 400 | `INSUFFICIENT_SHARES`(含在途占用) | | `BelowMinQty` | 400 | `BELOW_MIN_QTY` | | `SameProduct` | 400 | `SAME_PRODUCT` | | `CrossEntityNotSupported` | 400 | `CROSS_ENTITY_NOT_SUPPORTED`(含跨 share_class 同基金判定 D30) | | `TooManyLots` | 400 | `TOO_MANY_LOTS` | | `NoTradingDay` | 400 | `NO_TRADING_DAY` | | `CancelNotAllowed` | **409** | `CANCEL_NOT_ALLOWED`(撤单超窗口/已确认) | | `LotConflict` | 409 | `LOT_CONFLICT`(确认段条件 UPDATE 哨兵) | | `IdempotencyUnavailable` | 503 | `IDEMPOTENCY_UNAVAILABLE` | | 未抢到执行权 | **202** | 返回 `{convert_group_id, status: "processing"}` | | 幂等命中 completed | **200** | 返回首次结果 | 异常类定义在 `app/utils/exceptions.py` + `service/convert/errors.py`。 > `reject_reason` 取值(`SUITABILITY_FAILED` 等,确认段失败原因)见 PRD §5.6;本节 `error_code` 为 HTTP 层错误码,两层不混用。 ### 8.4 确认触发接口(FR-C23,运维/批处理) ```text POST /api/simulate/trade/convert/confirm?accept_date=2026-09-09 ``` 指定业务日,串行捞 `accepted`/`nav_pending` 受理单逐单确认。手动触发(D24),模拟登记机构批处理。 ### 8.5 撤单接口(FR-C22) ```text POST /api/simulate/trade/convert/{convert_group_id}/cancel ``` T 日 15:00 前 + 状态 accepted/nav_pending → 200 置 cancelled 释放占用;否则 409 `CANCEL_NOT_ALLOWED`。 --- ## 9. 数据模型落点清单(DDL 与种子) | 对象 | 落点文件 | 动作 | | --- | --- | --- | | `core_fee_rule` | `01-ddl.sql` | 新建(to_fund_ratio) | | `core_share_lot` | `01-ddl.sql` | 新建 | | `core_convert_lot_detail` | `01-ddl.sql` | 新建(nav/nav_date;完整列集含 hold_days/fee_rate/lot_count 等,详见 PRD §4.1) | | `core_convert_request` | `01-ddl.sql` | 新建(权威状态机,6 态 ENUM) | | `core_trade_calendar` | `01-ddl.sql` | 新建(交易日历) | | `core_cash_flow` | `01-ddl.sql` | 新建(D29,注册登记账户中转);列:`convert_group_id` / `from_account` / `to_account` / `amount DECIMAL(18,4)` / `direction`(枚举 `OUT_TO_TA`/`TA_TO_IN`/`NET`,落地单笔 `NET` 行 amount=in_amount)/ `trade_date`;「一次转换 1 笔资金划转」为建模假设(非法规明文);写入方=confirm_convert,读取方=报表/重建(core_ro) | | `core_trade.convert_group_id` + `KEY` | `01-ddl.sql` | 改表 | | `core_product` | `01-ddl.sql` | 加列(合计 8 列,含 share_class(D30) + allow_ac_convert TINYINT DEFAULT 0(D30 开关) + rounding_mode/share_digits(D27) + can_subscribe/can_redeem + min_hold_qty/min_redeem_qty + subscribe_fee_rate + fund_company/ta_code + fee_rate COMMENT) | | `risk_convert_detail` | `02-mysql-agent专用.sql` | 新建(status ENUM 5 值,语义收窄为 agent 写入进度) | | `core_share_rule` | `11-seed-share-rule.sql` | 新建(D27,key=product_id+business_type,逐产品 (share_digits, rounding_mode) 组合) | | 赎回费分档种子 | `07-seed-fee-rule.sql` | 新建 | | 份额批次种子 | `08-seed-share-lot.sql` | 新建 | | 机构 + 申购费率种子 | `09-seed-org.sql` | 新建(含 share_class 标注 D30) | | reset 流程 | `reset.ps1` | `$files` 追加 07/08/09/10/11 | **D25 数据落点(适当性 T+1 复核)**:投资者风险等级 + 测评版本号取自既有投资者信息表(非新建表);落地前须确认该表已含 `risk_level` + `assess_version` 列,否则在 `01-ddl.sql` 补建并写入本清单。 **种子文件版本注释约定(PRD L-6)**:新增种子 SQL 文件头带版本注释块。 **sqlite 测试 DDL 同步(`tests/_ddl.py`)**:5 新表 + core_product 新列;`test_db.py::test_core_holding_columns` 启动期列名断言。 --- ## 10. 测试策略 | 层 | 文件 | 覆盖点 | 预计用例 | | --- | --- | --- | --- | | 纯函数 | `test_convert_calc.py` | FIFO / 分档费率 / 补差费双口径 / 舍入(含按产品 rounding_mode/share_digits D27)/ 部分确认 D28 / T+1 起算 | ~40 | | 受理编排 | `test_convert_accept.py` | 受理段 6 步 / 可用份额校验(含在途占用)/ 幂等 / 202 回执 | ~15 | | 确认编排 | `test_convert_confirm.py` | 确认段 7 步 / 批量串行 / 部分确认 D28 / nav_pending 挂起 / 适当性 T+1 复核 D25 / 资金流中转 D29 | ~25 | | 撤单 | `test_convert_cancel.py` | 15:00 前撤单 / 超窗口 409 / 占用释放 | ~8 | | 赎回份额申报 | `test_redeem_qty.py` | redeem 份额申报(D26)+ T+2 校验 FR-C25 | ~10 | | 并发 | `test_convert_concurrency.py` | 可用份额短临界区 / 批处理串行 / 同键 202 / 50 并发压测 | ~10 | | 批次 | `test_share_lot.py` | 普通申赎批次维护 + 兜底补建 + rebuild_lots | ~12 | | 集成 | `test_convert_integration.py` | 真 MySQL,端到端折算与 §7 calc.py 纯函数签名 + calc_convert_demo 实算回填吻合 | ~6 | | 回归 | `test_trade_gateway.py` | 补批次维护 + redeem 份额申报断言;现有用例全绿 | +10 | 基线:现有全量 → 预计 **650~700**。 > **50 并发压测**:同一 (customer, product) 上 50 并发争抢可用份额(短临界区),断言不超卖 + confirm 串行无重复流水。 --- ## 11. 配置新增(`app/config/settings.py`) | 项 | 默认 | 说明 | | --- | --- | --- | | `convert_cutoff_time` | `"15:00"` | 受理截点(不得硬编码) | | `convert_confirm_sla_days` | `2` | 超 SLA 置 expired(交易日) | | `convert_batch_max_lots` | `200` | 超 200 触发;返回 HTTP 400 `TOO_MANY_LOTS`(**400 为 HTTP 状态码、非阈值,阈值=200**) | | `convert_diff_fee_mode` | `"amount_diff"` | 补差费口径 B 默认(D13) | | `convert_lock_ttl_seconds` | `30` | 锁 TTL | | `convert_nav_stale_days` | `3` | **退役**(不再回退取旧净值) | | `mysql_core_ro_user` / `mysql_core_rw_user` / `mysql_agent_user` | `""` | D20 三账号(空回退 root) | ### 11.1 DB 账号与最小权限(D20) `xh_core_ro`(SELECT)/ `xh_core_rw`(4 表写,扩展至 core_convert_request/core_cash_flow)/ `xh_agent_rw`(audit 仅 INSERT);`get_engine(db, role)`。 --- ## 12. 风险与应对 | # | 风险 | 应对 | | --- | --- | --- | | 1 | sqlite/MySQL DDL 列名失配 | T-0 先统一 + 启动断言 | | 2 | 批次补建规则写两处 → 漂移 | D18 抽纯函数同调 | | 3 | `Decimal` 默认 HALF_EVEN | 所有 quantize 显式传舍入(按产品 rounding_mode 覆盖) | | 4 | 引擎异常不丢预警 | 不阻断、落 engine_error + 本地日志(D19 钩子预留) | | 5 | 批次数超限 | 400 不自动分拆 | | 6 | 双库不一致 | 两段 Core 单库事务 + agent 附加 + 补偿 | | 7 | 锁 TTL 被确认段超时突破 | 锁 + 状态二次校验双保险;批处理串行 | | 8 | redeem 改份额申报打穿现有用例 | D26:redeem 接口/网关/折算全改,现有申赎用例重跑基线 | | 9 | 部分确认语义复杂 | D28:确认段部分成交 + 占用释放 + 不顺延,单测覆盖 | | 10 | 按产品舍入/位数配置 | D27:core_share_rule(product_id+business_type) 表 + calc 按产品取,老产品缺规则回退主流 2 位 HALF_UP(显式标注) | --- ## 13. 拍板结论与溯源 ### 13.1 关键拍板(2026-09-11) | # | 事项 | 拍板 | 依据 | | --- | --- | --- | --- | | 1 | 模型策略 | **全量重写 T+1**(T 日受理/确认分离) | 用户拍板(D23) | | 2 | 确认触发 | 运维接口 + 脚本(不引调度器) | 用户拍板「按真实业务」(D24) | | 3 | 适当性 T+1 复核 | **复核** | 用户拍板(D25) | | 4 | SLA | 2 交易日 | 用户拍板「按真实业务」(D24 派生) | | 5 | redeem 份额申报 | **本期收敛**(份额赎回) | 用户拍板「按真实业务走」(D26) | | 6 | 全差异收敛 | ①~⑧ 全部本期对齐真实 | 用户拍板「都按真实业务走」(D26~D30) | ### 13.2 独立评审结论 架构 v2.1 精简重写版已通过独立 AI 联网审查(0 阻断级,5 项外部事实联网核验全部与文档相符),**已定稿**(2026-09-11 用户批准)。第 4 步开发计划 **v2.0 已按本版 §15 任务映射落地**(`docs/项目框架设计/开发计划-基金转换交易.md`,经三轮独立审核闭环:挑毛病 / 验证型 / 白纸重审 + 终验)。 --- ## 14. 设计自检十三问复核 | # | 问 | 本期答案 | | --- | --- | --- | | 1 | 新增表/字段谁写? | core_convert_request(convert_request_repository)/ core_cash_flow(confirm_convert)/ core_share_lot(确认段+普通申赎)/ risk_convert_detail(agent 写入进度)/ share_class/rounding_mode/share_digits(seed 写入,代码只读) | | 2 | 谁读? | core_ro(批次/净值/费率/持仓/日历/产品规则)/ rules._amount_view / core_tools / rebuild_alerts / cleanup_pending_convert | | 3 | 枚举/常量 DDL? | core_convert_request.status ENUM 6 值(建表即全量);risk_convert_detail.status ENUM 5 值(agent 进度);audit_log.decision VARCHAR | | 4 | 事务跨库? | 两段 Core 单库事务 + agent 附加(补偿) | | 5 | 种子数据? | 06~11 种子(含真实产品舍入规则 D27、share_class D30) | | 6 | 并发安全? | 可用份额短临界区(run_locked)+ 确认条件 UPDATE 哨兵 + 批处理串行锁 + 幂等锚点同库 | | 7 | 汇总语义唯一? | _amount_view 取转出端;out/convert/in_amount 语义分立 | | 8 | 向后兼容? | core_trade 加可空列;core_product 新列有 DEFAULT;get_engine(role) 默认回退 | | 9 | 示例自证? | §7 纯函数签名(calc_convert_demo 实算回填,禁手算) | | 10 | 外部事实核验? | §0.1/§0.2/§0.5 列 22 号文 + 12 个来源条目;份额赎回/巨额赎回/资金流/舍入差异均有出处 | | 11 | 重置类伴随数据? | cleanup_pending_convert 扫描 core_convert_request;reset.ps1 显式清单 | | 12 | 类型与口径匹配? | 申购费率按 product_type 取档(PRD §4.3);share_class 同基金判定(D30) | | 13 | 同一规则/公式单副本? | 折算/费率/舍入抽 calc.py 纯函数;lot_bootstrap 单点;格式出口 _q 单点 | **二补 · 格式契约三问** | # | 问 | 答案 | | --- | --- | --- | | 补-1 | 展示位数/精度写在哪? | §1 设计原则#10/#11 + §7 calc.py + `convert_service._q()`:金额/份额 2 位(按产品 share_digits 覆盖 D27)、净值/费率/尾差 4 位 | | 补-2 | 同一逻辑数据产出路径几条? | 首次公式算 / 库 DECIMAL(18,4) 重建 → 均经 `_q()` 单点,逐字节一致 | | 补-3 | 格式出口单点? | 响应/审计/日志共用 `_q()`;**禁止裸 `str(Decimal)` 出网** | **新持久对象谁清理**:`core_convert_request` 生命周期 = accepted → confirmed/rejected/cancelled/nav_pending → expired;非终态超 SLA 由 `cleanup_pending_convert.py` 置 expired 释放占用(功能正确性要求,非可选运维)。 --- ## 15. 与第 4 步的衔接(任务映射) | 任务 | 内容 | 依赖 | 说明 | | --- | --- | --- | --- | | **T-0** ✅ | 列名统一 + 启动断言 | 无 | 已完成 | | **T-0b** ✅ | DB 账号分离(D20) | 无 | 已完成 | | T-1 | DDL + 种子(含 core_convert_request / core_trade_calendar / core_cash_flow / share_class / rounding_mode / share_digits / 11-seed-share-rule) | T-0+T-0b | 新增 5 表 + 8 列 + 2 种子 | | T-2 | service/convert/ 纯函数(含 D27 按产品舍入/位数、D28 partial_confirm、D26 redeem_amount) | 无 | 扩展 calc.py | | T-2b | calc_convert_demo 实算回填 | T-2 | 示例随真实净值重算 | | T-3 | core_ro 新方法 + share_lot_repository + convert_request_repository | T-1 | 新增受理单仓储 | | T-4 | convert_repository(agent 进度) | T-1 | 语义收窄 | | T-5 | run_locked 复用 | 无 | 统一锁原语 | | T-6 | **accept_convert(受理事务)** | T-1/T-3 | 受理单 INSERT | | T-7 | convert_service 编排(受理/确认/撤单三段)+ confirm_service 批处理 | T-2~T-6 | 核心编排 | | T-8 | rules._amount_view + engine.process_convert_event(确认后跑) | 无 | 引擎时机改 | | T-9 | api/simulate.py 模型(redeem 份额申报 D26)+ 撤单/确认接口 + trade_gateway 分派 | T-7 | redeem 改份额申报 | | T-10 | 普通申赎批次维护 + T+2 校验(FR-C25)+ rebuild_lots | T-3 | 扩展 | | T-11 | core_tools 汇总去重 + 持仓过滤 | T-8 | 沿用 | | T-12 | 补偿脚本 + cleanup_pending_convert(扫 core_convert_request) | T-4/T-7 | 改写扫描对象 | | T-13 | 全量回归 + 集成 + 50 并发 + 性能补录 | 全部 | 基线重估 | | **T-14** | **D27 按产品 `core_share_rule`(product_id+business_type) 舍入/位数 seed + calc 接入** | T-1/T-2 | 新增 | | **T-15** | **D28 投资者层部分确认** | T-7 | 新增 | | **T-16** | **D29 资金流中转 core_cash_flow 建模** | T-6 | 新增 | | **T-17** | **D30 share_class + A/C 互转** | T-1/T-9 | 新增 | --- > **收尾说明**:本版落实 PRD v1.1「下游同步令」+ 用户四项拍板(全量重写 / 确认触发 / T+1 适当性复核 / SLA)+「全差异按真实业务走」硬指令(①~⑧ 收敛)。已落地且复用的资产(D20 / T-0 / T-0b / 纯函数包)继续生效。**第 4 步开发计划 v2.0 已按本表 §15 落地**(`docs/项目框架设计/开发计划-基金转换交易.md`,三轮独立审核闭环);本架构 v2.1 经独立 AI 联网审查通过,**已定稿**(2026-09-11 用户批准)。