pbl_blueprint/scripts/seed_m1b_template.py
2026-09-16 16:25:33 +08:00

123 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
M1b 离线兜底模板种子加载器build.sh 调用)
用法:
python scripts/seed_m1b_template.py # 建表 + 落种子(幂等)
python scripts/seed_m1b_template.py --ddl-only # 仅建表
python scripts/seed_m1b_template.py --dry-run # 只打印 DDL/种子,不连库
硬约束M1b-annex §4
* 离线模板**必须**由 build.sh 种子落库(不依赖运行时下载),确保 test 环境冷启动即可用;
* 落库为**平台公共行**tenant_id NULL全租户可见
* 升级 append 新 template_version**不覆盖**旧版本行Q-OPEN-9
* 幂等:按 (tenant_key, template_code, template_version) upsert重复执行零副作用
* Q-OPEN-3本脚本只写 M1b 自有 2 表,绝不 ALTER 复用域基表assert_no_base_table_change 守卫)。
"""
from __future__ import annotations
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
from pbl_blueprint.models.pbl_template import ( # noqa: E402
assert_no_base_table_change,
build_all_ddl,
normalize_tenant_key,
)
from pbl_blueprint.subobject_ext import tpl_hash, validate_tpl_schema # noqa: E402
SEED_FILE = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"..", "pbl_blueprint", "json", "seed_template_offline.json")
def load_seed(path=None):
"""读种子文件并逐条过 schema 校验(不合法直接失败,不落半截数据)。"""
with open(path or SEED_FILE, "r", encoding="utf-8") as fh:
data = json.load(fh)
rows = []
for tpl in data.get("templates", []):
tpl_json = tpl["tpl_json"]
normalized = validate_tpl_schema(tpl_json) # fail-closed
rows.append({
"tenant_id": tpl.get("tenant_id"),
"tenant_key": normalize_tenant_key(tpl.get("tenant_id")),
"template_code": tpl["template_code"],
"template_version": int(tpl.get("template_version", 1)),
"template_name": tpl["template_name"],
"subject": tpl.get("subject"),
"grade": tpl.get("grade"),
"tpl_json": json.dumps(normalized, ensure_ascii=False, sort_keys=True),
"tpl_hash": tpl_hash(normalized),
"offline_flag": tpl.get("offline_flag", "Y"),
"tpl_status": tpl.get("tpl_status", "active"),
})
return rows
UPSERT_SQL = (
"INSERT INTO pbl_template "
"(tenant_id, template_code, template_version, template_name, subject, grade, "
" tpl_json, tpl_hash, offline_flag, tpl_status, create_time, update_time) "
"VALUES (%(tenant_id)s, %(template_code)s, %(template_version)s, %(template_name)s, "
" %(subject)s, %(grade)s, %(tpl_json)s, %(tpl_hash)s, %(offline_flag)s, %(tpl_status)s, "
" NOW(), NOW()) "
"ON DUPLICATE KEY UPDATE "
" template_name=VALUES(template_name), subject=VALUES(subject), grade=VALUES(grade), "
" tpl_json=VALUES(tpl_json), tpl_hash=VALUES(tpl_hash), "
" offline_flag=VALUES(offline_flag), tpl_status=VALUES(tpl_status), update_time=NOW()"
)
def seed(conn=None, dry_run=False):
"""
建表 + 落种子。
:param conn: DB-API 连接None 且非 dry_run 时尝试从模块 db 适配层取)
:return: dict {ddl, seeded: n}
"""
ddl = build_all_ddl()
assert_no_base_table_change(ddl) # Q-OPEN-3 守卫
rows = load_seed()
if dry_run or conn is None:
print(ddl)
print("-- seed rows: %d" % len(rows))
for r in rows:
print("-- %s v%s scope=%s offline=%s hash=%s" % (
r["template_code"], r["template_version"],
"platform" if r["tenant_id"] is None else r["tenant_id"],
r["offline_flag"], r["tpl_hash"][:12]))
return {"ddl": ddl, "seeded": len(rows), "executed": False}
cur = conn.cursor()
for stmt in ddl.split(";\n"):
stmt = stmt.strip()
if stmt:
cur.execute(stmt)
for r in rows:
cur.execute(UPSERT_SQL, r)
conn.commit()
cur.close()
return {"ddl": ddl, "seeded": len(rows), "executed": True}
def main(argv=None):
argv = list(argv if argv is not None else sys.argv[1:])
dry_run = "--dry-run" in argv
ddl_only = "--ddl-only" in argv
if ddl_only:
print(build_all_ddl())
return 0
res = seed(dry_run=dry_run)
print("[M1b seed] rows=%s executed=%s" % (res["seeded"], res["executed"]))
return 0
if __name__ == "__main__":
raise SystemExit(main())