Files
group_xinghuo_jinrong/docs/项目框架设计/评审待办-风控主架构与基金转换.md
GaoYiYuan_0626 f056fcd007 基金转换 T+1 模型:T-1~T-6 落地(DDL/数据层/语义收窄/锁/受理事务)
AIcoding 第 5 步 todo 开发(开发计划 v2.0)前半段:

- T-1 3 张新表 DDL(core_convert_request 6 态 ENUM / core_trade_calendar /
  core_share_rule)+ 种子 + sqlite 单一事实源同步 + 列清单断言
- T-2/T-2b calc 扩展(product_round/redeem_amount/partial_qty)+ 真实净值实算回填
- T-3 convert_request_repository(6 态 + 条件 UPDATE 守卫)+ core_ro 三读方法
  + share_lot_repository.available_qty_with_inflight(R-3 在途占用推导)
- T-4 convert_repository.sync_mirror 成为 risk_convert_detail 唯一进度镜像写入口
  (旧三方法标 Deprecated,T-7 后删)
- T-5 locks.py 锁键构造器 convert_req_lock_key / convert_confirm_lock_key
- T-6 convert_service.accept_convert 受理事务(八步:锁→幂等→校验→受理日顺延
  →在途占用校验→落单→镜像+审计→202;不扣份额/不折算/不写流水)
  + tests/test_convert_accept.py(16 用例)
  + scripts/dev/verify_convert_accept.py(真库 36/36 一致)
  + trading_calendar.py 纯函数包(R-5)+ 21 用例

T-6 真库实测暴露并修复:confirm_eta 在日历数据边界抛 ValueError,会让已落库
的受理单在调用方眼里变 500;改为展示性字段容错 + 单测守护。

基线:798 passed / 10 skipped,零回归。
2026-09-11 19:21:15 +08:00

