feat(cma): 司库阶段1 DDL 并入特权迁移步 + cma_api 授权
ci/woodpecker/push/main Pipeline was canceled

- db_migrate.py: create_all 后调用 run_structural() → 复用 treasury_migrate(幂等、特权连接)
- treasury_migrate.py: 换特权连接(CMA_MIGRATE_DB_USER 或本机 root socket,禁 cma_api);up() 增 grant_app_account()(cma_api 对 6 张 treasury 表 DML,双 host)
- 效果:deploy 链由特权账号一次性完成 建表+补列(cash_plans 4列/1索引)+种子(8规则/6限额)+授权

Ref: 0-P0-project-20260915-cma-treasury-stage1
This commit is contained in:
Hermes CI Fix
2026-09-16 14:39:54 +08:00
parent 5f6ced1388
commit f1fa781e08
2 changed files with 76 additions and 3 deletions
+52 -2
View File
@@ -30,8 +30,7 @@ from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import text # noqa: E402
from app.database import get_engine # noqa: E402
from sqlalchemy import create_engine, text # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__))
BACKUP_DIR = os.path.join(HERE, "backups")
@@ -55,6 +54,53 @@ CASH_PLAN_ALTER = """ALTER TABLE cash_plans
ADD COLUMN pool_id INT NULL COMMENT '归属资金池(阶段三启用,本阶段恒 NULL)',
ADD COLUMN is_locked TINYINT DEFAULT 0 COMMENT '是否已被预算/内控锁定'"""
# ─────────────────────────────────────────────────────────────
# 特权迁移连接(2026-09-16 并入):与应用运行账号 cma_api 剥离,走有 DDL 权限的通道
# ─────────────────────────────────────────────────────────────
DEFAULT_SOCKET = "/var/run/mysqld/mysqld.sock"
FORBIDDEN_RUN_ACCOUNTS = {"cma_api"}
GRANT_APP_ACCOUNT = "cma_api"
def get_engine():
"""特权引擎:CMA_MIGRATE_DB_USER/PASS(TCP)优先,否则本机 root unix socket(无口令)。
绝不使用应用运行账号(无 DDL)——与 db_migrate.py 同口径。"""
from app import database as db
db_name = (os.getenv("CMA_MIGRATE_DB_NAME") or getattr(db, "DB_NAME", None) or "cma")
user = os.getenv("CMA_MIGRATE_DB_USER", "").strip()
if user:
if user in FORBIDDEN_RUN_ACCOUNTS:
raise SystemExit(
"[FAIL] CMA_MIGRATE_DB_USER=cma_api 是应用运行账号(无 DDL 权限),已拒绝执行")
pw = os.getenv("CMA_MIGRATE_DB_PASS", "")
host = os.getenv("CMA_MIGRATE_DB_HOST", "127.0.0.1")
port = int(os.getenv("CMA_MIGRATE_DB_PORT", "3306"))
return create_engine(
f"mysql+pymysql://{user}:{pw}@{host}:{port}/{db_name}?charset=utf8mb4",
pool_pre_ping=True)
sock = os.getenv("CMA_MIGRATE_DB_SOCKET", DEFAULT_SOCKET)
if not os.path.exists(sock):
raise SystemExit(
f"[FAIL] 未设 CMA_MIGRATE_DB_USER 且 unix socket 不存在:{sock}")
return create_engine(
f"mysql+pymysql://root@localhost/{db_name}?unix_socket={sock}&charset=utf8mb4",
pool_pre_ping=True)
def grant_app_account(conn):
"""授权应用运行账号 cma_api 对 6 张 treasury 新表的 DML(幂等 GRANT,双 host)。
必须由特权连接执行;否则应用端点读不到/写不进新表(1142/1044)。"""
from app import database as db
dbn = (os.getenv("CMA_MIGRATE_DB_NAME") or getattr(db, "DB_NAME", None) or "cma")
granted = []
for host in ("localhost", "127.0.0.1"):
for t in NEW_TABLES:
conn.execute(text(
f"GRANT SELECT, INSERT, UPDATE, DELETE ON `{dbn}`.`{t}` "
f"TO '{GRANT_APP_ACCOUNT}'@{host}"))
granted.append(f"{t}@{host}")
return granted
# ─────────────────────────────────────────────────────────────
# DDL 解析(P0-1:先剔除注释行再判定语句类型,不再用 startswith 裸判)
@@ -233,6 +279,9 @@ def up(conn):
if CASH_PLAN_NEW_INDEX not in before_idx:
conn.execute(text(p["index"]))
# ③b 授权应用账号 cma_api 对本 6 表 DML(幂等 GRANT,须特权连接)
granted = grant_app_account(conn)
# ④ 种子(ON DUPLICATE KEY UPDATE,数据级幂等)
for stmt in p["seeds"]:
conn.execute(text(stmt))
@@ -266,6 +315,7 @@ def up(conn):
"limits_rows": counts["limits_rows"],
"import_logs_row": action,
"batch": batch,
"granted": granted,
"missing_tables": post["missing_tables"],
"cash_plans_rows": post["cash_plans_rows"],
}, ensure_ascii=False))