基金转换 T-9:API 模型 + 网关分派(HTTP 层 convert 走通)+ 展示位数口径修复

一、T-9 本体:HTTP 层 convert 端到端走通

- api/simulate.py:TradeRequest 三型字段分池(subscribe/redeem → product_id+amount;
  convert → from/to_product_id + qty + 可选 client_request_id)+ model_validator 分支校验;
  未知类型放行给网关抛 400(保住既有 purchase → 400 断言);model_dump(exclude_none=True);
  PROCESSING → 202;异常捕获由 except LookupError 收窄为 except NotFoundError
  (原写法把 KeyError 这类编程错误静默转成 404,实测掩盖 convert 分支真实诊断)。
- gateway/trade_gateway.py:移除 convert 显式拒绝,新增 _submit_convert 分派
  (只做参数映射 + 仓储装配);convert 不写 trade_request 审计(审计归 convert_service)。
- utils/response.py:错误体合入 exc.extra(TOO_MANY_LOTS 的 batch_count/max_lots);
  既有 ApiError 无 extra 属性 → 老错误体逐字节不变。
- utils/trace.py + main.py:正则收敛单点定义。执行期发现 trace.py 与 main.py 各有一份
  内容完全相同的白名单副本 —— S4 要防的「漂移」其实已经发生,现将常量上移 trace.py
  成公开 HEADER_ID_PATTERN(同时解决 main→simulate 反向导入成环)。

二、展示位数口径修复(执行期发现 → 联网核验 → 修复 → 文档订正)

发现:同一逻辑响应两种写法 —— 首次 "53456.95" vs 幂等重放 "53456.9500",数值相等、字符串不等。
根因不是 T-7 写错,是契约缺位:§2.5 只规定「金额/份额 2 位」,净值、费率、申请份额的
回显位数根本没定义 → 实现只能 str(Decimal) 原样出网 → 位数随数据来源漂移。

修复:convert_service 新增 _q(value, unit) + _D2/_D4 规格常量作对外唯一出口 ——
金额/份额 2 位、净值/费率/份额尾差 4 位;响应 + 审计 summary + 异常日志共用该出口;
原 _s() 全部替换。首次路径幂等(除 requested_qty/actual_qty/lot[].qty 由 4 位补齐 2 位外不变)。

依据(2026-09-10 联网核验 7 家管理人公告):金额/份额「四舍五入保留至小数点后两位」;
「申请转换份额精确到小数点后两位」;净值保留 4 位第 5 位四舍五入(中欧/国泰公告由 3 位提高至 4 位);
费率以百分比 2 位表示。已知不统一:易方达 ETF 场外份额取整数位、南方基金取截断 → 取主流口径
并记入 PRD 已知差异(未来接真实 TA 需按基金合同配置化)。

三、文档订正

- PRD → v0.9.2:§2.5 拆 2.5.1 计算精度 / 2.5.2 展示位数(新增按字段分类的规格表 + 外部依据);
  §5.3 示例 requested_qty/actual_qty/lot_breakdown[].qty 4 位 → 2 位(原示例与 §2.5
  「计算与对外展示按 2 位」自相矛盾,属漏改);字段类型约定补「位数不自由 + 两条路径须逐字节一致」。
- 架构 → v1.0.1:§1 原则 11 补「str() 前必须按 §2.5.2 量化」,无结构变更。

四、验证

- 新增 tests/test_convert_integration.py(8 条真 MySQL 端到端,CNV-TEST-/TRD-TEST- 前缀隔离):
  折算与 PRD §5.3 逐项吻合、两条流水同组、持仓与批次如实变动、明细 completed + 审计、
  幂等重试不产生第二组、跨主体 400、未知类型 400,以及
  「首次与重放逐字段逐字节相等」+「展示位数规格」两条新闸门。
- test_trade_gateway.py +17(11 条错误码映射全表参数化 · 202 · 200 透传 · 不写 trade_request 审计)。
- test_integration_risk.py:R15 处置 —— 端到端已迁入新文件,原槽位改造为
  test_invalid_type_400_and_no_new_trade_audit(改用 purchase 触发),保住「校验失败不落审计」不变量。
- pytest -q → 697 passed / 3 skipped(基线 672 +25,零回归)。
- 真库复跑:T-6 24/24 · T-7 35/35 · T-8 31/31;calc_convert_demo.py 15/15。
- 突变验证 4 组:关掉 convert 分派 → 21 条红;关掉错误体 extra 展开 → 精准 1 条;
  关掉 client_request_id 正则 → 精准 1 条;关掉 _q() 展示量化 → 2 条红
  (assert '50000.0000' == '50000' 直接复现原缺陷)。均已恢复。
