2026-09-09 21:55:37 +08:00
|
|
|
|
# 数据库结构审计基线
|
|
|
|
|
|
|
2026-09-10 15:55:54 +08:00
|
|
|
|
## 一、审计工具
|
2026-09-09 21:55:37 +08:00
|
|
|
|
|
2026-09-10 15:55:54 +08:00
|
|
|
|
`tools/schema_fingerprint.py` 读取 MySQL information_schema 中每张表的字段名、类型、可空性、
|
|
|
|
|
|
默认值、额外属性、字符集、排序规则和生成表达式,并输出 SHA-256 指纹。2026-09-09 起指纹
|
|
|
|
|
|
**同时覆盖索引与唯一键(STATISTICS)以及外键(KEY_COLUMN_USAGE + REFERENTIAL_CONSTRAINTS)**,
|
|
|
|
|
|
因为只对字段做指纹无法发现约束漂移。它不读取业务行数据,支持 `--out` 直接落 UTF-8 文件。
|
|
|
|
|
|
|
|
|
|
|
|
`tools/audit_constraints.py` 做三向比对:`docs/00` 声明的唯一键 ↔ 库实际唯一索引、
|
|
|
|
|
|
ORM 映射 ↔ 库列名(生成列单独归类为 note)。任一不一致返回非零退出码,可纳入提交前检查。
|
|
|
|
|
|
|
|
|
|
|
|
`tools/audit_schema.py` 仍负责表名与列存在性比对,与上述两个工具互补。
|
|
|
|
|
|
|
|
|
|
|
|
## 二、历史指纹记录
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-09 在补建会话表前保存了 49 张既有表指纹;迁移 `20260909_session` 之后再次比对,
|
2026-09-09 21:55:37 +08:00
|
|
|
|
49 张既有表全部一致,新增 `svc_conversation_session`。随后新增 `api_request_receipt`,
|
|
|
|
|
|
该表是公共 HTTP 幂等回执,不能替代 `request_idempotency`。
|
|
|
|
|
|
|
2026-09-10 15:55:54 +08:00
|
|
|
|
## 三、2026-09-09 约束纠偏
|
|
|
|
|
|
|
|
|
|
|
|
`tools/generate_baseline_sql.py` 旧实现只按规则列里的"唯一"字样逐字段生成 `UNIQUE KEY`,
|
|
|
|
|
|
无法表达 `docs/00` 中的 `唯一键 (a, b)` 写法,导致四张表的联合唯一键被拆成两个单列唯一键:
|
|
|
|
|
|
|
|
|
|
|
|
| 表 | 纠偏前(错误) | 基线要求 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `fin_market_price` | UNIQUE(product_id)、UNIQUE(trade_date) | UNIQUE(product_id, trade_date) |
|
|
|
|
|
|
| `fin_nav_history` | UNIQUE(product_id)、UNIQUE(nav_date) | UNIQUE(product_id, nav_date) |
|
|
|
|
|
|
| `fin_holding` | UNIQUE(customer_id)、UNIQUE(product_id) | UNIQUE(customer_id, product_id) |
|
|
|
|
|
|
| `sys_customer_assignment` | UNIQUE(customer_id)、UNIQUE(employee_role) | UNIQUE(customer_id, employee_role) |
|
|
|
|
|
|
|
|
|
|
|
|
后果:场内日线与净值每产品只能存一行、持仓每客户只能有一个产品,属结构性阻断。
|
|
|
|
|
|
四张表在纠偏时均为 0 行,故纠偏无数据风险。迁移 `20260909_constraint_fix` 删除 8 个错误
|
|
|
|
|
|
唯一键、新增 4 个联合唯一键;`docs/00` 基线文档未做任何修改,纠偏的是库与生成器。
|
|
|
|
|
|
|
|
|
|
|
|
验证证据(`docs/evidence/`):
|
|
|
|
|
|
|
|
|
|
|
|
- `20260909-before-constraint-fix.sql`:纠偏前 52 张表 `SHOW CREATE TABLE` 全量快照;
|
|
|
|
|
|
- `20260909-fingerprint-before.json` 与 `20260909-fingerprint-after.json`:纠偏前后结构指纹;
|
|
|
|
|
|
- 两份指纹的差异**恰好 4 张表**(上表四张),其余 47 张结构未变;
|
|
|
|
|
|
- `tools/audit_constraints.py`:约束差异由 12 条降为 0。
|
|
|
|
|
|
|
|
|
|
|
|
## 四、既有库对齐(方案 A)
|
|
|
|
|
|
|
2026-09-11 20:00:56 +08:00
|
|
|
|
> **⚠️ 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)。
|
|
|
|
|
|
|
2026-09-10 15:55:54 +08:00
|
|
|
|
基线表补入 Alembic 链首(`20260909_baseline_schema`)后,按库的来源分三种情况:
|
|
|
|
|
|
|
|
|
|
|
|
| 库状态 | 特征 | 操作 |
|
|
|
|
|
|
|---|---|---|
|
2026-09-11 20:00:56 +08:00
|
|
|
|
| 空库 | 无 `alembic_version` 记录、无业务表 | `alembic upgrade head`,全量建出 **68 张业务表**(原为 51,见上方更新) |
|
2026-09-10 15:55:54 +08:00
|
|
|
|
| 由 Alembic 建起 | `alembic_version` 已位于链上 | 直接 `alembic upgrade head`;Alembic 不会重跑祖先迁移,**无需 stamp** |
|
|
|
|
|
|
| 手工或脚本建库 | 有业务表但 `alembic_version` 为空 | 直接 upgrade 会重建已有表而失败,必须先 `alembic stamp <当前结构对应版本>`,再 `upgrade head` |
|
|
|
|
|
|
|
2026-09-11 20:00:56 +08:00
|
|
|
|
> ⚠️ **本次合并实测到的第四种坏状态**(原文档未覆盖,值得记一笔):
|
|
|
|
|
|
> `alembic_version` **已指向较新的 revision,但部分表并不存在** —— 即"版本号跑了、表没建"。
|
|
|
|
|
|
> 本库当时就是这种状态(`alembic_version = 20260910_drop_review_separation`,而
|
|
|
|
|
|
> `offsite_*`/`promotion_*` 一张都没有)。它靠 `alembic upgrade heads` 收敛:
|
|
|
|
|
|
> Alembic 只跑 `alembic_version` 之后的迁移,所以恰好补建了缺失的表。
|
|
|
|
|
|
> 架构师那边是另一种坏状态("表没建、版本号也没跑"),同样靠这一条收敛。
|
|
|
|
|
|
|
2026-09-10 15:55:54 +08:00
|
|
|
|
`tools/migration_state_check.py` 只读诊断这三种状态:读取迁移链 head、`alembic_version` 记录与
|
|
|
|
|
|
业务表数量,并按结构特征(生成列、联合唯一键、代表性表)由新到旧推断库实际所处版本,输出可执行
|
|
|
|
|
|
的建议命令。三种状态均已实测:空库判定正确;`jr` 库判定为"已在 head、无需操作";手工库场景
|
|
|
|
|
|
(建表后清空版本记录)提示 stamp 到 `20260909_memory_active_key`,按其建议执行后 `upgrade head`
|
|
|
|
|
|
无任何 `Running upgrade` 输出,即未误重建表。
|
|
|
|
|
|
|
|
|
|
|
|
## 五、边界
|
|
|
|
|
|
|
|
|
|
|
|
数据库约束仍以 `docs/00-新数据库基线设计.md` 为业务权威。指纹与约束审计只证明结构一致,
|
|
|
|
|
|
不替代状态机、并发语义和跨存储投影(Milvus/Neo4j)的业务验收。
|
2026-09-12 11:27:28 +08:00
|
|
|
|
|
|
|
|
|
|
## 六、已知文档偏差(`docs/00` 与真实 schema 不一致之处)
|
|
|
|
|
|
|
|
|
|
|
|
`docs/00` 是不可变业务基线(`AGENTS.md` 规则 1),**不因下列偏差修改**;但偏差必须登记,
|
|
|
|
|
|
否则每换一个人接手就会重新踩一次。发现新偏差请追加到本节。
|
|
|
|
|
|
|
|
|
|
|
|
| # | 位置 | 文档描述 | 真实 schema | 处置 |
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
| 1 | `profile_snapshots.current_customer_id` | `docs/00` 第 783 行描述为**「生成列」** | **不是生成列**:`information_schema.COLUMNS` 实测 `EXTRA=''`、`GENERATION_EXPRESSION=''`;`alembic/baseline_generated.sql` 的建表语句无 `GENERATED` 子句;`tools/seed_profile_demo.py` 注释写明「不是生成列,必须显式写入」;`profile_repository.py` 用原生 SQL 显式写入 | 按真实 schema 映射成**普通可空列**;**不改 `docs/00`**;**不补迁移**(DDL 本身正确,错的只是文档描述;为"让文档成真"而加生成列会改变既有列语义,违反规则 4)。四处证据一致,2026-09-11 复核 |
|
|
|
|
|
|
|
|
|
|
|
|
> 发现方式:ZSY 那条线在合并前先按 `AGENTS.md` 与 `docs/00/01/05/09/14/20` 逐条过规则,
|
|
|
|
|
|
> 撞到"文档说生成列、代码却显式写入"这处矛盾后做了实测。这个自查习惯值得保持——
|
|
|
|
|
|
> 本次三家里,只有这条线在合之前把"撞到底座规则"的地方单独清理成了一个提交。
|
|
|
|
|
|
|