123 lines
4.5 KiB
Python
123 lines
4.5 KiB
Python
# -*- 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())
|