# CMA 数据库结构变更运行手册(DDL 与应用启动剥离) - 任务:`cma-create-all-minpriv-rootfix-20260916`(P2) - 事故档案:`wecom-project/_verification/2026-09-16-cma-release-incident-verification.md` - 结论一句话:**DDL 只在部署链的 [4b] 特权迁移步发生,应用启动不再建表;运行库账号 `cma_api` 无需 CREATE。** ## 1. 三条不变式 | # | 不变式 | 落地位置 | |---|---|---| | 1 | 应用启动**不**执行 DDL(`create_all` 默认关,`CMA_AUTO_CREATE=1` 仅限开发/测试) | `backend/app/database.py::auto_create_enabled/init_db` | | 2 | 结构变更由**有 DDL 权限的账号**在**应用重启前**执行(独立、幂等、可回滚、留痕) | `backend/scripts/db_migrate.py` + `deploy.sh [4b]` | | 3 | 运行库账号保持最小权限(DML 表级白名单,**无** CREATE/ALTER/DROP) | DB 授权侧(由 ops/项目Bot 回收,见 §5) | ## 2. 部署链顺序(`deploy.sh`) ``` [0] 提交纪律检查(忽略未跟踪) ← 勿回退 [1] git pull origin main [1b] 鉴权面审计闸门(B 类缺口即中止) ← 勿回退 [2] 前端构建 pnpm build [3] 前端重铺 /var/www/cma [4] 后端依赖 [4b] 数据库结构迁移(特权;重启前;幂等;失败即中止部署) ← 本单新增 [5] 重启 cma-backend + reload nginx 冒烟:就绪轮询 → BOT桥接 → schema_check → 登录/KPI ``` `[4b]` 失败时**不重启**后端 → 旧版本继续服务(不再出现「新模型建不出 + 服务被重启」的崩溃循环)。 ## 3. 迁移步用法(`backend/scripts/db_migrate.py`) ```bash cd /root/cma-management/backend ./venv/bin/python3 scripts/db_migrate.py --check # 只读预检(默认动作) ./venv/bin/python3 scripts/db_migrate.py --apply # 幂等建表(缺什么建什么)+ journal ./venv/bin/python3 scripts/db_migrate.py --status # 历史 journal ./venv/bin/python3 scripts/db_migrate.py --rollback latest # 回滚最近一次新建的表(先 dump 后删) ``` - **连接**:优先 `CMA_MIGRATE_DB_USER/CMA_MIGRATE_DB_PASS`(专用迁移账号,TCP);未设则走本机 `unix socket + root`(socket 认证,无口令落盘)。**显式拒绝**用应用运行账号(`cma_api`)执行本步。 - **幂等**:`create_all(checkfirst=True)` —— 已存在的表零动作;二跑 `created_new=0`。 - **journal**:`backend/scripts/backups/db-migrate-journal/-.json`(含 db/git head/执行者/新建表清单)。 - **回滚**:只删该次 journal 记录为「新建」的表;表内有数据则**拒绝**(exit 3),`--force` 才放行;回滚前默认 `mysqldump` 备份(dump 失败即拒绝回滚,fail-closed)。 - **列级差异**:`create_all` **不**补列。`--check/--apply` 会报告「ORM 有列、库里没有」并提示需要独立 `ALTER` 迁移;部署链以此告警(不阻断)。 ## 4. 新增模型的正确姿势(发布 checklist) 1. 在 `backend/app/models/` 加模型并确保被 `app/models/__init__.py` 导入。 2. 本地 `--check` 应能列出该表缺失(证明 ORM 已注册)。 3. 提交推送 → 部署链 `[4b]` 用特权账号建表 → 重启 → 冒烟 `schema_check` 通过。 4. 若模型**改列**(新增/改名/改类型):`create_all` 不处理 → 必须写独立迁移脚本/`ALTER`(参考 `backend/scripts/migrate_ext_input_ledgers_20260915.py`、`treasury_migrate.py` 的 `up()/down()` 模式)。 5. **不要**让运行库账号重新获得 CREATE 来「绕过」:那正是事故根因。 ## 5. `cma_api` 的 CREATE 授权回收(项目Bot 执行) 2026-09-16 事故应急时授予了 `GRANT CREATE ON cma.* TO 'cma_api'@'localhost'/'127.0.0.1'`(保留至今)。 本单上线后(`[4b]` 已生效、应用启动不再建表)可安全回收: ```sql REVOKE CREATE ON cma.* FROM 'cma_api'@'localhost'; REVOKE CREATE ON cma.* FROM 'cma_api'@'127.0.0.1'; SHOW GRANTS FOR 'cma_api'@'127.0.0.1'; -- 期望:仅 SELECT + 表级 DML,无 CREATE ``` 回收后再发布一次(含任意新模型的版本)验证不中断即可闭环(证据:`[4b]` journal `created_new>=0` + 冒烟通过 + 无 `CREATE command denied`)。 ## 6. 自验(可重复) ```bash bash backend/scripts/tests/db-migrate-sim-e2e.sh # 需 root;scratch 库 cma_migrate_sim,零生产触碰 ``` 覆盖 17 项断言:负控制复现(`CREATE command denied` → 启动失败)、迁移幂等(76→0)、修复后启动零 DDL、 「新增模型未迁移」不中断 + 迁移补齐、回滚(含 dump 与有数据拒绝)。