pbl_blueprint/scripts/gen_defs.py

774 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

# -*- coding: utf-8 -*-
"""pbl_blueprint 表定义/CRUD定义生成器M1a
单一事实源:本文件的 TABLES 声明 -> 生成
pbl_blueprint/models/{tbl}.json 四段式表定义summary/fields/indexes/codes
pbl_blueprint/json/{tbl}.json CRUD 定义tblname/params/editable/browserfields
用法(机构工作空间根目录):
python3 modules/pbl_blueprint/scripts/gen_defs.py # 生成 + 清理陈旧定义
python3 modules/pbl_blueprint/scripts/gen_defs.py --clean # 仅清理非 11 表的陈旧文件
python3 modules/pbl_blueprint/scripts/gen_defs.py --dump-ddl # 打印建表 DDL
约定QC 逐条核验点):
* primary 恒为 ["id"]id 为 str(32) notnull
* tenant_id 为首业务字段(紧跟 idstr(32) notnull
* 金额/分值字段一律 double(18,2)
* 所有索引 tenant_id 打头
* CRUD 定义根键仅 tblname/params/editable/browserfieldseditable 为 3 个 api/*.dspy
"""
import io
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
MODULE_ROOT = os.path.dirname(HERE)
PKG = os.path.join(MODULE_ROOT, "pbl_blueprint")
MODEL_DIR = os.path.join(PKG, "models")
JSON_DIR = os.path.join(PKG, "json")
TABLE_ORDER = [
"pbl_blueprint",
"pbl_blueprint_node",
"pbl_blueprint_edge",
"pbl_blueprint_version",
"pbl_blueprint_version_delta",
"pbl_blueprint_template",
"pbl_blueprint_publish",
"pbl_blueprint_offline",
"pbl_blueprint_fork",
"pbl_blueprint_lock",
"pbl_blueprint_audit",
]
AUDIT_FIELDS = ("create_user", "create_time", "update_user", "update_time")
IMMUTABLE_FIELDS = AUDIT_FIELDS + ("deleted",)
# 服务端写入的大字段CRUD 定义里 editable=false前端只读
SERVER_BLOB_FIELDS = ("snapshot_json", "pkg_json", "before_json", "after_json",
"game_def_json", "checksum", "file_size", "use_count",
"node_count", "edge_count", "current_version", "rev",
"publish_time", "expire_time", "cost_ms", "err_code")
QUALITY_CODE = {
"summary": "质量状态5 级M2 校验引擎回写)",
"items": {
"q0": "未校验",
"q1": "不合格",
"q2": "基本合格",
"q3": "合格",
"q4": "优秀",
"q5": "标杆",
},
}
def F(name, typ, size=None, nn=False, default=None, summary=""):
"""字段声明。"""
spec = {"type": typ}
if size is not None:
spec["size"] = size
if nn:
spec["notnull"] = True
if default is not None:
spec["default"] = default
spec["summary"] = summary
return (name, spec)
def ID():
return F("id", "str", 32, True, None, "主键IDstr32UUID去横线")
def TENANT(extra=""):
return F("tenant_id", "str", 32, True, None,
"租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)" + extra)
def DELETED():
return F("deleted", "int", None, True, 0, "逻辑删除标记 0正常 1删除")
def IDX(name, fields, unique, summary):
return (name, {"fields": list(fields), "unique": bool(unique), "summary": summary})
# ------------------------------------------------------------------ 11 表定义
TABLES = {}
TABLES["pbl_blueprint"] = {
"summary": "PBL 蓝图聚合根表:一个租户下的一份项目式学习蓝图(学科/年级/学段/状态/质量状态/当前版本/预算)。所有读写 tenant_id 强制打头。",
"fields": [
ID(), TENANT(),
F("code", "str", 64, True, None, "蓝图编码租户内唯一PBL-BP-序号)"),
F("name", "str", 128, True, None, "蓝图名称"),
F("subject", "str", 64, False, "", "学科"),
F("grade", "str", 32, False, "", "年级"),
F("phase", "str", 32, False, "", "学段"),
F("status", "str", 16, True, "draft", "蓝图状态"),
F("quality_status", "str", 16, True, "q0", "质量状态5 级)"),
F("source", "str", 16, True, "manual", "来源方式"),
F("source_id", "str", 32, False, "", "来源对象ID模板ID或父蓝图ID"),
F("owner_id", "str", 32, False, "", "负责人教师ID"),
F("class_id", "str", 32, False, "", "关联班级ID"),
F("current_version", "int", None, True, 0, "当前版本号save_version 回写)"),
F("duration_hours", "int", None, True, 0, "预计课时数"),
F("budget_amount", "double", [18, 2], True, 0, "预算金额double(18,2)"),
F("summary", "str", 512, False, "", "蓝图简介"),
F("ext_json", "text", None, False, "", "扩展属性JSON"),
F("publish_time", "datetime", None, False, None, "最近发布时间"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
F("update_user", "str", 32, False, "", "更新人"),
F("update_time", "datetime", None, False, None, "更新时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_blueprint_code", ["tenant_id", "code"], True, "租户内蓝图编码唯一"),
IDX("idx_pbl_blueprint_tenant", ["tenant_id", "deleted", "status"], False, "租户蓝图列表主索引tenant_id 打头)"),
IDX("idx_pbl_blueprint_owner", ["tenant_id", "owner_id"], False, "按负责人查蓝图"),
IDX("idx_pbl_blueprint_source", ["tenant_id", "source", "source_id"], False, "按来源(模板/fork追溯"),
IDX("idx_pbl_blueprint_class", ["tenant_id", "class_id"], False, "按班级查蓝图"),
],
"codes": {
"status": {
"summary": "蓝图状态",
"items": {
"draft": "草稿",
"validating": "校验中",
"ready": "就绪",
"published": "已发布",
"archived": "已归档",
"disabled": "已停用",
},
},
"quality_status": QUALITY_CODE,
"source": {
"summary": "来源方式",
"items": {
"manual": "手工创建",
"template": "模板实例化",
"fork": "蓝图fork派生",
"agent": "Designer Agent 生成",
"import": "离线包导入",
},
},
},
"browserfields": ["code", "name", "subject", "status", "quality_status", "owner_id", "update_time"],
"queryfields": ["code", "name", "subject", "grade", "phase", "status", "quality_status",
"source_id", "owner_id", "class_id"],
}
TABLES["pbl_blueprint_node"] = {
"summary": "蓝图子对象节点表7 类子对象泛化契约role/task/rule/artifact/scene/assessment/resource 统一存一张表,差异载荷放 spec_json按 node_type 分派校验器M2",
"fields": [
ID(), TENANT(),
F("blueprint_id", "str", 32, True, None, "所属蓝图ID"),
F("node_type", "str", 16, True, None, "子对象类型7 类泛化)"),
F("parent_id", "str", 32, True, "", "父节点ID构成蓝图树空串=根节点)"),
F("code", "str", 64, True, "", "节点编码(蓝图内同类型唯一)"),
F("name", "str", 128, True, None, "节点名称"),
F("sort_no", "int", None, True, 0, "同级排序号"),
F("spec_json", "text", None, False, "", "泛化载荷JSON结构随 node_type 不同)"),
F("ref_module", "str", 32, True, "", "引用模块名scense/world/assessment 等)"),
F("ref_id", "str", 32, True, "", "引用对象ID"),
F("status", "str", 16, True, "active", "节点状态"),
F("required", "int", None, True, 0, "是否必需节点 0否 1是"),
F("weight", "double", [18, 2], True, 0, "权重/分值double(18,2)"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
F("update_user", "str", 32, False, "", "更新人"),
F("update_time", "datetime", None, False, None, "更新时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_node_code", ["tenant_id", "blueprint_id", "node_type", "code"], True, "蓝图内同类型节点编码唯一"),
IDX("idx_pbl_node_bp", ["tenant_id", "blueprint_id", "deleted", "node_type"], False, "按蓝图+类型列节点tenant_id 打头)"),
IDX("idx_pbl_node_parent", ["tenant_id", "blueprint_id", "parent_id", "sort_no"], False, "构树查询"),
IDX("idx_pbl_node_ref", ["tenant_id", "ref_module", "ref_id"], False, "按引用对象反查节点"),
],
"codes": {
"node_type": {
"summary": "子对象类型7 类泛化契约)",
"items": {
"role": "角色",
"task": "任务",
"rule": "规则",
"artifact": "产出物",
"scene": "场景引用",
"assessment": "评估项",
"resource": "资源",
},
},
"status": {
"summary": "节点状态",
"items": {
"active": "生效",
"draft": "草稿",
"invalid": "校验不通过",
"disabled": "停用",
},
},
},
"browserfields": ["code", "name", "node_type", "parent_id", "sort_no", "status"],
"queryfields": ["blueprint_id", "node_type", "parent_id", "code", "name",
"ref_module", "ref_id", "status"],
}
TABLES["pbl_blueprint_edge"] = {
"summary": "蓝图节点关系边表:描述子对象之间的依赖/解锁/产出/消耗/评估/包含关系,供 CompilerM3编译为 Game Definition 图结构。",
"fields": [
ID(), TENANT(),
F("blueprint_id", "str", 32, True, None, "所属蓝图ID"),
F("from_node_id", "str", 32, True, None, "起点节点ID"),
F("to_node_id", "str", 32, True, None, "终点节点ID"),
F("edge_type", "str", 16, True, "depends", "边类型"),
F("weight", "double", [18, 2], True, 0, "边权重double(18,2)"),
F("condition_json", "text", None, False, "", "触发条件JSON"),
F("sort_no", "int", None, True, 0, "排序号"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
F("update_time", "datetime", None, False, None, "更新时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_edge", ["tenant_id", "blueprint_id", "from_node_id", "to_node_id", "edge_type"], True, "同类型边唯一,防重复连边"),
IDX("idx_pbl_edge_bp", ["tenant_id", "blueprint_id", "deleted"], False, "按蓝图取全部边"),
IDX("idx_pbl_edge_from", ["tenant_id", "from_node_id"], False, "出边查询"),
IDX("idx_pbl_edge_to", ["tenant_id", "to_node_id"], False, "入边查询(删节点前校验)"),
],
"codes": {
"edge_type": {
"summary": "边类型",
"items": {
"depends": "依赖",
"unlocks": "解锁",
"produces": "产出",
"consumes": "消耗",
"assesses": "评估",
"contains": "包含",
},
},
},
"browserfields": ["from_node_id", "to_node_id", "edge_type", "weight", "sort_no"],
"queryfields": ["blueprint_id", "from_node_id", "to_node_id", "edge_type"],
}
TABLES["pbl_blueprint_version"] = {
"summary": "蓝图版本表:每次 save_version 生成一个版本快照snapshot_json 存整棵树支持版本回溯、change_delta 对比与回滚。",
"fields": [
ID(), TENANT(),
F("blueprint_id", "str", 32, True, None, "所属蓝图ID"),
F("version_no", "int", None, True, None, "版本号(蓝图内递增)"),
F("version_type", "str", 16, True, "draft", "版本类型"),
F("change_summary", "str", 512, True, "", "变更说明"),
F("snapshot_json", "text", None, False, "", "蓝图整树快照JSON主表字段+节点+边)"),
F("node_count", "int", None, True, 0, "快照节点数"),
F("edge_count", "int", None, True, 0, "快照边数"),
F("quality_status", "str", 16, True, "q0", "该版本质量状态"),
F("score_amount", "double", [18, 2], True, 0, "该版本评估得分double(18,2)M6 回写)"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_version", ["tenant_id", "blueprint_id", "version_no"], True, "蓝图内版本号唯一"),
IDX("idx_pbl_version_bp", ["tenant_id", "blueprint_id", "deleted", "version_no"], False, "版本列表(倒序取最新)"),
],
"codes": {
"version_type": {
"summary": "版本类型",
"items": {
"draft": "草稿版本",
"minor": "小版本",
"major": "大版本",
"publish": "发布版本",
"rollback": "回滚版本",
},
},
"quality_status": QUALITY_CODE,
},
"browserfields": ["version_no", "version_type", "change_summary", "node_count",
"edge_count", "quality_status", "create_time"],
"queryfields": ["blueprint_id", "version_no", "version_type", "quality_status"],
}
TABLES["pbl_blueprint_version_delta"] = {
"summary": "蓝图版本差异表:记录两个版本之间的 change_delta新增/修改/删除的节点、边与主表字段),供版本对比与回滚审计。",
"fields": [
ID(), TENANT(),
F("blueprint_id", "str", 32, True, None, "所属蓝图ID"),
F("from_version", "int", None, True, 0, "基准版本号"),
F("to_version", "int", None, True, 0, "目标版本号"),
F("delta_type", "str", 16, True, None, "差异对象类型"),
F("op_type", "str", 16, True, None, "操作类型"),
F("target_id", "str", 32, True, "", "差异对象ID节点ID/边ID/蓝图ID"),
F("target_name", "str", 128, True, "", "差异对象名称(冗余便于展示)"),
F("before_json", "text", None, False, "", "变更前JSON"),
F("after_json", "text", None, False, "", "变更后JSON"),
F("sort_no", "int", None, True, 0, "排序号"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_delta", ["tenant_id", "blueprint_id", "from_version", "to_version",
"delta_type", "op_type", "target_id"], True,
"同一版本对同一对象同一操作唯一,保证幂等写入"),
IDX("idx_pbl_delta_bp", ["tenant_id", "blueprint_id", "deleted", "to_version"], False, "按蓝图+版本查差异"),
],
"codes": {
"delta_type": {
"summary": "差异对象类型",
"items": {"blueprint": "蓝图主表字段", "node": "节点", "edge": ""},
},
"op_type": {
"summary": "操作类型",
"items": {"add": "新增", "update": "修改", "delete": "删除"},
},
},
"browserfields": ["from_version", "to_version", "delta_type", "op_type", "target_name"],
"queryfields": ["blueprint_id", "from_version", "to_version", "delta_type",
"op_type", "target_id"],
}
TABLES["pbl_blueprint_template"] = {
"summary": "蓝图模板表可复用的蓝图骨架payload_json 存模板树),支持实例化为新蓝图;离线兜底时作为本地模板源。",
"fields": [
ID(), TENANT(";空串=平台公共模板"),
F("code", "str", 64, True, None, "模板编码(租户内唯一)"),
F("name", "str", 128, True, None, "模板名称"),
F("category", "str", 32, True, "general", "模板分类"),
F("subject", "str", 64, True, "", "适用学科"),
F("grade", "str", 32, True, "", "适用年级"),
F("scope", "str", 16, True, "tenant", "可见范围"),
F("payload_json", "text", None, False, "", "模板树载荷JSONblueprint+nodes+edges"),
F("node_count", "int", None, True, 0, "模板节点数"),
F("edge_count", "int", None, True, 0, "模板边数"),
F("price_amount", "double", [18, 2], True, 0, "模板定价金额double(18,2)0=免费)"),
F("use_count", "int", None, True, 0, "被实例化次数"),
F("status", "str", 16, True, "enabled", "模板状态"),
F("summary", "str", 512, True, "", "模板说明"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
F("update_user", "str", 32, False, "", "更新人"),
F("update_time", "datetime", None, False, None, "更新时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_template_code", ["tenant_id", "code"], True, "租户内模板编码唯一"),
IDX("idx_pbl_template_list", ["tenant_id", "deleted", "status", "category"], False, "模板列表查询tenant_id 打头)"),
IDX("idx_pbl_template_subject", ["tenant_id", "subject", "grade"], False, "按学科年级筛模板"),
],
"codes": {
"scope": {
"summary": "可见范围",
"items": {"tenant": "本租户", "platform": "平台公共", "private": "仅创建人"},
},
"status": {
"summary": "模板状态",
"items": {"enabled": "启用", "disabled": "停用", "draft": "草稿"},
},
"category": {
"summary": "模板分类",
"items": {
"stem": "STEM",
"science": "自然科学",
"humanity": "人文社科",
"art": "艺术",
"labor": "劳动实践",
"general": "通用",
},
},
},
"browserfields": ["code", "name", "category", "subject", "scope", "status", "use_count"],
"queryfields": ["code", "name", "category", "subject", "grade", "scope", "status"],
}
TABLES["pbl_blueprint_publish"] = {
"summary": "蓝图发布/投放记录表:蓝图某版本投放到班级/团队/学生的一次发布,记录运行时句柄与编译产物,供 M11 runtime_ext 关联。",
"fields": [
ID(), TENANT(),
F("blueprint_id", "str", 32, True, None, "蓝图ID"),
F("version_no", "int", None, True, 0, "发布的版本号"),
F("class_id", "str", 32, True, "", "投放班级ID"),
F("target_type", "str", 16, True, "class", "投放对象类型"),
F("target_id", "str", 32, True, "", "投放对象ID"),
F("runtime_ref", "str", 64, True, "", "运行时引用scense_runtime 会话/世界ID"),
F("game_def_json", "text", None, False, "", "编译产物 Game Definition JSONM3 回写)"),
F("start_time", "datetime", None, False, None, "开始时间"),
F("end_time", "datetime", None, False, None, "结束时间"),
F("budget_amount", "double", [18, 2], True, 0, "本次投放预算金额double(18,2)"),
F("status", "str", 16, True, "published", "投放状态"),
F("remark", "str", 512, True, "", "备注"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
F("update_time", "datetime", None, False, None, "更新时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_publish", ["tenant_id", "blueprint_id", "version_no", "target_type", "target_id"], True, "同版本同对象只投放一次,保证幂等"),
IDX("idx_pbl_publish_bp", ["tenant_id", "blueprint_id", "deleted"], False, "按蓝图查投放记录"),
IDX("idx_pbl_publish_class", ["tenant_id", "class_id", "status"], False, "按班级查在跑的投放"),
],
"codes": {
"target_type": {
"summary": "投放对象类型",
"items": {"class": "班级", "team": "团队", "student": "学生", "preview": "预览"},
},
"status": {
"summary": "投放状态",
"items": {
"published": "已发布",
"running": "进行中",
"finished": "已结束",
"revoked": "已撤回",
},
},
},
"browserfields": ["blueprint_id", "version_no", "target_type", "target_id", "status",
"start_time", "end_time"],
"queryfields": ["blueprint_id", "version_no", "class_id", "target_type", "target_id",
"runtime_ref", "status"],
}
TABLES["pbl_blueprint_offline"] = {
"summary": "蓝图离线兜底包表:把蓝图/模板打包为自描述 JSON 包(含 schema_version 与 sha256 校验和),网络或服务不可用时本地兜底实例化。",
"fields": [
ID(), TENANT(),
F("blueprint_id", "str", 32, True, "", "来源蓝图ID与 template_id 二选一)"),
F("template_id", "str", 32, True, "", "来源模板ID"),
F("pkg_code", "str", 64, True, None, "离线包编码"),
F("pkg_name", "str", 128, True, None, "离线包名称"),
F("pkg_version", "int", None, True, 1, "离线包版本"),
F("pkg_json", "text", None, False, "", "离线包内容JSON自描述schema_version+蓝图+节点+边)"),
F("file_size", "int", None, True, 0, "包体字节数"),
F("checksum", "str", 64, True, "", "内容校验和sha256"),
F("status", "str", 16, True, "ready", "离线包状态"),
F("expire_time", "datetime", None, False, None, "过期时间"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
F("update_time", "datetime", None, False, None, "更新时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_offline_code", ["tenant_id", "pkg_code", "pkg_version"], True, "离线包编码+版本唯一"),
IDX("idx_pbl_offline_bp", ["tenant_id", "blueprint_id", "deleted"], False, "按蓝图查离线包"),
IDX("idx_pbl_offline_status", ["tenant_id", "status", "expire_time"], False, "过期包清理扫描"),
],
"codes": {
"status": {
"summary": "离线包状态",
"items": {
"ready": "可用",
"building": "构建中",
"expired": "已过期",
"invalid": "校验失败",
},
},
},
"browserfields": ["pkg_code", "pkg_name", "pkg_version", "status", "file_size", "create_time"],
"queryfields": ["blueprint_id", "template_id", "pkg_code", "pkg_name", "pkg_version", "status"],
}
TABLES["pbl_blueprint_fork"] = {
"summary": "蓝图 fork 血缘表:记录蓝图之间的派生关系(源蓝图→新蓝图)与模板实例化来源,支持 fork 溯源与派生树查询。",
"fields": [
ID(), TENANT(),
F("source_blueprint_id", "str", 32, True, "", "源蓝图ID模板实例化时为空串"),
F("source_version", "int", None, True, 0, "fork 时的源版本号"),
F("target_blueprint_id", "str", 32, True, None, "派生出的新蓝图ID"),
F("fork_type", "str", 16, True, "copy", "fork 方式"),
F("template_id", "str", 32, True, "", "若由模板实例化记录模板ID"),
F("node_count", "int", None, True, 0, "复制的节点数"),
F("edge_count", "int", None, True, 0, "复制的边数"),
F("cost_amount", "double", [18, 2], True, 0, "本次 fork 产生的费用金额double(18,2),付费模板)"),
F("remark", "str", 512, True, "", "备注"),
F("create_user", "str", 32, False, "", "创建人"),
F("create_time", "datetime", None, False, None, "创建时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_fork", ["tenant_id", "source_blueprint_id", "target_blueprint_id"], True, "同一对蓝图血缘唯一,保证幂等"),
IDX("idx_pbl_fork_source", ["tenant_id", "source_blueprint_id", "deleted"], False, "查某蓝图的派生列表"),
IDX("idx_pbl_fork_target", ["tenant_id", "target_blueprint_id"], False, "反查某蓝图的来源"),
IDX("idx_pbl_fork_template", ["tenant_id", "template_id"], False, "查模板被实例化记录"),
],
"codes": {
"fork_type": {
"summary": "fork 方式",
"items": {
"copy": "完整复制",
"structure": "仅结构(不带载荷)",
"template": "模板实例化",
"branch": "分支派生",
},
},
},
"browserfields": ["source_blueprint_id", "source_version", "target_blueprint_id",
"fork_type", "node_count", "create_time"],
"queryfields": ["source_blueprint_id", "target_blueprint_id", "fork_type", "template_id"],
}
TABLES["pbl_blueprint_lock"] = {
"summary": "蓝图编辑锁表:多人协作时对蓝图/节点加编辑锁(悲观锁+rev 乐观修订号),锁带过期时间,过期可被抢占。",
"fields": [
ID(), TENANT(),
F("blueprint_id", "str", 32, True, None, "被锁蓝图ID"),
F("node_id", "str", 32, True, "", "被锁节点ID空串=整蓝图锁)"),
F("lock_type", "str", 16, True, "edit", "锁类型"),
F("holder_id", "str", 32, True, None, "持锁人ID"),
F("holder_name", "str", 64, True, "", "持锁人名称"),
F("rev", "int", None, True, 0, "乐观锁修订号(每次抢占/续锁 +1"),
F("expire_time", "datetime", None, False, None, "锁过期时间"),
F("status", "str", 16, True, "holding", "锁状态"),
F("create_time", "datetime", None, False, None, "加锁时间"),
F("update_time", "datetime", None, False, None, "续锁时间"),
DELETED(),
],
"indexes": [
IDX("uk_pbl_lock", ["tenant_id", "blueprint_id", "node_id", "lock_type"], True, "同对象同类型仅一把锁记录"),
IDX("idx_pbl_lock_holder", ["tenant_id", "holder_id", "status"], False, "查某人持有的锁"),
IDX("idx_pbl_lock_expire", ["tenant_id", "status", "expire_time"], False, "过期锁清理扫描"),
],
"codes": {
"lock_type": {
"summary": "锁类型",
"items": {"edit": "编辑锁", "publish": "发布锁", "compile": "编译锁"},
},
"status": {
"summary": "锁状态",
"items": {"holding": "持有中", "released": "已释放", "expired": "已过期"},
},
},
"browserfields": ["blueprint_id", "node_id", "lock_type", "holder_name", "status", "expire_time"],
"queryfields": ["blueprint_id", "node_id", "lock_type", "holder_id", "status"],
}
TABLES["pbl_blueprint_audit"] = {
"summary": "蓝图操作审计表append-only记录蓝图/节点/边/版本/模板/发布/锁的关键写操作与结果,只增不改不删,供追溯与合规审计。",
"fields": [
ID(), TENANT(),
F("blueprint_id", "str", 32, True, "", "蓝图ID"),
F("target_type", "str", 16, True, None, "操作对象类型"),
F("target_id", "str", 32, True, "", "操作对象ID"),
F("action", "str", 32, True, None, "操作动作"),
F("version_no", "int", None, True, 0, "关联版本号"),
F("before_json", "text", None, False, "", "操作前数据JSON"),
F("after_json", "text", None, False, "", "操作后数据JSON"),
F("result", "str", 16, True, "success", "操作结果"),
F("err_code", "str", 32, True, "", "失败错误码"),
F("cost_ms", "int", None, True, 0, "耗时毫秒"),
F("op_user", "str", 32, True, "", "操作人"),
F("op_source", "str", 32, True, "api", "操作来源web/api/agent/offline"),
F("client_ip", "str", 64, True, "", "客户端IP"),
F("create_time", "datetime", None, False, None, "操作时间"),
],
"indexes": [
IDX("idx_pbl_audit_bp", ["tenant_id", "blueprint_id", "create_time"], False, "按蓝图时间线查审计"),
IDX("idx_pbl_audit_target", ["tenant_id", "target_type", "target_id"], False, "按对象查审计"),
IDX("idx_pbl_audit_user", ["tenant_id", "op_user", "create_time"], False, "按操作人查审计"),
IDX("idx_pbl_audit_action", ["tenant_id", "action", "result"], False, "按动作与结果统计"),
],
"codes": {
"target_type": {
"summary": "操作对象类型",
"items": {
"blueprint": "蓝图",
"node": "节点",
"edge": "",
"version": "版本",
"delta": "版本差异",
"template": "模板",
"publish": "发布",
"offline": "离线包",
"fork": "血缘",
"lock": "编辑锁",
},
},
"action": {
"summary": "操作动作",
"items": {
"create": "创建",
"update": "更新",
"delete": "删除",
"fork": "fork派生",
"save_version": "存版本",
"rollback": "回滚",
"publish": "发布",
"revoke": "撤回发布",
"instantiate": "模板实例化",
"validate": "校验",
"compile": "编译",
"lock": "加锁",
"unlock": "解锁",
"export_offline": "导出离线包",
"import_offline": "导入离线包",
},
},
"result": {
"summary": "操作结果",
"items": {"success": "成功", "fail": "失败", "denied": "被拒绝"},
},
},
"browserfields": ["blueprint_id", "target_type", "action", "result", "op_user",
"op_source", "create_time"],
"queryfields": ["blueprint_id", "target_type", "target_id", "action", "version_no",
"result", "op_user", "op_source", "create_time"],
}
# ------------------------------------------------------------------ 生成
def build_model(tbl):
d = TABLES[tbl]
fields = {}
for name, spec in d["fields"]:
fields[name] = spec
return {
"summary": d["summary"],
"primary": ["id"],
"fields": fields,
"indexes": dict(d["indexes"]),
"codes": d["codes"],
}
def build_crud(tbl):
d = TABLES[tbl]
codes = d.get("codes") or {}
params = {}
for name, spec in d["fields"]:
p = {"type": spec["type"]}
if "size" in spec:
p["size"] = spec["size"]
if spec.get("notnull"):
p["notnull"] = True
if "default" in spec:
p["default"] = spec["default"]
if name in IMMUTABLE_FIELDS or name in SERVER_BLOB_FIELDS:
p["editable"] = False
else:
p["editable"] = True
p["query"] = name in (d.get("queryfields") or ()) or name == "tenant_id"
if name in codes:
p["code"] = name
p["summary"] = spec["summary"]
params[name] = p
return {
"tblname": tbl,
"params": params,
"editable": [
"api/%s_list.dspy" % tbl,
"api/%s_edit.dspy" % tbl,
"api/%s_view.dspy" % tbl,
],
"browserfields": list(d.get("browserfields") or []),
}
def col_type(spec):
t = (spec.get("type") or "str").lower()
size = spec.get("size")
if t in ("str", "string", "varchar", "char"):
return "varchar(%d)" % int(size or 64)
if t == "int":
return "int"
if t == "bigint":
return "bigint"
if t == "double":
if isinstance(size, (list, tuple)) and len(size) == 2:
return "decimal(%d,%d)" % (int(size[0]), int(size[1]))
return "double"
if t in ("text", "longtext", "json"):
return "text"
if t in ("datetime", "timestamp"):
return "datetime"
if t == "date":
return "date"
return "varchar(64)"
def ddl():
out = ["-- pbl_blueprint M1a DDL%d 表,由 scripts/gen_defs.py 生成)" % len(TABLE_ORDER), ""]
for tbl in TABLE_ORDER:
m = build_model(tbl)
cols = []
for name, spec in m["fields"].items():
nn = " not null" if spec.get("notnull") else ""
df = ""
if "default" in spec:
dv = spec["default"]
df = (" default '%s'" % dv) if isinstance(dv, str) else (" default %s" % dv)
cm = " comment '%s'" % str(spec.get("summary") or "").replace("'", "")
cols.append(" `%s` %s%s%s%s" % (name, col_type(spec), nn, df, cm))
cols.append(" primary key (`%s`)" % "` , `".join(m["primary"]))
for iname, ispec in m["indexes"].items():
uq = "unique " if ispec.get("unique") else ""
cols.append(" %skey `%s` (%s)" % (uq, iname,
", ".join(["`%s`" % c for c in ispec["fields"]])))
out.append("-- %s" % m["summary"])
out.append("create table if not exists `%s` (" % tbl)
out.append(",\n".join(cols))
out.append(") engine=InnoDB default charset=utf8mb4 comment='%s';" % tbl.replace("'", ""))
out.append("")
return "\n".join(out)
def clean_stale():
"""删除 models/ 与 json/ 下不属于 11 表清单的陈旧定义文件。"""
removed = []
for d in (MODEL_DIR, JSON_DIR):
if not os.path.isdir(d):
continue
for fn in sorted(os.listdir(d)):
if not fn.endswith(".json"):
continue
if fn[:-5] not in TABLE_ORDER:
p = os.path.join(d, fn)
os.remove(p)
removed.append(os.path.relpath(p, MODULE_ROOT))
return removed
def write_json(path, obj):
d = os.path.dirname(path)
if not os.path.isdir(d):
os.makedirs(d)
with io.open(path, "w", encoding="utf-8") as f:
f.write(json.dumps(obj, ensure_ascii=False, indent=2) + "\n")
def generate():
removed = clean_stale()
written = []
for tbl in TABLE_ORDER:
p1 = os.path.join(MODEL_DIR, "%s.json" % tbl)
write_json(p1, build_model(tbl))
written.append(os.path.relpath(p1, MODULE_ROOT))
p2 = os.path.join(JSON_DIR, "%s.json" % tbl)
write_json(p2, build_crud(tbl))
written.append(os.path.relpath(p2, MODULE_ROOT))
return removed, written
def main(argv=None):
argv = list(sys.argv[1:] if argv is None else argv)
if "--dump-ddl" in argv:
print(ddl())
return 0
if "--clean" in argv:
rm = clean_stale()
print("已清理陈旧定义 %d 个: %s" % (len(rm), rm))
return 0
rm, wr = generate()
print("清理陈旧定义 %d 个: %s" % (len(rm), rm))
print("生成定义文件 %dmodels 11 + json 11:" % len(wr))
for w in wr:
print(" %s" % w)
return 0
if __name__ == "__main__":
sys.exit(main())