774 lines
35 KiB
Python
774 lines
35 KiB
Python
# -*- 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 为首业务字段(紧跟 id),str(32) notnull
|
||
* 金额/分值字段一律 double(18,2)
|
||
* 所有索引 tenant_id 打头
|
||
* CRUD 定义根键仅 tblname/params/editable/browserfields,editable 为 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, "主键ID(str32,UUID去横线)")
|
||
|
||
|
||
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": "蓝图节点关系边表:描述子对象之间的依赖/解锁/产出/消耗/评估/包含关系,供 Compiler(M3)编译为 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, "", "模板树载荷JSON(blueprint+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 JSON(M3 回写)"),
|
||
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("生成定义文件 %d 个(models 11 + json 11):" % len(wr))
|
||
for w in wr:
|
||
print(" %s" % w)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|