diff --git a/docs/08-数据库结构审计基线.md b/docs/08-数据库结构审计基线.md index 476e7f2..c3f5103 100644 --- a/docs/08-数据库结构审计基线.md +++ b/docs/08-数据库结构审计基线.md @@ -43,14 +43,37 @@ ORM 映射 ↔ 库列名(生成列单独归类为 note)。任一不一致返 ## 四、既有库对齐(方案 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_version` 记录、无业务表 | `alembic upgrade head`,全量建出 51 张业务表 | +| 空库 | 无 `alembic_version` 记录、无业务表 | `alembic upgrade head`,全量建出 **68 张业务表**(原为 51,见上方更新) | | 由 Alembic 建起 | `alembic_version` 已位于链上 | 直接 `alembic upgrade head`;Alembic 不会重跑祖先迁移,**无需 stamp** | | 手工或脚本建库 | 有业务表但 `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` 记录与 业务表数量,并按结构特征(生成列、联合唯一键、代表性表)由新到旧推断库实际所处版本,输出可执行 的建议命令。三种状态均已实测:空库判定正确;`jr` 库判定为"已在 head、无需操作";手工库场景 diff --git a/docs/28-场外与推广域数据表登记.md b/docs/28-场外与推广域数据表登记.md new file mode 100644 index 0000000..9ccf5cb --- /dev/null +++ b/docs/28-场外与推广域数据表登记.md @@ -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` 的期望值推导机制核对。* diff --git a/docs/接手文档-NL_develop-给架构师.md b/docs/接手文档-NL_develop-给架构师.md index 8afd3f6..357ec37 100644 --- a/docs/接手文档-NL_develop-给架构师.md +++ b/docs/接手文档-NL_develop-给架构师.md @@ -45,9 +45,15 @@ promotion_*( 7) promotion_attachment / promotion_compliance_check / promotion_d ``` - `tools/audit_schema.py` 现在报 **68 business tables**(原来 51) -- **但 `docs/00` 基线与 `docs/02` 建表设计都还是 51 张** —— 这需要你裁决: - 是把这 17 张补进基线文档,还是确认它们属"场外/推广"独立域、另立文档。 - **我没有擅自改 `docs/00`**(它是不可变业务基线,规则 1)。 +- **但 `docs/00` 基线与 `docs/02` 建表设计都还是 51 张** —— 你第二轮的裁决是: + **不动 `docs/00`**(它冻结的是场内交易域,把独立域塞进去会让"基线"概念失效), + 改为**另立登记文档**。我方已按此落地:新增 **`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、但表没建"的状态,**合并后需补跑迁移**。 ### ③ 测试基线不是"全绿",但 3 个失败都**不是代码缺陷** @@ -77,7 +83,32 @@ pytest -q → 3 failed, 1218 passed, 2 skipped | Milvus | `http://localhost:19530`(只监听 IPv6,写 `127.0.0.1` 可能连不上);容器 `milvus-standalone` | | ⚠️ Docker Desktop | **不常驻**:它没运行时 Milvus 不可用(`docker` CLI 报连不上守护进程)。跑真机验证前先确认它在运行 —— 我这次就被它挡过一次 | | 迁移 | `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 钉到具体补丁版(属公共约定,**我没擅自改**)。 ---