130 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 评审待办 · 风控主架构 + 基金转换架构
> ⚠️ **历史参考文档(2026-09-10 产物)**:文中「基金转换架构 v1.0 评审」对应的是 **v0.x 模型**,已被 **T+1 受理/确认分离模型(架构 v2.1 / PRD v1.1 / 开发计划 v2.0)取代**;当前评审结论见各文档「独立审查」节,**当前以 v1.1/v2.1/v2.0 为准**。
>
> **用途**:两份架构评审意见**合并处理**的执行清单。
> **用户指示(2026-09-10)**:「等基金架构的评审也出来了一起改」。
> **当前状态**:**基金转换架构 v1.0 评审 13 条已全部落地**(落点索引见架构 §13.2,0 条悬空,2026-09-10);
> **P0-1 账号方案已拍板**(§三 → 采纳独立账号方案,落 D20 / T-0b);其余风控主架构 **P0/P1 待处置**(见 §一)。
> **关联文档**:`架构设计-基金转换交易.md`(v1.0)· `架构设计-风控模块.md` · `基金转换-审查意见处置表.md`
---
## 一、风控主架构评审(2026-09-10 收到 · 已抽查核实)
### P0 · 安全与正确性底线
| # | 风险点 | 核实结论 | 建议修复 |
| --- | --- | --- | --- |
| 1 | Core 只读靠约定、**无 DB 级强制** | **✅ 属实**(`settings.py:17` 单 root 账号;`db.py:23` 仅按库名缓存 → `core_ro:55` 与 `gateway_repository:24` 共用同一连接池) | **✅ 已拍板并落地设计**:**D20 双账号分离**(见 §三 + 架构 §11.1)—— `xh_core_ro`(SELECT) / `xh_core_rw`(4 表写) / `xh_agent_rw`(audit 仅 INSERT);`get_engine(db, role)`;DDL 走管理员。**T-0b 阻断前置** |
| 2 | `agent_message(seq_no)` 并发重号 | **✅ 属实**:`表设计/01-mysql-共用底座.sql:47` 是普通 `KEY idx_session_seq`,非 UNIQUE | 加 `UNIQUE(session_id, seq_no)` + `insert_turn` 捕获 `IntegrityError` 重试 |
| 3 | dev debug 通道双闸门 | **⚠️ 已实现,且外审行号引错**:双闸门实际在 `app/api/deps.py:258`(非 237-238) | 属**加固**:非 development 或配了公钥时加显式启动断言 |
| 4 | `scoring.py` 仅预留桩,L3 `risk_score` 恒 NULL | **✅ 属实,但是设计意图**:`scoring.py:18` 抛 `NotImplementedError`,docstring 明写「FR-7 本期静态映射、评分模型后置」,签名已冻结 | 列入下一里程碑实现(PRD R-05) |
| 5 | 跨库最终一致无自动补偿 | 未实证(按架构判断基本属实) | `rebuild_alerts.py` 纳入 cron + 监控告警 |
### P1 · 可维护性债务
| # | 问题 | 核实结论 | 建议 |
| --- | --- | --- | --- |
| 1 | C5 红线权限逻辑分散三处(`deps.py` 矩阵 / `chat.py:122` / `tool_service.py:138`) | 未核 | 抽 `PermissionPolicy` 类集中,或至少加文档交叉引用清单 |
| 2 | 意图识别仅关键词 | 属实(架构已声明为有意取舍) | 先做同义词扩展表(低成本),再评估 LLM 路由(会损失可复现性) |
| 3 | `entities.py` 31 个 ORM 类闲置 | **✅ 属实**(实测 `^class ` 计数 = 31),查询走 text SQL + dict | 渐进引入 TypedDict,先从高频 Tool 入手 |
| 4 | 注入词表硬编码 45 条 | 未核 | 配置化(表或 `.env` 多行)+ 热加载 |
| 5 | 日志基建缺失 | **✅ 属实**:`app/utils/logger.py` 全文仅一行 `"""日志模块。"""` | 接入 structlog + `logging.config.dictConfig`,统一 JSON 落盘 |
### 无需反悔(外审已确认的既有架构决策)
四 Agent 差异表达(一张图 + 数据驱动)· Tool 与 LLM 流式(Tool 同步跑完再流式)· 意图识别(关键词,可复现可单测)· 适当性矩阵(查表数据驱动)· 降级策略(限流 fail-open、Embedding fail-closed)· 阻断逻辑(网关专用分支,物理隔离防误扩权,**仅 R-02 可阻断**)。
---
## 二、基金转换架构 v1.0 独立评审(2026-09-10 已出 · 已逐条判定 · **已全部落地**)
**评审结论:✅ 通过(M-7 满足),唯一硬前置 = R1。**
**判定结果:接受 10 · 修正性接受 3 · 驳回 0。**
**落地状态:✅ 13 条全部闭合**(R2~R5 / S1~S5 / Q4·Q6·Q10 落架构正文各节 + 架构 §13.2 索引表;
R1 升级为 T-0 阻断前置;S2 另回填 PRD v0.9 枚举;R2 回填 PRD v0.9 响应字段)。
### 硬风险(R1~R5)
| # | 意见 | 判定 | 处置与证据 |
| --- | --- | --- | --- |
| **R1** | sqlite/MySQL `core_holding` 列名不同名 | **接受(升级为 T-0 阻断前置)** | **已核实属实**:`tests/_ddl.py:50-54` 只有 `market_value`/`quantity`,且**无 PK/唯一约束**(额外发现)。处置:**T-0 先于 T-1** —— 以 MySQL 为准统一 + `conftest.py` 启动期列名断言 |
| R2 | T+1 自然日近似 vs 真实工作日 | **接受** | 响应与审计加 `confirm_basis: "natural_day_approx"`;真实 Core 接入后补交易日历 |
| R3 | 阶段 1.5 异常不阻断、不重试 | **接受(一期备注)** | 一期人工 SLA 24h;评审建议的「补偿队列 / Redis 事件 + cleanup 顺带巡检补跑」记**二期** |
| R4 | `convert_batch_max_lots=200` 超限 400 | **接受** | 保留 400 `TOO_MANY_LOTS` + 注释「自动分拆多笔留二期」 |
| R5 | `to_fund_ratio=1.0` 不参与计算 | **接受** | 已在 §0.4 / D16 注明「本期不参与计算」;二期启用时同步改 `fee.py` |
### 设计建议(S1~S5)
| # | 建议 | 判定 | 处置与证据 |
| --- | --- | --- | --- |
| S1 | `lot_id` 改 BIGINT + UNIQUE | **修正性接受** | **理由部分不成立**:FIFO 排序依据是 `confirmed_at`(架构 §7 + PRD §4.1 已建 `KEY(customer_id,product_id,confirmed_at)`),**不是 `lot_id`**;且 `lot_id` 已是 PK(唯一)。处置:`ORDER BY confirmed_at, lot_id` 作确定性 tiebreaker,**主键类型不动**(真实 TA 注册登记批次号是字符串) |
| S2 | cleanup 改 `status='expired'`,不硬删 | **接受** | 建表即含 `'expired'`(新表零 ALTER);同步改 PRD §4.1 ENUM |
| S3 | `PlanResult` 加 dataclass/TypedDict | **接受** | 防 dict 键拼写错误;呼应主架构 `entities.py` 闲置问题 |
| S4 | `client_request_id` 加正则 | **接受** | **有现成参照**:`app/main.py:44` `_TRACE_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,64}$")`,直接复用 |
| S5 | nav stale 按产品类型区分 | **修正性接受** | 模拟库 14 产品均日频、无频次差异;真实 Core 接入后再按产品类型分档 |
### 自检十问追问(Q4/Q6/Q10)
| # | 追问 | 判定 | 处置与证据 |
| --- | --- | --- | --- |
| Q4 | `rebuild_alerts.py` 是否幂等 | **修正性接受** | **已幂等**:`scripts/demo/rebuild_alerts.py:8-10` docstring + `:54-66` 的 `find_alerts_by_trade(trade_id)` 命中即 `skipped`,**不需要「再加 uk_idem」**。但 convert 补偿须以**转出端 `out_trade_id` 为幂等锚点**(一次转换两条流水) |
| Q6 | 并发重试间隔是否够 | **接受** | §10 增「50 并发压力用例」验证重试成功率 |
| Q10 | `rounding_diff` 谁承担 | **接受** | 审计落 `rounding_diff` 正负向与金额,供二期对账 |
### 任务优先级
**接受**:插入 **T-0(列名统一 + 启动断言)** 于 T-1 之前。
---
## 三、交叉点(✅ 已拍板 · 2026-09-10)
**P0-1 / C1 · Core 读写账号分离** —— 用户拍板结论:**「按真实项目走,贴近真实项目做法」= 采纳独立账号方案**。
**历史脉络(重要 · 勿重复设计)**:本项在主架构线**已评审过** ——
`改进方案评审-问题清单与对比.md` **§C1**(`core_ro` 无 DB 级只读账号,红线相关,**推荐方案 A:独立只读用户 GRANT SELECT ONLY**,
否决 B「代码层 SQL 白名单」:易被注释/子查询绕过),并已挂进 `开发计划-架构改进.md **§5.3**`(标注「需运维配合」,**未实施**)。
本次由基金转换线**接手落地**。
**落地清单** → `架构设计-基金转换交易.md` **§11.1**(账号矩阵 / 配置 / `db.py` 改造 / 3 个权限断言)+ **D20** + **T-0b**:
| 账号 | 授权 | 使用者 |
| --- | --- | --- |
| `xh_core_ro` | `SELECT` 全库 | `core_ro` / `core_tools` / `deps` / risk / **gateway 的校验读** |
| `xh_core_rw` | 4 表 `SELECT/INSERT/UPDATE`,**无 DELETE / DDL** | **仅 gateway 写路径** |
| `xh_agent_rw` | 业务表读写 + **`audit_log` 只授 INSERT** | Agent 侧全部 |
| `root` | 全权 | **仅** `00-grant.sql` / `01-ddl.sql` / `reset.ps1` |
> **对基金转换的意义**:`app/gateway/convert_core_repository.py` 是**唯一写 `jinrong_core` 的层** →
> 必须持有 `rw` 账号。若按「只读连接池」一刀切,T-1/T-6 会直接返工 —— **该风险已由 D20 消除**。
> 附带收益:把项目红线「**审计表只 INSERT**」从代码约定升级为 **DB 级强制**(`audit_log` 回收 UPDATE/DELETE)。
---
## 四、处理时机
| 阶段 | 动作 | 状态 |
| --- | --- | --- |
| 基金转换评审出来 | 两份**合并**,逐条判定 → 更新各自架构文档 | ✅ 已完成(13 条落地,架构 §13.2 索引) |
| 基金转换开工前(T-1 之前) | **必须定 P0-1 账号方案**(见 §三) | ✅ **已拍板**:按真实项目走 = 独立账号方案(D20 / T-0b) |
| 基金转换 T-0 | sqlite/MySQL 列名统一 + `conftest.py` 启动断言(R1) | ⏳ 待开发 |
| 基金转换 T-0b | DB 账号分离:`00-grant.sql` + `db.py(db, role)` + 3 权限断言(D20) | ⏳ 待开发 |
| 下一迭代 | P0-4 `scoring.py` 实现 · P1-5 日志基建 · P1-2 同义词表 · §一其余 P0/P1 | ⏳ 排期 |
> **收口结论**:基金转换线**文档阶段已闭环**(PRD v0.9 + 架构 v1.0 + 评审 0 悬空),
> 门控 M-7 满足、可进第 4 步开发计划;**唯一开工前置**是 §三 的 gateway 可写账号方案(用户拍板)+ T-0(开发首个任务)。
### 补充:执行期风险 5 条(用户 2026-09-10 提供 · 已并入架构)
| # | 风险 | 架构落点 |
| --- | --- | --- |
| 1 | T-0 未完成就跑 T-1~ | 架构 §12 #1(CI 门禁 `tests/test_db.py::test_core_holding_columns`)+ §15 T-0 依赖 |
| 2 | 批次补建规则写两处漂移 | 架构 **D18**(抽 `lot_bootstrap.bootstrap_lots()` 纯函数)+ §12 #2 + §15 T-2/T-10 |
| 3 | `Decimal` 量化未传 `ROUND_HALF_UP` | 架构 §1 原则 10 + §12 #3(`.5` 边界断言) |
| 4 | 阶段 1.5 异常不丢预警 | 架构 **D19**(`on_error_hook` 可插拔)+ §6.2 + §12 #4 |
| 5 | `convert_batch_max_lots` 超限 | 架构 §1 原则 12 + §8.2 `batch_count`/§8.3 响应体 + §12 #5(前端文案) |
> 另:用户提供的任务拓扑已落 **架构 §15.1**(并行组 A/B、关键路径、高风险任务 T-10、门禁与基线)。