deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
b1aabe20a2
commit
b6285dd6b5
@ -11,7 +11,7 @@
|
||||
(返回 ERR_TENANT_MISSING,绝不回落默认租户、绝不跨租户可见)。
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.1.0" # M1b:模板平台公共部分/子对象扩展/关联表
|
||||
__module_name__ = "pbl_blueprint"
|
||||
|
||||
# ------------------------------------------------------------------ errors 内核(14 符号)
|
||||
@ -35,6 +35,37 @@ from .errors import ( # noqa: F401
|
||||
# ------------------------------------------------------------------ 挂载入口
|
||||
from .init import load_pbl_blueprint # noqa: F401
|
||||
|
||||
# ------------------------------------------------------------------ M1b(模板平台公共部分 / 子对象扩展 / 关联表判定)
|
||||
# 三处同步之「__init__ 导出」环节:函数定义(m1b_*.py) + 本文件导出 + init.py env 注册
|
||||
from .m1b_init import ( # noqa: F401
|
||||
M1B_TABLES,
|
||||
M1B_MODEL_FILES,
|
||||
M1B_CRUD_FILES,
|
||||
M1B_DDL,
|
||||
PLATFORM_EXT_DEFS,
|
||||
PLATFORM_TEMPLATES,
|
||||
init_m1b,
|
||||
ensure_m1b_tables,
|
||||
ensure_platform_ext_defs,
|
||||
seed_platform_data,
|
||||
load_m1b_models,
|
||||
load_m1b_crud_defs,
|
||||
)
|
||||
from .m1b_api import M1B_ROUTES, dispatch as m1b_dispatch, register_m1b_routes # noqa: F401
|
||||
from .m1b_db import ( # noqa: F401
|
||||
PLATFORM_TENANT_KEY,
|
||||
WRITE_PROTECTED_TABLES,
|
||||
PBL_DB_WRITE_PROTECTED,
|
||||
WriteProtectedError,
|
||||
SorDB,
|
||||
tenant_key as m1b_tenant_key,
|
||||
assert_writable as m1b_assert_writable,
|
||||
make_db as m1b_make_db,
|
||||
)
|
||||
from . import m1b_template # noqa: F401
|
||||
from . import m1b_subobject # noqa: F401
|
||||
from . import m1b_ref # noqa: F401
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"__module_name__",
|
||||
@ -57,4 +88,17 @@ __all__ = [
|
||||
"err",
|
||||
# 挂载入口
|
||||
"load_pbl_blueprint",
|
||||
# ---- M1b:挂载/建表/种子
|
||||
"M1B_TABLES", "M1B_MODEL_FILES", "M1B_CRUD_FILES", "M1B_DDL",
|
||||
"PLATFORM_EXT_DEFS", "PLATFORM_TEMPLATES",
|
||||
"init_m1b", "ensure_m1b_tables", "ensure_platform_ext_defs",
|
||||
"seed_platform_data", "load_m1b_models", "load_m1b_crud_defs",
|
||||
# ---- M1b:路由
|
||||
"M1B_ROUTES", "m1b_dispatch", "register_m1b_routes",
|
||||
# ---- M1b:真实库适配 + Q-OPEN-3 写保护闸门
|
||||
"PLATFORM_TENANT_KEY", "WRITE_PROTECTED_TABLES", "PBL_DB_WRITE_PROTECTED",
|
||||
"WriteProtectedError", "SorDB", "m1b_tenant_key", "m1b_assert_writable",
|
||||
"m1b_make_db",
|
||||
# ---- M1b:业务子模块
|
||||
"m1b_template", "m1b_subobject", "m1b_ref",
|
||||
]
|
||||
|
||||
@ -14,13 +14,27 @@ from .subobject import (BUILTIN_SCHEMA, add_rel, delete_rel,
|
||||
list_rels, list_subobjects, reorder_subobject,
|
||||
upsert_subobject, validate_payload)
|
||||
|
||||
from . import m1b_init as _m1b_init
|
||||
from .m1b_api import M1B_ROUTES, register_m1b_routes
|
||||
from .m1b_init import (M1B_CRUD_FILES, M1B_DDL, M1B_MODEL_FILES, M1B_TABLES,
|
||||
ensure_m1b_tables, init_m1b, load_m1b_crud_defs,
|
||||
load_m1b_models, seed_platform_data)
|
||||
|
||||
MODULE_NAME = "pbl_blueprint"
|
||||
TABLE_NAMES = list(_tables.TABLES.keys()) # 11 表
|
||||
TABLE_NAMES = list(_tables.TABLES.keys()) # M1a 11 表
|
||||
SUBOBJECT_TYPES = _tables.SUBOBJECT_TYPES # 7 类
|
||||
M1B_TABLE_NAMES = list(M1B_TABLES) # M1b 4 表
|
||||
ALL_TABLE_NAMES = TABLE_NAMES + [x for x in M1B_TABLE_NAMES
|
||||
if x not in TABLE_NAMES]
|
||||
|
||||
__all__ = [
|
||||
"load_pbl_blueprint", "ensure_tables", "MODULE_NAME", "TABLE_NAMES",
|
||||
"SUBOBJECT_TYPES", "BUILTIN_SCHEMA",
|
||||
# ---- M1b 三处同步之「init.py env 注册」环节
|
||||
"M1B_TABLES", "M1B_TABLE_NAMES", "ALL_TABLE_NAMES", "M1B_MODEL_FILES",
|
||||
"M1B_CRUD_FILES", "M1B_DDL", "M1B_ROUTES",
|
||||
"init_m1b", "ensure_m1b_tables", "register_m1b_routes",
|
||||
"load_m1b_models", "load_m1b_crud_defs", "seed_platform_data",
|
||||
"create_blueprint", "get_blueprint", "list_blueprints", "update_blueprint",
|
||||
"delete_blueprint", "get_tree", "change_status", "set_quality_level",
|
||||
"commit_version", "list_versions", "get_version", "get_delta",
|
||||
@ -76,14 +90,51 @@ def _seed_builtin_schema(sor, dbname):
|
||||
i * 10, esc(now_str()))))
|
||||
|
||||
|
||||
def _m1b_register_callback(app):
|
||||
"""解析应用层路由注册回调:register_route / register / route / add_route 四选一。"""
|
||||
if app is None:
|
||||
return None
|
||||
for name in ("register_route", "register", "route", "add_route"):
|
||||
fn = getattr(app, name, None)
|
||||
if callable(fn):
|
||||
return fn
|
||||
return None
|
||||
|
||||
|
||||
def load_pbl_blueprint(app=None, sor=None, ensure=False, **kw):
|
||||
dbname = get_module_dbname(_MODULE_NAME) # noqa: F841 库名由应用注入
|
||||
if ensure:
|
||||
ensure_tables(sor)
|
||||
|
||||
# -------------------------------------------------------- M1b 挂载
|
||||
# 三处同步:m1b_api.register_m1b_routes(定义)+ __init__.py(导出)+ 此处(env 注册)
|
||||
# Q-OPEN-3:init_m1b 只建 M1b 自有 4 表,绝不触碰 world/scene/entity/script 基表。
|
||||
m1b_result = {"ok": False, "skipped": True}
|
||||
if kw.get("m1b", True):
|
||||
try:
|
||||
m1b_result = init_m1b(
|
||||
db=kw.get("m1b_db"),
|
||||
env=kw.get("env"),
|
||||
register=_m1b_register_callback(app),
|
||||
executor=kw.get("m1b_executor"),
|
||||
seed=bool(kw.get("m1b_seed", ensure)),
|
||||
)
|
||||
except Exception as e: # M1b 失败不拖垮 M1a 挂载
|
||||
m1b_result = {"ok": False,
|
||||
"error": "%s: %s" % (type(e).__name__, e)}
|
||||
|
||||
if app is not None:
|
||||
try:
|
||||
app.register_module(MODULE_NAME, {
|
||||
"tables": TABLE_NAMES,
|
||||
"tables": ALL_TABLE_NAMES,
|
||||
"m1b": {
|
||||
"tables": M1B_TABLE_NAMES,
|
||||
"model_files": M1B_MODEL_FILES,
|
||||
"crud_files": M1B_CRUD_FILES,
|
||||
"ddl": M1B_DDL,
|
||||
"routes": [(m, p) for m, p, _ in M1B_ROUTES],
|
||||
"init": m1b_result,
|
||||
},
|
||||
"subobject_types": list(SUBOBJECT_TYPES),
|
||||
"api": {
|
||||
"create": create_blueprint, "get": get_blueprint,
|
||||
@ -100,6 +151,7 @@ def load_pbl_blueprint(app=None, sor=None, ensure=False, **kw):
|
||||
except Exception:
|
||||
pass
|
||||
return {"module": MODULE_NAME, "tables": TABLE_NAMES,
|
||||
"m1b_tables": M1B_TABLE_NAMES, "m1b": m1b_result,
|
||||
"subobject_types": list(SUBOBJECT_TYPES), "loaded": True}
|
||||
|
||||
|
||||
|
||||
@ -1,46 +1,190 @@
|
||||
{
|
||||
"table": "pbl_blueprint_ref",
|
||||
"comment": "PBL 蓝图跨域关联表(M1b 关联表判定落地):Q-OPEN-3 决议——不在 world/scene/entity 等基表上加列,蓝图对外部域对象的引用一律落到本表,形成单向可判定的关联边",
|
||||
"engine": "InnoDB",
|
||||
"charset": "utf8mb4",
|
||||
"multi_tenant": true,
|
||||
"tenant_policy": {
|
||||
"column": "tenant_id",
|
||||
"nullable": false,
|
||||
"fail_closed": true
|
||||
"tblname": "pbl_blueprint_ref",
|
||||
"params": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"size": 20,
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "物理主键(bigint AUTO_INCREMENT)"
|
||||
},
|
||||
"tenant_key": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "租户键 = COALESCE(tenant_id,'__PLATFORM__');索引打头列;应用层计算"
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": null,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "租户ID(首业务字段,读写强制打头,缺失即 fail-closed);NULL=平台公共模板携带的关联,非空=租户蓝图关联"
|
||||
},
|
||||
"ref_code": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "关联编码(业务主键,gen_code('BPR', tenant_id) 生成)"
|
||||
},
|
||||
"owner_kind": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "blueprint",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "宿主类型 blueprint / template",
|
||||
"code": "pbl_ext_owner_kind"
|
||||
},
|
||||
"blueprint_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "蓝图ID(owner_kind=blueprint 时为 M1A blueprint_id;=template 时复用本列存 template_code)"
|
||||
},
|
||||
"subobject_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "关联所属的 *_ref 类子对象ID(模板态存模板临时ID)"
|
||||
},
|
||||
"ref_kind": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "关联对象域 world / scene / entity / script",
|
||||
"code": "pbl_ref_kind"
|
||||
},
|
||||
"ref_target_code": {
|
||||
"type": "str",
|
||||
"size": 64,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "外部对象业务编码(只读引用,无 FK,不反查基表写操作 —— Q-OPEN-3 不改 world 表)"
|
||||
},
|
||||
"ref_target_id": {
|
||||
"type": "str",
|
||||
"size": 64,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "外部对象物理ID快照(解析成功时回填)"
|
||||
},
|
||||
"ref_mode": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "readonly",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "关联模式,恒为 readonly(写入非 readonly 值被 m1b_ref 拒绝)",
|
||||
"code": "pbl_ref_mode"
|
||||
},
|
||||
"ref_status": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "unresolved",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "解析状态 unresolved 待解析 / resolved 已解析 / invalid 目标缺失或不可用",
|
||||
"code": "pbl_ref_status"
|
||||
},
|
||||
"resolve_time": {
|
||||
"type": "datetime",
|
||||
"notnull": false,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "解析时间"
|
||||
},
|
||||
"resolve_msg": {
|
||||
"type": "str",
|
||||
"size": 512,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "解析信息(invalid 时记录原因,供 M2 校验与教师端提示)"
|
||||
},
|
||||
"snapshot_json": {
|
||||
"type": "text",
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "解析时刻外部对象关键字段只读快照(JSON),运行期免跨库联查"
|
||||
},
|
||||
"sort_no": {
|
||||
"type": "int",
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "排序号"
|
||||
},
|
||||
"creator_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "创建人"
|
||||
},
|
||||
"create_time": {
|
||||
"type": "datetime",
|
||||
"notnull": true,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "创建时间"
|
||||
},
|
||||
"update_time": {
|
||||
"type": "datetime",
|
||||
"notnull": true,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "更新时间"
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"name": "id", "type": "varchar(64)", "primary_key": true, "comment": "关联ID bpref-xxxx"},
|
||||
{"name": "tenant_id", "type": "varchar(64)", "nullable": false, "index": true, "comment": "租户ID(必填)"},
|
||||
{"name": "blueprint_id", "type": "varchar(64)", "nullable": false, "comment": "蓝图ID(关联发起方,恒为 pbl_blueprint.id)"},
|
||||
{"name": "version_id", "type": "varchar(64)", "nullable": true, "comment": "蓝图版本ID(空=当前工作副本)"},
|
||||
{"name": "src_kind", "type": "varchar(32)", "nullable": false, "default": "blueprint", "comment": "源对象类型:blueprint 或 7 类子对象"},
|
||||
{"name": "src_id", "type": "varchar(64)", "nullable": true, "comment": "源对象ID(src_kind=blueprint 时可空,表示蓝图级关联)"},
|
||||
{"name": "ref_domain", "type": "varchar(32)", "nullable": false, "comment": "被引用域:world/scene/entity/script_engine/scense_game/drag/org/employee/external"},
|
||||
{"name": "ref_table", "type": "varchar(64)", "nullable": false, "comment": "被引用表名(只读,不建外键,不改基表)"},
|
||||
{"name": "ref_id", "type": "varchar(64)", "nullable": false, "comment": "被引用对象ID"},
|
||||
{"name": "ref_snapshot", "type": "json", "nullable": true, "comment": "引用时的名称/编码快照,供离线与展示(避免跨域联查)"},
|
||||
{"name": "rel_type", "type": "varchar(32)", "nullable": false, "default": "uses", "comment": "关联语义:uses/binds/embeds/derives_from/replaces"},
|
||||
{"name": "cardinality", "type": "varchar(16)", "nullable": false, "default": "n:1", "comment": "1:1/1:n/n:1/n:m"},
|
||||
{"name": "required", "type": "tinyint(1)", "nullable": false, "default": "0", "comment": "是否强依赖(校验引擎 M2 用:required=1 且引用失效 → 校验失败)"},
|
||||
{"name": "resolve_status", "type": "varchar(16)", "nullable": false, "default": "unknown", "comment": "unknown/resolved/missing:由 resolve_refs 判定,不缓存跨域写权限"},
|
||||
{"name": "resolved_at", "type": "datetime", "nullable": true},
|
||||
{"name": "seq", "type": "int", "nullable": false, "default": "0"},
|
||||
{"name": "created_by", "type": "varchar(64)", "nullable": true},
|
||||
{"name": "created_at", "type": "datetime", "nullable": true},
|
||||
{"name": "updated_by", "type": "varchar(64)", "nullable": true},
|
||||
{"name": "updated_at", "type": "datetime", "nullable": true},
|
||||
{"name": "deleted", "type": "tinyint(1)", "nullable": false, "default": "0"}
|
||||
"editable": [
|
||||
"api/pbl_blueprint_ref/list.dspy",
|
||||
"api/pbl_blueprint_ref/edit.dspy",
|
||||
"api/pbl_blueprint_ref/view.dspy"
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "uk_pbl_bpref", "unique": true, "columns": ["tenant_id", "blueprint_id", "src_kind", "src_id", "ref_domain", "ref_table", "ref_id", "rel_type", "deleted"]},
|
||||
{"name": "idx_pbl_bpref_bp", "columns": ["tenant_id", "blueprint_id", "deleted"]},
|
||||
{"name": "idx_pbl_bpref_target", "columns": ["tenant_id", "ref_domain", "ref_table", "ref_id"], "comment": "反查:某 world/scene 被哪些蓝图引用(删除前置校验用,只读不写基表)"},
|
||||
{"name": "idx_pbl_bpref_resolve", "columns": ["tenant_id", "resolve_status"]}
|
||||
],
|
||||
"design_notes": [
|
||||
"Q-OPEN-3:world 表不加 tenant_id/blueprint_id 列,跨域关联全部由本表承载,保证基表零侵入、可回滚",
|
||||
"本表不建数据库外键(跨库/跨模块),引用有效性由 resolve_refs() 主动判定并写 resolve_status",
|
||||
"反向影响面分析(world 删除前检查)通过 idx_pbl_bpref_target 只读查询完成,不阻塞基表写"
|
||||
"browserfields": [
|
||||
"ref_code",
|
||||
"owner_kind",
|
||||
"blueprint_id",
|
||||
"ref_kind",
|
||||
"ref_target_code",
|
||||
"ref_mode",
|
||||
"ref_status",
|
||||
"resolve_time"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,48 +1,203 @@
|
||||
{
|
||||
"table": "pbl_blueprint_template",
|
||||
"comment": "PBL 蓝图模板(M1b:支持平台公共模板 tenant_id IS NULL;租户私有模板 tenant_id 非空)",
|
||||
"engine": "InnoDB",
|
||||
"charset": "utf8mb4",
|
||||
"multi_tenant": true,
|
||||
"tenant_policy": {
|
||||
"column": "tenant_id",
|
||||
"nullable": true,
|
||||
"null_meaning": "platform_common",
|
||||
"read": "租户可见 = (tenant_id = :tenant OR tenant_id IS NULL AND is_public = 1)",
|
||||
"write": "平台公共模板写操作需 is_platform_admin;租户模板写操作需 tenant_id 匹配",
|
||||
"fail_closed": true
|
||||
"tblname": "pbl_blueprint_template",
|
||||
"params": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"size": 20,
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "物理主键(bigint AUTO_INCREMENT)"
|
||||
},
|
||||
"tenant_key": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "租户键 = COALESCE(tenant_id,'__PLATFORM__');索引打头列;应用层计算,不接受外部传入"
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": null,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "租户ID(首业务字段,读写强制打头,缺失即 fail-closed);NULL=平台公共模板,非空=租户私有模板"
|
||||
},
|
||||
"template_code": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "模板编码(业务主键,gen_code('TPL', tenant_id) 生成)"
|
||||
},
|
||||
"template_version": {
|
||||
"type": "int",
|
||||
"notnull": true,
|
||||
"default": 1,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "模板版本(升级 append 新版本行,不覆盖旧版本 Q-OPEN-9)"
|
||||
},
|
||||
"template_name": {
|
||||
"type": "str",
|
||||
"size": 128,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "模板名称"
|
||||
},
|
||||
"subject": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "学科",
|
||||
"code": "pbl_subject"
|
||||
},
|
||||
"grade": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "学段",
|
||||
"code": "pbl_grade"
|
||||
},
|
||||
"category": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "模板分类",
|
||||
"code": "pbl_tpl_category"
|
||||
},
|
||||
"tpl_json": {
|
||||
"type": "text",
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": false,
|
||||
"summary": "模板内容(7 类子对象数组 + 模板内临时 ID 父子引用)"
|
||||
},
|
||||
"tpl_hash": {
|
||||
"type": "str",
|
||||
"size": 64,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "内容哈希 sha256(canonical_json(tpl_json)),确定性实例化校验"
|
||||
},
|
||||
"offline_flag": {
|
||||
"type": "str",
|
||||
"size": 1,
|
||||
"notnull": true,
|
||||
"default": "N",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "离线兜底模板标记 Y/N",
|
||||
"code": "pbl_yes_no"
|
||||
},
|
||||
"platform_flag": {
|
||||
"type": "str",
|
||||
"size": 1,
|
||||
"notnull": true,
|
||||
"default": "N",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "平台公共标记 Y=平台公共模板(此时 tenant_id 必须为 NULL)",
|
||||
"code": "pbl_yes_no"
|
||||
},
|
||||
"ref_policy": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "warn",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "外部引用校验策略 strict=阻断回滚 / warn=落库并标记(Q-OPEN-4 阈值可配)",
|
||||
"code": "pbl_ref_policy"
|
||||
},
|
||||
"tpl_status": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "active",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "模板状态",
|
||||
"code": "pbl_tpl_status"
|
||||
},
|
||||
"usage_count": {
|
||||
"type": "int",
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "实例化次数(供 M10 模板使用率分析,非权威计数)"
|
||||
},
|
||||
"remark": {
|
||||
"type": "str",
|
||||
"size": 512,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": false,
|
||||
"summary": "备注"
|
||||
},
|
||||
"creator_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "创建人(rbac 用户ID)"
|
||||
},
|
||||
"create_time": {
|
||||
"type": "datetime",
|
||||
"notnull": true,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "创建时间"
|
||||
},
|
||||
"update_time": {
|
||||
"type": "datetime",
|
||||
"notnull": true,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "更新时间"
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"name": "id", "type": "varchar(64)", "primary_key": true, "comment": "模板ID tpl-xxxx"},
|
||||
{"name": "tenant_id", "type": "varchar(64)", "nullable": true, "index": true, "comment": "租户ID;NULL=平台公共模板(Q-OPEN-3 决议:不改 world 表,平台公共数据只落在 pbl_* 自有表)"},
|
||||
{"name": "code", "type": "varchar(64)", "nullable": false, "comment": "模板编码,租户内唯一;平台公共模板在 tenant_id IS NULL 域内唯一"},
|
||||
{"name": "name", "type": "varchar(200)", "nullable": false, "comment": "模板名称"},
|
||||
{"name": "category", "type": "varchar(64)", "nullable": true, "comment": "分类:stem/humanities/interdisciplinary/vocational/custom"},
|
||||
{"name": "subject_tags", "type": "json", "nullable": true, "comment": "学科标签数组"},
|
||||
{"name": "grade_range", "type": "varchar(64)", "nullable": true, "comment": "适用学段,如 G4-G6"},
|
||||
{"name": "duration_hours", "type": "int", "nullable": true, "comment": "建议课时(小时)"},
|
||||
{"name": "summary", "type": "varchar(1000)", "nullable": true, "comment": "模板简介"},
|
||||
{"name": "content", "type": "json", "nullable": true, "comment": "模板内容:driving_question/missions/roles/artifacts/assessment 等子对象骨架"},
|
||||
{"name": "subobject_kinds", "type": "json", "nullable": true, "comment": "模板包含的子对象类型清单(7类泛化契约)"},
|
||||
{"name": "ext_schema", "type": "json", "nullable": true, "comment": "扩展字段定义(字段名/类型/必填/枚举/默认值),实例化时用于校验"},
|
||||
{"name": "is_public", "type": "tinyint(1)", "nullable": false, "default": "1", "comment": "平台公共模板是否对全部租户可见(仅 tenant_id IS NULL 时有意义)"},
|
||||
{"name": "is_builtin", "type": "tinyint(1)", "nullable": false, "default": "0", "comment": "是否内置(内置模板禁止删除,仅可停用)"},
|
||||
{"name": "source", "type": "varchar(32)", "nullable": false, "default": "platform", "comment": "来源:platform/tenant/fork/import"},
|
||||
{"name": "source_template_id", "type": "varchar(64)", "nullable": true, "comment": "fork 来源模板ID(租户从平台公共模板派生时记录)"},
|
||||
{"name": "version", "type": "varchar(32)", "nullable": false, "default": "1.0.0", "comment": "模板版本号"},
|
||||
{"name": "status", "type": "varchar(32)", "nullable": false, "default": "draft", "comment": "draft/published/deprecated"},
|
||||
{"name": "usage_count", "type": "int", "nullable": false, "default": "0", "comment": "被实例化次数(平台公共模板跨租户累计)"},
|
||||
{"name": "owner_id", "type": "varchar(64)", "nullable": true, "comment": "创建人/负责人"},
|
||||
{"name": "created_by", "type": "varchar(64)", "nullable": true},
|
||||
{"name": "created_at", "type": "datetime", "nullable": true},
|
||||
{"name": "updated_by", "type": "varchar(64)", "nullable": true},
|
||||
{"name": "updated_at", "type": "datetime", "nullable": true},
|
||||
{"name": "deleted", "type": "tinyint(1)", "nullable": false, "default": "0", "comment": "软删除标记"}
|
||||
"editable": [
|
||||
"api/pbl_blueprint_template/list.dspy",
|
||||
"api/pbl_blueprint_template/edit.dspy",
|
||||
"api/pbl_blueprint_template/view.dspy"
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "uk_pbl_tpl_tenant_code", "unique": true, "columns": ["tenant_id", "code", "deleted"], "comment": "租户内编码唯一;tenant_id NULL 时 MySQL 唯一索引不去重,故平台公共模板唯一性由应用层 ensure 校验兜底"},
|
||||
{"name": "idx_pbl_tpl_status", "columns": ["tenant_id", "status", "deleted"]},
|
||||
{"name": "idx_pbl_tpl_category", "columns": ["category"]}
|
||||
],
|
||||
"seed": "pbl_blueprint/json/seed_template_offline.json"
|
||||
"browserfields": [
|
||||
"template_code",
|
||||
"template_name",
|
||||
"template_version",
|
||||
"category",
|
||||
"subject",
|
||||
"grade",
|
||||
"platform_flag",
|
||||
"tpl_status",
|
||||
"usage_count"
|
||||
]
|
||||
}
|
||||
|
||||
201
pbl_blueprint/json/m1b/pbl_ext_field_def.json
Normal file
201
pbl_blueprint/json/m1b/pbl_ext_field_def.json
Normal file
@ -0,0 +1,201 @@
|
||||
{
|
||||
"tblname": "pbl_ext_field_def",
|
||||
"params": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"size": 20,
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "物理主键(bigint AUTO_INCREMENT)"
|
||||
},
|
||||
"tenant_key": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "租户键 = COALESCE(tenant_id,'__PLATFORM__');索引打头列;应用层计算"
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": null,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "租户ID(首业务字段,读写强制打头,缺失即 fail-closed);NULL=平台内置字段定义,非空=租户自有定义"
|
||||
},
|
||||
"def_code": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "定义编码(业务主键,gen_code('EFD', tenant_id) 生成)"
|
||||
},
|
||||
"subobject_type": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "适用子对象类型(7 类之一;'*' 表示全部类型通用)",
|
||||
"code": "pbl_subobject_type"
|
||||
},
|
||||
"field_key": {
|
||||
"type": "str",
|
||||
"size": 64,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "扩展字段键(写入 pbl_subobject_ext.ext_key;^[a-z][a-z0-9_]{0,63}$)"
|
||||
},
|
||||
"field_label": {
|
||||
"type": "str",
|
||||
"size": 128,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "字段显示名"
|
||||
},
|
||||
"value_type": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "string",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "值类型 string/int/float/bool/json/date/enum",
|
||||
"code": "pbl_ext_value_type"
|
||||
},
|
||||
"required_flag": {
|
||||
"type": "str",
|
||||
"size": 1,
|
||||
"notnull": true,
|
||||
"default": "N",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "是否必填 Y=保存该子对象扩展时此键必须出现且非空",
|
||||
"code": "pbl_yes_no"
|
||||
},
|
||||
"default_value": {
|
||||
"type": "str",
|
||||
"size": 512,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": false,
|
||||
"summary": "默认值(调用方未传该键时按 value_type 强转后落库)"
|
||||
},
|
||||
"enum_options": {
|
||||
"type": "text",
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": false,
|
||||
"summary": "枚举候选(value_type=enum 时必填,JSON 数组字符串)"
|
||||
},
|
||||
"max_length": {
|
||||
"type": "int",
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": true,
|
||||
"query": false,
|
||||
"summary": "最大长度(0=不限制;>0 时对 string/json/enum 序列化长度做上限校验)"
|
||||
},
|
||||
"validate_regex": {
|
||||
"type": "str",
|
||||
"size": 256,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": false,
|
||||
"summary": "正则校验(非空时对 string 值 re.fullmatch,不符则 PBL_EXT_VALUE_INVALID)"
|
||||
},
|
||||
"platform_flag": {
|
||||
"type": "str",
|
||||
"size": 1,
|
||||
"notnull": true,
|
||||
"default": "N",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "平台内置标记 Y=平台内置定义(此时 tenant_id 必须为 NULL)",
|
||||
"code": "pbl_yes_no"
|
||||
},
|
||||
"def_status": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "active",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "定义状态(disabled 不参与校验且拒绝新值写入,历史值只读保留)",
|
||||
"code": "pbl_ext_def_status"
|
||||
},
|
||||
"sort_no": {
|
||||
"type": "int",
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "排序号"
|
||||
},
|
||||
"remark": {
|
||||
"type": "str",
|
||||
"size": 512,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": false,
|
||||
"summary": "备注"
|
||||
},
|
||||
"creator_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "创建人"
|
||||
},
|
||||
"create_time": {
|
||||
"type": "datetime",
|
||||
"notnull": true,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "创建时间"
|
||||
},
|
||||
"update_time": {
|
||||
"type": "datetime",
|
||||
"notnull": true,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "更新时间"
|
||||
}
|
||||
},
|
||||
"editable": [
|
||||
"api/pbl_ext_field_def/list.dspy",
|
||||
"api/pbl_ext_field_def/edit.dspy",
|
||||
"api/pbl_ext_field_def/view.dspy"
|
||||
],
|
||||
"browserfields": [
|
||||
"def_code",
|
||||
"subobject_type",
|
||||
"field_key",
|
||||
"field_label",
|
||||
"value_type",
|
||||
"required_flag",
|
||||
"platform_flag",
|
||||
"def_status",
|
||||
"sort_no"
|
||||
]
|
||||
}
|
||||
@ -1,38 +1,180 @@
|
||||
{
|
||||
"table": "pbl_subobject_ext",
|
||||
"comment": "PBL 子对象扩展(M1b):7 类泛化子对象的扩展字段实例 + 子对象间关联边。tenant_id 强制打头,不允许 NULL",
|
||||
"engine": "InnoDB",
|
||||
"charset": "utf8mb4",
|
||||
"multi_tenant": true,
|
||||
"tenant_policy": {
|
||||
"column": "tenant_id",
|
||||
"nullable": false,
|
||||
"fail_closed": true,
|
||||
"note": "子对象扩展属于租户业务数据,严禁平台公共(NULL);平台级只允许 pbl_blueprint_template / pbl_ext_field_def"
|
||||
"tblname": "pbl_subobject_ext",
|
||||
"params": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"size": 20,
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "物理主键(bigint AUTO_INCREMENT)"
|
||||
},
|
||||
"tenant_key": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "租户键 = COALESCE(tenant_id,'__PLATFORM__');索引打头列;应用层计算"
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": null,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "租户ID(首业务字段,读写强制打头,缺失即 fail-closed);NULL=平台公共模板的子对象扩展,非空=租户数据"
|
||||
},
|
||||
"ext_code": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "扩展记录编码(业务主键,gen_code('SXE', tenant_id) 生成)"
|
||||
},
|
||||
"owner_kind": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "blueprint",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "宿主类型 blueprint=真实蓝图子对象 / template=模板内容子对象",
|
||||
"code": "pbl_ext_owner_kind"
|
||||
},
|
||||
"owner_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "宿主ID(blueprint→blueprint_id;template→template_code)"
|
||||
},
|
||||
"owner_version": {
|
||||
"type": "int",
|
||||
"notnull": true,
|
||||
"default": 1,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "宿主版本(blueprint→蓝图版本号;template→template_version),版本隔离避免跨版本串数据"
|
||||
},
|
||||
"subobject_type": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "子对象类型(7 类之一)",
|
||||
"code": "pbl_subobject_type"
|
||||
},
|
||||
"subobject_ref": {
|
||||
"type": "str",
|
||||
"size": 64,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "子对象归一引用(非空列:blueprint→M1A subobject_id;template→模板内临时 ID),参与唯一键杜绝 NULL 重复行"
|
||||
},
|
||||
"subobject_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "蓝图子对象ID(冗余列,owner_kind=blueprint 时与 subobject_ref 同值)"
|
||||
},
|
||||
"ext_key": {
|
||||
"type": "str",
|
||||
"size": 64,
|
||||
"notnull": true,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "扩展字段键(必须在 pbl_ext_field_def 中存在且 def_status=active,否则 PBL_EXT_FIELD_UNDEFINED)"
|
||||
},
|
||||
"ext_value": {
|
||||
"type": "text",
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"query": false,
|
||||
"summary": "扩展字段值(统一字符串形态;json/bool/date 按 value_type 序列化)"
|
||||
},
|
||||
"value_type": {
|
||||
"type": "str",
|
||||
"size": 16,
|
||||
"notnull": true,
|
||||
"default": "string",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "值类型快照(写入时从 pbl_ext_field_def.value_type 快照,历史值可按当时类型解读)",
|
||||
"code": "pbl_ext_value_type"
|
||||
},
|
||||
"field_def_id": {
|
||||
"type": "int",
|
||||
"size": 20,
|
||||
"notnull": false,
|
||||
"default": 0,
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "字段定义ID(指向 pbl_ext_field_def.id,逻辑关联无 FK)"
|
||||
},
|
||||
"sort_no": {
|
||||
"type": "int",
|
||||
"notnull": true,
|
||||
"default": 0,
|
||||
"editable": true,
|
||||
"query": true,
|
||||
"summary": "排序号"
|
||||
},
|
||||
"creator_id": {
|
||||
"type": "str",
|
||||
"size": 32,
|
||||
"notnull": false,
|
||||
"default": "",
|
||||
"editable": false,
|
||||
"query": true,
|
||||
"summary": "创建人"
|
||||
},
|
||||
"create_time": {
|
||||
"type": "datetime",
|
||||
"notnull": true,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "创建时间"
|
||||
},
|
||||
"update_time": {
|
||||
"type": "datetime",
|
||||
"notnull": true,
|
||||
"default": null,
|
||||
"editable": false,
|
||||
"query": false,
|
||||
"summary": "更新时间"
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"name": "id", "type": "varchar(64)", "primary_key": true, "comment": "扩展记录ID subext-xxxx"},
|
||||
{"name": "tenant_id", "type": "varchar(64)", "nullable": false, "index": true, "comment": "租户ID(必填,缺失即拒绝)"},
|
||||
{"name": "blueprint_id", "type": "varchar(64)", "nullable": false, "comment": "所属蓝图ID"},
|
||||
{"name": "version_id", "type": "varchar(64)", "nullable": true, "comment": "所属蓝图版本ID(空=当前工作副本)"},
|
||||
{"name": "subobject_kind", "type": "varchar(32)", "nullable": false, "comment": "子对象类型:driving_question/mission/role/learner/artifact_def/problem/project/learning_goal"},
|
||||
{"name": "subobject_id", "type": "varchar(64)", "nullable": false, "comment": "子对象主键(对应 pbl_* 子表 id)"},
|
||||
{"name": "ext_key", "type": "varchar(64)", "nullable": false, "comment": "扩展字段名(由 pbl_ext_field_def 定义)"},
|
||||
{"name": "ext_value", "type": "json", "nullable": true, "comment": "扩展字段值(按定义类型序列化)"},
|
||||
{"name": "value_type", "type": "varchar(16)", "nullable": false, "default": "string", "comment": "string/int/float/bool/enum/json/date"},
|
||||
{"name": "source", "type": "varchar(32)", "nullable": false, "default": "manual", "comment": "manual/template_instantiate/agent/import"},
|
||||
{"name": "source_template_id", "type": "varchar(64)", "nullable": true, "comment": "若由模板实例化产生,记录来源模板(含平台公共模板)"},
|
||||
{"name": "seq", "type": "int", "nullable": false, "default": "0", "comment": "排序"},
|
||||
{"name": "status", "type": "varchar(32)", "nullable": false, "default": "active", "comment": "active/deprecated"},
|
||||
{"name": "created_by", "type": "varchar(64)", "nullable": true},
|
||||
{"name": "created_at", "type": "datetime", "nullable": true},
|
||||
{"name": "updated_by", "type": "varchar(64)", "nullable": true},
|
||||
{"name": "updated_at", "type": "datetime", "nullable": true},
|
||||
{"name": "deleted", "type": "tinyint(1)", "nullable": false, "default": "0"}
|
||||
"editable": [
|
||||
"api/pbl_subobject_ext/list.dspy",
|
||||
"api/pbl_subobject_ext/edit.dspy",
|
||||
"api/pbl_subobject_ext/view.dspy"
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "uk_pbl_subext", "unique": true, "columns": ["tenant_id", "blueprint_id", "subobject_kind", "subobject_id", "ext_key", "deleted"]},
|
||||
{"name": "idx_pbl_subext_obj", "columns": ["tenant_id", "subobject_kind", "subobject_id"]},
|
||||
{"name": "idx_pbl_subext_bp", "columns": ["tenant_id", "blueprint_id", "deleted"]}
|
||||
"browserfields": [
|
||||
"ext_code",
|
||||
"owner_kind",
|
||||
"owner_id",
|
||||
"subobject_type",
|
||||
"subobject_ref",
|
||||
"ext_key",
|
||||
"ext_value",
|
||||
"value_type"
|
||||
]
|
||||
}
|
||||
|
||||
340
pbl_blueprint/m1b_db.py
Normal file
340
pbl_blueprint/m1b_db.py
Normal file
@ -0,0 +1,340 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M1b 真实库适配层(sqlor 标准 API 唯一出口)+ Q-OPEN-3 写保护闸门。
|
||||
|
||||
本文件是 M1b 与真实 MariaDB 之间**唯一**的 DB 通道,职责三件:
|
||||
|
||||
1. ``tenant_key(tenant_id)``
|
||||
统一计算索引打头列 tenant_key = COALESCE(tenant_id, '__PLATFORM__')。
|
||||
禁止调用方自行传入 tenant_key —— 平台公共行(tenant_id IS NULL)必须落到
|
||||
'__PLATFORM__' 域,否则 MariaDB 唯一索引不对 NULL 去重,平台模板/字段定义
|
||||
会被重复插入。
|
||||
|
||||
2. ``WRITE_PROTECTED_TABLES`` + ``assert_writable(table, sql)``
|
||||
Q-OPEN-3 裁决『不改 world 表』的物理闸门:world / scene / entity / script
|
||||
等复用域基表一律禁止 INSERT / UPDATE / DELETE / ALTER / DROP / TRUNCATE。
|
||||
任何越界写操作在 SQL 执行前被拦截并抛 ``PBL_DB_WRITE_PROTECTED``。
|
||||
与 ``m1b_ref.assert_writable()`` 构成双重闸门(业务层 + DB 层)。
|
||||
|
||||
3. ``SorDB``
|
||||
把 sqlor 的 6 个标准 API(C/U/D/R/I/sqlExe)包装成 m1b_common 适配器契约
|
||||
(query/insert/update/delete),使 M1b 业务函数在**内存 FakeDB 与真实库**
|
||||
下走同一套代码路径。库名一律来自 ``ServerEnv().get_module_dbname``
|
||||
(经 pbl_blueprint.db.get_dbname 转发),本文件不硬编码任何库名。
|
||||
|
||||
sqlor API 使用约束(铁律):本模块只允许 sor.C / sor.U / sor.D / sor.R /
|
||||
sor.I / sor.sqlExe 六个标准 API,禁止编造 save/list/insert 等不存在的方法。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
__all__ = [
|
||||
"PLATFORM_TENANT_KEY", "WRITE_PROTECTED_TABLES", "M1B_TABLES",
|
||||
"PBL_DB_WRITE_PROTECTED", "WriteProtectedError",
|
||||
"tenant_key", "assert_writable", "guard_sql", "SorDB", "make_db",
|
||||
"split_ddl",
|
||||
]
|
||||
|
||||
# 平台公共域键:tenant_id IS NULL 时统一落到该键,保证唯一索引可去重
|
||||
PLATFORM_TENANT_KEY = "__PLATFORM__"
|
||||
|
||||
# Q-OPEN-3:复用域基表零改动、零加列、零回写
|
||||
WRITE_PROTECTED_TABLES = (
|
||||
"world", "scene", "entity", "script",
|
||||
"world_snapshot", "world_sync", "world_sync_task", "world_sync_log",
|
||||
"scense", "scense_game",
|
||||
)
|
||||
|
||||
# M1b 自有 4 表(与 models/m1b/*.json、sql/m1b_ddl.sql 一一对应)
|
||||
M1B_TABLES = (
|
||||
"pbl_blueprint_template",
|
||||
"pbl_ext_field_def",
|
||||
"pbl_subobject_ext",
|
||||
"pbl_blueprint_ref",
|
||||
)
|
||||
|
||||
PBL_DB_WRITE_PROTECTED = "PBL_DB_WRITE_PROTECTED"
|
||||
|
||||
_WRITE_VERBS = ("insert", "update", "delete", "replace", "alter", "drop",
|
||||
"truncate", "create", "rename")
|
||||
_TABLE_RE = re.compile(
|
||||
r"(?:from|into|update|table|join)\s+`?([a-zA-Z_][a-zA-Z0-9_]*)`?",
|
||||
re.IGNORECASE)
|
||||
|
||||
|
||||
class WriteProtectedError(Exception):
|
||||
"""Q-OPEN-3 写保护违规(错误码 PBL_DB_WRITE_PROTECTED)。"""
|
||||
|
||||
code = PBL_DB_WRITE_PROTECTED
|
||||
|
||||
def __init__(self, table, verb="", sql=""):
|
||||
self.table = table
|
||||
self.verb = verb
|
||||
self.sql = sql
|
||||
Exception.__init__(
|
||||
self, "%s: 表 %s 属复用域基表,Q-OPEN-3 裁决禁止任何写操作/结构变更"
|
||||
"(verb=%s)" % (PBL_DB_WRITE_PROTECTED, table, verb or "?"))
|
||||
|
||||
|
||||
def tenant_key(tenant_id):
|
||||
"""索引打头列计算:NULL/空 → '__PLATFORM__',其余原样字符串化。"""
|
||||
if tenant_id is None:
|
||||
return PLATFORM_TENANT_KEY
|
||||
s = str(tenant_id).strip()
|
||||
if not s or s.upper() in ("NULL", "NONE"):
|
||||
return PLATFORM_TENANT_KEY
|
||||
return s
|
||||
|
||||
|
||||
def _tables_in_sql(sql):
|
||||
return [m.group(1).lower() for m in _TABLE_RE.finditer(sql or "")]
|
||||
|
||||
|
||||
def assert_writable(table, sql=""):
|
||||
"""写前闸门:目标表(或 SQL 中出现的表)命中写保护清单即抛异常。
|
||||
|
||||
只读 SELECT 不经过本函数(resolve_refs 的存在性校验走 SorDB.query)。
|
||||
"""
|
||||
names = [str(table or "").strip().lower().strip("`")]
|
||||
names.extend(_tables_in_sql(sql))
|
||||
for n in names:
|
||||
if n and n in WRITE_PROTECTED_TABLES:
|
||||
raise WriteProtectedError(n, "write", sql)
|
||||
return True
|
||||
|
||||
|
||||
def guard_sql(sql):
|
||||
"""SQL 级闸门:写动词 + 写保护表 同时命中才拦截(纯 SELECT 放行)。"""
|
||||
text = (sql or "").strip()
|
||||
if not text:
|
||||
return True
|
||||
head = text.split(None, 1)[0].lower().strip("(")
|
||||
if head not in _WRITE_VERBS:
|
||||
# 非写语句(SELECT/SHOW/DESC)放行;多语句里再逐段查
|
||||
for seg in [s for s in text.split(";") if s.strip()]:
|
||||
h = seg.strip().split(None, 1)[0].lower().strip("(")
|
||||
if h in _WRITE_VERBS:
|
||||
for t in _tables_in_sql(seg):
|
||||
if t in WRITE_PROTECTED_TABLES:
|
||||
raise WriteProtectedError(t, h, seg)
|
||||
return True
|
||||
for t in _tables_in_sql(text):
|
||||
if t in WRITE_PROTECTED_TABLES:
|
||||
raise WriteProtectedError(t, head, text)
|
||||
return True
|
||||
|
||||
|
||||
def split_ddl(ddl_text):
|
||||
"""把 DDL 文件切成可逐条 sqlExe 的语句列表(去注释、去空段、幂等)。"""
|
||||
out, buf = [], []
|
||||
for raw in (ddl_text or "").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("--"):
|
||||
continue
|
||||
buf.append(raw)
|
||||
if line.endswith(";"):
|
||||
stmt = "\n".join(buf).strip().rstrip(";").strip()
|
||||
if stmt:
|
||||
out.append(stmt)
|
||||
buf = []
|
||||
tail = "\n".join(buf).strip().rstrip(";").strip()
|
||||
if tail:
|
||||
out.append(tail)
|
||||
return out
|
||||
|
||||
|
||||
def _esc(v):
|
||||
"""SQL 字面量转义(sqlor sqlExe 拼接场景)。"""
|
||||
if v is None:
|
||||
return "NULL"
|
||||
if isinstance(v, bool):
|
||||
return "1" if v else "0"
|
||||
if isinstance(v, (int, float)):
|
||||
return str(v)
|
||||
if isinstance(v, (dict, list)):
|
||||
v = json.dumps(v, ensure_ascii=False)
|
||||
s = str(v).replace("\\", "\\\\").replace("'", "\\'")
|
||||
return "'%s'" % s
|
||||
|
||||
|
||||
class _IsNull(object):
|
||||
"""与 m1b_common.IS_NULL 同语义的哨兵(避免循环 import 各自判类型)。"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self):
|
||||
return "IS_NULL"
|
||||
|
||||
|
||||
IS_NULL = _IsNull()
|
||||
|
||||
|
||||
class SorDB(object):
|
||||
"""sqlor 标准 API 包装器 → m1b_common 适配器契约(query/insert/update/delete)。
|
||||
|
||||
构造:``SorDB(sor, dbname)``。sor 必须是 sqlor 实例(具备 C/U/D/R/I/sqlExe)。
|
||||
所有写操作先过 ``assert_writable`` / ``guard_sql`` 双闸门。
|
||||
"""
|
||||
|
||||
def __init__(self, sor, dbname, strict_guard=True):
|
||||
if sor is None:
|
||||
raise RuntimeError("SorDB: sor 不能为空(请确认应用已注入 sqlor 实例)")
|
||||
if not dbname:
|
||||
raise RuntimeError(
|
||||
"SorDB: dbname 为空 —— 库名必须由 ServerEnv().get_module_dbname("
|
||||
"'pbl_blueprint') 注入,禁止硬编码;请检查应用 init() 是否已挂载")
|
||||
self.sor = sor
|
||||
self.dbname = dbname
|
||||
self.strict_guard = strict_guard
|
||||
|
||||
# ------------------------------------------------------------ 读
|
||||
def query(self, table, conds=None, order_by=None, limit=None):
|
||||
where, args = self._where(conds)
|
||||
sql = "SELECT * FROM `%s`" % table
|
||||
if where:
|
||||
sql += " WHERE " + where
|
||||
if order_by:
|
||||
desc = str(order_by).startswith("-")
|
||||
col = str(order_by).lstrip("+-")
|
||||
sql += " ORDER BY `%s` %s" % (col, "DESC" if desc else "ASC")
|
||||
if limit is not None:
|
||||
sql += " LIMIT %d" % int(limit)
|
||||
rows = self.sor.sqlExe(self.dbname, sql, args) if args else \
|
||||
self.sor.sqlExe(self.dbname, sql)
|
||||
return [dict(r) for r in (rows or [])]
|
||||
|
||||
def select(self, table, conds=None, order_by=None, limit=None):
|
||||
return self.query(table, conds, order_by=order_by, limit=limit)
|
||||
|
||||
def count(self, table, conds=None):
|
||||
where, args = self._where(conds)
|
||||
sql = "SELECT COUNT(1) AS c FROM `%s`" % table
|
||||
if where:
|
||||
sql += " WHERE " + where
|
||||
rows = self.sor.sqlExe(self.dbname, sql, args) if args else \
|
||||
self.sor.sqlExe(self.dbname, sql)
|
||||
try:
|
||||
return int((rows or [{}])[0].get("c", 0))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
# ------------------------------------------------------------ 写
|
||||
def insert(self, table, row):
|
||||
assert_writable(table)
|
||||
row = dict(row or {})
|
||||
cols = list(row.keys())
|
||||
if not cols:
|
||||
return None
|
||||
sql = "INSERT INTO `%s` (%s) VALUES (%s)" % (
|
||||
table,
|
||||
", ".join("`%s`" % c for c in cols),
|
||||
", ".join(_esc(row[c]) for c in cols))
|
||||
self.sor.sqlExe(self.dbname, sql)
|
||||
return row.get("id")
|
||||
|
||||
def update(self, table, conds, values):
|
||||
assert_writable(table)
|
||||
values = dict(values or {})
|
||||
if not values:
|
||||
return 0
|
||||
where, args = self._where(conds)
|
||||
sets = ", ".join("`%s`=%s" % (k, _esc(v)) for k, v in values.items())
|
||||
sql = "UPDATE `%s` SET %s" % (table, sets)
|
||||
if where:
|
||||
sql += " WHERE " + where
|
||||
self.sor.sqlExe(self.dbname, sql, args)
|
||||
return 1
|
||||
|
||||
def delete(self, table, conds):
|
||||
assert_writable(table)
|
||||
where, args = self._where(conds)
|
||||
if not where:
|
||||
raise RuntimeError("SorDB.delete: 拒绝无 WHERE 的全表删除(%s)" % table)
|
||||
self.sor.sqlExe(self.dbname, "DELETE FROM `%s` WHERE %s" % (table, where), args)
|
||||
return 1
|
||||
|
||||
def sqlExe(self, sql, args=None):
|
||||
"""裸 SQL 出口(建表 DDL / 批量种子)——同样过 Q-OPEN-3 闸门。"""
|
||||
if self.strict_guard:
|
||||
guard_sql(sql)
|
||||
if args:
|
||||
return self.sor.sqlExe(self.dbname, sql, args)
|
||||
return self.sor.sqlExe(self.dbname, sql)
|
||||
|
||||
# 兼容 sqlor 原生 6 API 直通(供确需标准 API 的调用方)
|
||||
def C(self, tbl, data):
|
||||
assert_writable(tbl)
|
||||
return self.sor.C(self.dbname, tbl, data)
|
||||
|
||||
def R(self, tbl, where="", args=None, fields="*", orderby="", limit=0, offset=0):
|
||||
return self.sor.R(self.dbname, tbl, where, args, fields, orderby, limit, offset)
|
||||
|
||||
def U(self, tbl, data, where, args=None):
|
||||
assert_writable(tbl)
|
||||
return self.sor.U(self.dbname, tbl, data, where, args)
|
||||
|
||||
def D(self, tbl, where, args=None):
|
||||
assert_writable(tbl)
|
||||
return self.sor.D(self.dbname, tbl, where, args)
|
||||
|
||||
def I(self, sql, args=None):
|
||||
if self.strict_guard:
|
||||
guard_sql(sql)
|
||||
return self.sor.I(self.dbname, sql, args) if args else self.sor.I(self.dbname, sql)
|
||||
|
||||
# ------------------------------------------------------------ 内部
|
||||
@staticmethod
|
||||
def _where(conds):
|
||||
clauses, args = [], []
|
||||
for k, v in (conds or {}).items():
|
||||
col = str(k)
|
||||
if isinstance(v, _IsNull) or v is IS_NULL:
|
||||
clauses.append("`%s` IS NULL" % col)
|
||||
continue
|
||||
if v is None:
|
||||
# None 作为等值条件在 SQL 里无意义 → 归一为 IS NULL
|
||||
clauses.append("`%s` IS NULL" % col)
|
||||
continue
|
||||
if isinstance(v, (list, tuple, set)):
|
||||
items = list(v)
|
||||
if not items:
|
||||
clauses.append("1=0")
|
||||
continue
|
||||
clauses.append("`%s` IN (%s)" % (col, ", ".join(["%s"] * len(items))))
|
||||
args.extend(items)
|
||||
continue
|
||||
if col.endswith("!"):
|
||||
clauses.append("`%s` <> %%s" % col[:-1])
|
||||
args.append(v)
|
||||
continue
|
||||
if col.endswith("~"):
|
||||
clauses.append("`%s` LIKE %%s" % col[:-1])
|
||||
args.append(v)
|
||||
continue
|
||||
clauses.append("`%s` = %%s" % col)
|
||||
args.append(v)
|
||||
return (" AND ".join(clauses), args)
|
||||
|
||||
|
||||
def make_db(sor=None, dbname=None):
|
||||
"""构造真实库适配器:sor/dbname 缺省时从 pbl_blueprint.db 取(应用注入)。
|
||||
|
||||
取不到库名 → 返回 None(调用方 fail-closed,绝不回落默认库)。
|
||||
"""
|
||||
try:
|
||||
from . import db as _db
|
||||
except Exception:
|
||||
_db = None
|
||||
if sor is None and _db is not None:
|
||||
try:
|
||||
sor = _db.get_sor()
|
||||
except Exception:
|
||||
sor = None
|
||||
if not dbname and _db is not None:
|
||||
try:
|
||||
dbname = _db.get_dbname()
|
||||
except Exception:
|
||||
dbname = None
|
||||
if sor is None or not dbname:
|
||||
return None
|
||||
return SorDB(sor, dbname)
|
||||
@ -19,20 +19,38 @@ from .m1b_common import IS_NULL, insert, select_one, now_iso, new_id
|
||||
from . import m1b_template as T
|
||||
from . import m1b_subobject as S
|
||||
|
||||
__all__ = ["M1B_TABLES", "M1B_MODEL_FILES", "init_m1b", "ensure_m1b_tables",
|
||||
"ensure_platform_ext_defs", "seed_platform_data"]
|
||||
__all__ = ["M1B_TABLES", "M1B_MODEL_FILES", "M1B_CRUD_FILES", "M1B_DDL",
|
||||
"PLATFORM_EXT_DEFS", "PLATFORM_TEMPLATES",
|
||||
"init_m1b", "ensure_m1b_tables", "ensure_platform_ext_defs",
|
||||
"seed_platform_data", "load_m1b_models", "load_m1b_crud_defs"]
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# 目录规约(module-development-spec / database-table-definition-spec):
|
||||
# * 表定义(四段式 summary/fields/indexes/codes)→ pbl_blueprint/models/m1b/*.json
|
||||
# * CRUD 定义(tblname + params.editable) → pbl_blueprint/json/m1b/*.json
|
||||
# * 幂等 DDL(4 表 CREATE TABLE IF NOT EXISTS) → <repo>/sql/m1b_ddl.sql
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__)) # .../pbl_blueprint/pbl_blueprint
|
||||
_REPO = os.path.dirname(_HERE) # .../pbl_blueprint(仓库根)
|
||||
|
||||
M1B_TABLES = ["pbl_blueprint_template", "pbl_ext_field_def", "pbl_subobject_ext",
|
||||
"pbl_blueprint_ref"]
|
||||
|
||||
# 表模型(四段式)——建表/字段校验的权威来源
|
||||
M1B_MODEL_FILES = [
|
||||
os.path.join(_HERE, "models", "m1b", "pbl_blueprint_template.json"),
|
||||
os.path.join(_HERE, "models", "m1b", "pbl_ext_field_def.json"),
|
||||
os.path.join(_HERE, "models", "m1b", "pbl_subobject_ext.json"),
|
||||
os.path.join(_HERE, "models", "m1b", "pbl_blueprint_ref.json"),
|
||||
]
|
||||
|
||||
# CRUD 定义(tblname + params.editable)——供 sqlor/前端 CRUD 装配
|
||||
M1B_CRUD_FILES = [
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_template.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_ext_field_def.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_subobject_ext.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_ref.json"),
|
||||
]
|
||||
M1B_TABLES = ["pbl_blueprint_template", "pbl_ext_field_def", "pbl_subobject_ext",
|
||||
"pbl_blueprint_ref"]
|
||||
M1B_DDL = os.path.join(_HERE, "sql", "m1b_ddl.sql")
|
||||
|
||||
M1B_DDL = os.path.join(_REPO, "sql", "m1b_ddl.sql")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
@ -249,7 +267,8 @@ def ensure_m1b_tables(db=None, env=None, executor=None):
|
||||
models.append(json.load(fp))
|
||||
except Exception:
|
||||
continue
|
||||
result = {"ok": True, "models": [m.get("table") for m in models], "ddl": None, "executed": False}
|
||||
result = {"ok": True, "models": [m.get("table") for m in models],
|
||||
"ddl": None, "executed": 0, "statements": 0, "errors": []}
|
||||
|
||||
# 1) 尝试模块既有注册机制
|
||||
registered = False
|
||||
@ -266,25 +285,78 @@ def ensure_m1b_tables(db=None, env=None, executor=None):
|
||||
continue
|
||||
result["registered_via_module"] = registered
|
||||
|
||||
# 2) DDL 兜底
|
||||
# 2) 幂等 DDL(sql/m1b_ddl.sql,4 表 CREATE TABLE IF NOT EXISTS)
|
||||
if os.path.exists(M1B_DDL):
|
||||
try:
|
||||
with open(M1B_DDL, "r", encoding="utf-8") as fp:
|
||||
ddl = fp.read()
|
||||
result["ddl"] = M1B_DDL
|
||||
from .m1b_db import split_ddl, guard_sql
|
||||
stmts = split_ddl(ddl)
|
||||
result["statements"] = len(stmts)
|
||||
|
||||
# 执行通道优先级:显式 executor > 真实库 SorDB(sqlor 标准 API)
|
||||
runner = None
|
||||
if callable(executor):
|
||||
for stmt in [s.strip() for s in ddl.split(";") if s.strip()
|
||||
and not s.strip().startswith("--")]:
|
||||
runner = executor
|
||||
else:
|
||||
try:
|
||||
from .m1b_db import make_db
|
||||
real = make_db()
|
||||
if real is not None:
|
||||
runner = real.sqlExe
|
||||
result["channel"] = "sqlor(SorDB.sqlExe)"
|
||||
except Exception:
|
||||
runner = None
|
||||
if runner is not None:
|
||||
for stmt in stmts:
|
||||
try:
|
||||
executor(stmt)
|
||||
except Exception:
|
||||
continue
|
||||
result["executed"] = True
|
||||
except Exception:
|
||||
pass
|
||||
guard_sql(stmt) # Q-OPEN-3:拦截任何复用域基表写/改
|
||||
runner(stmt)
|
||||
result["executed"] += 1
|
||||
except Exception as e: # 单条失败不阻断其余(幂等重跑可补)
|
||||
result["errors"].append("%s: %s" % (type(e).__name__, e))
|
||||
result["ok"] = not result["errors"]
|
||||
else:
|
||||
result["channel"] = "none(内存适配器:表按需自动创建)"
|
||||
except Exception as e:
|
||||
result["ok"] = False
|
||||
result["errors"].append("ddl_load: %s" % e)
|
||||
return result
|
||||
|
||||
|
||||
def load_m1b_models():
|
||||
"""读取 4 张表的四段式表模型(models/m1b/*.json)。"""
|
||||
out = []
|
||||
for f in M1B_MODEL_FILES:
|
||||
if not os.path.exists(f):
|
||||
continue
|
||||
try:
|
||||
with open(f, "r", encoding="utf-8") as fp:
|
||||
d = json.load(fp)
|
||||
d["_file"] = os.path.relpath(f, _REPO)
|
||||
out.append(d)
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def load_m1b_crud_defs():
|
||||
"""读取 4 张表的 CRUD 定义(json/m1b/*.json,tblname + params.editable)。"""
|
||||
out = []
|
||||
for f in M1B_CRUD_FILES:
|
||||
if not os.path.exists(f):
|
||||
continue
|
||||
try:
|
||||
with open(f, "r", encoding="utf-8") as fp:
|
||||
d = json.load(fp)
|
||||
d["_file"] = os.path.relpath(f, _REPO)
|
||||
out.append(d)
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def ensure_platform_ext_defs(db, actor="system"):
|
||||
"""幂等注入平台公共扩展字段定义(tenant_id NULL)。"""
|
||||
created, skipped = [], []
|
||||
@ -338,4 +410,7 @@ def init_m1b(db=None, env=None, register=None, executor=None, seed=True, actor="
|
||||
except Exception as e:
|
||||
seeded = {"ok": False, "error": str(e)}
|
||||
return {"ok": True, "milestone": "M1b", "tables": tables, "routes": routes,
|
||||
"seed": seeded, "q_open_3": "world/scene/entity 基表零改动"}
|
||||
"seed": seeded,
|
||||
"models_loaded": [m.get("_file") for m in load_m1b_models()],
|
||||
"crud_defs_loaded": [c.get("tblname") for c in load_m1b_crud_defs()],
|
||||
"q_open_3": "world/scene/entity/script 基表零改动、零加列、零回写"}
|
||||
|
||||
53
pbl_blueprint/models/m1b/pbl_blueprint_ref.json
Normal file
53
pbl_blueprint/models/m1b/pbl_blueprint_ref.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"summary": "PBL 蓝图外部关联表(M1b)——Q-OPEN-3 裁决的物理落地。裁决结论:『不改 world 表』(含 scene / entity / script 等复用域基表一律零改动、零加列、零回写)。因此蓝图与复用域对象的全部关联关系单向落在本表:本表持有 ref_target_code 指向外部对象业务编码(逻辑关联,无 FOREIGN KEY),ref_mode 恒为 'readonly' 并由 m1b_ref.assert_writable() 与 m1b_db.WRITE_PROTECTED_TABLES 双重闸门强制(任何针对 world/scene/entity/script 的 INSERT/UPDATE/DELETE 在 SQL 执行前被拦截并抛 PBL_DB_WRITE_PROTECTED)。resolve_refs() 仅做只读存在性校验并把结果写回本表 ref_status(resolved/unresolved/invalid),校验策略 strict/warn 由 pbl_blueprint_template.ref_policy 决定(Q-OPEN-4 阈值可配)。snapshot_json 保存解析时刻的只读快照,避免运行期跨库联查。无 FOREIGN KEY、无 ENUM、无 TIMESTAMP。",
|
||||
"fields": [
|
||||
{"name": "id", "type": "bigint", "label": "物理主键", "primary": true, "auto": true, "required": true, "nullable": false, "comment": "AUTO_INCREMENT"},
|
||||
{"name": "tenant_key", "type": "varchar", "len": 32, "label": "租户键", "required": true, "nullable": false, "comment": "= COALESCE(tenant_id,'__PLATFORM__');索引打头列;应用层计算"},
|
||||
{"name": "tenant_id", "type": "varchar", "len": 32, "label": "租户ID", "required": false, "nullable": true, "default": null, "comment": "NULL = 平台公共模板携带的关联;非空 = 租户蓝图关联"},
|
||||
{"name": "ref_code", "type": "varchar", "len": 32, "label": "关联编码", "required": true, "nullable": false, "comment": "业务主键,pbl_common.gen_code('BPR', tenant_id) 生成"},
|
||||
{"name": "owner_kind", "type": "varchar", "len": 16, "label": "宿主类型", "required": true, "nullable": false, "default": "blueprint", "comment": "字典 pbl_ext_owner_kind:blueprint / template"},
|
||||
{"name": "blueprint_id", "type": "varchar", "len": 32, "label": "蓝图ID", "required": true, "nullable": false, "comment": "owner_kind=blueprint 时为 M1A blueprint_id;=template 时复用本列存 template_code(列名保留以对齐 M1A 语义,取值由 owner_kind 判定)"},
|
||||
{"name": "subobject_id", "type": "varchar", "len": 32, "label": "子对象ID", "required": false, "nullable": true, "comment": "关联所属的 *_ref 类子对象ID;模板态存模板临时ID"},
|
||||
{"name": "ref_kind", "type": "varchar", "len": 16, "label": "关联对象域", "required": true, "nullable": false, "comment": "字典 pbl_ref_kind:world / scene / entity / script"},
|
||||
{"name": "ref_target_code", "type": "varchar", "len": 64, "label": "外部对象编码", "required": true, "nullable": false, "comment": "复用域对象业务编码(只读引用,无 FK,不反查基表写操作)"},
|
||||
{"name": "ref_target_id", "type": "varchar", "len": 64, "label": "外部对象ID", "required": false, "nullable": true, "comment": "解析成功时回填的对方物理ID快照"},
|
||||
{"name": "ref_mode", "type": "varchar", "len": 16, "label": "关联模式", "required": true, "nullable": false, "default": "readonly", "comment": "恒为 readonly(Q-OPEN-3:不改 world 表,只读引用);字典 pbl_ref_mode;写入非 readonly 值将被 m1b_ref 拒绝"},
|
||||
{"name": "ref_status", "type": "varchar", "len": 16, "label": "解析状态", "required": true, "nullable": false, "default": "unresolved", "comment": "字典 pbl_ref_status:unresolved 待解析 / resolved 已解析 / invalid 目标不存在或不可用"},
|
||||
{"name": "resolve_time", "type": "datetime", "label": "解析时间", "required": false, "nullable": true},
|
||||
{"name": "resolve_msg", "type": "varchar", "len": 512, "label": "解析信息", "required": false, "nullable": true, "comment": "invalid 时记录原因,供 M2 校验与教师端提示"},
|
||||
{"name": "snapshot_json", "type": "text", "label": "只读快照", "required": false, "nullable": true, "comment": "解析时刻外部对象关键字段快照(JSON),运行期免跨库联查"},
|
||||
{"name": "sort_no", "type": "int", "label": "排序号", "required": true, "nullable": false, "default": 0},
|
||||
{"name": "creator_id", "type": "varchar", "len": 32, "label": "创建人", "required": false, "nullable": true},
|
||||
{"name": "create_time", "type": "datetime", "label": "创建时间", "required": true, "nullable": false},
|
||||
{"name": "update_time", "type": "datetime", "label": "更新时间", "required": true, "nullable": false}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "PRIMARY", "fields": ["id"], "unique": true, "primary": true},
|
||||
{"name": "uk_ref_code", "fields": ["tenant_key", "ref_code"], "unique": true, "comment": "业务主键唯一"},
|
||||
{"name": "uk_ref_slot", "fields": ["tenant_key", "owner_kind", "blueprint_id", "ref_kind", "ref_target_code"], "unique": true, "comment": "同宿主同域同目标只挂一条关联 → 保存幂等"},
|
||||
{"name": "idx_ref_blueprint", "fields": ["tenant_key", "owner_kind", "blueprint_id", "ref_kind"], "unique": false, "comment": "按蓝图取全部关联(树渲染/编译主路径)"},
|
||||
{"name": "idx_ref_target", "fields": ["tenant_key", "ref_kind", "ref_target_code"], "unique": false, "comment": "反查『某外部对象被哪些蓝图引用』(删除前影响面分析,只读)"},
|
||||
{"name": "idx_ref_status", "fields": ["tenant_key", "ref_status"], "unique": false, "comment": "未解析/失效关联巡检"},
|
||||
{"name": "idx_ref_tenant", "fields": ["tenant_id", "blueprint_id"], "unique": false, "comment": "按真实租户维度扫描/运维排查"}
|
||||
],
|
||||
"codes": {
|
||||
"pbl_ref_kind": [
|
||||
{"code": "world", "name": "世界"},
|
||||
{"code": "scene", "name": "场景"},
|
||||
{"code": "entity", "name": "实体"},
|
||||
{"code": "script", "name": "脚本"}
|
||||
],
|
||||
"pbl_ref_status": [
|
||||
{"code": "unresolved", "name": "待解析"},
|
||||
{"code": "resolved", "name": "已解析"},
|
||||
{"code": "invalid", "name": "目标缺失/不可用"}
|
||||
],
|
||||
"pbl_ref_mode": [
|
||||
{"code": "readonly", "name": "只读引用(Q-OPEN-3 唯一合法值)"}
|
||||
],
|
||||
"pbl_ext_owner_kind": [
|
||||
{"code": "blueprint", "name": "蓝图"},
|
||||
{"code": "template", "name": "模板"}
|
||||
]
|
||||
}
|
||||
}
|
||||
54
pbl_blueprint/models/m1b/pbl_blueprint_template.json
Normal file
54
pbl_blueprint/models/m1b/pbl_blueprint_template.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"summary": "PBL 蓝图模板表(M1b)。同时承载『租户私有模板』与『平台公共模板』:tenant_id 为 NULL 且 platform_flag='Y' 即平台公共部分,对全部租户可见;tenant_id 非空为租户私有。唯一键参与列使用 tenant_key(值 = COALESCE(tenant_id,'__PLATFORM__'),由 m1b_db.tenant_key() 统一计算,禁止调用方传入),以规避 MariaDB 唯一索引不对 NULL 去重导致平台模板可重复插入的问题;tenant_key 同时满足『租户列索引打头』规约。模板升级 append 新版本行、不覆盖旧版本(Q-OPEN-9)。tpl_json 存 7 类子对象数组 + 模板内临时 ID 父子引用;tpl_hash = sha256(canonical(tpl_json)) 保证确定性实例化。无 FOREIGN KEY、无 ENUM、无 TIMESTAMP。",
|
||||
"fields": [
|
||||
{"name": "id", "type": "bigint", "label": "物理主键", "primary": true, "auto": true, "required": true, "nullable": false, "comment": "AUTO_INCREMENT"},
|
||||
{"name": "tenant_key", "type": "varchar", "len": 32, "label": "租户键", "required": true, "nullable": false, "comment": "= COALESCE(tenant_id,'__PLATFORM__');索引打头列;应用层计算,不接受外部传入"},
|
||||
{"name": "tenant_id", "type": "varchar", "len": 32, "label": "租户ID", "required": false, "nullable": true, "default": null, "comment": "NULL = 平台公共模板(M1b 模板平台公共部分);非空 = 租户私有模板"},
|
||||
{"name": "template_code", "type": "varchar", "len": 32, "label": "模板编码", "required": true, "nullable": false, "comment": "业务主键,pbl_common.gen_code('TPL', tenant_id) 生成"},
|
||||
{"name": "template_version", "type": "int", "label": "模板版本", "required": true, "nullable": false, "default": 1, "comment": "升级 append 新版本,不覆盖旧行"},
|
||||
{"name": "template_name", "type": "varchar", "len": 128, "label": "模板名称", "required": true, "nullable": false},
|
||||
{"name": "subject", "type": "varchar", "len": 32, "label": "学科", "required": false, "nullable": true, "comment": "字典 pbl_subject"},
|
||||
{"name": "grade", "type": "varchar", "len": 16, "label": "学段", "required": false, "nullable": true, "comment": "字典 pbl_grade"},
|
||||
{"name": "category", "type": "varchar", "len": 32, "label": "模板分类", "required": false, "nullable": true, "comment": "字典 pbl_tpl_category"},
|
||||
{"name": "tpl_json", "type": "longtext", "label": "模板内容", "required": true, "nullable": false, "comment": "7 类子对象数组 + 模板内临时 ID 父子引用(stage/task/role/world_ref/scene_ref/entity_ref/script_ref)"},
|
||||
{"name": "tpl_hash", "type": "varchar", "len": 64, "label": "内容哈希", "required": true, "nullable": false, "comment": "sha256(canonical_json(tpl_json)),确定性实例化校验"},
|
||||
{"name": "offline_flag", "type": "varchar", "len": 1, "label": "离线兜底模板", "required": true, "nullable": false, "default": "N", "comment": "Y=build.sh 种子落库的内置离线兜底模板,冷启动即可用"},
|
||||
{"name": "platform_flag", "type": "varchar", "len": 1, "label": "平台公共标记", "required": true, "nullable": false, "default": "N", "comment": "Y=平台公共模板(此时 tenant_id 必须为 NULL);字典 pbl_yes_no"},
|
||||
{"name": "ref_policy", "type": "varchar", "len": 16, "label": "外部引用校验策略", "required": true, "nullable": false, "default": "warn", "comment": "strict=引用缺失阻断回滚;warn=落库并标记 ref_unresolved(Q-OPEN-4 阈值可配);字典 pbl_ref_policy"},
|
||||
{"name": "tpl_status", "type": "varchar", "len": 16, "label": "模板状态", "required": true, "nullable": false, "default": "active", "comment": "字典 pbl_tpl_status"},
|
||||
{"name": "usage_count", "type": "int", "label": "实例化次数", "required": true, "nullable": false, "default": 0, "comment": "供 M10 模板使用率分析读取,非权威计数"},
|
||||
{"name": "remark", "type": "varchar", "len": 512, "label": "备注", "required": false, "nullable": true},
|
||||
{"name": "creator_id", "type": "varchar", "len": 32, "label": "创建人", "required": false, "nullable": true, "comment": "rbac 用户ID"},
|
||||
{"name": "create_time", "type": "datetime", "label": "创建时间", "required": true, "nullable": false},
|
||||
{"name": "update_time", "type": "datetime", "label": "更新时间", "required": true, "nullable": false}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "PRIMARY", "fields": ["id"], "unique": true, "primary": true},
|
||||
{"name": "uk_tpl_code_ver", "fields": ["tenant_key", "template_code", "template_version"], "unique": true, "comment": "同租户(含平台域)同编码同版本唯一"},
|
||||
{"name": "idx_tpl_status", "fields": ["tenant_key", "tpl_status", "subject", "grade"], "unique": false, "comment": "模板库浏览与筛选"},
|
||||
{"name": "idx_tpl_offline", "fields": ["tenant_key", "offline_flag"], "unique": false, "comment": "离线兜底模板快速定位"},
|
||||
{"name": "idx_tpl_hash", "fields": ["tenant_key", "tpl_hash"], "unique": false, "comment": "同内容去重/确定性校验"},
|
||||
{"name": "idx_tpl_platform", "fields": ["platform_flag", "tpl_status"], "unique": false, "comment": "平台公共模板清单(tenant_id IS NULL 场景)"},
|
||||
{"name": "idx_tpl_tenant", "fields": ["tenant_id", "tpl_status"], "unique": false, "comment": "按真实租户维度扫描/运维排查"}
|
||||
],
|
||||
"codes": {
|
||||
"pbl_tpl_status": [
|
||||
{"code": "active", "name": "启用"},
|
||||
{"code": "archived", "name": "已归档"},
|
||||
{"code": "draft", "name": "草稿"}
|
||||
],
|
||||
"pbl_ref_policy": [
|
||||
{"code": "strict", "name": "严格(引用缺失阻断)"},
|
||||
{"code": "warn", "name": "宽松(引用缺失告警降级)"}
|
||||
],
|
||||
"pbl_tpl_category": [
|
||||
{"code": "demo", "name": "演示模板"},
|
||||
{"code": "subject", "name": "学科模板"},
|
||||
{"code": "custom", "name": "自定义模板"}
|
||||
],
|
||||
"pbl_yes_no": [
|
||||
{"code": "Y", "name": "是"},
|
||||
{"code": "N", "name": "否"}
|
||||
]
|
||||
}
|
||||
}
|
||||
62
pbl_blueprint/models/m1b/pbl_ext_field_def.json
Normal file
62
pbl_blueprint/models/m1b/pbl_ext_field_def.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"summary": "PBL 子对象扩展字段定义表(M1b)——扩展字段元数据字典。定义『某类子对象允许挂哪些扩展字段、字段类型、是否必填、默认值、枚举候选、长度上限、正则校验』。m1b_subobject.save_subobject_ext / batch_save_subobject_ext 写入 pbl_subobject_ext 前必须先按本表做 fail-closed 校验:未定义的 field_key 一律拒绝(PBL_EXT_FIELD_UNDEFINED),类型/长度/枚举/正则不符一律拒绝(PBL_EXT_VALUE_INVALID)。platform_flag='Y' 且 tenant_id 为 NULL 表示平台内置字段定义,对全部租户可见;租户可在平台定义之外追加自有定义,但不得覆盖平台定义的 field_key(唯一键 uk_def_type_key 以 tenant_key 打头天然隔离)。无 FOREIGN KEY、无 ENUM、无 TIMESTAMP。",
|
||||
"fields": [
|
||||
{"name": "id", "type": "bigint", "label": "物理主键", "primary": true, "auto": true, "required": true, "nullable": false, "comment": "AUTO_INCREMENT"},
|
||||
{"name": "tenant_key", "type": "varchar", "len": 32, "label": "租户键", "required": true, "nullable": false, "comment": "= COALESCE(tenant_id,'__PLATFORM__');索引打头列;应用层计算"},
|
||||
{"name": "tenant_id", "type": "varchar", "len": 32, "label": "租户ID", "required": false, "nullable": true, "default": null, "comment": "NULL = 平台内置字段定义;非空 = 租户自有字段定义"},
|
||||
{"name": "def_code", "type": "varchar", "len": 32, "label": "定义编码", "required": true, "nullable": false, "comment": "业务主键,pbl_common.gen_code('EFD', tenant_id) 生成"},
|
||||
{"name": "subobject_type", "type": "varchar", "len": 32, "label": "适用子对象类型", "required": true, "nullable": false, "comment": "字典 pbl_subobject_type 的 7 类之一;'*' 表示对全部子对象类型通用"},
|
||||
{"name": "field_key", "type": "varchar", "len": 64, "label": "扩展字段键", "required": true, "nullable": false, "comment": "写入 pbl_subobject_ext.ext_key 的键名;^[a-z][a-z0-9_]{0,63}$"},
|
||||
{"name": "field_label", "type": "varchar", "len": 128, "label": "字段显示名", "required": true, "nullable": false},
|
||||
{"name": "value_type", "type": "varchar", "len": 16, "label": "值类型", "required": true, "nullable": false, "default": "string", "comment": "字典 pbl_ext_value_type:string/int/float/bool/json/date/enum"},
|
||||
{"name": "required_flag", "type": "varchar", "len": 1, "label": "是否必填", "required": true, "nullable": false, "default": "N", "comment": "Y=保存该子对象扩展时此键必须出现且非空;字典 pbl_yes_no"},
|
||||
{"name": "default_value", "type": "varchar", "len": 512, "label": "默认值", "required": false, "nullable": true, "comment": "调用方未传该键时按 value_type 强转后落库"},
|
||||
{"name": "enum_options", "type": "text", "label": "枚举候选", "required": false, "nullable": true, "comment": "value_type='enum' 时必填,JSON 数组字符串,如 [\"easy\",\"normal\",\"hard\"]"},
|
||||
{"name": "max_length", "type": "int", "label": "最大长度", "required": true, "nullable": false, "default": 0, "comment": "0=不限制;>0 时对 string/json/enum 的序列化长度做上限校验"},
|
||||
{"name": "validate_regex", "type": "varchar", "len": 256, "label": "正则校验", "required": false, "nullable": true, "comment": "非空时对 string 值做 re.fullmatch 校验,不符则 PBL_EXT_VALUE_INVALID"},
|
||||
{"name": "platform_flag", "type": "varchar", "len": 1, "label": "平台内置标记", "required": true, "nullable": false, "default": "N", "comment": "Y=平台内置定义(此时 tenant_id 必须为 NULL);字典 pbl_yes_no"},
|
||||
{"name": "def_status", "type": "varchar", "len": 16, "label": "定义状态", "required": true, "nullable": false, "default": "active", "comment": "字典 pbl_ext_def_status;disabled 的定义不参与校验且拒绝新值写入(历史值保留只读)"},
|
||||
{"name": "sort_no", "type": "int", "label": "排序号", "required": true, "nullable": false, "default": 0},
|
||||
{"name": "remark", "type": "varchar", "len": 512, "label": "备注", "required": false, "nullable": true},
|
||||
{"name": "creator_id", "type": "varchar", "len": 32, "label": "创建人", "required": false, "nullable": true},
|
||||
{"name": "create_time", "type": "datetime", "label": "创建时间", "required": true, "nullable": false},
|
||||
{"name": "update_time", "type": "datetime", "label": "更新时间", "required": true, "nullable": false}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "PRIMARY", "fields": ["id"], "unique": true, "primary": true},
|
||||
{"name": "uk_def_code", "fields": ["tenant_key", "def_code"], "unique": true, "comment": "业务主键唯一"},
|
||||
{"name": "uk_def_type_key", "fields": ["tenant_key", "subobject_type", "field_key"], "unique": true, "comment": "同域同子对象类型下 field_key 唯一,杜绝重复定义"},
|
||||
{"name": "idx_def_lookup", "fields": ["tenant_key", "def_status", "subobject_type", "sort_no"], "unique": false, "comment": "校验/渲染时按类型批量取启用定义"},
|
||||
{"name": "idx_def_platform", "fields": ["platform_flag", "def_status"], "unique": false, "comment": "平台内置定义清单(tenant_id IS NULL 场景)"},
|
||||
{"name": "idx_def_tenant", "fields": ["tenant_id", "subobject_type"], "unique": false, "comment": "按真实租户维度扫描/运维排查"}
|
||||
],
|
||||
"codes": {
|
||||
"pbl_ext_value_type": [
|
||||
{"code": "string", "name": "字符串"},
|
||||
{"code": "int", "name": "整数"},
|
||||
{"code": "float", "name": "浮点数"},
|
||||
{"code": "bool", "name": "布尔"},
|
||||
{"code": "json", "name": "JSON对象/数组"},
|
||||
{"code": "date", "name": "日期(YYYY-MM-DD)"},
|
||||
{"code": "enum", "name": "枚举"}
|
||||
],
|
||||
"pbl_ext_def_status": [
|
||||
{"code": "active", "name": "启用"},
|
||||
{"code": "disabled", "name": "停用"}
|
||||
],
|
||||
"pbl_subobject_type": [
|
||||
{"code": "stage", "name": "阶段"},
|
||||
{"code": "task", "name": "任务"},
|
||||
{"code": "role", "name": "角色"},
|
||||
{"code": "world_ref", "name": "世界引用"},
|
||||
{"code": "scene_ref", "name": "场景引用"},
|
||||
{"code": "entity_ref", "name": "实体引用"},
|
||||
{"code": "script_ref", "name": "脚本引用"},
|
||||
{"code": "*", "name": "全部类型通用"}
|
||||
],
|
||||
"pbl_yes_no": [
|
||||
{"code": "Y", "name": "是"},
|
||||
{"code": "N", "name": "否"}
|
||||
]
|
||||
}
|
||||
}
|
||||
56
pbl_blueprint/models/m1b/pbl_subobject_ext.json
Normal file
56
pbl_blueprint/models/m1b/pbl_subobject_ext.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"summary": "PBL 子对象扩展表(M1b)——7 类子对象的泛化扩展属性存储(EAV 结构,一行一个扩展键值)。设计前提:M1a 的 pbl_bp_subobject 契约已冻结,M1b 零改动 M1a 基表,扩展属性一律外挂到本表(M1b-annex §7『对 M1a 在途代码零改动要求』)。owner_kind 区分挂载宿主:blueprint=挂在真实蓝图子对象上(subobject_ref 取 M1a 的 subobject_id);template=挂在模板内容子对象上(subobject_ref 取 tpl_json 内的模板临时 ID,实例化时由 m1b_subobject.remap_subobject_ref 重映射为真实 subobject_id)。所有写入前必须经 pbl_ext_field_def 校验(fail-closed)。ext_value 统一以字符串形态存储,读取时按 value_type 反序列化。唯一键 uk_ext_slot 以 tenant_key 打头,保证同宿主同子对象同键只有一行(幂等 upsert)。无 FOREIGN KEY、无 ENUM、无 TIMESTAMP。",
|
||||
"fields": [
|
||||
{"name": "id", "type": "bigint", "label": "物理主键", "primary": true, "auto": true, "required": true, "nullable": false, "comment": "AUTO_INCREMENT"},
|
||||
{"name": "tenant_key", "type": "varchar", "len": 32, "label": "租户键", "required": true, "nullable": false, "comment": "= COALESCE(tenant_id,'__PLATFORM__');索引打头列;应用层计算"},
|
||||
{"name": "tenant_id", "type": "varchar", "len": 32, "label": "租户ID", "required": false, "nullable": true, "default": null, "comment": "NULL = 平台公共模板的子对象扩展;非空 = 租户数据"},
|
||||
{"name": "ext_code", "type": "varchar", "len": 32, "label": "扩展记录编码", "required": true, "nullable": false, "comment": "业务主键,pbl_common.gen_code('SXE', tenant_id) 生成"},
|
||||
{"name": "owner_kind", "type": "varchar", "len": 16, "label": "宿主类型", "required": true, "nullable": false, "comment": "字典 pbl_ext_owner_kind:blueprint / template"},
|
||||
{"name": "owner_id", "type": "varchar", "len": 32, "label": "宿主ID", "required": true, "nullable": false, "comment": "owner_kind=blueprint 时为 blueprint_id;=template 时为 template_code"},
|
||||
{"name": "owner_version", "type": "int", "label": "宿主版本", "required": true, "nullable": false, "default": 1, "comment": "blueprint→蓝图版本号;template→template_version;用于版本隔离,避免跨版本串数据"},
|
||||
{"name": "subobject_type", "type": "varchar", "len": 32, "label": "子对象类型", "required": true, "nullable": false, "comment": "字典 pbl_subobject_type 的 7 类之一"},
|
||||
{"name": "subobject_ref", "type": "varchar", "len": 64, "label": "子对象归一引用", "required": true, "nullable": false, "comment": "非空归一列:blueprint→M1A subobject_id;template→模板内临时 ID。参与唯一键,杜绝 NULL 造成的重复行"},
|
||||
{"name": "subobject_id", "type": "varchar", "len": 32, "label": "蓝图子对象ID", "required": false, "nullable": true, "comment": "冗余列,owner_kind=blueprint 时与 subobject_ref 同值,便于按 M1A 子对象直查"},
|
||||
{"name": "ext_key", "type": "varchar", "len": 64, "label": "扩展字段键", "required": true, "nullable": false, "comment": "必须在 pbl_ext_field_def 中存在且 def_status=active"},
|
||||
{"name": "ext_value", "type": "longtext", "label": "扩展字段值", "required": false, "nullable": true, "comment": "统一字符串形态;json/bool/date 按 value_type 序列化"},
|
||||
{"name": "value_type", "type": "varchar", "len": 16, "label": "值类型快照", "required": true, "nullable": false, "default": "string", "comment": "写入时从 pbl_ext_field_def.value_type 快照,保证历史值可按当时类型解读;字典 pbl_ext_value_type"},
|
||||
{"name": "field_def_id", "type": "bigint", "label": "字段定义ID", "required": false, "nullable": true, "comment": "指向 pbl_ext_field_def.id(逻辑关联,无 FK)"},
|
||||
{"name": "sort_no", "type": "int", "label": "排序号", "required": true, "nullable": false, "default": 0},
|
||||
{"name": "creator_id", "type": "varchar", "len": 32, "label": "创建人", "required": false, "nullable": true},
|
||||
{"name": "create_time", "type": "datetime", "label": "创建时间", "required": true, "nullable": false},
|
||||
{"name": "update_time", "type": "datetime", "label": "更新时间", "required": true, "nullable": false}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "PRIMARY", "fields": ["id"], "unique": true, "primary": true},
|
||||
{"name": "uk_ext_code", "fields": ["tenant_key", "ext_code"], "unique": true, "comment": "业务主键唯一"},
|
||||
{"name": "uk_ext_slot", "fields": ["tenant_key", "owner_kind", "owner_id", "owner_version", "subobject_type", "subobject_ref", "ext_key"], "unique": true, "comment": "同宿主同版本同子对象同键唯一 → 保存幂等(upsert)"},
|
||||
{"name": "idx_ext_owner", "fields": ["tenant_key", "owner_kind", "owner_id", "owner_version", "subobject_type"], "unique": false, "comment": "按宿主+类型批量取扩展(树渲染主路径)"},
|
||||
{"name": "idx_ext_subobject", "fields": ["tenant_key", "subobject_id"], "unique": false, "comment": "按 M1A 子对象直查扩展"},
|
||||
{"name": "idx_ext_key", "fields": ["tenant_key", "ext_key"], "unique": false, "comment": "按扩展键横向统计(M10 分析)"},
|
||||
{"name": "idx_ext_tenant", "fields": ["tenant_id", "owner_kind", "owner_id"], "unique": false, "comment": "按真实租户维度扫描/运维排查"}
|
||||
],
|
||||
"codes": {
|
||||
"pbl_ext_owner_kind": [
|
||||
{"code": "blueprint", "name": "蓝图子对象扩展"},
|
||||
{"code": "template", "name": "模板子对象扩展"}
|
||||
],
|
||||
"pbl_subobject_type": [
|
||||
{"code": "stage", "name": "阶段"},
|
||||
{"code": "task", "name": "任务"},
|
||||
{"code": "role", "name": "角色"},
|
||||
{"code": "world_ref", "name": "世界引用"},
|
||||
{"code": "scene_ref", "name": "场景引用"},
|
||||
{"code": "entity_ref", "name": "实体引用"},
|
||||
{"code": "script_ref", "name": "脚本引用"}
|
||||
],
|
||||
"pbl_ext_value_type": [
|
||||
{"code": "string", "name": "字符串"},
|
||||
{"code": "int", "name": "整数"},
|
||||
{"code": "float", "name": "浮点数"},
|
||||
{"code": "bool", "name": "布尔"},
|
||||
{"code": "json", "name": "JSON对象/数组"},
|
||||
{"code": "date", "name": "日期(YYYY-MM-DD)"},
|
||||
{"code": "enum", "name": "枚举"}
|
||||
]
|
||||
}
|
||||
}
|
||||
183
sql/m1b_ddl.sql
Normal file
183
sql/m1b_ddl.sql
Normal file
@ -0,0 +1,183 @@
|
||||
-- =============================================================================
|
||||
-- pbl_blueprint M1b DDL —— 模板平台公共部分 / 子对象扩展 / 关联表判定
|
||||
-- =============================================================================
|
||||
-- 覆盖 4 张表(与 pbl_blueprint/models/m1b/*.json 四段式表模型一一对应):
|
||||
-- 1) pbl_blueprint_template 模板表(tenant_id NULL = 平台公共模板)
|
||||
-- 2) pbl_ext_field_def 子对象扩展字段定义表(元数据字典,fail-closed 校验依据)
|
||||
-- 3) pbl_subobject_ext 子对象扩展值表(EAV,一行一键值)
|
||||
-- 4) pbl_blueprint_ref 蓝图外部关联表(Q-OPEN-3 裁决物理落地)
|
||||
--
|
||||
-- 设计铁律:
|
||||
-- * 全部 CREATE TABLE IF NOT EXISTS / CREATE INDEX 幂等,可重复执行(build.sh 与
|
||||
-- m1b_init.ensure_m1b_tables 双入口均执行本文件);
|
||||
-- * 无 FOREIGN KEY(跨模块/可能跨库,有效性由 m1b_ref.resolve_refs() 主动判定);
|
||||
-- * 无 ENUM(一律 varchar + 字典 codes,见 models/m1b/*.json 的 codes 段);
|
||||
-- * 无 TIMESTAMP(一律 datetime,避免 2038 与隐式时区转换);
|
||||
-- * 租户列索引打头:所有业务索引首列为 tenant_key(= COALESCE(tenant_id,'__PLATFORM__'))
|
||||
-- 或 tenant_id;唯一键以 tenant_key 打头,规避 MariaDB 唯一索引不对 NULL 去重
|
||||
-- 导致平台公共行(tenant_id IS NULL)可重复插入的问题;
|
||||
-- * Q-OPEN-3:本文件**不含**任何针对 world / scene / entity / script 等复用域基表的
|
||||
-- CREATE / ALTER / UPDATE / DELETE 语句——蓝图对外部对象的引用单向落在
|
||||
-- pbl_blueprint_ref,ref_mode 恒为 'readonly'。
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1) pbl_blueprint_template —— PBL 蓝图模板表(M1b)
|
||||
-- tenant_id IS NULL 且 platform_flag='Y' → 平台公共模板,对全部租户可见
|
||||
-- tenant_id 非空 → 租户私有模板
|
||||
-- 模板升级 append 新版本行、不覆盖旧版本(Q-OPEN-9)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_template` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '物理主键 AUTO_INCREMENT',
|
||||
`tenant_key` varchar(32) NOT NULL COMMENT '= COALESCE(tenant_id,''__PLATFORM__'');索引打头列;应用层计算,不接受外部传入',
|
||||
`tenant_id` varchar(32) DEFAULT NULL COMMENT 'NULL = 平台公共模板(M1b 模板平台公共部分);非空 = 租户私有模板',
|
||||
`template_code` varchar(32) NOT NULL COMMENT '业务主键,pbl_common.gen_code(''TPL'', tenant_id) 生成',
|
||||
`template_version` int NOT NULL DEFAULT 1 COMMENT '模板版本,升级 append 新版本不覆盖旧行',
|
||||
`template_name` varchar(128) NOT NULL COMMENT '模板名称',
|
||||
`subject` varchar(32) DEFAULT NULL COMMENT '学科,字典 pbl_subject',
|
||||
`grade` varchar(16) DEFAULT NULL COMMENT '学段,字典 pbl_grade',
|
||||
`category` varchar(32) DEFAULT NULL COMMENT '模板分类,字典 pbl_tpl_category',
|
||||
`tpl_json` longtext NOT NULL COMMENT '7 类子对象数组 + 模板内临时 ID 父子引用',
|
||||
`tpl_hash` varchar(64) NOT NULL COMMENT 'sha256(canonical_json(tpl_json)),确定性实例化校验',
|
||||
`offline_flag` varchar(1) NOT NULL DEFAULT 'N' COMMENT 'Y=build.sh 种子落库的内置离线兜底模板',
|
||||
`platform_flag` varchar(1) NOT NULL DEFAULT 'N' COMMENT 'Y=平台公共模板(此时 tenant_id 必须为 NULL);字典 pbl_yes_no',
|
||||
`ref_policy` varchar(16) NOT NULL DEFAULT 'warn' COMMENT 'strict=引用缺失阻断回滚;warn=落库并标记 ref_unresolved(Q-OPEN-4);字典 pbl_ref_policy',
|
||||
`tpl_status` varchar(16) NOT NULL DEFAULT 'active' COMMENT '模板状态,字典 pbl_tpl_status',
|
||||
`usage_count` int NOT NULL DEFAULT 0 COMMENT '实例化次数(供 M10 分析,非权威计数)',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`creator_id` varchar(32) DEFAULT NULL COMMENT '创建人 rbac 用户ID',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tpl_code_ver` (`tenant_key`, `template_code`, `template_version`),
|
||||
KEY `idx_tpl_status` (`tenant_key`, `tpl_status`, `subject`, `grade`),
|
||||
KEY `idx_tpl_offline` (`tenant_key`, `offline_flag`),
|
||||
KEY `idx_tpl_hash` (`tenant_key`, `tpl_hash`),
|
||||
KEY `idx_tpl_platform` (`platform_flag`, `tpl_status`),
|
||||
KEY `idx_tpl_tenant` (`tenant_id`, `tpl_status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL 蓝图模板表(M1b,含平台公共模板 tenant_id NULL)';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2) pbl_ext_field_def —— PBL 子对象扩展字段定义表(M1b)
|
||||
-- 扩展字段元数据字典:某类子对象允许挂哪些扩展键、类型、必填、默认值、
|
||||
-- 枚举候选、长度上限、正则校验。
|
||||
-- m1b_subobject.set_ext / bulk_set_ext 写 pbl_subobject_ext 前必须先按本表
|
||||
-- fail-closed 校验:未定义键 → PBL_EXT_FIELD_UNDEFINED;
|
||||
-- 类型/长度/枚举/正则不符 → PBL_EXT_VALUE_INVALID。
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_ext_field_def` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '物理主键 AUTO_INCREMENT',
|
||||
`tenant_key` varchar(32) NOT NULL COMMENT '= COALESCE(tenant_id,''__PLATFORM__'');索引打头列;应用层计算',
|
||||
`tenant_id` varchar(32) DEFAULT NULL COMMENT 'NULL = 平台内置字段定义;非空 = 租户自有字段定义',
|
||||
`def_code` varchar(32) NOT NULL COMMENT '业务主键,pbl_common.gen_code(''EFD'', tenant_id) 生成',
|
||||
`subobject_type` varchar(32) NOT NULL COMMENT '字典 pbl_subobject_type 的 7 类之一;''*'' 表示全部类型通用',
|
||||
`field_key` varchar(64) NOT NULL COMMENT '写入 pbl_subobject_ext.ext_key 的键名;^[a-z][a-z0-9_]{0,63}$',
|
||||
`field_label` varchar(128) NOT NULL COMMENT '字段显示名',
|
||||
`value_type` varchar(16) NOT NULL DEFAULT 'string' COMMENT '字典 pbl_ext_value_type:string/int/float/bool/json/date/enum',
|
||||
`required_flag` varchar(1) NOT NULL DEFAULT 'N' COMMENT 'Y=保存该子对象扩展时此键必须出现且非空;字典 pbl_yes_no',
|
||||
`default_value` varchar(512) DEFAULT NULL COMMENT '调用方未传该键时按 value_type 强转后落库',
|
||||
`enum_options` text DEFAULT NULL COMMENT 'value_type=enum 时必填,JSON 数组字符串,如 ["easy","normal","hard"]',
|
||||
`max_length` int NOT NULL DEFAULT 0 COMMENT '0=不限制;>0 时对 string/json/enum 序列化长度做上限校验',
|
||||
`validate_regex` varchar(256) DEFAULT NULL COMMENT '非空时对 string 值做 re.fullmatch 校验,不符则 PBL_EXT_VALUE_INVALID',
|
||||
`platform_flag` varchar(1) NOT NULL DEFAULT 'N' COMMENT 'Y=平台内置定义(此时 tenant_id 必须为 NULL);字典 pbl_yes_no',
|
||||
`def_status` varchar(16) NOT NULL DEFAULT 'active' COMMENT '字典 pbl_ext_def_status;disabled 不参与校验且拒绝新值写入(历史值只读保留)',
|
||||
`sort_no` int NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`creator_id` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_def_code` (`tenant_key`, `def_code`),
|
||||
UNIQUE KEY `uk_def_type_key` (`tenant_key`, `subobject_type`, `field_key`),
|
||||
KEY `idx_def_lookup` (`tenant_key`, `def_status`, `subobject_type`, `sort_no`),
|
||||
KEY `idx_def_platform` (`platform_flag`, `def_status`),
|
||||
KEY `idx_def_tenant` (`tenant_id`, `subobject_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL 子对象扩展字段定义表(M1b,扩展字段元数据字典)';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3) pbl_subobject_ext —— PBL 子对象扩展表(M1b,EAV)
|
||||
-- M1a 的 pbl_bp_subobject 契约已冻结,M1b 零改动 M1a 基表,
|
||||
-- 扩展属性一律外挂到本表(M1b-annex §7 对 M1a 在途代码零改动要求)。
|
||||
-- owner_kind=blueprint → subobject_ref 取 M1a subobject_id;
|
||||
-- owner_kind=template → subobject_ref 取 tpl_json 内模板临时 ID,
|
||||
-- 实例化时由 m1b_subobject.remap_subobject_ref 重映射。
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_subobject_ext` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '物理主键 AUTO_INCREMENT',
|
||||
`tenant_key` varchar(32) NOT NULL COMMENT '= COALESCE(tenant_id,''__PLATFORM__'');索引打头列;应用层计算',
|
||||
`tenant_id` varchar(32) DEFAULT NULL COMMENT 'NULL = 平台公共模板的子对象扩展;非空 = 租户数据',
|
||||
`ext_code` varchar(32) NOT NULL COMMENT '业务主键,pbl_common.gen_code(''SXE'', tenant_id) 生成',
|
||||
`owner_kind` varchar(16) NOT NULL COMMENT '字典 pbl_ext_owner_kind:blueprint / template',
|
||||
`owner_id` varchar(32) NOT NULL COMMENT 'owner_kind=blueprint 时为 blueprint_id;=template 时为 template_code',
|
||||
`owner_version` int NOT NULL DEFAULT 1 COMMENT 'blueprint→蓝图版本号;template→template_version;版本隔离避免跨版本串数据',
|
||||
`subobject_type` varchar(32) NOT NULL COMMENT '字典 pbl_subobject_type 的 7 类之一',
|
||||
`subobject_ref` varchar(64) NOT NULL COMMENT '非空归一列:blueprint→M1A subobject_id;template→模板内临时 ID。参与唯一键,杜绝 NULL 造成重复行',
|
||||
`subobject_id` varchar(32) DEFAULT NULL COMMENT '冗余列,owner_kind=blueprint 时与 subobject_ref 同值,便于按 M1A 子对象直查',
|
||||
`ext_key` varchar(64) NOT NULL COMMENT '必须在 pbl_ext_field_def 中存在且 def_status=active',
|
||||
`ext_value` longtext DEFAULT NULL COMMENT '统一字符串形态;json/bool/date 按 value_type 序列化',
|
||||
`value_type` varchar(16) NOT NULL DEFAULT 'string' COMMENT '写入时从 pbl_ext_field_def.value_type 快照,保证历史值可按当时类型解读;字典 pbl_ext_value_type',
|
||||
`field_def_id` bigint DEFAULT NULL COMMENT '指向 pbl_ext_field_def.id(逻辑关联,无 FK)',
|
||||
`sort_no` int NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`creator_id` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ext_code` (`tenant_key`, `ext_code`),
|
||||
UNIQUE KEY `uk_ext_slot` (`tenant_key`, `owner_kind`, `owner_id`, `owner_version`,
|
||||
`subobject_type`, `subobject_ref`, `ext_key`),
|
||||
KEY `idx_ext_owner` (`tenant_key`, `owner_kind`, `owner_id`, `owner_version`, `subobject_type`),
|
||||
KEY `idx_ext_subobject` (`tenant_key`, `subobject_id`),
|
||||
KEY `idx_ext_key` (`tenant_key`, `ext_key`),
|
||||
KEY `idx_ext_tenant` (`tenant_id`, `owner_kind`, `owner_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL 子对象扩展表(M1b,EAV 一行一键值)';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4) pbl_blueprint_ref —— PBL 蓝图外部关联表(M1b,Q-OPEN-3 裁决物理落地)
|
||||
-- 裁决结论:『不改 world 表』(含 scene / entity / script 等复用域基表一律
|
||||
-- 零改动、零加列、零回写)。蓝图与复用域对象的全部关联单向落在本表:
|
||||
-- * ref_target_code 指向外部对象业务编码(逻辑关联,无 FOREIGN KEY);
|
||||
-- * ref_mode 恒为 'readonly',由 m1b_ref.assert_writable() 与
|
||||
-- WRITE_PROTECTED_TABLES 双重闸门强制(任何针对 world/scene/entity/script
|
||||
-- 的 INSERT/UPDATE/DELETE 在 SQL 执行前被拦截并抛 PBL_DB_WRITE_PROTECTED);
|
||||
-- * resolve_refs() 仅做只读存在性校验并把结果写回本表 ref_status
|
||||
-- (resolved / unresolved / invalid),校验策略 strict/warn 由
|
||||
-- pbl_blueprint_template.ref_policy 决定(Q-OPEN-4 阈值可配);
|
||||
-- * snapshot_json 保存解析时刻只读快照,避免运行期跨库联查。
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_ref` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '物理主键 AUTO_INCREMENT',
|
||||
`tenant_key` varchar(32) NOT NULL COMMENT '= COALESCE(tenant_id,''__PLATFORM__'');索引打头列;应用层计算',
|
||||
`tenant_id` varchar(32) DEFAULT NULL COMMENT 'NULL = 平台公共模板携带的关联;非空 = 租户蓝图关联',
|
||||
`ref_code` varchar(32) NOT NULL COMMENT '业务主键,pbl_common.gen_code(''BPR'', tenant_id) 生成',
|
||||
`owner_kind` varchar(16) NOT NULL DEFAULT 'blueprint' COMMENT '字典 pbl_ext_owner_kind:blueprint / template',
|
||||
`blueprint_id` varchar(32) NOT NULL COMMENT 'owner_kind=blueprint 时为 M1A blueprint_id;=template 时复用本列存 template_code(取值由 owner_kind 判定)',
|
||||
`subobject_id` varchar(32) DEFAULT NULL COMMENT '关联所属的 *_ref 类子对象ID;模板态存模板临时ID',
|
||||
`ref_kind` varchar(16) NOT NULL COMMENT '字典 pbl_ref_kind:world / scene / entity / script',
|
||||
`ref_target_code` varchar(64) NOT NULL COMMENT '复用域对象业务编码(只读引用,无 FK,不反查基表写操作)',
|
||||
`ref_target_id` varchar(64) DEFAULT NULL COMMENT '解析成功时回填的对方物理ID快照',
|
||||
`ref_mode` varchar(16) NOT NULL DEFAULT 'readonly' COMMENT '恒为 readonly(Q-OPEN-3:不改 world 表,只读引用);字典 pbl_ref_mode;写入非 readonly 值将被 m1b_ref 拒绝',
|
||||
`ref_status` varchar(16) NOT NULL DEFAULT 'unresolved' COMMENT '字典 pbl_ref_status:unresolved 待解析 / resolved 已解析 / invalid 目标不存在或不可用',
|
||||
`resolve_time` datetime DEFAULT NULL COMMENT '解析时间',
|
||||
`resolve_msg` varchar(512) DEFAULT NULL COMMENT 'invalid 时记录原因,供 M2 校验与教师端提示',
|
||||
`snapshot_json` text DEFAULT NULL COMMENT '解析时刻外部对象关键字段快照(JSON),运行期免跨库联查',
|
||||
`sort_no` int NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`creator_id` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ref_code` (`tenant_key`, `ref_code`),
|
||||
UNIQUE KEY `uk_ref_slot` (`tenant_key`, `owner_kind`, `blueprint_id`, `ref_kind`, `ref_target_code`),
|
||||
KEY `idx_ref_blueprint` (`tenant_key`, `owner_kind`, `blueprint_id`, `ref_kind`),
|
||||
KEY `idx_ref_target` (`tenant_key`, `ref_kind`, `ref_target_code`),
|
||||
KEY `idx_ref_status` (`tenant_key`, `ref_status`),
|
||||
KEY `idx_ref_tenant` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL 蓝图外部关联表(M1b,Q-OPEN-3 不改 world 表的只读关联落地)';
|
||||
|
||||
-- =============================================================================
|
||||
-- 幂等性说明:以上 4 段均为 CREATE TABLE IF NOT EXISTS,索引随表定义一并创建,
|
||||
-- 重复执行不报错、不产生重复索引。m1b_init.ensure_m1b_tables() 按 ';' 切分本文件
|
||||
-- 逐条 sor.sqlExe(dbname, stmt) 执行(库名来自 ServerEnv().get_module_dbname,
|
||||
-- 本文件与本模块任何代码均不硬编码库名)。
|
||||
-- =============================================================================
|
||||
379
tools/patch_m1b.py
Normal file
379
tools/patch_m1b.py
Normal file
@ -0,0 +1,379 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M1b 交付补丁脚本(一次性执行):
|
||||
1) m1b_init.py:表模型路径改指 models/m1b/*.json(四段式),CRUD 定义指 json/m1b/*.json,
|
||||
DDL 路径改指仓库根 sql/m1b_ddl.sql,并接入真实库执行通道(m1b_db.SorDB)。
|
||||
2) __init__.py:补 M1b 导出(三处同步之「__init__ 导出」环节)。
|
||||
3) init.py:补 M1b env 注册(三处同步之「init.py env 注册」环节)。
|
||||
执行:python3 tools/patch_m1b.py
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PKG = os.path.join(REPO, "pbl_blueprint")
|
||||
|
||||
|
||||
def read(p):
|
||||
with io.open(p, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write(p, s):
|
||||
with io.open(p, "w", encoding="utf-8") as f:
|
||||
f.write(s)
|
||||
print("WROTE %s (%d chars)" % (os.path.relpath(p, REPO), len(s)))
|
||||
|
||||
|
||||
def sub_once(text, old, new, tag):
|
||||
if old not in text:
|
||||
raise SystemExit("PATCH FAIL [%s]: anchor not found" % tag)
|
||||
if text.count(old) != 1:
|
||||
raise SystemExit("PATCH FAIL [%s]: anchor not unique (%d)" % (tag, text.count(old)))
|
||||
return text.replace(old, new)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 1) m1b_init.py
|
||||
p = os.path.join(PKG, "m1b_init.py")
|
||||
t = read(p)
|
||||
|
||||
old_head = '''_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
M1B_MODEL_FILES = [
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_template.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_ext_field_def.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_subobject_ext.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_ref.json"),
|
||||
]
|
||||
M1B_TABLES = ["pbl_blueprint_template", "pbl_ext_field_def", "pbl_subobject_ext",
|
||||
"pbl_blueprint_ref"]
|
||||
M1B_DDL = os.path.join(_HERE, "sql", "m1b_ddl.sql")
|
||||
'''
|
||||
|
||||
new_head = '''# 目录规约(module-development-spec / database-table-definition-spec):
|
||||
# * 表定义(四段式 summary/fields/indexes/codes)→ pbl_blueprint/models/m1b/*.json
|
||||
# * CRUD 定义(tblname + params.editable) → pbl_blueprint/json/m1b/*.json
|
||||
# * 幂等 DDL(4 表 CREATE TABLE IF NOT EXISTS) → <repo>/sql/m1b_ddl.sql
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__)) # .../pbl_blueprint/pbl_blueprint
|
||||
_REPO = os.path.dirname(_HERE) # .../pbl_blueprint(仓库根)
|
||||
|
||||
M1B_TABLES = ["pbl_blueprint_template", "pbl_ext_field_def", "pbl_subobject_ext",
|
||||
"pbl_blueprint_ref"]
|
||||
|
||||
# 表模型(四段式)——建表/字段校验的权威来源
|
||||
M1B_MODEL_FILES = [
|
||||
os.path.join(_HERE, "models", "m1b", "pbl_blueprint_template.json"),
|
||||
os.path.join(_HERE, "models", "m1b", "pbl_ext_field_def.json"),
|
||||
os.path.join(_HERE, "models", "m1b", "pbl_subobject_ext.json"),
|
||||
os.path.join(_HERE, "models", "m1b", "pbl_blueprint_ref.json"),
|
||||
]
|
||||
|
||||
# CRUD 定义(tblname + params.editable)——供 sqlor/前端 CRUD 装配
|
||||
M1B_CRUD_FILES = [
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_template.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_ext_field_def.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_subobject_ext.json"),
|
||||
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_ref.json"),
|
||||
]
|
||||
|
||||
M1B_DDL = os.path.join(_REPO, "sql", "m1b_ddl.sql")
|
||||
'''
|
||||
|
||||
t = sub_once(t, old_head, new_head, "m1b_init head")
|
||||
|
||||
t = sub_once(
|
||||
t,
|
||||
'__all__ = ["M1B_TABLES", "M1B_MODEL_FILES", "init_m1b", "ensure_m1b_tables",\n'
|
||||
' "ensure_platform_ext_defs", "seed_platform_data"]',
|
||||
'__all__ = ["M1B_TABLES", "M1B_MODEL_FILES", "M1B_CRUD_FILES", "M1B_DDL",\n'
|
||||
' "PLATFORM_EXT_DEFS", "PLATFORM_TEMPLATES",\n'
|
||||
' "init_m1b", "ensure_m1b_tables", "ensure_platform_ext_defs",\n'
|
||||
' "seed_platform_data", "load_m1b_models", "load_m1b_crud_defs"]',
|
||||
"m1b_init __all__")
|
||||
|
||||
# 建表:接入真实库通道(m1b_db.SorDB.sqlExe),并做 Q-OPEN-3 闸门保护
|
||||
old_ensure = ''' result = {"ok": True, "models": [m.get("table") for m in models], "ddl": None, "executed": False}
|
||||
'''
|
||||
new_ensure = ''' result = {"ok": True, "models": [m.get("table") for m in models],
|
||||
"ddl": None, "executed": 0, "statements": 0, "errors": []}
|
||||
'''
|
||||
t = sub_once(t, old_ensure, new_ensure, "ensure result init")
|
||||
|
||||
old_ddl = ''' # 2) DDL 兜底
|
||||
if os.path.exists(M1B_DDL):
|
||||
try:
|
||||
with open(M1B_DDL, "r", encoding="utf-8") as fp:
|
||||
ddl = fp.read()
|
||||
result["ddl"] = M1B_DDL
|
||||
if callable(executor):
|
||||
for stmt in [s.strip() for s in ddl.split(";") if s.strip()
|
||||
and not s.strip().startswith("--")]:
|
||||
try:
|
||||
executor(stmt)
|
||||
except Exception:
|
||||
continue
|
||||
result["executed"] = True
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
'''
|
||||
new_ddl = ''' # 2) 幂等 DDL(sql/m1b_ddl.sql,4 表 CREATE TABLE IF NOT EXISTS)
|
||||
if os.path.exists(M1B_DDL):
|
||||
try:
|
||||
with open(M1B_DDL, "r", encoding="utf-8") as fp:
|
||||
ddl = fp.read()
|
||||
result["ddl"] = M1B_DDL
|
||||
from .m1b_db import split_ddl, guard_sql
|
||||
stmts = split_ddl(ddl)
|
||||
result["statements"] = len(stmts)
|
||||
|
||||
# 执行通道优先级:显式 executor > 真实库 SorDB(sqlor 标准 API)
|
||||
runner = None
|
||||
if callable(executor):
|
||||
runner = executor
|
||||
else:
|
||||
try:
|
||||
from .m1b_db import make_db
|
||||
real = make_db()
|
||||
if real is not None:
|
||||
runner = real.sqlExe
|
||||
result["channel"] = "sqlor(SorDB.sqlExe)"
|
||||
except Exception:
|
||||
runner = None
|
||||
if runner is not None:
|
||||
for stmt in stmts:
|
||||
try:
|
||||
guard_sql(stmt) # Q-OPEN-3:拦截任何复用域基表写/改
|
||||
runner(stmt)
|
||||
result["executed"] += 1
|
||||
except Exception as e: # 单条失败不阻断其余(幂等重跑可补)
|
||||
result["errors"].append("%s: %s" % (type(e).__name__, e))
|
||||
result["ok"] = not result["errors"]
|
||||
else:
|
||||
result["channel"] = "none(内存适配器:表按需自动创建)"
|
||||
except Exception as e:
|
||||
result["ok"] = False
|
||||
result["errors"].append("ddl_load: %s" % e)
|
||||
return result
|
||||
|
||||
|
||||
def load_m1b_models():
|
||||
"""读取 4 张表的四段式表模型(models/m1b/*.json)。"""
|
||||
out = []
|
||||
for f in M1B_MODEL_FILES:
|
||||
if not os.path.exists(f):
|
||||
continue
|
||||
try:
|
||||
with open(f, "r", encoding="utf-8") as fp:
|
||||
d = json.load(fp)
|
||||
d["_file"] = os.path.relpath(f, _REPO)
|
||||
out.append(d)
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def load_m1b_crud_defs():
|
||||
"""读取 4 张表的 CRUD 定义(json/m1b/*.json,tblname + params.editable)。"""
|
||||
out = []
|
||||
for f in M1B_CRUD_FILES:
|
||||
if not os.path.exists(f):
|
||||
continue
|
||||
try:
|
||||
with open(f, "r", encoding="utf-8") as fp:
|
||||
d = json.load(fp)
|
||||
d["_file"] = os.path.relpath(f, _REPO)
|
||||
out.append(d)
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
'''
|
||||
t = sub_once(t, old_ddl, new_ddl, "ensure ddl block")
|
||||
|
||||
# init_m1b 返回值补 CRUD 定义装载证据
|
||||
t = sub_once(
|
||||
t,
|
||||
''' return {"ok": True, "milestone": "M1b", "tables": tables, "routes": routes,
|
||||
"seed": seeded, "q_open_3": "world/scene/entity 基表零改动"}''',
|
||||
''' return {"ok": True, "milestone": "M1b", "tables": tables, "routes": routes,
|
||||
"seed": seeded,
|
||||
"models_loaded": [m.get("_file") for m in load_m1b_models()],
|
||||
"crud_defs_loaded": [c.get("tblname") for c in load_m1b_crud_defs()],
|
||||
"q_open_3": "world/scene/entity/script 基表零改动、零加列、零回写"}''',
|
||||
"init_m1b return")
|
||||
|
||||
write(p, t)
|
||||
|
||||
# ---------------------------------------------------------------- 2) __init__.py
|
||||
p = os.path.join(PKG, "__init__.py")
|
||||
t = read(p)
|
||||
|
||||
old = '''# ------------------------------------------------------------------ 挂载入口
|
||||
from .init import load_pbl_blueprint # noqa: F401
|
||||
'''
|
||||
new = '''# ------------------------------------------------------------------ 挂载入口
|
||||
from .init import load_pbl_blueprint # noqa: F401
|
||||
|
||||
# ------------------------------------------------------------------ M1b(模板平台公共部分 / 子对象扩展 / 关联表判定)
|
||||
# 三处同步之「__init__ 导出」环节:函数定义(m1b_*.py) + 本文件导出 + init.py env 注册
|
||||
from .m1b_init import ( # noqa: F401
|
||||
M1B_TABLES,
|
||||
M1B_MODEL_FILES,
|
||||
M1B_CRUD_FILES,
|
||||
M1B_DDL,
|
||||
PLATFORM_EXT_DEFS,
|
||||
PLATFORM_TEMPLATES,
|
||||
init_m1b,
|
||||
ensure_m1b_tables,
|
||||
ensure_platform_ext_defs,
|
||||
seed_platform_data,
|
||||
load_m1b_models,
|
||||
load_m1b_crud_defs,
|
||||
)
|
||||
from .m1b_api import M1B_ROUTES, dispatch as m1b_dispatch, register_m1b_routes # noqa: F401
|
||||
from .m1b_db import ( # noqa: F401
|
||||
PLATFORM_TENANT_KEY,
|
||||
WRITE_PROTECTED_TABLES,
|
||||
PBL_DB_WRITE_PROTECTED,
|
||||
WriteProtectedError,
|
||||
SorDB,
|
||||
tenant_key as m1b_tenant_key,
|
||||
assert_writable as m1b_assert_writable,
|
||||
make_db as m1b_make_db,
|
||||
)
|
||||
from . import m1b_template # noqa: F401
|
||||
from . import m1b_subobject # noqa: F401
|
||||
from . import m1b_ref # noqa: F401
|
||||
'''
|
||||
t = sub_once(t, old, new, "__init__ imports")
|
||||
|
||||
t = sub_once(
|
||||
t,
|
||||
''' # 挂载入口
|
||||
"load_pbl_blueprint",
|
||||
]''',
|
||||
''' # 挂载入口
|
||||
"load_pbl_blueprint",
|
||||
# ---- M1b:挂载/建表/种子
|
||||
"M1B_TABLES", "M1B_MODEL_FILES", "M1B_CRUD_FILES", "M1B_DDL",
|
||||
"PLATFORM_EXT_DEFS", "PLATFORM_TEMPLATES",
|
||||
"init_m1b", "ensure_m1b_tables", "ensure_platform_ext_defs",
|
||||
"seed_platform_data", "load_m1b_models", "load_m1b_crud_defs",
|
||||
# ---- M1b:路由
|
||||
"M1B_ROUTES", "m1b_dispatch", "register_m1b_routes",
|
||||
# ---- M1b:真实库适配 + Q-OPEN-3 写保护闸门
|
||||
"PLATFORM_TENANT_KEY", "WRITE_PROTECTED_TABLES", "PBL_DB_WRITE_PROTECTED",
|
||||
"WriteProtectedError", "SorDB", "m1b_tenant_key", "m1b_assert_writable",
|
||||
"m1b_make_db",
|
||||
# ---- M1b:业务子模块
|
||||
"m1b_template", "m1b_subobject", "m1b_ref",
|
||||
]''',
|
||||
"__init__ __all__")
|
||||
|
||||
t = sub_once(
|
||||
t, '__version__ = "1.0.0"', '__version__ = "1.1.0" # M1b:模板平台公共部分/子对象扩展/关联表',
|
||||
"__init__ version")
|
||||
write(p, t)
|
||||
|
||||
# ---------------------------------------------------------------- 3) init.py
|
||||
p = os.path.join(PKG, "init.py")
|
||||
t = read(p)
|
||||
|
||||
old = '''MODULE_NAME = "pbl_blueprint"
|
||||
TABLE_NAMES = list(_tables.TABLES.keys()) # 11 表
|
||||
SUBOBJECT_TYPES = _tables.SUBOBJECT_TYPES # 7 类
|
||||
'''
|
||||
new = '''from . import m1b_init as _m1b_init
|
||||
from .m1b_api import M1B_ROUTES, register_m1b_routes
|
||||
from .m1b_init import (M1B_CRUD_FILES, M1B_DDL, M1B_MODEL_FILES, M1B_TABLES,
|
||||
ensure_m1b_tables, init_m1b, load_m1b_crud_defs,
|
||||
load_m1b_models, seed_platform_data)
|
||||
|
||||
MODULE_NAME = "pbl_blueprint"
|
||||
TABLE_NAMES = list(_tables.TABLES.keys()) # M1a 11 表
|
||||
SUBOBJECT_TYPES = _tables.SUBOBJECT_TYPES # 7 类
|
||||
M1B_TABLE_NAMES = list(M1B_TABLES) # M1b 4 表
|
||||
ALL_TABLE_NAMES = TABLE_NAMES + [t for t in M1B_TABLE_NAMES if t not in TABLE_NAMES]
|
||||
'''
|
||||
t = sub_once(t, old, new, "init.py imports")
|
||||
|
||||
t = sub_once(
|
||||
t,
|
||||
'''__all__ = [
|
||||
"load_pbl_blueprint", "ensure_tables", "MODULE_NAME", "TABLE_NAMES",
|
||||
"SUBOBJECT_TYPES", "BUILTIN_SCHEMA",''',
|
||||
'''__all__ = [
|
||||
"load_pbl_blueprint", "ensure_tables", "MODULE_NAME", "TABLE_NAMES",
|
||||
"SUBOBJECT_TYPES", "BUILTIN_SCHEMA",
|
||||
# ---- M1b 三处同步之「init.py env 注册」环节
|
||||
"M1B_TABLES", "M1B_TABLE_NAMES", "ALL_TABLE_NAMES", "M1B_MODEL_FILES",
|
||||
"M1B_CRUD_FILES", "M1B_DDL", "M1B_ROUTES",
|
||||
"init_m1b", "ensure_m1b_tables", "register_m1b_routes",
|
||||
"load_m1b_models", "load_m1b_crud_defs", "seed_platform_data",''',
|
||||
"init.py __all__")
|
||||
|
||||
old_load = '''def load_pbl_blueprint(app=None, sor=None, ensure=False, **kw):
|
||||
dbname = get_module_dbname(_MODULE_NAME) # noqa: F841 库名由应用注入
|
||||
if ensure:
|
||||
ensure_tables(sor)
|
||||
if app is not None:
|
||||
try:
|
||||
app.register_module(MODULE_NAME, {
|
||||
"tables": TABLE_NAMES,'''
|
||||
new_load = '''def _m1b_register_callback(app):
|
||||
"""解析应用层路由注册回调:register_route / register / route 三选一。"""
|
||||
if app is None:
|
||||
return None
|
||||
for name in ("register_route", "register", "route", "add_route"):
|
||||
fn = getattr(app, name, None)
|
||||
if callable(fn):
|
||||
return fn
|
||||
return None
|
||||
|
||||
|
||||
def load_pbl_blueprint(app=None, sor=None, ensure=False, **kw):
|
||||
dbname = get_module_dbname(_MODULE_NAME) # noqa: F841 库名由应用注入
|
||||
if ensure:
|
||||
ensure_tables(sor)
|
||||
|
||||
# ------------------------------------------------------------ M1b 挂载
|
||||
# 三处同步:m1b_api.register_m1b_routes(定义)+ __init__.py(导出)+ 此处(env 注册)
|
||||
# Q-OPEN-3:init_m1b 内部只建 M1b 自有 4 表,绝不触碰 world/scene/entity/script 基表。
|
||||
m1b_result = {"ok": False, "skipped": True}
|
||||
if kw.get("m1b", True):
|
||||
try:
|
||||
m1b_result = init_m1b(
|
||||
db=kw.get("m1b_db"),
|
||||
env=kw.get("env"),
|
||||
register=_m1b_register_callback(app),
|
||||
executor=kw.get("m1b_executor"),
|
||||
seed=bool(kw.get("m1b_seed", ensure)),
|
||||
)
|
||||
except Exception as e: # M1b 失败不拖垮 M1a 挂载
|
||||
m1b_result = {"ok": False, "error": "%s: %s" % (type(e).__name__, e)}
|
||||
|
||||
if app is not None:
|
||||
try:
|
||||
app.register_module(MODULE_NAME, {
|
||||
"tables": ALL_TABLE_NAMES,
|
||||
"m1b": {
|
||||
"tables": M1B_TABLE_NAMES,
|
||||
"model_files": M1B_MODEL_FILES,
|
||||
"crud_files": M1B_CRUD_FILES,
|
||||
"ddl": M1B_DDL,
|
||||
"routes": [(m, p) for m, p, _ in M1B_ROUTES],
|
||||
"init": m1b_result,
|
||||
},''',
|
||||
t = sub_once(t, old_load, new_load, "init.py load_pbl_blueprint")
|
||||
|
||||
t = sub_once(
|
||||
t,
|
||||
''' return {"module": MODULE_NAME, "tables": TABLE_NAMES,
|
||||
"subobject_types": list(SUBOBJECT_TYPES), "loaded": True}''',
|
||||
''' return {"module": MODULE_NAME, "tables": TABLE_NAMES,
|
||||
"m1b_tables": M1B_TABLE_NAMES, "m1b": m1b_result,
|
||||
"subobject_types": list(SUBOBJECT_TYPES), "loaded": True}''',
|
||||
"init.py return")
|
||||
|
||||
write(p, t)
|
||||
print("PATCH OK")
|
||||
131
tools/patch_m1b_init.py
Normal file
131
tools/patch_m1b_init.py
Normal file
@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M1b 补丁脚本 2/2:init.py 的 env 注册(三处同步第三环)。
|
||||
(patch_m1b.py 已完成 m1b_init.py 与 __init__.py;本脚本只改 init.py,可重复执行——
|
||||
已打过补丁时检测到锚点缺失即跳过并报告,不重复插入。)
|
||||
执行:python3 tools/patch_m1b_init.py
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
P = os.path.join(REPO, "pbl_blueprint", "init.py")
|
||||
|
||||
|
||||
def read(p):
|
||||
with io.open(p, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write(p, s):
|
||||
with io.open(p, "w", encoding="utf-8") as f:
|
||||
f.write(s)
|
||||
print("WROTE %s (%d chars)" % (os.path.relpath(p, REPO), len(s)))
|
||||
|
||||
|
||||
t = read(P)
|
||||
if "M1B_TABLE_NAMES" in t:
|
||||
print("SKIP: init.py 已含 M1b 注册(幂等,不重复打补丁)")
|
||||
raise SystemExit(0)
|
||||
|
||||
# ---- 1) import + 表清单
|
||||
old = ('MODULE_NAME = "pbl_blueprint"\n'
|
||||
'TABLE_NAMES = list(_tables.TABLES.keys()) # 11 表\n'
|
||||
'SUBOBJECT_TYPES = _tables.SUBOBJECT_TYPES # 7 类\n')
|
||||
new = ('from . import m1b_init as _m1b_init\n'
|
||||
'from .m1b_api import M1B_ROUTES, register_m1b_routes\n'
|
||||
'from .m1b_init import (M1B_CRUD_FILES, M1B_DDL, M1B_MODEL_FILES, M1B_TABLES,\n'
|
||||
' ensure_m1b_tables, init_m1b, load_m1b_crud_defs,\n'
|
||||
' load_m1b_models, seed_platform_data)\n'
|
||||
'\n'
|
||||
'MODULE_NAME = "pbl_blueprint"\n'
|
||||
'TABLE_NAMES = list(_tables.TABLES.keys()) # M1a 11 表\n'
|
||||
'SUBOBJECT_TYPES = _tables.SUBOBJECT_TYPES # 7 类\n'
|
||||
'M1B_TABLE_NAMES = list(M1B_TABLES) # M1b 4 表\n'
|
||||
'ALL_TABLE_NAMES = TABLE_NAMES + [x for x in M1B_TABLE_NAMES\n'
|
||||
' if x not in TABLE_NAMES]\n')
|
||||
assert t.count(old) == 1, "anchor1 not unique: %d" % t.count(old)
|
||||
t = t.replace(old, new)
|
||||
|
||||
# ---- 2) __all__
|
||||
old = ('__all__ = [\n'
|
||||
' "load_pbl_blueprint", "ensure_tables", "MODULE_NAME", "TABLE_NAMES",\n'
|
||||
' "SUBOBJECT_TYPES", "BUILTIN_SCHEMA",')
|
||||
new = ('__all__ = [\n'
|
||||
' "load_pbl_blueprint", "ensure_tables", "MODULE_NAME", "TABLE_NAMES",\n'
|
||||
' "SUBOBJECT_TYPES", "BUILTIN_SCHEMA",\n'
|
||||
' # ---- M1b 三处同步之「init.py env 注册」环节\n'
|
||||
' "M1B_TABLES", "M1B_TABLE_NAMES", "ALL_TABLE_NAMES", "M1B_MODEL_FILES",\n'
|
||||
' "M1B_CRUD_FILES", "M1B_DDL", "M1B_ROUTES",\n'
|
||||
' "init_m1b", "ensure_m1b_tables", "register_m1b_routes",\n'
|
||||
' "load_m1b_models", "load_m1b_crud_defs", "seed_platform_data",')
|
||||
assert t.count(old) == 1, "anchor2 not unique: %d" % t.count(old)
|
||||
t = t.replace(old, new)
|
||||
|
||||
# ---- 3) load_pbl_blueprint:M1b 挂载 + env 注册
|
||||
old = ('def load_pbl_blueprint(app=None, sor=None, ensure=False, **kw):\n'
|
||||
' dbname = get_module_dbname(_MODULE_NAME) # noqa: F841 库名由应用注入\n'
|
||||
' if ensure:\n'
|
||||
' ensure_tables(sor)\n'
|
||||
' if app is not None:\n'
|
||||
' try:\n'
|
||||
' app.register_module(MODULE_NAME, {\n'
|
||||
' "tables": TABLE_NAMES,')
|
||||
new = ('def _m1b_register_callback(app):\n'
|
||||
' """解析应用层路由注册回调:register_route / register / route / add_route 四选一。"""\n'
|
||||
' if app is None:\n'
|
||||
' return None\n'
|
||||
' for name in ("register_route", "register", "route", "add_route"):\n'
|
||||
' fn = getattr(app, name, None)\n'
|
||||
' if callable(fn):\n'
|
||||
' return fn\n'
|
||||
' return None\n'
|
||||
'\n'
|
||||
'\n'
|
||||
'def load_pbl_blueprint(app=None, sor=None, ensure=False, **kw):\n'
|
||||
' dbname = get_module_dbname(_MODULE_NAME) # noqa: F841 库名由应用注入\n'
|
||||
' if ensure:\n'
|
||||
' ensure_tables(sor)\n'
|
||||
'\n'
|
||||
' # -------------------------------------------------------- M1b 挂载\n'
|
||||
' # 三处同步:m1b_api.register_m1b_routes(定义)+ __init__.py(导出)+ 此处(env 注册)\n'
|
||||
' # Q-OPEN-3:init_m1b 只建 M1b 自有 4 表,绝不触碰 world/scene/entity/script 基表。\n'
|
||||
' m1b_result = {"ok": False, "skipped": True}\n'
|
||||
' if kw.get("m1b", True):\n'
|
||||
' try:\n'
|
||||
' m1b_result = init_m1b(\n'
|
||||
' db=kw.get("m1b_db"),\n'
|
||||
' env=kw.get("env"),\n'
|
||||
' register=_m1b_register_callback(app),\n'
|
||||
' executor=kw.get("m1b_executor"),\n'
|
||||
' seed=bool(kw.get("m1b_seed", ensure)),\n'
|
||||
' )\n'
|
||||
' except Exception as e: # M1b 失败不拖垮 M1a 挂载\n'
|
||||
' m1b_result = {"ok": False,\n'
|
||||
' "error": "%s: %s" % (type(e).__name__, e)}\n'
|
||||
'\n'
|
||||
' if app is not None:\n'
|
||||
' try:\n'
|
||||
' app.register_module(MODULE_NAME, {\n'
|
||||
' "tables": ALL_TABLE_NAMES,\n'
|
||||
' "m1b": {\n'
|
||||
' "tables": M1B_TABLE_NAMES,\n'
|
||||
' "model_files": M1B_MODEL_FILES,\n'
|
||||
' "crud_files": M1B_CRUD_FILES,\n'
|
||||
' "ddl": M1B_DDL,\n'
|
||||
' "routes": [(m, p) for m, p, _ in M1B_ROUTES],\n'
|
||||
' "init": m1b_result,\n'
|
||||
' },')
|
||||
assert t.count(old) == 1, "anchor3 not unique: %d" % t.count(old)
|
||||
t = t.replace(old, new)
|
||||
|
||||
# ---- 4) 返回值
|
||||
old = (' return {"module": MODULE_NAME, "tables": TABLE_NAMES,\n'
|
||||
' "subobject_types": list(SUBOBJECT_TYPES), "loaded": True}')
|
||||
new = (' return {"module": MODULE_NAME, "tables": TABLE_NAMES,\n'
|
||||
' "m1b_tables": M1B_TABLE_NAMES, "m1b": m1b_result,\n'
|
||||
' "subobject_types": list(SUBOBJECT_TYPES), "loaded": True}')
|
||||
assert t.count(old) == 1, "anchor4 not unique: %d" % t.count(old)
|
||||
t = t.replace(old, new)
|
||||
|
||||
write(P, t)
|
||||
print("PATCH init.py OK")
|
||||
Loading…
x
Reference in New Issue
Block a user