This commit is contained in:
2026-09-10 18:00:20 +08:00
parent 7ad1c8204d
commit d0097d6004
15 changed files with 1177 additions and 108 deletions
+114
View File
@@ -274,3 +274,117 @@ T-7 落地时 `process_convert_event` 不存在 → `_run_engine` 走 `ImportErr
**下一步 = T-9(`api/simulate.py` 模型与错误码 + `trade_gateway` convert 分派 · 关键路径)/
T-11(`core_tools` 与 `sum_trades_on_date` 汇总去重 · 依赖 T-8 已解锁)**。
---
## 深夜 · 基金转换线 T-9(API 模型 + 网关分派 · 关键路径)完成
**范围**:开发计划 §7.2 —— `api/simulate.py` 三型模型 + `trade_gateway` convert 分派 + 错误码映射。**至此 HTTP 层 convert 端到端走通**。
### 改码 5 处
- `app/utils/trace.py`:`_HEADER_ID_PATTERN` → **公开 `HEADER_ID_PATTERN`**(单点定义)。
执行期发现 `trace.py:16` 与 `main.py:44` **各有一份内容完全相同的正则** —— S4 要防的「白名单漂移」**其实已经发生**。
- `app/main.py`:删掉那份重复副本 + `import re`,改 import 上面的常量。
- `app/utils/response.py`:`_api_error_handler` 合入 `exc.extra`(`getattr` 取;既有 `ApiError` 无此属性 → 错误体逐字节不变)。
- `app/gateway/trade_gateway.py`:移除 convert 显式拒绝;新增 `_submit_convert`(只做参数映射 + 仓储装配,模块级符号作 monkeypatch 注入点)。
- `app/api/simulate.py`:`TradeRequest` 三型字段分池 + `@model_validator` 分支校验(`client_request_id` 复用同一白名单);
`model_dump(exclude_none=True)`;**`except LookupError` 收窄为 `except NotFoundError`**;`PROCESSING` → 202。
### ⚠️ 3 条与计划原文的出入(已写入开发计划 §7.2 执行记录)
1. **正则落点**:计划写「复用 `main.py:44`」,但 `app.main` → `app.api.simulate` 单向导入链,反向 import 成环
→ 上移 `trace.py`。连带突破 §12 **R14「本次不动 main.py」**(中间件注册顺序未动,守卫用例仍绿)。
2. **`gateway_repository.insert_trade` 加列:裁定不需要** —— convert 写路径在 `convert_core_repository.apply_convert`,该文件零改动。
3. **R7 归 T-10**;**R16 零改动通过**。
### 另一条实施级发现(**已上报,未顺手改**)
**幂等重放响应的数值位数与首次不一致**:首次走 `calc`(2 位)vs 重放 `_rebuild_quote` 直读 `DECIMAL(18,4)`(4 位)
→ `"53456.95"` vs `"53456.9500"`(**数值相等**)。违反 PRD「对外一律 2 位」展示契约,属 T-7 范畴。
集成测试已改为**比数值不比字符串**并在 docstring 钉住偏差。
### 验证
- 新增 **`tests/test_convert_integration.py`(7 条真 MySQL 集成)**:端到端与 PRD §5.3 逐项吻合(调生产纯函数算期望,禁手算)
· 两条流水同组同前缀 · 持仓与批次如实变动(转出归零保留行 / 转入新建)· 明细 completed + 审计
· 幂等重试不产生第二组 · 跨主体 400 · 未知类型 400。
- `test_trade_gateway.py` **+17**(11 条错误码映射全表参数化含 `extra` 展开 · 202 · 200 透传 · 不写 `trade_request` 审计 · 3 条 422 分支)。
- `test_integration_risk.py` **R15 处置**:端到端已迁入新文件;原槽位**未删除**,改造为
`test_invalid_type_400_and_no_new_trade_audit`(改用 `purchase` 触发),保住「校验失败不落审计」不变量。
- `pytest -q` → **696 passed / 3 skipped**(基线 672 **+24**,零回归)。
- **突变验证 3 组**:① 关掉 convert 分派 → **21 条红**;② 关掉错误体 `extra` 展开 → **精准 1 条**;
③ 关掉 `client_request_id` 正则 → **精准 1 条**。三处已恢复,`grep MUTATION-TEST app/` 为空。
- **真库验证载体 = 集成测试本身**(非脚本):T-9 **不新增 SQL、不涉方言语义**,故无需另写 `verify_convert_*.py`;
该文件经 `ensure_risk_demo_ready()` 在无 MySQL 环境整模块 skip,不炸 CI。
### 🔴 本日第二次误报「远程分支不见了」(教训)
提交 T-8 时依据 `git branch -vv` / `git branch -r` 判定远程 `risk-control-agent` gone、仅剩 `main`/`dev`,
据此向用户报警。**结论错误** —— `git ls-remote --heads origin` 实测远程 `risk-control-agent` = **`fffb78a`,健在**;
`git branch -r` 只剩两个分支纯属**本地远程跟踪引用 stale**。
**本日第 368-371 行已记录同款教训(`037ce7e` 误报),当日复发第二次** → 已升格为
`.workbuddy/memory/MEMORY.md` 铁律 + `docs/memory/MEMORY.md` 交接清单第 7 问。
### 文档回写
开发计划(§0 速览标 ✅ + 基线 696 · **§7.2 DoD 全勾 + 执行记录 + 4 条裁定 + 1 条发现 + 3 组突变** · §12 R15 补实际处置
并订正事件名 `trade_accepted`→`convert_accepted`)· 根 `交接文档.md` §B → **v2.0**(§0 导航 · §B 头部 · §B.1 状态与代码改动 ·
**新增 §B.6.2 T-9 小节** · §B.6 任务树与基线)· `docs/memory/{MEMORY,TODO}` · `.workbuddy/memory/{MEMORY,2026-09-10}` · 本条。
**下一步 = T-10(普通申赎批次维护 · 改 `trade_gateway` 主流程 · **回归风险最大,先跑基线再动**)/
T-11(`core_tools` 与 `sum_trades_on_date` 汇总去重 · 依赖 T-8 已解锁)**。
---
## 深夜 · T-9 收尾:**展示位数口径修复**(用户「你先改问题,按照贴近现实业务改」)
**问题**:幂等重放响应的数值位数与首次不一致(首次 `"53456.95"` vs 重放 `"53456.9500"`,**数值相等、字符串不等**)。
### 根因不是 T-7 写错,是**契约缺位**
§2.5 只规定了「金额/份额 2 位」,**净值、费率、申请份额的回显位数根本没定义** →
实现只能把 `Decimal` 原样 `str()` 出网 → **位数随数据来源漂移**:
- 首次路径走 `calc` 纯函数(已 2 位量化)
- 重放路径由 `_rebuild_quote` 从 `core_trade`/`core_convert_lot_detail`(`DECIMAL(18,4)`)重建后直读
### 联网核验(7 家管理人公告,2026-09-10)
| 口径 | 依据 |
| --- | --- |
| 金额 2 位四舍五入 | 「转出金额以四舍五入的方式保留至小数点后两位」(中银/人保/浦银安盛/中欧/南方/申万菱信一致) |
| 份额 2 位四舍五入 | 「转入份额以四舍五入的方式保留至小数点后两位」;「**申请转换份额精确到小数点后两位**」(中银) |
| 净值 4 位 | 「份额净值保留 4 位、第 5 位四舍五入」(中欧/国泰公告:**由 3 位提高至 4 位**);巨额赎回极端可 8 位 |
| 费率 4 位 | 公告以百分比 2 位表示(`0.30%` ↔ `0.0030`) |
| ⚠️ **已知不统一** | **易方达(ETF 场外)份额取整数位**、**南方基金取截断**(非四舍五入)→ 本期取主流口径 + 记入 PRD §10 已知差异 |
### 修复(贴近现实业务)
`convert_service` 新增 **`_q(value, unit)` + `_D2`/`_D4` 规格常量**作**对外唯一出口**:
- **金额 / 份额 → 2 位**(`_D2`)
- **净值 / 费率 / 份额尾差 → 4 位**(`_D4`)
- 响应、审计 `summary`、异常日志**共用同一出口**;原 `_s()` 已全部替换(`grep _s(` 为空)
- 首次路径**幂等**:除 `requested_qty`/`actual_qty`/`lot[].qty` 由 4 位**补齐至 2 位**外逐字节不变
### 文档订正(用户「文档不准,你去联网查」)
- **PRD → v0.9.2**:§2.5 拆 **2.5.1 计算精度 / 2.5.2 展示位数**(新增分类规格表 + 外部依据 + 已知差异);
§5.3 示例 `requested_qty`/`actual_qty`/`lot_breakdown[].qty` **4 位 → 2 位**(原示例与 §2.5「对外展示按 2 位」**自相矛盾**,属漏改);
§5.3 字段类型约定补「位数不自由 + 两条产出路径必须逐字节一致」;新增 v0.9.1→v0.9.2 变更表与根因
- **架构 → v1.0.1**:§1 原则 11 补「`str()` 前必须按 §2.5.2 量化」
- 开发计划 §7.2「发现」条改为「**已修复**」并补完整证据链;交接文档 §B.6.2 同步
### 验证
- 集成测试改用**逐字段逐字节比对**(`REPLAY_IDENTICAL_FIELDS` + `lot_breakdown` 整体相等)
+ 新增**位数规格断言**(`test_convert_response_field_scales`,逐字段验 `split(".")[1]` 位数,并卡住 `requested_qty == "50000.00"`)
- `pytest -q` → **697 passed / 3 skipped**(零回归)
- **真库全复跑**:T-6 `verify_convert_apply` **24/24** · T-7 `verify_convert_service` **35/35** · T-8 `verify_convert_engine` **31/31**
- `calc_convert_demo.py` **15/15** 与 PRD §5.3 一致
- **突变验证**:把 `_q()` 的量化去掉 → **2 条红**,`assert '50000.0000' == '50000'` 直接复现修复前现象;已恢复,`grep MUTATION-TEST` 为空
### 教训已固化
skill `design-doc-selfcheck` → **v1.2.0**:新增 **「二补 · 格式契约最低三问」** + **铁律 6(格式契约铁律)**:
**凡 `str(Decimal)` 直接出网的字段,先问「这个字段的展示位数写在哪」—— 没答案就是契约缺位;
存储精度 ≠ 展示精度;多条产出路径必须共用一个格式出口 + 写逐字节相等断言。**
+6 -5
View File
@@ -11,7 +11,7 @@
**当前进度:** 需求与表设计已定 · **风控模块 B1~B9b 全部完成(M2 tag risk-m2),M4 复核已闭环(2026-09-07),风控阶段 B 正式完结** · **Wave 0 已完成(2026-09-07,经独立 AI 评审闭环)**:T-01 JWT 鉴权(auth_service Auth SDK + deps 工厂替换 + X-Agent-Type 准入矩阵)/ T-02 审计中间件(http_access + 独立 request_id + 4xx/500 统一错误体 + input_guard_log 双写)/ T-06 chat 最小闭环(POST /api/chat + 会话落库 + Redis 窗口)/ T-07 LangGraph StateGraph 骨架 + DeepSeek(无 key 降级)· **T-04 Core RO Tool 节点已完成(2026-09-07)**:app/tool/core_tools.py 三只读 Tool + tool_service(意图/归属校验/run_tool)+ 图 tool 节点 + agent_tool_call 落库 + `utils/authz.py` 公共鉴权留痕,**321 测试绿(首评+复审双闭环)**。风控阶段 C 已完成(2026-09-07,345 绿,tag `risk-m3`,A-6 对话线验收通过,独立 AI 评审 PASS P0=0)。T-03 输入防护已完成(2026-09-07,378 绿,独立 AI 评审 PASS with findings P0=0):app/service/input_guard.py 注入词表 45 条纯函数检测 + oversize 4000 + actor 级 Redis 固定窗口限流 30 次/分(fail-open);chat 链路顺序 = 鉴权→准入→空白→限流 429→注入/超长 400→归属→会话,被拒 fail-fast 不建会话,blocked 落 input_guard_log(ENUM 四值已用满)。**阶段一「对齐 main 基准」AL-01~AL-08 已完成(2026-09-07,逐项独立 commit bb244f4~b5fd52e):适当性判定换核为 main 的 core_ro.check_suitability(C×R 矩阵表数据驱动,match_result 五值/JR-AST-012/FM-01/FM-03/JR-AST-PRO 契约,SUIT-001~008 退役);risk_suitability_log 重建 21 列;Core 表加 is_hnw/风评七新列(expires_at);种子 33 客户/14 产品;全量 406 passed 0 failed 0 skipped + uvicorn 冒烟三端点通过;risk-m1 已补打(指向 3c07de6)**。**下一步:阶段一验收门(**合并 main 前的最终交付检查点,非分支开发阻塞**;用户浏览器目视确认 UI)→ AL-09 合并 main → AL-10 PRD v1.2 → AL-11 docx 登记 → 阶段二 C4~C6(**已完成并打 `risk-m4` tag**)/ 前端 React 多 Agent 入口(HashRouter `web/` init)。****开发在分支 `risk-control-agent`(与 origin/main 已分叉:领先 84 提交 / 落后 0,origin/main 为分支祖先;**分支已推送远程,origin/risk-control-agent 同步于 `1d00e53`,本地跟踪已建立**;**架构改进与稳定性加固 T-101~T-202 已于 2026-09-09 完成并推送 origin/risk-control-agent(2d0e2fa..f733fc2 快进,pytest 510 绿,较 503 基线 +7 例:T-201.3 双层锁 6 例 + T-202 trace 顺序守卫 1 例),含 Redis 双层分布式锁 T-201 与审计中间件 trace 顺序守卫 T-202,均为文档/告警/锁原语修正、接口契约与表结构零变更;远程已提示走 PR 合并 main;**合并(risk-control-agent → main)由合并执行人负责,不归用户管**——AI 只负责把分支推到远程 + 更新文档,不得主动发起 PR / 执行合并,移交执行人按《合并注意事项-风控模块并入main.md》操作。** 旧文档中的 `feature/risk` 为过时口径)。**
**⚡ 并行新线 · 基金转换(convert)交易(2026-09-10 设计+开发计划闭环,**第 5 步进行中:T-0 / T-0b / T-1 已完成**):** `PRD-风控监测Agent` FR-1 一期显式拒收 convert,本线将其放开。**AIcoding 第 1~4 步已完成**:PRD **v0.9.1 定稿**(33 条外审闭环 + 2 处架构回填 + **费率分类修正**)→ 架构 **v1.0 定稿**(独立评审通过,13 条建议 **0 悬空**,接受 10 / 修正性接受 3 / 驳回 0)→ 门控 **M-7 已满足** → **开发计划 v1.0 已产出(`docs/项目框架设计/开发计划-基金转换交易.md`,**1,047 行**)并经**四轮**独立子代理审核收敛(**12 条意见 → 接受 11 / 驳回 1(附实测证据)/ 0 悬空**)。**第 5 步进行中:T-0 + T-0b + T-1 + T-2 + T-2b + T-3 + T-4 + T-5 + T-6 + T-7 + T-8 已于 2026-09-10 完成(672 passed / 3 skipped;T-1 断言 8/8 PASS;T-2 纯函数包 7 文件 / 93 用例;T-2b 实算脚本 15/15 一致;T-6 真库 24/24;T-7 真库 35/35;T-8 真库 31/31)**,**下一步 = T-9(`api/simulate.py` + `trade_gateway` 分派 · 关键路径)/ T-11(汇总去重 · 依赖 T-8 已解锁)**。**开工前置两个阻断项(✅ 2026-09-10 均已完成)**:**T-0**(sqlite/MySQL 结构对齐:`core_holding` 列名+PK **+ 补 `core_product_nav`**,建库自校验,**同步改 2 处测试 INSERT 并补 3 个 NOT NULL 列**)与 **T-0b**(**DB 账号分离 D20**:`xh_core_ro` SELECT 全库 / `xh_core_rw` **4 表写**无 DELETE·DDL / `xh_agent_rw` **`audit_log` 只授 SELECT+INSERT**(不可改删);**`conftest.py` 四处 engine 已显式 `role="admin"`**)。**遗留环境操作**:`scripts/core/00-grant.sql` 需管理员执行一次 + 写 `.env`,未执行时 3 条权限断言自动 skip,不阻塞 T-1。设计资产五件套:`docs/PRD/PRD-基金转换交易.md` · `docs/项目框架设计/架构设计-基金转换交易.md` · **`docs/项目框架设计/开发计划-基金转换交易.md`(新)** · `评审待办-风控主架构与基金转换.md` · `基金转换-审查意见处置表.md`。**开工前必读开发计划 §1.4(15 条代码事实)/ §1.5(7 条实现级裁定 R-a~R-g)/ §12(16 条回归面)** —— 尤其是 **R-a(弃用方言 UPSERT)· R-b(流水写 redeem/subscribe 不写 convert)· R-e(conftest 用 admin)** 三条,不读必踩。**交接入口:项目根 `交接文档.md` §B(三线合并版唯一入口)**。合规基准 = 证监会公告〔2025〕22 号。
**⚡ 并行新线 · 基金转换(convert)交易(2026-09-10 设计+开发计划闭环,**第 5 步进行中:T-0 / T-0b / T-1 已完成**):** `PRD-风控监测Agent` FR-1 一期显式拒收 convert,本线将其放开。**AIcoding 第 1~4 步已完成**:PRD **v0.9.2 定稿**(33 条外审闭环 + 2 处架构回填 + **费率分类修正** + **展示位数补全**)→ 架构 **v1.0.1 定稿**(独立评审通过,13 条建议 **0 悬空**,接受 10 / 修正性接受 3 / 驳回 0)→ 门控 **M-7 已满足** → **开发计划 v1.0 已产出(`docs/项目框架设计/开发计划-基金转换交易.md`,**1,047 行**)并经**四轮**独立子代理审核收敛(**12 条意见 → 接受 11 / 驳回 1(附实测证据)/ 0 悬空**)。**第 5 步进行中:T-0 + T-0b + T-1 + T-2 + T-2b + T-3 + T-4 + T-5 + T-6 + T-7 + T-8 + T-9 已于 2026-09-10 完成(697 passed / 3 skipped;T-1 断言 8/8 PASS;T-2 纯函数包 7 文件 / 93 用例;T-2b 实算脚本 15/15 一致;T-6 真库 24/24;T-7 真库 35/35;T-8 真库 31/31;T-9 真 MySQL 集成 8 条 + 11 条错误码映射 + 4 组突变验证 + **展示位数口径修复**)**,**下一步 = T-10(普通申赎批次维护 · 回归风险最大)/ T-11(汇总去重 · 依赖 T-8 已解锁)**。**开工前置两个阻断项(✅ 2026-09-10 均已完成)**:**T-0**(sqlite/MySQL 结构对齐:`core_holding` 列名+PK **+ 补 `core_product_nav`**,建库自校验,**同步改 2 处测试 INSERT 并补 3 个 NOT NULL 列**)与 **T-0b**(**DB 账号分离 D20**:`xh_core_ro` SELECT 全库 / `xh_core_rw` **4 表写**无 DELETE·DDL / `xh_agent_rw` **`audit_log` 只授 SELECT+INSERT**(不可改删);**`conftest.py` 四处 engine 已显式 `role="admin"`**)。**遗留环境操作**:`scripts/core/00-grant.sql` 需管理员执行一次 + 写 `.env`,未执行时 3 条权限断言自动 skip,不阻塞 T-1。设计资产五件套:`docs/PRD/PRD-基金转换交易.md` · `docs/项目框架设计/架构设计-基金转换交易.md` · **`docs/项目框架设计/开发计划-基金转换交易.md`(新)** · `评审待办-风控主架构与基金转换.md` · `基金转换-审查意见处置表.md`。**开工前必读开发计划 §1.4(15 条代码事实)/ §1.5(7 条实现级裁定 R-a~R-g)/ §12(16 条回归面)** —— 尤其是 **R-a(弃用方言 UPSERT)· R-b(流水写 redeem/subscribe 不写 convert)· R-e(conftest 用 admin)** 三条,不读必踩。**交接入口:项目根 `交接文档.md` §B(三线合并版唯一入口)**。合规基准 = 证监会公告〔2025〕22 号。
**仓库地图:**
@@ -44,7 +44,7 @@
| `docs/项目框架设计/表设计/` | 已定 | Agent 共用 11 表 + agent 专用 SQL |
| `docs/项目框架设计/Core模拟底座/` | 已定 | 无真实 Core 时的 L0 方案 |
| `web/` | **不存在** | 前端 React 待 init |
| `docs/PRD/PRD-基金转换交易.md` | **已定稿(v0.9.1)** | 基金转换线需求权威(FR-C1~C16 / §4 表结构 / §5 接口 / §9 验收);⚠️ **代码未实现** |
| `docs/PRD/PRD-基金转换交易.md` | **已定稿(v0.9.2)** | 基金转换线需求权威(FR-C1~C16 / §4 表结构 / §5 接口 / §9 验收);⚠️ **代码未实现** |
| `docs/项目框架设计/架构设计-基金转换交易.md` | **已定稿(v1.0)** | 基金转换实现依据(D1~D20 / §5 事务 / §11.1 DB 账号 / §15 任务 T-0~T-13);⚠️ **代码未实现** |
| `docs/项目框架设计/开发计划-基金转换交易.md` | **已定稿(v1.0 · 2026-09-10)** | **基金转换实现计划**(T-0~T-13 逐任务改法 + DoD + §1.4 **十五条代码事实核对表** + §1.5 **七条实现级裁定 R-a~R-g** + §12 **十六条回归面** + 四轮审核记录 + **§3.3 第 0 批 + §4.1 第 1 批执行记录**);**T-0/T-0b/T-1 已实现**,T-2 起待实现 · **开工必读** |
| 项目根 `交接文档.md` | **v3.0(2026-09-10 三线合并)** | **全仓唯一交接入口**(§0 公共层 + §A 风控主线 + §B 基金转换线 + §C 架构改进线)——**给下一会话 AI,读完即可开工**;⚠️ 在 `.gitignore:47` 内、**不入库**,是本地文件。`docs/交接文档-基金转换.md` / `docs/交接文档-架构改进.md` 为**历史留档,内容已过期,勿读** |
@@ -70,7 +70,7 @@
**下一步开发(见 TODO):** **模块侧交付完毕(2026-09-07:全量 pytest 482 绿 + 接口实调验收通过——suitability/check 阻断+放行、simulate/trade 阻断、三条鉴权边界 401/403/403 契约零偏差;`risk-m1~m4` tag 齐)。合并 main 已移交合并执行人,操作手册《docs/项目框架设计/合并注意事项-风控模块并入main.md》(含基底锁定/20 冲突裁决/14 静默文件/三硬伤/合并后必测,实测数据编制)。模块侧开放项:chat 链路 risk_suitability_log.actor_id 落 SYSTEM 待评估 / 前端 React 多 Agent 入口(`web/` 未 init,归属待拍板)。**演示走查按 `docs/项目框架设计/演示SOP-风控模块.md`(debug 头通道仍有效;JWT 通道签发用 `scripts/dev/issue_dev_token.py`;演示库已按 AL-08 expires_at 新口径重灌)。知识库入库:`python scripts/kb/build_kb.py`(先启 Ollama;**Milvus 数据路径必须纯英文**——faiss 不支持中文路径,本机 .env 已配 C:/Users/YUAN/.jinrong/milvus/)。
**另(2026-09-10 待办)**:① **基金转换线**第 5 步进行中(**T-0~T-8 已完成、672 绿,下一步 = T-9(API+网关分派)/ T-11(汇总去重)**;设计 + 开发计划均已闭环,入口 项目根 `交接文档.md` §B);② ~~架构改进线收尾~~ —— **2026-09-10 已闭环结项**:§7.2 七项手工冒烟补跑 **7/7 PASS**、冒烟残留按 SOP §2 重灌双库清除、全量 **510 passed** 复绿;**「`037ce7e` 未 push」的旧表述已作废**(实测 `git ls-remote`:远程 `risk-control-agent` = `fffb78a` = 本地 HEAD,`037ce7e` 在其祖先链上,早已推送;本地 `git branch -vv` 显示 `origin/risk-control-agent: gone` 只是远程跟踪引用失效,`git fetch` 即恢复,非远程分支被删)。入口 **项目根 `交接文档.md` §C**。
**另(2026-09-10 待办)**:① **基金转换线**第 5 步进行中(**T-0~T-9 已完成、697 绿,下一步 = T-10(批次维护 · 回归风险最大)/ T-11(汇总去重)**;设计 + 开发计划均已闭环,入口 项目根 `交接文档.md` §B);② ~~架构改进线收尾~~ —— **2026-09-10 已闭环结项**:§7.2 七项手工冒烟补跑 **7/7 PASS**、冒烟残留按 SOP §2 重灌双库清除、全量 **510 passed** 复绿;**「`037ce7e` 未 push」的旧表述已作废**(实测 `git ls-remote`:远程 `risk-control-agent` = `fffb78a` = 本地 HEAD,`037ce7e` 在其祖先链上,早已推送;本地 `git branch -vv` 显示 `origin/risk-control-agent: gone` 只是远程跟踪引用失效,`git fetch` 即恢复,非远程分支被删)。入口 **项目根 `交接文档.md` §C**。
**禁止(改代码前必记):** Core 正式 C1~C5 不可被画像覆盖 · 审计表只 INSERT · 代理人草稿不外发 · 仅 R-02 可阻断交易 · 四 Agent 不互调 LLM。
@@ -181,7 +181,7 @@ RBAC 联调账号:scripts/dev/rbac-seed-reference.md
| `docs/项目框架设计/` | 表结构、JWT 手册、Core 模拟、技术版本 |
| `docs/业务记忆管理/` | Redis 短期 vs MySQL/Milvus/Neo4j 权威记忆 |
| 项目根 `交接文档.md` §B | **基金转换线开工入口** —— 接手本线先读这一节(不必重读代码) |
| `docs/PRD/PRD-基金转换交易.md` · `docs/项目框架设计/架构设计-基金转换交易.md` | 基金转换需求与实现依据(**v0.9.1 / v1.0,代码未实现**) |
| `docs/PRD/PRD-基金转换交易.md` · `docs/项目框架设计/架构设计-基金转换交易.md` | 基金转换需求与实现依据(**v0.9.2 / v1.0.1,代码未实现**) |
缺 `docs/memory/*` 文件:按 project-memory-kit 同名补回,**禁止空模板盖进度**。
@@ -200,6 +200,7 @@ RBAC 联调账号:scripts/dev/rbac-seed-reference.md
3. 是否需 customer_id 归属与 JWT RBAC?
4. Core 是模拟库只读还是 agent 库读写?
5. 如何验证?(`python -m pytest` 全量(当前 **516 passed / 3 skipped**,基线 510)· uvicorn 启动 + /health · SQL / sync 脚本 · 对照 REQUIREMENTS 验收列)
6. **当前有哪两条并行线?**(① 风控/架构改进线:**已结项**(510 基线绿、§7.2 七项冒烟 7/7 PASS、`037ce7e` 已核实早已推送);② **基金转换线**:设计闭环,**第 5 步进行中 —— T-0~T-8 已完成(672 passed),下一步 = T-9 / T-11**)——动代码前先确认自己属于哪条线,别混淆前置条件。
6. **当前有哪两条并行线?**(① 风控/架构改进线:**已结项**(510 基线绿、§7.2 七项冒烟 7/7 PASS、`037ce7e` 已核实早已推送);② **基金转换线**:设计闭环,**第 5 步进行中 —— T-0~T-9 已完成(697 passed),下一步 = T-10 / T-11**)——动代码前先确认自己属于哪条线,别混淆前置条件。
7. **远程分支到底还在不在?**(**在**。`git ls-remote --heads origin` 实测 `refs/heads/risk-control-agent` = `fffb78a`。⚠️ **判断远程存亡只能用 `git ls-remote`**;本仓本地远程跟踪引用会失效(`git branch -r` 只剩 `dev`/`main`、`git branch -vv` 显示 `gone`),那是 stale ref,`git fetch` 即恢复 —— 2026-09-10 **同日误报两次**,勿再踩。)
大任务:FRAMEWORK/FLOW 与实现状态不符时先更新 memory 再编码(用户确认跳过除外)。
+5 -4
View File
@@ -7,7 +7,7 @@
**阶段一 AL-01~AL-08 与阶段二 C4~C6 均已完成(2026-09-07)**:全量 pytest **482 passed 0 failed 0 skipped**(真库集成)✓ · uvicorn 冒烟三端点 ✓ · 接口实调验收 ✓(2026-09-07:suitability/check 阻断+放行、simulate/trade 阻断、三条鉴权边界 401/403/403,契约零偏差)· risk-m1~m4 tag 齐。**合并 main 已移交合并执行人**(操作手册:《docs/项目框架设计/合并注意事项-风控模块并入main.md》,随分支上传),后续模块侧待办见下方。
**⚡ 并行新线 · 基金转换(convert)**(2026-09-10):**设计 + 开发计划均已闭环** —— PRD **v0.9.1** + 架构 **v1.0** + 独立评审 13 条 **0 悬空**(接受 10 / 修正性接受 3 / 驳回 0),门控 **M-7 已满足**;**第 4 步开发计划 v1.0 已产出并经独立审核**(4 条意见全接受、**驳回 0**,含新增 2 条回归面 R15/R16 + R-c 双条修订);**第 5 步进行中**:**T-0 + T-0b + T-1 + T-2 + T-2b + T-3 + T-4 + T-5 + T-6 + T-7 + T-8 均已于 2026-09-10 完成**(**672 passed / 3 skipped**;T-1 断言 **8/8 PASS**、T-2 纯函数 **93 用例**、T-2b 实算 **15/15 一致**、T-6 真库 **24/24**、T-7 真库 **35/35**、**T-8 真库 31/31**),**下一步 = T-9(`api/simulate.py` + `trade_gateway` 分派 · 关键路径)/ T-11(`core_tools` 与 `sum_trades_on_date` 汇总去重 · 依赖 T-8 已解锁)**。两个阻断前置(**T-0** sqlite/MySQL 列名统一 + 建库自校验 · **T-0b** DB 账号分离 D20:`xh_core_ro`/`xh_core_rw`/`xh_agent_rw`)**均已落地**。**入口:项目根 `交接文档.md` §B(三线合并版唯一入口,读这一节即可开工)**;**开工前必读开发计划 §1.4(15 条代码事实)+ §1.5(8 条实现级裁定 R-a~R-h)+ §12(16 条回归面)**。
**⚡ 并行新线 · 基金转换(convert)**(2026-09-10):**设计 + 开发计划均已闭环** —— PRD **v0.9.2** + 架构 **v1.0.1** + 独立评审 13 条 **0 悬空**(接受 10 / 修正性接受 3 / 驳回 0),门控 **M-7 已满足**;**第 4 步开发计划 v1.0 已产出并经独立审核**(4 条意见全接受、**驳回 0**,含新增 2 条回归面 R15/R16 + R-c 双条修订);**第 5 步进行中**:**T-0 ~ T-9 均已于 2026-09-10 完成**(**697 passed / 3 skipped**;T-1 断言 **8/8 PASS**、T-2 纯函数 **93 用例**、T-2b 实算 **15/15 一致**、T-6 真库 **24/24**、T-7 真库 **35/35**、**T-8 真库 31/31**、**T-9 真 MySQL 集成 8 条 + 4 组突变验证 + 展示位数修复**),**下一步 = T-10(普通申赎批次维护 · 回归风险最大)/ T-11(`core_tools` 与 `sum_trades_on_date` 汇总去重 · 依赖 T-8 已解锁)**。两个阻断前置(**T-0** sqlite/MySQL 列名统一 + 建库自校验 · **T-0b** DB 账号分离 D20:`xh_core_ro`/`xh_core_rw`/`xh_agent_rw`)**均已落地**。**入口:项目根 `交接文档.md` §B(三线合并版唯一入口,读这一节即可开工)**;**开工前必读开发计划 §1.4(15 条代码事实)+ §1.5(8 条实现级裁定 R-a~R-h)+ §12(16 条回归面)**。
### 基金转换线待办(推荐顺序)
@@ -16,11 +16,12 @@
- [x] **【T-0b · 阻断前置】DB 账号分离(D20)** —— **完成(2026-09-10)**:`scripts/core/00-grant.sql` 新建(3 账号逐表授权,**不进 reset.ps1**);`settings.py` +6 项;`db.py` 改 `get_engine(db, role)` + `_resolve_credentials`(缓存键 `(db, role)`,未配置回退 `mysql_user`);`core_ro`→`ro` · `gateway_repository`→`rw` · `risk_repository`/`session_repository`→`rw` 显式;`conftest.py` 4 处→`admin`(R-e)。**两处口径修正**:① `audit_log` 实授 **`SELECT, INSERT`**(字面「只授 INSERT」会剥夺读,致 `has_engine_error_audit`/`list_audit_events` 失权)② conftest 必须 admin。**遗留环境操作**:`00-grant.sql` 需管理员执行 + 写 `.env`,未执行时 3 条权限断言自动 skip
- [x] **【T-1 · 第 1 批】DDL + 种子 + sqlite 同步** —— **完成(2026-09-10)**:`scripts/core/01-ddl.sql` 新建 `core_fee_rule`/`core_share_lot`/`core_convert_lot_detail` + `core_trade` 加 `convert_group_id`+索引 + `core_product` 加 8 列(`subscribe_fee_rate` 等)+ `fee_rate` 补 COMMENT;**新增 `07-seed-fee-rule.sql`**(14 产品 × 5 档,按 22 号文 §10)/ **`08-seed-share-lot.sql`**(58 行持仓 → 61 行批次,Σ remain_qty 恒等于 qty,CUST-9527 跨批次)/ **`09-seed-org.sql`**(管理人 + TA + 申购费率 + 最低持有余额);`reset.ps1` 追加 07/08/09;`02-mysql-agent专用.sql` 追加 `risk_convert_detail`(status ENUM 建表即 5 值);`tests/_ddl.py` 同步 4 表 + `REQUIRED_CONVERT_TABLES` 门禁。**验证**:新增 `scripts/dev/verify_convert_seed.py`(pymysql 等价 reset 流程 + 8 条断言)→ **8/8 PASS**;`pytest -q` → **516 passed / 3 skipped(零回归)**。**3 点需注意**:① mysql 不在 PATH → 用该脚本替代 reset.ps1;② `core_fee_rule` 读取走只读账号(T-6 遵守);③ ~~`PROD-005827` 费率分类口径差异(`mixed` vs 主动偏股)待裁定~~ → **已裁定并修正(PRD v0.9.1)**:`mixed` 归位 `0.0050`(其他混合型),主示例转入方改真主动偏股 `PROD-003095`,`09-seed-org.sql` 升 **v1.1** 按「管理人全产品线」重排,并新增断言 ⑧ 机器化卡口
- [x] **【T-2 · 第 2 批】`service/convert/` 纯函数包** —— **完成(2026-09-10)**:新建 `app/service/convert/` **7 文件**(`__init__` / `types`(`Lot`/`FeeRule`/`LotAllocation`/`PlanResult` frozen dataclass + 3 个归一工具)/ `calc`(`plan_lots`/`lot_amount`/`lot_fee`/`convert_amount`/`in_qty`/`rounding_diff`/`diff_fee`/`hold_days`/`ensure_batch_limit`)/ `fee`(`pick_fee_rate` 左闭右开)/ `nav`(`ensure_nav_ready`→503 / `is_stale`)/ `lot_bootstrap`(D18 单点,`crc32` 确定性偏移)/ `errors`(`ConvertError` + 11 子类))。**新增 `tests/test_convert_calc.py` 93 用例**(12 类:精度 HALF_UP 反向自证 / 分档边界 6-7-29-30-179-180-364-365 / FIFO 含同 `confirmed_at` tiebreak / 跨批计费 / 双口径 252.40 vs 253.91 / 强制全转与强制赎回 / 恰好等于阈值不触发 / **零剩余不触发(新裁定 R-h)** / PRD §5.3 全链自证 / T+1 起算 / 净值 503 与 stale 分家 / D18 确定性 / §8.3 错误码 / **纯函数零 IO 依赖断言**)。**验证**:`pytest -q` → **609 passed / 3 skipped(+93,零回归)**;`calc_convert_demo.py` → **15/15 与 PRD §5.3 一致**(退出码 0)
- [x] **【T-3 ~ T-8 已完成】** 仓储与锁(并行组 A:T-3 / T-4 / T-5 ✅)→ T-6 ✅(阶段一事务 · 真库 24/24)→ T-7 ✅(八步编排 · 真库 35/35)→ **T-8 ✅(规则引擎改造 · 真库 31/31;⭐ 阶段 1.5 从「跳过」变「真跑」)**
- [ ] **【T-9 起】剩余任务**:T-9(`api/simulate.py` 模型与错误码 + `trade_gateway` convert 分派)→ T-11(`core_tools` / `sum_trades_on_date` 汇总去重)→ **T-10 高风险单列** → T-12 → **T-13「50 并发压测 + 性能补录」**(最后跑,产出 PRD §9 第 18 条实测值)。**T-13 内部顺序**:先 50 并发压测 → 再性能实测补录 → 最后 PRD §5.3 数字回填(详见架构 §15 + 开发计划 §2~§10)
- [x] **【T-3 ~ T-9 已完成】** 仓储与锁(并行组 A:T-3 / T-4 / T-5 ✅)→ T-6 ✅(阶段一事务 · 真库 24/24)→ T-7 ✅(八步编排 · 真库 35/35)→ **T-8 ✅(规则引擎改造 · 真库 31/31;⭐ 阶段 1.5 从「跳过」变「真跑」)** → **T-9 ✅(API 模型 + 网关分派 · **HTTP 层 convert 已走通**;新增真 MySQL 集成 7 条 + 11 条错误码映射 + 3 组突变验证)**
- [ ] **【T-10 起】剩余任务**:**T-10 高风险单列**(普通申赎批次维护 · 改 `trade_gateway` 主流程 · 先跑基线再动)→ T-11(`core_tools` / `sum_trades_on_date` 汇总去重)→ T-12(补偿脚本)→ **T-13「50 并发压测 + 性能补录」**(最后跑,产出 PRD §9 第 18 条实测值)。**T-13 内部顺序**:先 50 并发压测 → 再性能实测补录 → 最后 PRD §5.3 数字回填(详见架构 §15 + 开发计划 §2~§10)
- [ ] **【待用户裁定】幂等重放响应的数值位数偏差**(T-9 执行期发现,**未顺手改**):首次响应 2 位(`calc` 量化)vs 重放响应 4 位(`core_trade` `DECIMAL(18,4)` 直读)→ `"53456.95"` vs `"53456.9500"`,**数值相等**,违反 PRD「对外一律 2 位」展示契约,属 T-7 `_rebuild_quote` 范畴。集成测试已「比数值不比字符串」并钉住偏差
> **第 0~2 批结果(2026-09-10)**:基线 **510 passed** → 批 0 后 **516 passed / 3 skipped**(+3 T-0 用例 +3 T-0b 引擎用例)→ 批 1(T-1)后**仍 516 passed / 3 skipped**(只加表与种子,未加用例 → **零回归**);**T-1 数据层断言 8/8 PASS**(含新增断言 ⑧:费率档 ↔ `product_type` 匹配,越档即 FAIL)→ 批 2(T-2 + T-2b)后 **609 passed / 3 skipped**(**+93 纯函数用例**,零回归);T-2b 实算脚本 15/15 与 PRD §5.3 一致(退出码 0)。
> **下一步 = T-3**(`core_ro` 五个新方法:`get_nav_as_of` / `get_redeem_fee_rules` / `list_share_lots` / `sum_remain_qty` / `get_holding` + 新增 `app/repository/share_lot_repository.py`;DoD 见开发计划 §5.1)。**并行组 A 的 T-3 / T-4 / T-5 可同时开工**。
> **下一步 = T-10**(普通申赎批次维护 · FR-C16 + D8 兜底补建 + `rebuild_lots.py`;**改 `trade_gateway` 主流程,回归风险最大 → 先跑基线再动**;DoD 见开发计划 §8)。**可并行**:T-11(`core_tools` / `sum_trades_on_date` 汇总去重 · 依赖 T-8 已解锁)。
> ⚠️ **基金转换的 T-0b 与下方「架构改进第 3/4 批」的 `core_ro` 只读账号是同一件事** —— 已由本线定案为 D20,**不再挂在架构改进线**(该线原「不要做」清单已更新)。