fix(env)+docs: 按架构师第二轮纠正 mypy 根因(环境版本);补场外/推广域表登记
架构师第二轮答复里有一条技术纠正是对的,我原归因错了:
- 我原判断"184 个 mypy 错是因为缺 SQLAlchemy 2.0 类型信息"——**方向对了一半、结论反了**。
SQLAlchemy 2.0 自带 py.typed,根本不需要 sqlalchemy2-stubs(那是 1.4 用的)。
- 实测复现矩阵(决定性证据):
SQLAlchemy 2.0.34 + mypy 1.14.1 → 173 错(我原来的环境)
SQLAlchemy 2.0.34 + mypy 1.20.2 → 173 错
SQLAlchemy 2.0.52 + mypy 1.14.1 → 6 错
SQLAlchemy 2.0.52 + mypy 1.20.2 → 3 错
⇒ 主因是 SQLAlchemy 的**补丁版本**(173→6),mypy 版本是次因(6→3)。
- 已把环境升到 SQLAlchemy 2.0.52 + mypy 1.20.2(均在 pyproject 约束内),
mypy 从 184 降到 **3 个错**(剩下的 3 个全在同事的 offsite 文件里,非本线代码)。
- 没有把 sqlalchemy2-stubs 写进依赖(采纳架构师明确要求)。
- 交接文档已重写该条,并留下"两个反面教训":不要装 1.4 的存根包;补丁版差异会造成量级差异,
若要门禁稳定需把 SQLAlchemy 钉到具体补丁版(属公共约定,未擅自改)。
按架构师裁决补文档(他裁定:不动 docs/00,另立登记):
- 新增 docs/28-场外与推广域数据表登记.md:17 张表逐表登记(表名/来源迁移/归属域/当前行数),
并做规则 8 的**两向边界核对**——场内代码零引用这 17 张表(config.py 里的 offsite_ 只是配置项名)、
场外代码零写场内交易表
- docs/08 审计口径更新为「场内 51 + 场外/推广 17 = 68」,并新增"第四种坏状态"
(alembic_version 已指向新 revision 但表没建)的处置说明
- 澄清一个易误读点:audit_schema.py 的期望集合是**动态推导**的(读 baseline_generated.sql
+ 扫描 alembic/versions/*.py),表数 51→68 是自动结果,**没有人手工改期望值**;
代价是只增不减(DROP 表会报 missing 假失败)
测试:3 failed(1 既有 + 2 环境相关)/ 1218 passed;文档守卫 35 份无重号;
mypy 3 错(180 文件);结构审计 68 张表通过。
This commit is contained in:
+24
-1
@@ -43,14 +43,37 @@ ORM 映射 ↔ 库列名(生成列单独归类为 note)。任一不一致返
|
|||||||
|
|
||||||
## 四、既有库对齐(方案 A)
|
## 四、既有库对齐(方案 A)
|
||||||
|
|
||||||
|
> **⚠️ 2026-09-11 更新:业务表数已从 51 变为 68。**
|
||||||
|
> 同事那条线(袁聪)合并进来的 11 个 alembic 迁移新建了 17 张表 ——
|
||||||
|
> `offsite_*`(场外基金运营,10 张)与 `promotion_*`(推广域,7 张)。
|
||||||
|
> **这 17 张不进 `docs/00` 基线**(它冻结的是场内交易域),由
|
||||||
|
> **`docs/28-场外与推广域数据表登记.md`** 单独登记。口径统一为:
|
||||||
|
>
|
||||||
|
> ```
|
||||||
|
> 场内 51(docs/00) + 场外/推广 17(docs/28) = 68 张业务表
|
||||||
|
> (另加 alembic_version,库内共 69 张)
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> `tools/audit_schema.py` 的期望值**随迁移自动增长**(它读 `baseline_generated.sql` +
|
||||||
|
> 扫描 `alembic/versions/*.py` 的 `CREATE TABLE`,不是手写清单),所以本次 68 是自动结果,
|
||||||
|
> 没有人手工改过期望值。**架构师合并后会看到表数从 51 跳到 68,那是这 11 个迁移的预期结果,
|
||||||
|
> 不是有人绕过基线偷偷建表**(机制与代价详见 `docs/28-场外与推广域数据表登记.md` §5)。
|
||||||
|
|
||||||
基线表补入 Alembic 链首(`20260909_baseline_schema`)后,按库的来源分三种情况:
|
基线表补入 Alembic 链首(`20260909_baseline_schema`)后,按库的来源分三种情况:
|
||||||
|
|
||||||
| 库状态 | 特征 | 操作 |
|
| 库状态 | 特征 | 操作 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 空库 | 无 `alembic_version` 记录、无业务表 | `alembic upgrade head`,全量建出 51 张业务表 |
|
| 空库 | 无 `alembic_version` 记录、无业务表 | `alembic upgrade head`,全量建出 **68 张业务表**(原为 51,见上方更新) |
|
||||||
| 由 Alembic 建起 | `alembic_version` 已位于链上 | 直接 `alembic upgrade head`;Alembic 不会重跑祖先迁移,**无需 stamp** |
|
| 由 Alembic 建起 | `alembic_version` 已位于链上 | 直接 `alembic upgrade head`;Alembic 不会重跑祖先迁移,**无需 stamp** |
|
||||||
| 手工或脚本建库 | 有业务表但 `alembic_version` 为空 | 直接 upgrade 会重建已有表而失败,必须先 `alembic stamp <当前结构对应版本>`,再 `upgrade head` |
|
| 手工或脚本建库 | 有业务表但 `alembic_version` 为空 | 直接 upgrade 会重建已有表而失败,必须先 `alembic stamp <当前结构对应版本>`,再 `upgrade head` |
|
||||||
|
|
||||||
|
> ⚠️ **本次合并实测到的第四种坏状态**(原文档未覆盖,值得记一笔):
|
||||||
|
> `alembic_version` **已指向较新的 revision,但部分表并不存在** —— 即"版本号跑了、表没建"。
|
||||||
|
> 本库当时就是这种状态(`alembic_version = 20260910_drop_review_separation`,而
|
||||||
|
> `offsite_*`/`promotion_*` 一张都没有)。它靠 `alembic upgrade heads` 收敛:
|
||||||
|
> Alembic 只跑 `alembic_version` 之后的迁移,所以恰好补建了缺失的表。
|
||||||
|
> 架构师那边是另一种坏状态("表没建、版本号也没跑"),同样靠这一条收敛。
|
||||||
|
|
||||||
`tools/migration_state_check.py` 只读诊断这三种状态:读取迁移链 head、`alembic_version` 记录与
|
`tools/migration_state_check.py` 只读诊断这三种状态:读取迁移链 head、`alembic_version` 记录与
|
||||||
业务表数量,并按结构特征(生成列、联合唯一键、代表性表)由新到旧推断库实际所处版本,输出可执行
|
业务表数量,并按结构特征(生成列、联合唯一键、代表性表)由新到旧推断库实际所处版本,输出可执行
|
||||||
的建议命令。三种状态均已实测:空库判定正确;`jr` 库判定为"已在 head、无需操作";手工库场景
|
的建议命令。三种状态均已实测:空库判定正确;`jr` 库判定为"已在 head、无需操作";手工库场景
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# 场外与推广域数据表登记(`docs/00` 基线的域外补充)
|
||||||
|
|
||||||
|
> **用途**:`docs/00-新数据库基线设计.md` 冻结的是**场内基金模拟交易域**的基线(不可变,规则 1)。
|
||||||
|
> 同事那条线(袁聪)合并进来的 **17 张表属场外基金运营域与推广域**,**不进 `docs/00`**,
|
||||||
|
> 由本文单独登记,避免"基线"这个概念因为塞入独立域而失效。
|
||||||
|
>
|
||||||
|
> **维护约定**:新增/删除这两个域的表时**必须同步本文**;本文是「68 张表从哪来」的唯一答案。
|
||||||
|
> **表数口径**:场内 **51** + 场外/推广 **17** = **68**(共 69 张含 `alembic_version`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 为什么另立文档(架构师裁决,2026-09-11)
|
||||||
|
|
||||||
|
依据 `AGENTS.md` 规则 8:
|
||||||
|
|
||||||
|
> 当前系统业务功能只针对场内基金模拟交易;**场外基金运营流程独立,不得写入场内交易表**。
|
||||||
|
|
||||||
|
⇒ 场外与推广是**独立域**。把它们塞进 `docs/00` 会让"基线"从"场内交易域的冻结定义"
|
||||||
|
变成"当前全部表",下次有人拿 `docs/00` 当依据时会错。因此:
|
||||||
|
|
||||||
|
| 文档 | 职责 |
|
||||||
|
|---|---|
|
||||||
|
| `docs/00-新数据库基线设计.md` | **场内交易域**基线(不可变,51 张) |
|
||||||
|
| **本文** | 场外/推广域(17 张)的登记与来源 |
|
||||||
|
| `docs/08-数据库结构审计基线.md` | 审计口径:场内 51 + 场外/推广 17 = 68 |
|
||||||
|
| `tools/audit_schema.py` | 期望值同为 68(已生效,见 §4) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 17 张表逐表登记
|
||||||
|
|
||||||
|
### 2.1 场外基金运营域(`offsite_*`,10 张)
|
||||||
|
|
||||||
|
| 表名 | 来源迁移 | 建立它的提交意图 |
|
||||||
|
|---|---|---|
|
||||||
|
| `offsite_fund_mail` | `20260910_offsite_fund` | 场外申购邮件记录(运营入口) |
|
||||||
|
| `offsite_fund_attachment` | `20260910_offsite_fund` | 邮件附件(**该域唯一有数据的表,见 §3**) |
|
||||||
|
| `offsite_fund_document` | `20260910_offsite_fund` | 识别后的业务单据 |
|
||||||
|
| `offsite_execution_plan_task` | `20260910_offsite_fund` | 执行计划任务 |
|
||||||
|
| `offsite_rule_result` | `20260910_offsite_fund` | 规则校验结果 |
|
||||||
|
| `offsite_query_record` | `20260910_offsite_fund` | NL2SQL 查询记录 |
|
||||||
|
| `offsite_notification` | `20260910_offsite_fund` | 对外通知 |
|
||||||
|
| `offsite_mail_cursor` | `20260910_offsite_worker` | 邮件 Worker 的游标状态 |
|
||||||
|
| `offsite_recognition_attempt` | `20260910_offsite_recognition_attempt` | 识别尝试追踪(OCR/LLM 各一次尝试) |
|
||||||
|
| `offsite_field_correction` | `20260911_offsite_field_correction` | 人工字段纠正记录 |
|
||||||
|
|
||||||
|
### 2.2 推广域(`promotion_*`,7 张)
|
||||||
|
|
||||||
|
| 表名 | 来源迁移 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `promotion_material_task` | `20260910_promotion_material` | 推广素材任务(主表) |
|
||||||
|
| `promotion_material_version` | `20260910_promotion_material` | 素材版本 |
|
||||||
|
| `promotion_input_snapshot` | `20260910_promotion_material` | 生成输入快照(可复现) |
|
||||||
|
| `promotion_attachment` | `20260910_promotion_material` | 素材附件(海报/PPT) |
|
||||||
|
| `promotion_compliance_check` | `20260910_promotion_material` | 合规校验结果 |
|
||||||
|
| `promotion_review_record` | `20260910_promotion_material` | 审核记录 |
|
||||||
|
| `promotion_delivery_record` | `20260910_promotion_material` | 投递记录 |
|
||||||
|
|
||||||
|
### 2.3 迁移文件清单(11 个,含 6 个 merge/辅助)
|
||||||
|
|
||||||
|
```
|
||||||
|
建表相关:20260910_offsite_fund / _offsite_worker / _offsite_recognition_attempt /
|
||||||
|
_promotion_material / _promotion_poster / 20260911_offsite_field_correction
|
||||||
|
合并链: 20260910_merge_heads / 20260911_merge_review_separation /
|
||||||
|
20260911_merge_risk_heads(当前 head)
|
||||||
|
其他: 20260910_offsite_worker_identity / 20260911_drop_review_separation
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **两个环境都踩过的坑**:本库曾处于"`alembic_version` 已指向同事的 revision,
|
||||||
|
> 但 `offsite_*`/`promotion_*` 表一张都没有"的状态(**版本号跑了、表没建**);
|
||||||
|
> 架构师那边则是"表没建、版本号也没跑"。**两种坏状态都靠 `alembic upgrade heads` 收敛。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 当前数据量(2026-09-11 实测)
|
||||||
|
|
||||||
|
| 表 | 行数 |
|
||||||
|
|---|---|
|
||||||
|
`offsite_fund_attachment` | **60** |
|
||||||
|
其余 16 张 | **0** |
|
||||||
|
|
||||||
|
> 说明:只有附件表有数据(同事调试/演示时落盘的附件)。**这不是"该域已在生产运行"的证据**,
|
||||||
|
> 也不代表其它表可以删——**禁止删表**(规则 3)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 跨域边界核对(规则 8 的两向检查)
|
||||||
|
|
||||||
|
### 4.1 场内代码是否引用这 17 张表 → **否** ✅
|
||||||
|
|
||||||
|
`app/` 下搜 `offsite_|promotion_` 只有 12 处命中,**全部是 `app/core/config.py` 里的环境变量名**
|
||||||
|
(`offsite_mailbox`、`offsite_allowed_senders` 等),**没有任何 SQL/ORM 级引用**。
|
||||||
|
场内模型(`app/model/fund.py` 等)与只读契约(`FUND_TABLES`)都只覆盖 `fin_*`。
|
||||||
|
|
||||||
|
### 4.2 场外代码是否写场内交易表 → **否** ✅
|
||||||
|
|
||||||
|
在 `app/service/offsite*.py` 与 `app/model/offsite*.py` 里搜
|
||||||
|
`fin_sim_order|fin_capital_flow|fin_cash_ledger|fin_transaction|fin_holding` —— **零命中**。
|
||||||
|
场外域只读写自己的 `offsite_*` 表。
|
||||||
|
|
||||||
|
⇒ **两个方向都不越界**,符合规则 8。这一点在合并评审时是要点,故在此留档。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 对审计工具的影响(**不是"有人偷偷建表"**)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ .\.venv\Scripts\python.exe tools\audit_schema.py
|
||||||
|
schema audit passed: 68 business tables, no missing or unexpected tables
|
||||||
|
```
|
||||||
|
|
||||||
|
**表数从 51 跳到 68 是同事那 11 个迁移的**预期结果**,不是有人绕过基线偷偷建表。**
|
||||||
|
|
||||||
|
机制要说清楚(否则会被误读成"期望值被手工改过"):`tools/audit_schema.py` 的期望表集合
|
||||||
|
是**动态推导**的,不是一份手写清单:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def expected_tables() -> set[str]:
|
||||||
|
tables = <从 alembic/baseline_generated.sql 正则提取 CREATE TABLE>
|
||||||
|
for path in (alembic/versions/*.py):
|
||||||
|
tables |= <从每份迁移里正则提取 CREATE TABLE> # ← 17 张新表由此自动进入
|
||||||
|
return tables
|
||||||
|
```
|
||||||
|
|
||||||
|
⇒ **期望值随迁移自动增长**,没有人手工改过它。优点是不会漏;**代价**是它只增不减 ——
|
||||||
|
若将来有迁移 **DROP** 掉某张表,期望集合仍会包含它,审计会报 `missing` 的假失败。
|
||||||
|
真要删表时(记得规则 3:**禁止删除已有表**)需同步调整该工具。
|
||||||
|
|
||||||
|
> ⚠️ 若你那边 `audit_schema.py` 报 `unexpected`,先确认是"库里多表"还是"迁移没进来"——
|
||||||
|
> 前者是绕过基线的违规,后者只是迁移没合。两者处置完全不同。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 与 `docs/00` 的关系(一句话)
|
||||||
|
|
||||||
|
**`docs/00` 一个字段、一张表都没动**(规则 1)。本文是它的**域外补充登记**,
|
||||||
|
两者合起来才是"当前库里的全部表":
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/00(场内 51) + 本文(场外/推广 17) = 68 张业务表
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*登记时点:2026-09-11 登记依据:现库 `information_schema` 实测 + `alembic/versions` 逐文件核对 +
|
||||||
|
`tools/audit_schema.py` 的期望值推导机制核对。*
|
||||||
@@ -45,9 +45,15 @@ promotion_*( 7) promotion_attachment / promotion_compliance_check / promotion_d
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `tools/audit_schema.py` 现在报 **68 business tables**(原来 51)
|
- `tools/audit_schema.py` 现在报 **68 business tables**(原来 51)
|
||||||
- **但 `docs/00` 基线与 `docs/02` 建表设计都还是 51 张** —— 这需要你裁决:
|
- **但 `docs/00` 基线与 `docs/02` 建表设计都还是 51 张** —— 你第二轮的裁决是:
|
||||||
是把这 17 张补进基线文档,还是确认它们属"场外/推广"独立域、另立文档。
|
**不动 `docs/00`**(它冻结的是场内交易域,把独立域塞进去会让"基线"概念失效),
|
||||||
**我没有擅自改 `docs/00`**(它是不可变业务基线,规则 1)。
|
改为**另立登记文档**。我方已按此落地:新增 **`docs/28-场外与推广域数据表登记.md`**
|
||||||
|
(17 张表逐表登记来源迁移/归属域/跨域引用核对),并同步 `docs/08` 的审计口径为
|
||||||
|
「场内 51 + 场外/推广 17 = 68」。
|
||||||
|
- ⚠️ **表数跳变不是"有人偷偷建表"**:`audit_schema.py` 的期望集合是**动态推导**的
|
||||||
|
(读 `baseline_generated.sql` + 扫描 `alembic/versions/*.py` 的 `CREATE TABLE`),
|
||||||
|
**随迁移自动增长、没人手工改过**。代价是只增不减:将来若有迁移 DROP 表,
|
||||||
|
期望集合仍含它、会报 `missing` 假失败(详见 `docs/28` §5)。
|
||||||
- 你的仓库若也是"`alembic_version` 指向同事 revision、但表没建"的状态,**合并后需补跑迁移**。
|
- 你的仓库若也是"`alembic_version` 指向同事 revision、但表没建"的状态,**合并后需补跑迁移**。
|
||||||
|
|
||||||
### ③ 测试基线不是"全绿",但 3 个失败都**不是代码缺陷**
|
### ③ 测试基线不是"全绿",但 3 个失败都**不是代码缺陷**
|
||||||
@@ -77,7 +83,32 @@ pytest -q → 3 failed, 1218 passed, 2 skipped
|
|||||||
| Milvus | `http://localhost:19530`(只监听 IPv6,写 `127.0.0.1` 可能连不上);容器 `milvus-standalone` |
|
| Milvus | `http://localhost:19530`(只监听 IPv6,写 `127.0.0.1` 可能连不上);容器 `milvus-standalone` |
|
||||||
| ⚠️ Docker Desktop | **不常驻**:它没运行时 Milvus 不可用(`docker` CLI 报连不上守护进程)。跑真机验证前先确认它在运行 —— 我这次就被它挡过一次 |
|
| ⚠️ Docker Desktop | **不常驻**:它没运行时 Milvus 不可用(`docker` CLI 报连不上守护进程)。跑真机验证前先确认它在运行 —— 我这次就被它挡过一次 |
|
||||||
| 迁移 | `python -m alembic upgrade heads`;当前我方=head `20260911_merge_risk_heads` |
|
| 迁移 | `python -m alembic upgrade heads`;当前我方=head `20260911_merge_risk_heads` |
|
||||||
| mypy | ⚠️ **本机数字不可比**:我这台报 184 个错,装 `sqlalchemy2-stubs` 后 181→43、卸载回 184 ⇒ **主因是缺 SQLAlchemy 2.0 类型信息,不是代码质量**。我方文件里的 8 个真实错误已修;`pyproject.toml` 的 mypy 配置(`strict=true`)**我没擅自改** |
|
| mypy | ✅ **已收敛到与你同量级:mypy 1.20.2 + SQLAlchemy 2.0.52 → 3 个错(180 文件)**。此前 184 的真因是**我的环境版本旧**,不是"缺类型存根"——你在第二轮答复里纠正过:SQLAlchemy 2.0 **自带 `py.typed`**,不需要 `sqlalchemy2-stubs`(那是 1.4 用的)。教训与实测矩阵见 §10 |
|
||||||
|
|
||||||
|
### 环境版本教训(第二轮答复纠正后的复现矩阵)
|
||||||
|
|
||||||
|
**症状**:同一份代码,`mypy app` 在我这台报 184 个错、架构师那台 0 错。
|
||||||
|
|
||||||
|
**我最初的归因是错的**("缺 SQLAlchemy 2.0 类型信息")。实测矩阵:
|
||||||
|
|
||||||
|
| 组合 | 报错数 |
|
||||||
|
|---|---|
|
||||||
|
| SQLAlchemy **2.0.34** + mypy 1.14.1(我原来的环境) | **173** |
|
||||||
|
| SQLAlchemy 2.0.34 + mypy 1.20.2 | 173 |
|
||||||
|
| SQLAlchemy **2.0.52** + mypy 1.14.1 | **6** |
|
||||||
|
| SQLAlchemy 2.0.52 + mypy **1.20.2** | **3** |
|
||||||
|
|
||||||
|
⇒ **主因是 SQLAlchemy 的补丁版本**(173 → 6):旧补丁版的类型标注不完整,
|
||||||
|
`BIGINT`/`DATETIME` 被判成未类型化函数,于是 `app/model/*.py` 每个列定义都报一条。
|
||||||
|
mypy 版本是次因(6 → 3)。
|
||||||
|
|
||||||
|
**两个反面教训(写在这里免得别人重走)**:
|
||||||
|
|
||||||
|
1. **`sqlalchemy2-stubs` 不能装** —— 它是给 SQLAlchemy **1.4** 用的。装上后 181→43
|
||||||
|
看着"变好了",实际是换了一批按 1.4 API 核对产生的错(`mapped_column`/`DeclarativeBase`
|
||||||
|
不存在)。我当时把它当成"缺存根"的证据,**方向对了一半、结论反了**。
|
||||||
|
2. **`pyproject.toml` 的 `sqlalchemy>=2.0,<3` 允许的范围内,补丁版差异会造成量级差异**。
|
||||||
|
若希望门禁数字稳定,需要把 SQLAlchemy 钉到具体补丁版(属公共约定,**我没擅自改**)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user