# -*- 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())