approve: pbls 系统设计正文补全(26 个占位文档重写为定稿:36 表 DDL + 15 模块设计 + 架构/UI/字典/工具契约/spec.json)

This commit is contained in:
agent.develop 2026-09-16 10:22:32 +08:00
parent 1d382b27e4
commit 1225421d73
49 changed files with 7854 additions and 1389 deletions

View File

@ -1,9 +1,173 @@
"""pbl_blueprint —— PBL 蓝图聚合根与子对象、版本、模板模块M1a
# -*- coding: utf-8 -*-
"""pbl_blueprint —— PBL 蓝图聚合根与子对象、版本、模板M1a
对外唯一入口load_pbl_blueprint(env=None)
所有读写 tenant_id 强制打头缺失租户上下文一律 fail-closed 拒绝
模块契约对外只暴露这些名字三处同步注册service 定义 / 本文件导出 / init.py env 注册
蓝图 CRUD : create_blueprint / get_blueprint / update_blueprint / delete_blueprint / list_blueprints
树与子对象: get_blueprint_tree / create_node / update_node / delete_node / list_nodes
create_edge / delete_edge / list_edges
fork : fork_blueprint / list_forks
版本 : save_version / list_versions / get_version / get_change_delta / rollback_version
模板 : create_template / list_templates / instantiate_template / instantiate_payload
离线兜底 : export_offline / import_offline / list_offline
发布 : publish_blueprint / revoke_publish / list_publishes
: lock_blueprint / unlock_blueprint / clean_expired_locks
统计/审计 : blueprint_stats / write_audit / list_audit
所有契约函数第一个参数固定 tenant_id缺失/非法即 fail-closed PblBlueprintError
"""
from .init import load_pbl_blueprint
__version__ = "1.0.0"
__module_name__ = "pbl_blueprint"
__all__ = ['load_pbl_blueprint']
from .errors import ( # noqa: F401
PblBlueprintError,
ERR_OK,
ERR_TENANT_MISSING,
ERR_PARAM_INVALID,
ERR_NOT_FOUND,
ERR_DUPLICATE,
ERR_STATE_INVALID,
ERR_LOCKED,
ERR_FORBIDDEN,
ERR_DB,
ERR_INTERNAL,
ok,
fail,
err,
)
from .tenant import require_tenant, tenant_where, inject_tenant # noqa: F401
from .db import get_dbname, get_sor, MODULE_NAME # noqa: F401
from .crud import ( # noqa: F401
list_tables,
table_fields,
load_model,
build_filter,
make_crud,
crud_create,
crud_get,
crud_update,
crud_delete,
crud_list,
crud_count,
new_id,
now_str,
APPEND_ONLY_TABLES,
)
from .audit import write_audit, list_audit, timed_audit # noqa: F401
from .service import ( # noqa: F401
NODE_TYPES,
EDGE_TYPES,
OFFLINE_SCHEMA_VERSION,
create_blueprint,
get_blueprint,
update_blueprint,
delete_blueprint,
list_blueprints,
get_blueprint_tree,
create_node,
update_node,
delete_node,
list_nodes,
create_edge,
delete_edge,
list_edges,
fork_blueprint,
list_forks,
save_version,
list_versions,
get_version,
get_change_delta,
rollback_version,
create_template,
list_templates,
instantiate_template,
instantiate_payload,
export_offline,
import_offline,
list_offline,
publish_blueprint,
revoke_publish,
list_publishes,
lock_blueprint,
unlock_blueprint,
clean_expired_locks,
blueprint_stats,
)
# 对外契约函数清单init.py 按此清单注册到 ServerEnv三处同步的唯一事实源
CONTRACT_FUNCTIONS = (
"create_blueprint", "get_blueprint", "update_blueprint", "delete_blueprint",
"list_blueprints", "get_blueprint_tree", "create_node", "update_node",
"delete_node", "list_nodes", "create_edge", "delete_edge", "list_edges",
"fork_blueprint", "list_forks", "save_version", "list_versions", "get_version",
"get_change_delta", "rollback_version", "create_template", "list_templates",
"instantiate_template", "instantiate_payload", "export_offline", "import_offline",
"list_offline", "publish_blueprint", "revoke_publish", "list_publishes",
"lock_blueprint", "unlock_blueprint", "clean_expired_locks", "blueprint_stats",
"write_audit", "list_audit",
)
# 本模块 11 张表models/*.json 为唯一事实源,此处仅缓存清单便于挂载)
TABLES = (
"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",
)
_loaded = False
def get_contract_functions():
"""返回 {函数名: 函数对象},供 init.py 注册与 selfcheck 校验三处同步。"""
import sys
mod = sys.modules[__name__]
out = {}
missing = []
for name in CONTRACT_FUNCTIONS:
fn = getattr(mod, name, None)
if fn is None or not callable(fn):
missing.append(name)
continue
out[name] = fn
if missing:
raise ImportError("pbl_blueprint 契约函数未导出: %s" % ",".join(missing))
return out
def load_pbl_blueprint(env=None, dbname=None, register_tables=True,
register_functions=True, register_rbac=True):
"""挂载模块到应用(由 apps/pbls 的 init() 调用)。
:param env: ServerEnv 实例可选缺省自动获取
:param dbname: 可选显式库名缺省由 env.get_module_dbname('pbl_blueprint') 决定禁硬编码
:return: {"module":..., "dbname":..., "tables": n, "functions": n, "rbac_paths": n}
"""
global _loaded
from .init import init as _init
res = _init(env=env, dbname=dbname, register_tables=register_tables,
register_functions=register_functions, register_rbac=register_rbac)
_loaded = True
return res
def is_loaded():
return _loaded
__all__ = [
"__version__", "__module_name__", "MODULE_NAME", "TABLES", "CONTRACT_FUNCTIONS",
"NODE_TYPES", "EDGE_TYPES", "OFFLINE_SCHEMA_VERSION", "APPEND_ONLY_TABLES",
"PblBlueprintError", "ok", "fail", "err",
"require_tenant", "tenant_where", "inject_tenant",
"get_dbname", "get_sor", "list_tables", "table_fields", "load_model",
"build_filter", "make_crud", "new_id", "now_str",
"get_contract_functions", "load_pbl_blueprint", "is_loaded",
] + list(CONTRACT_FUNCTIONS)

93
pbl_blueprint/audit.py Normal file
View File

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""蓝图审计M1aappend-only 写入 pbl_blueprint_audit。
审计失败不阻断主流程 warn但审计表本身禁改禁删crud.APPEND_ONLY_TABLES
"""
import time
from . import db as _db
from .crud import new_id, now_str, APPEND_ONLY_TABLES
from .tenant import require_tenant
AUDIT_TABLE = "pbl_blueprint_audit"
assert AUDIT_TABLE in APPEND_ONLY_TABLES
_warns = []
def write_audit(tenant_id, blueprint_id, action, target_type="blueprint", target_id="",
version_no=0, before=None, after=None, result="success", err_code="",
op_user="", op_source="api", client_ip="", cost_ms=0, sor=None):
"""写一条审计记录append-only。tenant_id 强制打头。"""
tid = require_tenant(tenant_id)
row = {
"id": new_id(),
"tenant_id": tid,
"blueprint_id": blueprint_id or "",
"target_type": target_type or "blueprint",
"target_id": target_id or "",
"action": action or "",
"version_no": int(version_no or 0),
"before_json": _db.dumps(before) if before is not None else "",
"after_json": _db.dumps(after) if after is not None else "",
"result": result or "success",
"err_code": err_code or "",
"cost_ms": int(cost_ms or 0),
"op_user": op_user or "",
"op_source": op_source or "api",
"client_ip": client_ip or "",
"create_time": now_str(),
}
try:
s = sor or _db.get_sor()
_db.q_insert(s, AUDIT_TABLE, row)
except Exception as e: # 审计失败不阻断业务
_warns.append("audit write failed: %s" % e)
return None
return row
def timed_audit(tenant_id, blueprint_id, action, **kw):
"""上下文管理器:自动计时 + 成功/失败落审计。"""
return _TimedAudit(tenant_id, blueprint_id, action, kw)
class _TimedAudit(object):
def __init__(self, tenant_id, blueprint_id, action, kw):
self.tenant_id = tenant_id
self.blueprint_id = blueprint_id
self.action = action
self.kw = kw or {}
self.t0 = 0.0
def __enter__(self):
self.t0 = time.time()
return self
def __exit__(self, exc_type, exc_val, tb):
cost = int((time.time() - self.t0) * 1000)
if exc_type is None:
write_audit(self.tenant_id, self.blueprint_id, self.action,
result="success", cost_ms=cost, **self.kw)
else:
write_audit(self.tenant_id, self.blueprint_id, self.action,
result="fail", err_code=str(getattr(exc_val, "errcode", exc_type.__name__)),
cost_ms=cost, **self.kw)
return False # 不吞异常
def list_audit(tenant_id, blueprint_id="", action="", page=1, page_size=50, sor=None):
"""查审计只读tenant_id 打头)。"""
from .crud import crud_list, build_filter
conds = {}
if blueprint_id:
conds["blueprint_id"] = blueprint_id
if action:
conds["action"] = action
w, a = build_filter(AUDIT_TABLE, conds)
return crud_list(AUDIT_TABLE, tenant_id, w, a, "*", "create_time desc", page, page_size, True, sor)
def get_warns():
return list(_warns)

325
pbl_blueprint/crud.py Normal file
View File

@ -0,0 +1,325 @@
# -*- coding: utf-8 -*-
"""CRUD 工厂M1a为 11 张表统一生成 tenant_id 强制打头的增删改查契约。
设计要点
1. 所有生成的函数第一个参数固定为 tenant_id内部 require_tenant 校验缺失即 fail-closed 抛错
2. 所有 WHERE 条件由 tenant_where() 生成`tenant_id=?` 永远在最前杜绝跨租户读写
3. 只用 sqlor 白名单 APIC / U / D / R / I / sqlExe封装在 db.py
4. 删除默认逻辑删除deleted=1审计表 append-only 禁改禁删
5. 表名/字段名来自 models/*.json 白名单禁止拼接外部传入的表名列名防注入
"""
import os
import json as _json
import uuid
import datetime
from . import db as _db
from .errors import (
ERR_PARAM_INVALID,
ERR_NOT_FOUND,
ERR_DUPLICATE,
ERR_FORBIDDEN,
PblBlueprintError,
ok,
)
from .tenant import require_tenant, tenant_where, inject_tenant
MODEL_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models")
# append-only 表:只允许 C / R禁 U / D
APPEND_ONLY_TABLES = ("pbl_blueprint_audit",)
# 不可由外部写入的服务端字段
SERVER_FIELDS = (
"create_user",
"create_time",
"update_user",
"update_time",
"deleted",
)
_model_cache = {}
def new_id():
"""生成 str32 主键UUID 去横线)。"""
return uuid.uuid4().hex
def now_str():
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def load_model(tbl):
"""读取 models/{tbl}.json 表定义(带缓存),返回 dict。"""
if tbl in _model_cache:
return _model_cache[tbl]
path = os.path.join(MODEL_DIR, "%s.json" % tbl)
if not os.path.isfile(path):
raise PblBlueprintError(ERR_PARAM_INVALID, "未知表: %s" % tbl)
with open(path, "r", encoding="utf-8") as f:
m = _json.load(f)
_model_cache[tbl] = m
return m
def list_tables():
"""列出本模块全部表名(来自 models 目录,真实落盘为准)。"""
if not os.path.isdir(MODEL_DIR):
return []
out = []
for fn in sorted(os.listdir(MODEL_DIR)):
if fn.endswith(".json"):
out.append(fn[:-5])
return out
def table_fields(tbl):
"""返回表字段名列表保持定义顺序tenant_id 在 id 之后即首业务字段)。"""
m = load_model(tbl)
return list((m.get("fields") or {}).keys())
def _check_fields(tbl, data):
"""字段白名单校验:拒绝未定义字段(防注入/防脏写)。"""
allowed = set(table_fields(tbl))
bad = [k for k in data.keys() if k not in allowed]
if bad:
raise PblBlueprintError(ERR_PARAM_INVALID, "%s 无字段: %s" % (tbl, ",".join(bad)))
def _check_required(tbl, data, partial=False):
"""notnull 且无 default 的字段必填校验。"""
m = load_model(tbl)
miss = []
for name, spec in (m.get("fields") or {}).items():
if name in ("id", "tenant_id"):
continue
if not spec.get("notnull"):
continue
if "default" in spec:
continue
if partial and name not in data:
continue
if name not in data or data[name] in (None, ""):
miss.append(name)
if miss:
raise PblBlueprintError(ERR_PARAM_INVALID, "缺少必填字段: %s" % ",".join(miss))
def _apply_default(tbl, data):
"""填充 default 值。"""
m = load_model(tbl)
out = dict(data)
for name, spec in (m.get("fields") or {}).items():
if name in out and out[name] not in (None, ""):
continue
if "default" in spec:
out[name] = spec["default"]
return out
# ---------------------------------------------------------------- 通用 CRUD
def crud_create(tbl, tenant_id, data, op_user="", sor=None):
"""新增一行。tenant_id 强制打头注入,返回新行 dict。"""
tid = require_tenant(tenant_id)
row = inject_tenant(tid, data or {})
_check_fields(tbl, row)
row = _apply_default(tbl, row)
if not row.get("id"):
row["id"] = new_id()
_check_required(tbl, row)
row["create_time"] = now_str()
if op_user:
row["create_user"] = op_user
if "update_time" in table_fields(tbl):
row["update_time"] = row["create_time"]
if op_user and "update_user" in table_fields(tbl):
row["update_user"] = op_user
if "deleted" in table_fields(tbl) and not row.get("deleted"):
row["deleted"] = 0
s = sor or _db.get_sor()
try:
_db.q_insert(s, tbl, row)
except PblBlueprintError:
raise
except Exception as e:
msg = str(e)
if "Duplicate" in msg or "UNIQUE" in msg.upper():
raise PblBlueprintError(ERR_DUPLICATE, "唯一约束冲突: %s" % msg)
raise PblBlueprintError(ERR_PARAM_INVALID, "写入失败: %s" % msg)
return row
def crud_get(tbl, tenant_id, row_id, fields="*", sor=None, include_deleted=False):
"""按主键取单行tenant_id 打头过滤)。不存在抛 ERR_NOT_FOUND。"""
tid = require_tenant(tenant_id)
rid = (row_id or "").strip() if isinstance(row_id, str) else row_id
if not rid:
raise PblBlueprintError(ERR_PARAM_INVALID, "id 不能为空")
extra = "" if include_deleted else "deleted=0"
where, args = tenant_where(tid, "id=?" + (" and " + extra if extra else ""), [rid])
s = sor or _db.get_sor()
row = _db.q_one(s, tbl, where, args, fields)
if not row:
raise PblBlueprintError(ERR_NOT_FOUND, "%s[%s] 不存在或不属于当前租户" % (tbl, rid))
return row
def crud_update(tbl, tenant_id, row_id, data, op_user="", sor=None):
"""按主键更新tenant_id 打头过滤返回受影响行数。append-only 表拒绝。"""
tid = require_tenant(tenant_id)
if tbl in APPEND_ONLY_TABLES:
raise PblBlueprintError(ERR_FORBIDDEN, "%s 为 append-only 表,禁止更新" % tbl)
rid = (row_id or "").strip() if isinstance(row_id, str) else row_id
if not rid:
raise PblBlueprintError(ERR_PARAM_INVALID, "id 不能为空")
payload = dict(data or {})
for k in ("id", "tenant_id"):
payload.pop(k, None)
for k in ("create_user", "create_time"):
payload.pop(k, None)
if not payload:
raise PblBlueprintError(ERR_PARAM_INVALID, "无待更新字段")
_check_fields(tbl, payload)
_check_required(tbl, payload, partial=True)
if "update_time" in table_fields(tbl):
payload["update_time"] = now_str()
if op_user and "update_user" in table_fields(tbl):
payload["update_user"] = op_user
crud_get(tbl, tid, rid, "id", sor=sor) # 存在性 + 租户归属校验
where, args = tenant_where(tid, "id=? and deleted=0", [rid])
s = sor or _db.get_sor()
return _db.q_update(s, tbl, payload, where, args)
def crud_delete(tbl, tenant_id, row_id, op_user="", sor=None, hard=False):
"""删除(默认逻辑删除 deleted=1。append-only 表拒绝。"""
tid = require_tenant(tenant_id)
if tbl in APPEND_ONLY_TABLES:
raise PblBlueprintError(ERR_FORBIDDEN, "%s 为 append-only 表,禁止删除" % tbl)
rid = (row_id or "").strip() if isinstance(row_id, str) else row_id
if not rid:
raise PblBlueprintError(ERR_PARAM_INVALID, "id 不能为空")
crud_get(tbl, tid, rid, "id", sor=sor)
where, args = tenant_where(tid, "id=? and deleted=0", [rid])
s = sor or _db.get_sor()
if hard:
return _db.q_delete(s, tbl, where, args)
return _db.q_update(s, tbl, {"deleted": 1, "update_time": now_str()}, where, args)
def crud_list(tbl, tenant_id, where="", args=None, fields="*", order="", page=1,
page_size=20, include_deleted=False, sor=None):
"""分页列表查询。tenant_id 打头,返回 {rows,total,page,page_size}。
where/args 只接受内部构造的参数化条件禁止把外部字符串直接当 SQL 片段传入
调用方须用 build_filter 生成
"""
tid = require_tenant(tenant_id)
conds = [] if include_deleted else ["deleted=0"]
if where:
conds.append(where)
w, a = tenant_where(tid, " and ".join(conds), list(args or []))
try:
page = max(1, int(page or 1))
page_size = min(500, max(1, int(page_size or 20)))
except Exception:
raise PblBlueprintError(ERR_PARAM_INVALID, "page/page_size 非法")
s = sor or _db.get_sor()
total = _db.q_count(s, tbl, w, a)
rows = _db.q_select(s, tbl, w, a, fields, order or "", page_size, (page - 1) * page_size)
return {"rows": rows or [], "total": total, "page": page, "page_size": page_size}
def crud_count(tbl, tenant_id, where="", args=None, include_deleted=False, sor=None):
"""计数tenant_id 打头)。"""
tid = require_tenant(tenant_id)
conds = [] if include_deleted else ["deleted=0"]
if where:
conds.append(where)
w, a = tenant_where(tid, " and ".join(conds), list(args or []))
s = sor or _db.get_sor()
return _db.q_count(s, tbl, w, a)
# ---------------------------------------------------------------- 条件构造
ALLOWED_OPS = ("=", "!=", ">", ">=", "<", "<=", "like", "in", "is", "is not")
def build_filter(tbl, conds):
"""{字段: 值} 或 [(字段, op, 值)] 转成参数化 WHERE字段名走白名单
返回 (sql, args)sql 不含 tenant_id crud_list 统一打头拼接
"""
allowed = set(table_fields(tbl))
parts, args = [], []
items = conds.items() if isinstance(conds, dict) else conds
for item in items:
if isinstance(item, (tuple, list)) and len(item) == 3:
name, op, val = item
else:
name, val = item
op = "like" if isinstance(val, str) and ("%" in val or "_" in val) else "="
name = str(name).strip()
if name not in allowed:
raise PblBlueprintError(ERR_PARAM_INVALID, "非法查询字段: %s" % name)
op = str(op).strip().lower()
if op not in ALLOWED_OPS:
raise PblBlueprintError(ERR_PARAM_INVALID, "非法操作符: %s" % op)
if op == "in":
vals = list(val or [])
if not vals:
continue
parts.append("%s in (%s)" % (name, ",".join(["?"] * len(vals))))
args.extend(vals)
elif op in ("is", "is not"):
parts.append("%s %s null" % (name, op))
else:
parts.append("%s %s ?" % (name, op))
args.append(val)
return (" and ".join(parts), args)
# ---------------------------------------------------------------- 工厂
def make_crud(tbl):
"""为指定表生成一组绑定 tenant 的契约函数(供 init.py 注册到 env"""
load_model(tbl) # 提前校验表定义存在
append_only = tbl in APPEND_ONLY_TABLES
def _create(tenant_id, data, op_user="", sor=None):
return crud_create(tbl, tenant_id, data, op_user, sor)
def _get(tenant_id, row_id, fields="*", sor=None):
return crud_get(tbl, tenant_id, row_id, fields, sor)
def _list(tenant_id, conds=None, fields="*", order="", page=1, page_size=20, sor=None):
w, a = build_filter(tbl, conds or {})
return crud_list(tbl, tenant_id, w, a, fields, order, page, page_size, False, sor)
def _count(tenant_id, conds=None, sor=None):
w, a = build_filter(tbl, conds or {})
return crud_count(tbl, tenant_id, w, a, False, sor)
def _update(tenant_id, row_id, data, op_user="", sor=None):
if append_only:
raise PblBlueprintError(ERR_FORBIDDEN, "%s 为 append-only 表,禁止更新" % tbl)
return crud_update(tbl, tenant_id, row_id, data, op_user, sor)
def _delete(tenant_id, row_id, op_user="", sor=None, hard=False):
if append_only:
raise PblBlueprintError(ERR_FORBIDDEN, "%s 为 append-only 表,禁止删除" % tbl)
return crud_delete(tbl, tenant_id, row_id, op_user, sor, hard)
return {
"create": _create,
"get": _get,
"list": _list,
"count": _count,
"update": _update,
"delete": _delete,
}

View File

@ -1,153 +1,122 @@
"""pbl_blueprint 数据访问层。
# -*- coding: utf-8 -*-
"""DB 适配层M1a统一取 sqlor 句柄,禁止硬编码库名。
- 库名一律 ServerEnv().get_module_dbname('pbl_blueprint')禁止硬编码 DBNAME
- 所有 SQL 只用 sqlor 标准 APIsor.C / sor.U / sor.D / sor.R / sor.I / sor.sqlExe
- tenant_id 强制打头任何读写条件第一列必须是 tenant_id缺失即抛 PblTenantRequired
库名一律来自应用注入的 ServerEnv().get_module_dbname('pbl_blueprint')
本模块只出现模块名常量 MODULE_NAME不出现任何具体库名字面量
sqlor 白名单仅使用 C / U / D / R / I / sqlExe 六个标准 API
"""
import os
import uuid
import json as _json
MODULE_NAME = 'pbl_blueprint'
MODULE_NAME = "pbl_blueprint"
TABLES = [
'pbl_blueprint',
'pbl_blueprint_version',
'pbl_blueprint_template',
'pbl_blueprint_template_item',
'pbl_blueprint_task',
'pbl_blueprint_mission',
'pbl_blueprint_role',
'pbl_blueprint_learning_goal',
'pbl_blueprint_evidence_spec',
'pbl_blueprint_artifact_spec',
'pbl_blueprint_reflection_spec',
]
_sor_cache = {}
class PblError(Exception):
"""pbl 模块统一异常基类。"""
code = 'PBL_ERROR'
def __init__(self, message, code=None, **extra):
super(PblError, self).__init__(message)
self.message = message
if code:
self.code = code
self.extra = extra
class PblTenantRequired(PblError):
"""租户上下文缺失fail-closed"""
code = 'PBL_TENANT_REQUIRED'
class PblNotFound(PblError):
"""记录不存在。"""
code = 'PBL_NOT_FOUND'
class PblValidationError(PblError):
"""入参校验失败。"""
code = 'PBL_VALIDATION_ERROR'
def new_id():
"""32 位无横线 UUID 主键。"""
return uuid.uuid4().hex
def get_env(env=None):
if env is not None:
return env
def _server_env():
"""取平台 ServerEnv延迟导入避免模块单独 import 时依赖平台)。"""
try:
from sage import ServerEnv
from sage import ServerEnv # type: ignore
return ServerEnv()
except Exception:
try:
from ahserver import ServerEnv # type: ignore
return ServerEnv()
except Exception:
return None
def get_dbname(env=None):
"""取本模块库名,禁止硬编码。"""
e = get_env(env)
if e is None:
return None
getter = getattr(e, 'get_module_dbname', None)
if callable(getter):
def get_dbname():
"""取本模块库名(由应用 init() 注入的 get_module_dbname 决定)。"""
env = _server_env()
if env is None:
return ""
fn = getattr(env, "get_module_dbname", None)
if not callable(fn):
return ""
try:
return getter(MODULE_NAME)
return fn(MODULE_NAME) or ""
except Exception:
return None
return None
return ""
def get_sor(env=None, dbname=None):
"""取 sqlor 数据访问对象。"""
e = get_env(env)
if e is None:
def get_sor(dbname=None):
"""取 sqlor 句柄(按库名缓存)。
:param dbname: 可选缺省用 get_dbname()
"""
db = dbname or get_dbname()
if not db:
raise RuntimeError("pbl_blueprint: 未取到库名,请确认应用 init() 已注入 get_module_dbname")
key = str(db)
if key in _sor_cache:
return _sor_cache[key]
from sqlor import sqlor # type: ignore
sor = sqlor(db)
_sor_cache[key] = sor
return sor
def clear_cache():
_sor_cache.clear()
# ---------- sqlor 白名单封装(仅 C/U/D/R/I/sqlExe ----------
def q_insert(sor, tbl, data):
"""新增一行 -> sor.C"""
return sor.C(tbl, data)
def q_update(sor, tbl, data, where, args=None):
"""按条件更新 -> sor.U"""
return sor.U(tbl, data, where, args or [])
def q_delete(sor, tbl, where, args=None):
"""按条件删除 -> sor.D"""
return sor.D(tbl, where, args or [])
def q_select(sor, tbl, where="", args=None, fields="*", order="", limit=0, offset=0):
"""条件查询 -> sor.R"""
return sor.R(tbl, where, args or [], fields, order, limit, offset)
def q_one(sor, tbl, where, args=None, fields="*"):
"""取单行(无则 None-> sor.R"""
rows = sor.R(tbl, where, args or [], fields, "", 1, 0)
if not rows:
return None
db = dbname or get_dbname(e)
for attr in ('sqlor', 'sor'):
obj = getattr(e, attr, None)
if obj is not None:
return rows[0]
def q_count(sor, tbl, where="", args=None):
"""计数 -> sor.I"""
return sor.I(tbl, where, args or [])
def q_sql(sor, sql, args=None):
"""原生 SQL只读/DDL 场景)-> sor.sqlExe"""
return sor.sqlExe(sql, args or [])
def dumps(obj):
"""JSON 序列化None 安全,中文不转义)。"""
if obj is None:
return ""
if isinstance(obj, str):
return obj
getter = getattr(e, 'get_sqlor', None)
if callable(getter):
return _json.dumps(obj, ensure_ascii=False, default=str)
def loads(txt, default=None):
"""JSON 反序列化(失败返回 default不抛"""
if txt is None or txt == "":
return default
if isinstance(txt, (dict, list)):
return txt
try:
return getter(db) if db else getter()
return _json.loads(txt)
except Exception:
return None
return None
def require_tenant(tenant_id):
"""tenant_id 强制打头校验:空/非字符串一律拒绝。"""
if tenant_id is None:
raise PblTenantRequired('tenant_id is required (租户上下文缺失,拒绝访问)')
if not isinstance(tenant_id, str) or not tenant_id.strip():
raise PblTenantRequired('tenant_id must be a non-empty string')
return tenant_id.strip()
def ensure_tables(env=None, dbname=None):
"""幂等建表:执行 sql/pbl_blueprint.core.sql 与 sql/pbl_blueprint.subobjects.sql。"""
e = get_env(env)
db = dbname or get_dbname(e)
sor = get_sor(e, db)
if sor is None:
return False
sql_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sql')
executed = []
for fname in ('pbl_blueprint.core.sql', 'pbl_blueprint.subobjects.sql'):
path = os.path.join(sql_dir, fname)
if not os.path.isfile(path):
continue
with open(path, 'r', encoding='utf-8') as fp:
content = fp.read()
for stmt in _split_sql(content):
try:
sor.sqlExe(stmt)
executed.append(stmt[:40])
except Exception:
continue
return executed
def _split_sql(content):
"""按分号切分 DDL剔除注释行与空语句。"""
lines = []
for line in content.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith('--'):
continue
lines.append(line)
stmts = []
for chunk in '\n'.join(lines).split(';'):
chunk = chunk.strip()
if chunk:
stmts.append(chunk)
return stmts
return default

View File

@ -1,162 +1,317 @@
"""pbl_blueprint 模块装配入口。
# -*- coding: utf-8 -*-
"""pbl_blueprint 模块挂载M1a建表 + 契约函数注册 + RBAC 路径注册。
三处同步注册module-development-spec
1) 函数定义本文件 load_pbl_blueprint()
2) 包导出 __init__.py -> from .init import load_pbl_blueprint
3) env 注册ServerEnv().register_module('pbl_blueprint', load_pbl_blueprint)
三处同步注册缺一即 selfcheck 失败
1) service.py / crud.py 中定义函数
2) __init__.py CONTRACT_FUNCTIONS 导出
3) 本文件 REGISTER_FUNCTIONS 注册到 ServerEnv
RBAC 路径显式枚举 %/* 通配符逐条列出便于权限审计
库名一律来自 env.get_module_dbname('pbl_blueprint')本文件不出现任何库名字面量
"""
import os
from .db import ensure_tables, get_dbname
from .blueprint_crud import (
create_blueprint,
update_blueprint,
delete_blueprint,
get_blueprint,
list_blueprints,
get_blueprint_tree,
fork_blueprint,
save_version,
list_versions,
diff_versions,
rollback_version,
)
from .subobjects import (
SUBOBJECT_TYPES,
create_subobject,
update_subobject,
delete_subobject,
get_subobject,
list_subobjects,
batch_upsert_subobjects,
)
from .templates import (
create_template,
update_template,
delete_template,
list_templates,
get_template,
instantiate_template,
build_offline_fallback_blueprint,
from . import db as _db
from .crud import list_tables, load_model, make_crud
from .errors import PblBlueprintError
MODULE_NAME = _db.MODULE_NAME # "pbl_blueprint"
# ---- 1) 表注册清单models/*.json 为唯一事实源) ----
REGISTER_TABLES = (
"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",
)
MODULE_NAME = 'pbl_blueprint'
# ---- 2) CRUD 契约注册清单(表名 -> 注册前缀) ----
REGISTER_CRUD = REGISTER_TABLES
# 本模块全部表(核心 4 + 子对象 7
TABLES = [
'pbl_blueprint',
'pbl_blueprint_version',
'pbl_blueprint_template',
'pbl_blueprint_template_item',
'pbl_blueprint_task',
'pbl_blueprint_mission',
'pbl_blueprint_role',
'pbl_blueprint_learning_goal',
'pbl_blueprint_evidence_spec',
'pbl_blueprint_artifact_spec',
'pbl_blueprint_reflection_spec',
]
# ---- 3) 业务契约函数注册清单(与 __init__.CONTRACT_FUNCTIONS 一致) ----
REGISTER_FUNCTIONS = (
"create_blueprint", "get_blueprint", "update_blueprint", "delete_blueprint",
"list_blueprints", "get_blueprint_tree", "create_node", "update_node",
"delete_node", "list_nodes", "create_edge", "delete_edge", "list_edges",
"fork_blueprint", "list_forks", "save_version", "list_versions", "get_version",
"get_change_delta", "rollback_version", "create_template", "list_templates",
"instantiate_template", "instantiate_payload", "export_offline", "import_offline",
"list_offline", "publish_blueprint", "revoke_publish", "list_publishes",
"lock_blueprint", "unlock_blueprint", "clean_expired_locks", "blueprint_stats",
"write_audit", "list_audit",
)
# ---- 4) RBAC 路径显式枚举(无通配符) ----
RBAC_PATHS = (
"/api/pbl_blueprint/list.dspy",
"/api/pbl_blueprint/create.dspy",
"/api/pbl_blueprint/get.dspy",
"/api/pbl_blueprint/update.dspy",
"/api/pbl_blueprint/delete.dspy",
"/api/pbl_blueprint/tree.dspy",
"/api/pbl_blueprint/fork.dspy",
"/api/pbl_blueprint/forks.dspy",
"/api/pbl_blueprint/stats.dspy",
"/api/pbl_blueprint_node/list.dspy",
"/api/pbl_blueprint_node/create.dspy",
"/api/pbl_blueprint_node/get.dspy",
"/api/pbl_blueprint_node/update.dspy",
"/api/pbl_blueprint_node/delete.dspy",
"/api/pbl_blueprint_edge/list.dspy",
"/api/pbl_blueprint_edge/create.dspy",
"/api/pbl_blueprint_edge/get.dspy",
"/api/pbl_blueprint_edge/update.dspy",
"/api/pbl_blueprint_edge/delete.dspy",
"/api/pbl_blueprint_version/list.dspy",
"/api/pbl_blueprint_version/get.dspy",
"/api/pbl_blueprint_version/save.dspy",
"/api/pbl_blueprint_version/rollback.dspy",
"/api/pbl_blueprint_version_delta/list.dspy",
"/api/pbl_blueprint_version_delta/get.dspy",
"/api/pbl_blueprint_template/list.dspy",
"/api/pbl_blueprint_template/create.dspy",
"/api/pbl_blueprint_template/get.dspy",
"/api/pbl_blueprint_template/update.dspy",
"/api/pbl_blueprint_template/delete.dspy",
"/api/pbl_blueprint_template/instantiate.dspy",
"/api/pbl_blueprint_publish/list.dspy",
"/api/pbl_blueprint_publish/create.dspy",
"/api/pbl_blueprint_publish/get.dspy",
"/api/pbl_blueprint_publish/revoke.dspy",
"/api/pbl_blueprint_offline/list.dspy",
"/api/pbl_blueprint_offline/export.dspy",
"/api/pbl_blueprint_offline/import.dspy",
"/api/pbl_blueprint_offline/get.dspy",
"/api/pbl_blueprint_fork/list.dspy",
"/api/pbl_blueprint_fork/get.dspy",
"/api/pbl_blueprint_lock/lock.dspy",
"/api/pbl_blueprint_lock/unlock.dspy",
"/api/pbl_blueprint_lock/list.dspy",
"/api/pbl_blueprint_lock/clean_expired.dspy",
"/api/pbl_blueprint_audit/list.dspy",
"/api/pbl_blueprint_audit/get.dspy",
)
# ---- 5) 菜单/页面注册(前端入口,显式枚举) ----
MENUS = (
{"code": "pbl_blueprint", "name": "PBL蓝图", "path": "/pbls/blueprint", "parent": "pbls", "sort": 10},
{"code": "pbl_blueprint_template", "name": "蓝图模板", "path": "/pbls/blueprint/template", "parent": "pbls", "sort": 11},
{"code": "pbl_blueprint_publish", "name": "蓝图发布", "path": "/pbls/blueprint/publish", "parent": "pbls", "sort": 12},
{"code": "pbl_blueprint_offline", "name": "离线包", "path": "/pbls/blueprint/offline", "parent": "pbls", "sort": 13},
{"code": "pbl_blueprint_audit", "name": "蓝图审计", "path": "/pbls/blueprint/audit", "parent": "pbls", "sort": 14},
)
_init_state = {"done": False, "dbname": "", "tables": 0, "functions": 0, "rbac": 0, "menus": 0}
def load_pbl_blueprint(env=None):
"""挂载 pbl_blueprint 模块到应用。
:param env: ServerEnv 实例应用 init() 里逐个 load 时传入为空则自建
:return: 模块契约字典供应用/其他模块按契约调用
"""
if env is None:
try:
from sage import ServerEnv
env = ServerEnv()
except Exception:
env = None
dbname = get_dbname(env)
# 建表幂等IF NOT EXISTS
ensure_tables(env, dbname)
# CRUD 定义注册json/*.json
_register_crud_json(env)
contract = {
'module': MODULE_NAME,
'dbname': dbname,
'tables': list(TABLES),
'subobject_types': list(SUBOBJECT_TYPES),
# 蓝图 CRUD 与查询契约
'create_blueprint': create_blueprint,
'update_blueprint': update_blueprint,
'delete_blueprint': delete_blueprint,
'get_blueprint': get_blueprint,
'list_blueprints': list_blueprints,
'get_blueprint_tree': get_blueprint_tree,
'fork_blueprint': fork_blueprint,
# 版本契约
'save_version': save_version,
'list_versions': list_versions,
'diff_versions': diff_versions,
'rollback_version': rollback_version,
# 7 类子对象泛化契约
'create_subobject': create_subobject,
'update_subobject': update_subobject,
'delete_subobject': delete_subobject,
'get_subobject': get_subobject,
'list_subobjects': list_subobjects,
'batch_upsert_subobjects': batch_upsert_subobjects,
# 模板契约
'create_template': create_template,
'update_template': update_template,
'delete_template': delete_template,
'get_template': get_template,
'list_templates': list_templates,
'instantiate_template': instantiate_template,
'build_offline_fallback_blueprint': build_offline_fallback_blueprint,
}
def _get_env(env=None):
if env is not None:
for fn in ('register_module', 'set_module_contract', 'register_contract'):
reg = getattr(env, fn, None)
if callable(reg):
return env
return _db._server_env()
def _register_tables(env, sor, create_ddl=True):
"""注册表定义models/*.json必要时建表。"""
n = 0
on_disk = set(list_tables())
for tbl in REGISTER_TABLES:
model = load_model(tbl) # 表定义必须真实存在缺失即抛错fail-closed
if tbl not in on_disk:
raise PblBlueprintError("PBL_BP_E_INTERNAL", "models/%s.json 缺失" % tbl)
if env is not None and hasattr(env, "register_table"):
try:
reg(MODULE_NAME, contract)
except TypeError:
try:
reg(contract)
env.register_table(MODULE_NAME, tbl, model)
except Exception:
pass
if create_ddl and sor is not None:
try:
_ensure_table(sor, tbl, model)
except Exception:
pass # 建表失败不阻断挂载(部署阶段由 DDL 脚本保证)
n += 1
return n
def _ensure_table(sor, tbl, model):
"""按 models 定义建表(不存在才建),只用 sqlor.sqlExe。"""
cols = []
for name, spec in (model.get("fields") or {}).items():
cols.append(" %s %s" % (name, _col_type(spec)))
pk = model.get("primary") or ["id"]
cols.append(" primary key (%s)" % ",".join(pk))
for idx_name, idx in (model.get("indexes") or {}).items():
uq = "unique " if idx.get("unique") else ""
cols.append(" %skey %s (%s)" % (uq, idx_name, ",".join(idx.get("fields") or [])))
ddl = "create table if not exists %s (\n%s\n)" % (tbl, ",\n".join(cols))
_db.q_sql(sor, ddl, [])
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 _register_crud(env, prefix=""):
"""注册 11 表的通用 CRUD 契约函数({表名}_{动作})。"""
n = 0
if env is None or not hasattr(env, "register_function"):
return 0
for tbl in REGISTER_CRUD:
api = make_crud(tbl)
for act, fn in api.items():
name = "%s%s_%s" % (prefix, tbl, act)
try:
env.register_function(MODULE_NAME, name, fn)
n += 1
except Exception:
pass
break
return contract
return n
def _register_crud_json(env):
"""把 json/*.json 的 CRUD 定义注册到 env若平台提供注册钩子"""
json_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'json')
if not os.path.isdir(json_dir):
return
defs = {}
for fname in sorted(os.listdir(json_dir)):
if not fname.endswith('.json'):
continue
path = os.path.join(json_dir, fname)
def _register_functions(env):
"""注册业务契约函数(三处同步的第 3 处)。"""
from . import get_contract_functions
fns = get_contract_functions()
n = 0
for name in REGISTER_FUNCTIONS:
fn = fns.get(name)
if fn is None:
raise PblBlueprintError("PBL_BP_E_INTERNAL",
"契约函数 %s 未在 __init__ 导出(三处同步失败)" % name)
if env is not None and hasattr(env, "register_function"):
try:
import json as _json
with open(path, 'r', encoding='utf-8') as fp:
defs[fname[:-5]] = _json.load(fp)
env.register_function(MODULE_NAME, name, fn)
except Exception:
continue
pass
n += 1
return n
def _register_rbac(env):
"""注册 RBAC 路径(显式枚举,无通配符)。"""
n = 0
if env is None:
return defs
for fn in ('register_crud', 'load_crud_json', 'set_crud_defs'):
reg = getattr(env, fn, None)
if callable(reg):
return 0
for p in RBAC_PATHS:
if "%" in p or "*" in p:
raise PblBlueprintError("PBL_BP_E_INTERNAL", "RBAC 路径含通配符: %s" % p)
fn = getattr(env, "register_rbac_path", None) or getattr(env, "add_rbac_path", None)
if callable(fn):
try:
reg(defs)
fn(MODULE_NAME, p)
except Exception:
pass
break
return defs
n += 1
return n
def _register_menus(env):
n = 0
if env is None:
return 0
fn = getattr(env, "register_menu", None) or getattr(env, "add_menu", None)
for m in MENUS:
if callable(fn):
try:
fn(MODULE_NAME, m)
except Exception:
pass
n += 1
return n
def init(env=None, dbname=None, register_tables=True, register_functions=True,
register_rbac=True, create_ddl=True):
"""模块挂载入口(幂等)。由 apps/pbls 的 init() 逐个 load_{模块}() 时调用。"""
if _init_state["done"] and not dbname:
return dict(_init_state)
e = _get_env(env)
db = dbname or (e.get_module_dbname(MODULE_NAME) if (e is not None and hasattr(e, "get_module_dbname")) else "")
if not db:
db = _db.get_dbname()
if not db:
raise PblBlueprintError("PBL_BP_E_INTERNAL",
"未取到 %s 库名:应用 init() 必须定义 get_module_dbname 并挂到 ServerEnv" % MODULE_NAME)
sor = _db.get_sor(db)
st = {"done": True, "dbname": db, "tables": 0, "crud": 0, "functions": 0, "rbac": 0, "menus": 0}
if register_tables:
st["tables"] = _register_tables(e, sor, create_ddl)
st["crud"] = _register_crud(e)
if register_functions:
st["functions"] = _register_functions(e)
if register_rbac:
st["rbac"] = _register_rbac(e)
st["menus"] = _register_menus(e)
_init_state.update(st)
return dict(st)
def get_state():
return dict(_init_state)
def reset():
"""测试用:重置挂载状态与 sqlor 缓存。"""
_init_state.update({"done": False, "dbname": "", "tables": 0, "functions": 0,
"rbac": 0, "menus": 0, "crud": 0})
_db.clear_cache()
def ddl_script(out_path=None):
"""生成全量建表 DDL 文本(供部署阶段执行 / 自查比对)。"""
lines = ["-- pbl_blueprint M1a DDL (%d tables)" % len(REGISTER_TABLES), ""]
for tbl in REGISTER_TABLES:
model = load_model(tbl)
cols = []
for name, spec in (model.get("fields") or {}).items():
nn = " not null" if spec.get("notnull") else ""
df = ""
if "default" in spec:
d = spec["default"]
df = " default '%s'" % d if isinstance(d, str) else " default %s" % d
cm = " comment '%s'" % (spec.get("summary") or "").replace("'", "")
cols.append(" `%s` %s%s%s%s" % (name, _col_type(spec), nn, df, cm))
pk = model.get("primary") or ["id"]
cols.append(" primary key (%s)" % ",".join(["`%s`" % c for c in pk]))
for idx_name, idx in (model.get("indexes") or {}).items():
uq = "unique " if idx.get("unique") else ""
cols.append(" %skey `%s` (%s)" % (uq, idx_name,
",".join(["`%s`" % c for c in (idx.get("fields") or [])])))
lines.append("-- %s" % (model.get("summary") or tbl))
lines.append("create table if not exists `%s` (" % tbl)
lines.append(",\n".join(cols))
lines.append(") engine=InnoDB default charset=utf8mb4 comment='%s';" % tbl)
lines.append("")
txt = "\n".join(lines)
if out_path:
d = os.path.dirname(os.path.abspath(out_path))
if d and not os.path.isdir(d):
os.makedirs(d)
with open(out_path, "w", encoding="utf-8") as f:
f.write(txt)
return txt

View File

@ -1,58 +1,214 @@
{
"tblname": "pbl_blueprint",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"code": {
"type": "str",
"size": 64,
"notnull": true,
"editable": true,
"query": true,
"summary": "蓝图编码租户内唯一PBL-BP-序号)"
},
"name": {
"type": "str",
"size": 128,
"notnull": true,
"editable": true,
"query": true,
"summary": "蓝图名称"
},
"subject": {
"type": "str",
"size": 64,
"default": "",
"editable": true,
"query": true,
"summary": "学科"
},
"grade": {
"type": "str",
"size": 32,
"default": "",
"editable": true,
"query": true,
"summary": "年级"
},
"phase": {
"type": "str",
"size": 32,
"default": "",
"editable": true,
"query": true,
"summary": "学段"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "draft",
"editable": true,
"query": true,
"code": "status",
"summary": "蓝图状态"
},
"quality_status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "q0",
"editable": true,
"query": true,
"code": "quality_status",
"summary": "质量状态5 级)"
},
"source": {
"type": "str",
"size": 16,
"notnull": true,
"default": "manual",
"editable": true,
"query": false,
"code": "source",
"summary": "来源方式"
},
"source_id": {
"type": "str",
"size": 32,
"default": "",
"editable": true,
"query": true,
"summary": "来源对象ID模板ID或父蓝图ID"
},
"owner_id": {
"type": "str",
"size": 32,
"default": "",
"editable": true,
"query": true,
"summary": "负责人教师ID"
},
"class_id": {
"type": "str",
"size": 32,
"default": "",
"editable": true,
"query": true,
"summary": "关联班级ID"
},
"current_version": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "当前版本号save_version 回写)"
},
"duration_hours": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "预计课时数"
},
"budget_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "预算金额double(18,2)"
},
"summary": {
"type": "str",
"size": 512,
"default": "",
"editable": true,
"query": false,
"summary": "蓝图简介"
},
"ext_json": {
"type": "text",
"default": "",
"editable": true,
"query": false,
"summary": "扩展属性JSON"
},
"publish_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "最近发布时间"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"update_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "更新人"
},
"update_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"code",
"name",
"subject",
"grade",
"duration_hours",
"status",
"summary",
"config",
"source_blueprint_id",
"template_id",
"owner_id",
"remark"
"api/pbl_blueprint_list.dspy",
"api/pbl_blueprint_edit.dspy",
"api/pbl_blueprint_view.dspy"
],
"browserfields": [
"code",
"name",
"subject",
"grade",
"duration_hours",
"version_no",
"status",
"quality_level",
"quality_status",
"owner_id",
"template_id",
"published_at",
"updated_at"
],
"searchfields": [
"tenant_id",
"code",
"name",
"subject",
"status",
"owner_id"
],
"orderby": "updated_at desc",
"defaultfilter": {
"deleted": 0
},
"caption": "PBL蓝图",
"readonly": [
"id",
"tenant_id",
"version_no",
"quality_level",
"published_at",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
"update_time"
]
}
}

View File

@ -1,57 +0,0 @@
{
"tblname": "pbl_blueprint_artifact_spec",
"params": {
"editable": [
"blueprint_id",
"task_id",
"mission_id",
"seq",
"code",
"name",
"artifact_type",
"description",
"spec",
"accept_criteria",
"required",
"weight",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"mission_id",
"seq",
"code",
"name",
"artifact_type",
"required",
"weight",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"mission_id",
"artifact_type",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"caption": "蓝图子对象-产出物规格",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}

View File

@ -0,0 +1,153 @@
{
"tblname": "pbl_blueprint_audit",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "蓝图ID"
},
"target_type": {
"type": "str",
"size": 16,
"notnull": true,
"editable": true,
"query": true,
"code": "target_type",
"summary": "操作对象类型"
},
"target_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "操作对象ID"
},
"action": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"code": "action",
"summary": "操作动作"
},
"version_no": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": true,
"summary": "关联版本号"
},
"before_json": {
"type": "text",
"default": "",
"editable": false,
"query": false,
"summary": "操作前数据JSON"
},
"after_json": {
"type": "text",
"default": "",
"editable": false,
"query": false,
"summary": "操作后数据JSON"
},
"result": {
"type": "str",
"size": 16,
"notnull": true,
"default": "success",
"editable": true,
"query": true,
"code": "result",
"summary": "操作结果"
},
"err_code": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": false,
"query": false,
"summary": "失败错误码"
},
"cost_ms": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "耗时毫秒"
},
"op_user": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "操作人"
},
"op_source": {
"type": "str",
"size": 32,
"notnull": true,
"default": "api",
"editable": true,
"query": true,
"summary": "操作来源web/api/agent/offline"
},
"client_ip": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"editable": true,
"query": false,
"summary": "客户端IP"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": true,
"summary": "操作时间"
}
},
"editable": [
"api/pbl_blueprint_audit_list.dspy",
"api/pbl_blueprint_audit_edit.dspy",
"api/pbl_blueprint_audit_view.dspy"
],
"browserfields": [
"blueprint_id",
"target_type",
"action",
"result",
"op_user",
"op_source",
"create_time"
]
}

View File

@ -0,0 +1,122 @@
{
"tblname": "pbl_blueprint_edge",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "所属蓝图ID"
},
"from_node_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "起点节点ID"
},
"to_node_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "终点节点ID"
},
"edge_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "depends",
"editable": true,
"query": true,
"code": "edge_type",
"summary": "边类型"
},
"weight": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "边权重double(18,2)"
},
"condition_json": {
"type": "text",
"default": "",
"editable": true,
"query": false,
"summary": "触发条件JSON"
},
"sort_no": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "排序号"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"update_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"api/pbl_blueprint_edge_list.dspy",
"api/pbl_blueprint_edge_edit.dspy",
"api/pbl_blueprint_edge_view.dspy"
],
"browserfields": [
"from_node_id",
"to_node_id",
"edge_type",
"weight",
"sort_no"
]
}

View File

@ -1,60 +0,0 @@
{
"tblname": "pbl_blueprint_evidence_spec",
"params": {
"editable": [
"blueprint_id",
"task_id",
"mission_id",
"learning_goal_id",
"seq",
"code",
"name",
"evidence_type",
"collect_mode",
"description",
"spec",
"rule",
"weight",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"mission_id",
"learning_goal_id",
"seq",
"code",
"name",
"evidence_type",
"collect_mode",
"weight",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"mission_id",
"learning_goal_id",
"evidence_type",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"caption": "蓝图子对象-证据规格",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}

View File

@ -0,0 +1,137 @@
{
"tblname": "pbl_blueprint_fork",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"source_blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "源蓝图ID模板实例化时为空串"
},
"source_version": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "fork 时的源版本号"
},
"target_blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "派生出的新蓝图ID"
},
"fork_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "copy",
"editable": true,
"query": true,
"code": "fork_type",
"summary": "fork 方式"
},
"template_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "若由模板实例化记录模板ID"
},
"node_count": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "复制的节点数"
},
"edge_count": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "复制的边数"
},
"cost_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "本次 fork 产生的费用金额double(18,2),付费模板)"
},
"remark": {
"type": "str",
"size": 512,
"notnull": true,
"default": "",
"editable": true,
"query": false,
"summary": "备注"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"api/pbl_blueprint_fork_list.dspy",
"api/pbl_blueprint_fork_edit.dspy",
"api/pbl_blueprint_fork_view.dspy"
],
"browserfields": [
"source_blueprint_id",
"source_version",
"target_blueprint_id",
"fork_type",
"node_count",
"create_time"
]
}

View File

@ -1,51 +0,0 @@
{
"tblname": "pbl_blueprint_learning_goal",
"params": {
"editable": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"dimension",
"description",
"spec",
"weight",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"dimension",
"weight",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"dimension",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"caption": "蓝图子对象-学习目标",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}

View File

@ -0,0 +1,122 @@
{
"tblname": "pbl_blueprint_lock",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "被锁蓝图ID"
},
"node_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "被锁节点ID空串=整蓝图锁)"
},
"lock_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "edit",
"editable": true,
"query": true,
"code": "lock_type",
"summary": "锁类型"
},
"holder_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "持锁人ID"
},
"holder_name": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"editable": true,
"query": false,
"summary": "持锁人名称"
},
"rev": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "乐观锁修订号(每次抢占/续锁 +1"
},
"expire_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "锁过期时间"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "holding",
"editable": true,
"query": true,
"code": "status",
"summary": "锁状态"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "加锁时间"
},
"update_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "续锁时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"api/pbl_blueprint_lock_list.dspy",
"api/pbl_blueprint_lock_edit.dspy",
"api/pbl_blueprint_lock_view.dspy"
],
"browserfields": [
"blueprint_id",
"node_id",
"lock_type",
"holder_name",
"status",
"expire_time"
]
}

View File

@ -1,47 +0,0 @@
{
"tblname": "pbl_blueprint_mission",
"params": {
"editable": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"description",
"spec",
"unlock_condition",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"caption": "蓝图子对象-关卡",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}

View File

@ -0,0 +1,176 @@
{
"tblname": "pbl_blueprint_node",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "所属蓝图ID"
},
"node_type": {
"type": "str",
"size": 16,
"notnull": true,
"editable": true,
"query": true,
"code": "node_type",
"summary": "子对象类型7 类泛化)"
},
"parent_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "父节点ID构成蓝图树空串=根节点)"
},
"code": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "节点编码(蓝图内同类型唯一)"
},
"name": {
"type": "str",
"size": 128,
"notnull": true,
"editable": true,
"query": true,
"summary": "节点名称"
},
"sort_no": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "同级排序号"
},
"spec_json": {
"type": "text",
"default": "",
"editable": true,
"query": false,
"summary": "泛化载荷JSON结构随 node_type 不同)"
},
"ref_module": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "引用模块名scense/world/assessment 等)"
},
"ref_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "引用对象ID"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "active",
"editable": true,
"query": true,
"code": "status",
"summary": "节点状态"
},
"required": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "是否必需节点 0否 1是"
},
"weight": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "权重/分值double(18,2)"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"update_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "更新人"
},
"update_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"api/pbl_blueprint_node_list.dspy",
"api/pbl_blueprint_node_edit.dspy",
"api/pbl_blueprint_node_view.dspy"
],
"browserfields": [
"code",
"name",
"node_type",
"parent_id",
"sort_no",
"status"
]
}

View File

@ -0,0 +1,144 @@
{
"tblname": "pbl_blueprint_offline",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "来源蓝图ID与 template_id 二选一)"
},
"template_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "来源模板ID"
},
"pkg_code": {
"type": "str",
"size": 64,
"notnull": true,
"editable": true,
"query": true,
"summary": "离线包编码"
},
"pkg_name": {
"type": "str",
"size": 128,
"notnull": true,
"editable": true,
"query": true,
"summary": "离线包名称"
},
"pkg_version": {
"type": "int",
"notnull": true,
"default": 1,
"editable": true,
"query": true,
"summary": "离线包版本"
},
"pkg_json": {
"type": "text",
"default": "",
"editable": false,
"query": false,
"summary": "离线包内容JSON自描述schema_version+蓝图+节点+边)"
},
"file_size": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "包体字节数"
},
"checksum": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"editable": false,
"query": false,
"summary": "内容校验和sha256"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "ready",
"editable": true,
"query": true,
"code": "status",
"summary": "离线包状态"
},
"expire_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "过期时间"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"update_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"api/pbl_blueprint_offline_list.dspy",
"api/pbl_blueprint_offline_edit.dspy",
"api/pbl_blueprint_offline_view.dspy"
],
"browserfields": [
"pkg_code",
"pkg_name",
"pkg_version",
"status",
"file_size",
"create_time"
]
}

View File

@ -0,0 +1,166 @@
{
"tblname": "pbl_blueprint_publish",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "蓝图ID"
},
"version_no": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": true,
"summary": "发布的版本号"
},
"class_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "投放班级ID"
},
"target_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "class",
"editable": true,
"query": true,
"code": "target_type",
"summary": "投放对象类型"
},
"target_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "投放对象ID"
},
"runtime_ref": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "运行时引用scense_runtime 会话/世界ID"
},
"game_def_json": {
"type": "text",
"default": "",
"editable": false,
"query": false,
"summary": "编译产物 Game Definition JSONM3 回写)"
},
"start_time": {
"type": "datetime",
"editable": true,
"query": false,
"summary": "开始时间"
},
"end_time": {
"type": "datetime",
"editable": true,
"query": false,
"summary": "结束时间"
},
"budget_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "本次投放预算金额double(18,2)"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "published",
"editable": true,
"query": true,
"code": "status",
"summary": "投放状态"
},
"remark": {
"type": "str",
"size": 512,
"notnull": true,
"default": "",
"editable": true,
"query": false,
"summary": "备注"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"update_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"api/pbl_blueprint_publish_list.dspy",
"api/pbl_blueprint_publish_edit.dspy",
"api/pbl_blueprint_publish_view.dspy"
],
"browserfields": [
"blueprint_id",
"version_no",
"target_type",
"target_id",
"status",
"start_time",
"end_time"
]
}

View File

@ -1,58 +0,0 @@
{
"tblname": "pbl_blueprint_reflection_spec",
"params": {
"editable": [
"blueprint_id",
"task_id",
"mission_id",
"seq",
"code",
"name",
"trigger_point",
"reflection_type",
"description",
"spec",
"rubric_ref",
"weight",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"mission_id",
"seq",
"code",
"name",
"trigger_point",
"reflection_type",
"weight",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"mission_id",
"trigger_point",
"reflection_type",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"caption": "蓝图子对象-反思规格",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}

View File

@ -1,47 +0,0 @@
{
"tblname": "pbl_blueprint_role",
"params": {
"editable": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"description",
"spec",
"permissions",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"caption": "蓝图子对象-角色",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}

View File

@ -1,46 +0,0 @@
{
"tblname": "pbl_blueprint_task",
"params": {
"editable": [
"blueprint_id",
"parent_id",
"seq",
"code",
"name",
"description",
"spec",
"status"
],
"browserfields": [
"blueprint_id",
"seq",
"code",
"name",
"parent_id",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"parent_id",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"caption": "蓝图子对象-任务",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}

View File

@ -1,49 +1,187 @@
{
"tblname": "pbl_blueprint_template",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝);空串=平台公共模板"
},
"code": {
"type": "str",
"size": 64,
"notnull": true,
"editable": true,
"query": true,
"summary": "模板编码(租户内唯一)"
},
"name": {
"type": "str",
"size": 128,
"notnull": true,
"editable": true,
"query": true,
"summary": "模板名称"
},
"category": {
"type": "str",
"size": 32,
"notnull": true,
"default": "general",
"editable": true,
"query": true,
"code": "category",
"summary": "模板分类"
},
"subject": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "适用学科"
},
"grade": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "适用年级"
},
"scope": {
"type": "str",
"size": 16,
"notnull": true,
"default": "tenant",
"editable": true,
"query": true,
"code": "scope",
"summary": "可见范围"
},
"payload_json": {
"type": "text",
"default": "",
"editable": true,
"query": false,
"summary": "模板树载荷JSONblueprint+nodes+edges"
},
"node_count": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "模板节点数"
},
"edge_count": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "模板边数"
},
"price_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "模板定价金额double(18,2)0=免费)"
},
"use_count": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "被实例化次数"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "enabled",
"editable": true,
"query": true,
"code": "status",
"summary": "模板状态"
},
"summary": {
"type": "str",
"size": 512,
"notnull": true,
"default": "",
"editable": true,
"query": false,
"summary": "模板说明"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"update_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "更新人"
},
"update_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"code",
"name",
"category",
"subject",
"grade",
"duration_hours",
"description",
"content",
"status"
"api/pbl_blueprint_template_list.dspy",
"api/pbl_blueprint_template_edit.dspy",
"api/pbl_blueprint_template_view.dspy"
],
"browserfields": [
"code",
"name",
"category",
"subject",
"grade",
"duration_hours",
"scope",
"status",
"use_count",
"updated_at"
],
"searchfields": [
"tenant_id",
"code",
"name",
"category",
"status"
],
"orderby": "use_count desc, updated_at desc",
"defaultfilter": {
"deleted": 0
},
"caption": "PBL蓝图模板",
"readonly": [
"id",
"tenant_id",
"use_count",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
"use_count"
]
}
}

View File

@ -1,43 +0,0 @@
{
"tblname": "pbl_blueprint_template_item",
"params": {
"editable": [
"template_id",
"obj_type",
"parent_ref",
"ref_key",
"seq",
"name",
"spec"
],
"browserfields": [
"template_id",
"obj_type",
"seq",
"name",
"ref_key",
"parent_ref",
"updated_at"
],
"searchfields": [
"tenant_id",
"template_id",
"obj_type",
"name"
],
"orderby": "obj_type asc, seq asc",
"defaultfilter": {
"deleted": 0
},
"caption": "PBL蓝图模板条目",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}

View File

@ -1,43 +1,136 @@
{
"tblname": "pbl_blueprint_version",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "所属蓝图ID"
},
"version_no": {
"type": "int",
"notnull": true,
"editable": true,
"query": true,
"summary": "版本号(蓝图内递增)"
},
"version_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "draft",
"editable": true,
"query": true,
"code": "version_type",
"summary": "版本类型"
},
"change_summary": {
"type": "str",
"size": 512,
"notnull": true,
"default": "",
"editable": true,
"query": false,
"summary": "变更说明"
},
"snapshot_json": {
"type": "text",
"default": "",
"editable": false,
"query": false,
"summary": "蓝图整树快照JSON主表字段+节点+边)"
},
"node_count": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "快照节点数"
},
"edge_count": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "快照边数"
},
"quality_status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "q0",
"editable": true,
"query": true,
"code": "quality_status",
"summary": "该版本质量状态"
},
"score_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "该版本评估得分double(18,2)M6 回写)"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"blueprint_id",
"version_no",
"snapshot",
"change_delta",
"quality_level",
"status",
"remark"
"api/pbl_blueprint_version_list.dspy",
"api/pbl_blueprint_version_edit.dspy",
"api/pbl_blueprint_version_view.dspy"
],
"browserfields": [
"blueprint_id",
"version_no",
"quality_level",
"status",
"remark",
"created_by",
"created_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"version_no",
"status"
],
"orderby": "version_no desc",
"defaultfilter": {
"deleted": 0
},
"caption": "PBL蓝图版本快照",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
"version_type",
"change_summary",
"node_count",
"edge_count",
"quality_status",
"create_time"
]
}
}

View File

@ -0,0 +1,137 @@
{
"tblname": "pbl_blueprint_version_delta",
"params": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": false,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"editable": true,
"query": true,
"summary": "所属蓝图ID"
},
"from_version": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": true,
"summary": "基准版本号"
},
"to_version": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": true,
"summary": "目标版本号"
},
"delta_type": {
"type": "str",
"size": 16,
"notnull": true,
"editable": true,
"query": true,
"code": "delta_type",
"summary": "差异对象类型"
},
"op_type": {
"type": "str",
"size": 16,
"notnull": true,
"editable": true,
"query": true,
"code": "op_type",
"summary": "操作类型"
},
"target_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"editable": true,
"query": true,
"summary": "差异对象ID节点ID/边ID/蓝图ID"
},
"target_name": {
"type": "str",
"size": 128,
"notnull": true,
"default": "",
"editable": true,
"query": false,
"summary": "差异对象名称(冗余便于展示)"
},
"before_json": {
"type": "text",
"default": "",
"editable": false,
"query": false,
"summary": "变更前JSON"
},
"after_json": {
"type": "text",
"default": "",
"editable": false,
"query": false,
"summary": "变更后JSON"
},
"sort_no": {
"type": "int",
"notnull": true,
"default": 0,
"editable": true,
"query": false,
"summary": "排序号"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"editable": false,
"query": false,
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"editable": false,
"query": false,
"summary": "创建时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"editable": false,
"query": false,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"editable": [
"api/pbl_blueprint_version_delta_list.dspy",
"api/pbl_blueprint_version_delta_edit.dspy",
"api/pbl_blueprint_version_delta_view.dspy"
],
"browserfields": [
"from_version",
"to_version",
"delta_type",
"op_type",
"target_name"
]
}

View File

@ -1,55 +1,230 @@
{
"summary": "PBL 蓝图聚合根主表M1a。一行=一个项目式学习蓝图,承载学科/年级/课时与质量状态所有子对象task/mission/role/learning_goal/evidence_spec/artifact_spec/reflection_spec通过 blueprint_id 挂靠。tenant_id 为强制打头字段,任何读写必须带租户条件,缺失即拒绝。",
"summary": "PBL 蓝图聚合根表:一个租户下的一份项目式学习蓝图(学科/年级/学段/状态/质量状态/当前版本/预算)。所有读写 tenant_id 强制打头。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"code": {
"type": "str",
"size": 64,
"notnull": true,
"summary": "蓝图编码租户内唯一PBL-BP-序号)"
},
"name": {
"type": "str",
"size": 128,
"notnull": true,
"summary": "蓝图名称"
},
"subject": {
"type": "str",
"size": 64,
"default": "",
"summary": "学科"
},
"grade": {
"type": "str",
"size": 32,
"default": "",
"summary": "年级"
},
"phase": {
"type": "str",
"size": 32,
"default": "",
"summary": "学段"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "draft",
"summary": "蓝图状态"
},
"quality_status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "q0",
"summary": "质量状态5 级)"
},
"source": {
"type": "str",
"size": 16,
"notnull": true,
"default": "manual",
"summary": "来源方式"
},
"source_id": {
"type": "str",
"size": 32,
"default": "",
"summary": "来源对象ID模板ID或父蓝图ID"
},
"owner_id": {
"type": "str",
"size": 32,
"default": "",
"summary": "负责人教师ID"
},
"class_id": {
"type": "str",
"size": 32,
"default": "",
"summary": "关联班级ID"
},
"current_version": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "当前版本号save_version 回写)"
},
"duration_hours": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "预计课时数"
},
"budget_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"summary": "预算金额double(18,2)"
},
"summary": {
"type": "str",
"size": 512,
"default": "",
"summary": "蓝图简介"
},
"ext_json": {
"type": "text",
"default": "",
"summary": "扩展属性JSON"
},
"publish_time": {
"type": "datetime",
"summary": "最近发布时间"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"update_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "更新人"
},
"update_time": {
"type": "datetime",
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_blueprint_code": {
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头所有查询第一过滤条件"},
{"name": "code", "type": "str", "len": 64, "notnull": true, "desc": "蓝图编码,租户内唯一,形如 PBL-{yyyyMMdd}-{seq4}"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "蓝图名称"},
{"name": "subject", "type": "str", "len": 64, "desc": "学科"},
{"name": "grade", "type": "str", "len": 32, "desc": "适用年级"},
{"name": "duration_hours", "type": "int", "desc": "总课时数"},
{"name": "version_no", "type": "int", "notnull": true, "default": "1", "desc": "当前版本号,每次发布快照自增"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "蓝图状态,见 codes.status"},
{"name": "quality_level", "type": "str", "len": 16, "default": "L1", "desc": "质量等级 L1-L5由 pbl_validation 回写"},
{"name": "summary", "type": "text", "desc": "蓝图简介/驱动性问题"},
{"name": "config", "type": "json", "desc": "扩展配置(分组策略/评分权重/运行时参数)"},
{"name": "source_blueprint_id", "type": "str", "len": 32, "desc": "fork 来源蓝图ID非fork为空"},
{"name": "template_id", "type": "str", "len": 32, "desc": "实例化来源模板ID非模板创建为空"},
{"name": "owner_id", "type": "str", "len": 32, "desc": "归属教师/创建者用户ID"},
{"name": "published_at", "type": "datetime", "desc": "最近发布时间"},
{"name": "remark", "type": "str", "len": 500, "desc": "备注"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"tenant_id",
"code"
],
"indexes": [
{"name": "uk_pbl_blueprint_tenant_code", "fields": ["tenant_id", "code"], "unique": true},
{"name": "idx_pbl_blueprint_tenant_status", "fields": ["tenant_id", "status", "deleted"], "unique": false},
{"name": "idx_pbl_blueprint_owner", "fields": ["tenant_id", "owner_id"], "unique": false},
{"name": "idx_pbl_blueprint_template", "fields": ["tenant_id", "template_id"], "unique": false}
"unique": true,
"summary": "租户内蓝图编码唯一"
},
"idx_pbl_blueprint_tenant": {
"fields": [
"tenant_id",
"deleted",
"status"
],
"unique": false,
"summary": "租户蓝图列表主索引tenant_id 打头)"
},
"idx_pbl_blueprint_owner": {
"fields": [
"tenant_id",
"owner_id"
],
"unique": false,
"summary": "按负责人查蓝图"
},
"idx_pbl_blueprint_source": {
"fields": [
"tenant_id",
"source",
"source_id"
],
"unique": false,
"summary": "按来源(模板/fork追溯"
},
"idx_pbl_blueprint_class": {
"fields": [
"tenant_id",
"class_id"
],
"unique": false,
"summary": "按班级查蓝图"
}
},
"codes": {
"status": [
["draft", "草稿"],
["validating", "校验中"],
["validated", "校验通过"],
["compiling", "编译中"],
["compiled", "已编译"],
["published", "已发布"],
["archived", "已归档"]
],
"quality_level": [
["L1", "L1-初始"],
["L2", "L2-基本完整"],
["L3", "L3-结构合格"],
["L4", "L4-可运行"],
["L5", "L5-优质"]
],
"deleted": [
["0", "正常"],
["1", "已删除"]
]
"status": {
"summary": "蓝图状态",
"items": {
"draft": "草稿",
"validating": "校验中",
"ready": "就绪",
"published": "已发布",
"archived": "已归档",
"disabled": "已停用"
}
},
"quality_status": {
"summary": "质量状态5 级M2 校验引擎回写)",
"items": {
"q0": "未校验",
"q1": "不合格",
"q2": "基本合格",
"q3": "合格",
"q4": "优秀",
"q5": "标杆"
}
},
"source": {
"summary": "来源方式",
"items": {
"manual": "手工创建",
"template": "模板实例化",
"fork": "蓝图fork派生",
"agent": "Designer Agent 生成",
"import": "离线包导入"
}
}
}
}

View File

@ -1,36 +0,0 @@
{
"summary": "PBL 蓝图子对象-产出物规格M1a7类子对象之一。一行=一种期望产出物的规格定义(作品/报告/模型/演示),供 pbl_evidence 归档产出物与 pbl_compiler 生成任务交付要求。统一泛化契约。tenant_id 强制打头。",
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
{"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID"},
{"name": "mission_id", "type": "str", "len": 32, "desc": "关联关卡ID"},
{"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
{"name": "code", "type": "str", "len": 64, "desc": "产出物规格编码,蓝图内唯一,缺省自动生成 ART-{seq}"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "产出物名称"},
{"name": "artifact_type", "type": "str", "len": 32, "notnull": true, "default": "document", "desc": "产出物类型,见 codes.artifact_type"},
{"name": "description", "type": "text", "desc": "产出物说明与要求"},
{"name": "spec", "type": "json", "desc": "产出物规格(格式/尺寸/时长/字数/文件类型白名单)"},
{"name": "accept_criteria", "type": "json", "desc": "验收标准条目数组"},
{"name": "required", "type": "int", "notnull": true, "default": "1", "desc": "是否必交 0否 1是"},
{"name": "weight", "type": "float", "default": "1.0", "desc": "评分权重"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
],
"indexes": [
{"name": "uk_pbl_bpart_tenant_bp_code", "fields": ["tenant_id", "blueprint_id", "code"], "unique": true},
{"name": "idx_pbl_bpart_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
{"name": "idx_pbl_bpart_task", "fields": ["tenant_id", "task_id"], "unique": false}
],
"codes": {
"artifact_type": [["document", "文档报告"], ["model", "三维模型"], ["scene", "场景作品"], ["video", "视频"], ["audio", "音频"], ["image", "图片"], ["code", "程序脚本"], ["presentation", "演示汇报"], ["dataset", "数据集"]],
"required": [["0", "选交"], ["1", "必交"]],
"status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
}
}

View File

@ -0,0 +1,190 @@
{
"summary": "蓝图操作审计表append-only记录蓝图/节点/边/版本/模板/发布/锁的关键写操作与结果,只增不改不删,供追溯与合规审计。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "蓝图ID"
},
"target_type": {
"type": "str",
"size": 16,
"notnull": true,
"summary": "操作对象类型"
},
"target_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "操作对象ID"
},
"action": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "操作动作"
},
"version_no": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "关联版本号"
},
"before_json": {
"type": "text",
"default": "",
"summary": "操作前数据JSON"
},
"after_json": {
"type": "text",
"default": "",
"summary": "操作后数据JSON"
},
"result": {
"type": "str",
"size": 16,
"notnull": true,
"default": "success",
"summary": "操作结果"
},
"err_code": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "失败错误码"
},
"cost_ms": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "耗时毫秒"
},
"op_user": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "操作人"
},
"op_source": {
"type": "str",
"size": 32,
"notnull": true,
"default": "api",
"summary": "操作来源web/api/agent/offline"
},
"client_ip": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"summary": "客户端IP"
},
"create_time": {
"type": "datetime",
"summary": "操作时间"
}
},
"indexes": {
"idx_pbl_audit_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"create_time"
],
"unique": false,
"summary": "按蓝图时间线查审计"
},
"idx_pbl_audit_target": {
"fields": [
"tenant_id",
"target_type",
"target_id"
],
"unique": false,
"summary": "按对象查审计"
},
"idx_pbl_audit_user": {
"fields": [
"tenant_id",
"op_user",
"create_time"
],
"unique": false,
"summary": "按操作人查审计"
},
"idx_pbl_audit_action": {
"fields": [
"tenant_id",
"action",
"result"
],
"unique": false,
"summary": "按动作与结果统计"
}
},
"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": "被拒绝"
}
}
}
}

View File

@ -0,0 +1,137 @@
{
"summary": "蓝图节点关系边表:描述子对象之间的依赖/解锁/产出/消耗/评估/包含关系,供 CompilerM3编译为 Game Definition 图结构。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "所属蓝图ID"
},
"from_node_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "起点节点ID"
},
"to_node_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "终点节点ID"
},
"edge_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "depends",
"summary": "边类型"
},
"weight": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"summary": "边权重double(18,2)"
},
"condition_json": {
"type": "text",
"default": "",
"summary": "触发条件JSON"
},
"sort_no": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "排序号"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"update_time": {
"type": "datetime",
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_edge": {
"fields": [
"tenant_id",
"blueprint_id",
"from_node_id",
"to_node_id",
"edge_type"
],
"unique": true,
"summary": "同类型边唯一,防重复连边"
},
"idx_pbl_edge_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"deleted"
],
"unique": false,
"summary": "按蓝图取全部边"
},
"idx_pbl_edge_from": {
"fields": [
"tenant_id",
"from_node_id"
],
"unique": false,
"summary": "出边查询"
},
"idx_pbl_edge_to": {
"fields": [
"tenant_id",
"to_node_id"
],
"unique": false,
"summary": "入边查询(删节点前校验)"
}
},
"codes": {
"edge_type": {
"summary": "边类型",
"items": {
"depends": "依赖",
"unlocks": "解锁",
"produces": "产出",
"consumes": "消耗",
"assesses": "评估",
"contains": "包含"
}
}
}
}

View File

@ -1,38 +0,0 @@
{
"summary": "PBL 蓝图子对象-证据规格M1a7类子对象之一。一行=一条学习证据的采集规格,定义证据类型/采集方式/校验规则,供 pbl_evidence 幂等采集与 pbl_assessment 评分引用。统一泛化契约。tenant_id 强制打头。",
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
{"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID"},
{"name": "mission_id", "type": "str", "len": 32, "desc": "关联关卡ID"},
{"name": "learning_goal_id", "type": "str", "len": 32, "desc": "关联学习目标ID"},
{"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
{"name": "code", "type": "str", "len": 64, "desc": "证据规格编码,蓝图内唯一,缺省自动生成 EVI-{seq}"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "证据规格名称"},
{"name": "evidence_type", "type": "str", "len": 32, "notnull": true, "default": "auto", "desc": "证据类型,见 codes.evidence_type"},
{"name": "collect_mode", "type": "str", "len": 32, "notnull": true, "default": "auto", "desc": "采集方式,见 codes.collect_mode"},
{"name": "description", "type": "text", "desc": "证据说明"},
{"name": "spec", "type": "json", "desc": "证据规格字段schema/取值范围/单位/幂等键规则)"},
{"name": "rule", "type": "json", "desc": "校验规则(阈值/表达式/必填项)"},
{"name": "weight", "type": "float", "default": "1.0", "desc": "评分权重"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
],
"indexes": [
{"name": "uk_pbl_bpev_tenant_bp_code", "fields": ["tenant_id", "blueprint_id", "code"], "unique": true},
{"name": "idx_pbl_bpev_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
{"name": "idx_pbl_bpev_goal", "fields": ["tenant_id", "learning_goal_id"], "unique": false},
{"name": "idx_pbl_bpev_mission", "fields": ["tenant_id", "mission_id"], "unique": false}
],
"codes": {
"evidence_type": [["auto", "系统自动采集"], ["manual", "人工提交"], ["artifact", "产出物派生"], ["interaction", "交互行为"], ["assessment", "测评结果"]],
"collect_mode": [["auto", "自动"], ["trigger", "事件触发"], ["manual", "手动上传"], ["scheduled", "定时采集"]],
"status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
}
}

View File

@ -0,0 +1,145 @@
{
"summary": "蓝图 fork 血缘表:记录蓝图之间的派生关系(源蓝图→新蓝图)与模板实例化来源,支持 fork 溯源与派生树查询。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"source_blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "源蓝图ID模板实例化时为空串"
},
"source_version": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "fork 时的源版本号"
},
"target_blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "派生出的新蓝图ID"
},
"fork_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "copy",
"summary": "fork 方式"
},
"template_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "若由模板实例化记录模板ID"
},
"node_count": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "复制的节点数"
},
"edge_count": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "复制的边数"
},
"cost_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"summary": "本次 fork 产生的费用金额double(18,2),付费模板)"
},
"remark": {
"type": "str",
"size": 512,
"notnull": true,
"default": "",
"summary": "备注"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_fork": {
"fields": [
"tenant_id",
"source_blueprint_id",
"target_blueprint_id"
],
"unique": true,
"summary": "同一对蓝图血缘唯一,保证幂等"
},
"idx_pbl_fork_source": {
"fields": [
"tenant_id",
"source_blueprint_id",
"deleted"
],
"unique": false,
"summary": "查某蓝图的派生列表"
},
"idx_pbl_fork_target": {
"fields": [
"tenant_id",
"target_blueprint_id"
],
"unique": false,
"summary": "反查某蓝图的来源"
},
"idx_pbl_fork_template": {
"fields": [
"tenant_id",
"template_id"
],
"unique": false,
"summary": "查模板被实例化记录"
}
},
"codes": {
"fork_type": {
"summary": "fork 方式",
"items": {
"copy": "完整复制",
"structure": "仅结构(不带载荷)",
"template": "模板实例化",
"branch": "分支派生"
}
}
}
}

View File

@ -1,32 +0,0 @@
{
"summary": "PBL 蓝图子对象-学习目标M1a7类子对象之一。一行=蓝图的一条学习目标(知识/能力/素养/态度四维可关联到任务并被评估量规引用。统一泛化契约。tenant_id 强制打头。",
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
{"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID为空表示蓝图级目标"},
{"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
{"name": "code", "type": "str", "len": 64, "desc": "目标编码,蓝图内唯一,缺省自动生成 GOAL-{seq}"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "目标名称"},
{"name": "dimension", "type": "str", "len": 32, "notnull": true, "default": "knowledge", "desc": "目标维度,见 codes.dimension"},
{"name": "description", "type": "text", "desc": "目标描述(可观测行为动词表述)"},
{"name": "spec", "type": "json", "desc": "目标规格(课标对应/掌握层级/评价方式)"},
{"name": "weight", "type": "float", "default": "1.0", "desc": "评估权重,供 pbl_assessment Rubric 使用"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
],
"indexes": [
{"name": "uk_pbl_bplg_tenant_bp_code", "fields": ["tenant_id", "blueprint_id", "code"], "unique": true},
{"name": "idx_pbl_bplg_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
{"name": "idx_pbl_bplg_dim", "fields": ["tenant_id", "blueprint_id", "dimension"], "unique": false}
],
"codes": {
"dimension": [["knowledge", "知识"], ["ability", "能力"], ["literacy", "素养"], ["attitude", "态度"]],
"status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
}
}

View File

@ -0,0 +1,132 @@
{
"summary": "蓝图编辑锁表:多人协作时对蓝图/节点加编辑锁(悲观锁+rev 乐观修订号),锁带过期时间,过期可被抢占。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "被锁蓝图ID"
},
"node_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "被锁节点ID空串=整蓝图锁)"
},
"lock_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "edit",
"summary": "锁类型"
},
"holder_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "持锁人ID"
},
"holder_name": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"summary": "持锁人名称"
},
"rev": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "乐观锁修订号(每次抢占/续锁 +1"
},
"expire_time": {
"type": "datetime",
"summary": "锁过期时间"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "holding",
"summary": "锁状态"
},
"create_time": {
"type": "datetime",
"summary": "加锁时间"
},
"update_time": {
"type": "datetime",
"summary": "续锁时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_lock": {
"fields": [
"tenant_id",
"blueprint_id",
"node_id",
"lock_type"
],
"unique": true,
"summary": "同对象同类型仅一把锁记录"
},
"idx_pbl_lock_holder": {
"fields": [
"tenant_id",
"holder_id",
"status"
],
"unique": false,
"summary": "查某人持有的锁"
},
"idx_pbl_lock_expire": {
"fields": [
"tenant_id",
"status",
"expire_time"
],
"unique": false,
"summary": "过期锁清理扫描"
}
},
"codes": {
"lock_type": {
"summary": "锁类型",
"items": {
"edit": "编辑锁",
"publish": "发布锁",
"compile": "编译锁"
}
},
"status": {
"summary": "锁状态",
"items": {
"holding": "持有中",
"released": "已释放",
"expired": "已过期"
}
}
}
}

View File

@ -1,30 +0,0 @@
{
"summary": "PBL 蓝图子对象-关卡M1a7类子对象之一。一行=任务下的一个游戏化关卡,是 Compiler 生成 Game Definition 的直接来源。统一泛化契约blueprint_id + task_id + seq + code + name + spec + status。tenant_id 强制打头。",
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
{"name": "task_id", "type": "str", "len": 32, "notnull": true, "desc": "所属任务ID"},
{"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "任务内排序号"},
{"name": "code", "type": "str", "len": 64, "desc": "关卡编码,蓝图内唯一,缺省自动生成 MISSION-{seq}"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "关卡名称"},
{"name": "description", "type": "text", "desc": "关卡说明"},
{"name": "spec", "type": "json", "desc": "关卡规格(目标/规则/场景引用/通关条件/奖励)"},
{"name": "unlock_condition", "type": "json", "desc": "解锁条件(前置关卡/分数门槛)"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
],
"indexes": [
{"name": "uk_pbl_bpms_tenant_bp_code", "fields": ["tenant_id", "blueprint_id", "code"], "unique": true},
{"name": "idx_pbl_bpms_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
{"name": "idx_pbl_bpms_task", "fields": ["tenant_id", "task_id", "seq"], "unique": false}
],
"codes": {
"status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
}
}

View File

@ -0,0 +1,190 @@
{
"summary": "蓝图子对象节点表7 类子对象泛化契约role/task/rule/artifact/scene/assessment/resource 统一存一张表,差异载荷放 spec_json按 node_type 分派校验器M2。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "所属蓝图ID"
},
"node_type": {
"type": "str",
"size": 16,
"notnull": true,
"summary": "子对象类型7 类泛化)"
},
"parent_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "父节点ID构成蓝图树空串=根节点)"
},
"code": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"summary": "节点编码(蓝图内同类型唯一)"
},
"name": {
"type": "str",
"size": 128,
"notnull": true,
"summary": "节点名称"
},
"sort_no": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "同级排序号"
},
"spec_json": {
"type": "text",
"default": "",
"summary": "泛化载荷JSON结构随 node_type 不同)"
},
"ref_module": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "引用模块名scense/world/assessment 等)"
},
"ref_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "引用对象ID"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "active",
"summary": "节点状态"
},
"required": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "是否必需节点 0否 1是"
},
"weight": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"summary": "权重/分值double(18,2)"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"update_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "更新人"
},
"update_time": {
"type": "datetime",
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_node_code": {
"fields": [
"tenant_id",
"blueprint_id",
"node_type",
"code"
],
"unique": true,
"summary": "蓝图内同类型节点编码唯一"
},
"idx_pbl_node_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"deleted",
"node_type"
],
"unique": false,
"summary": "按蓝图+类型列节点tenant_id 打头)"
},
"idx_pbl_node_parent": {
"fields": [
"tenant_id",
"blueprint_id",
"parent_id",
"sort_no"
],
"unique": false,
"summary": "构树查询"
},
"idx_pbl_node_ref": {
"fields": [
"tenant_id",
"ref_module",
"ref_id"
],
"unique": false,
"summary": "按引用对象反查节点"
}
},
"codes": {
"node_type": {
"summary": "子对象类型7 类泛化契约)",
"items": {
"role": "角色",
"task": "任务",
"rule": "规则",
"artifact": "产出物",
"scene": "场景引用",
"assessment": "评估项",
"resource": "资源"
}
},
"status": {
"summary": "节点状态",
"items": {
"active": "生效",
"draft": "草稿",
"invalid": "校验不通过",
"disabled": "停用"
}
}
}
}

View File

@ -0,0 +1,141 @@
{
"summary": "蓝图离线兜底包表:把蓝图/模板打包为自描述 JSON 包(含 schema_version 与 sha256 校验和),网络或服务不可用时本地兜底实例化。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "来源蓝图ID与 template_id 二选一)"
},
"template_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "来源模板ID"
},
"pkg_code": {
"type": "str",
"size": 64,
"notnull": true,
"summary": "离线包编码"
},
"pkg_name": {
"type": "str",
"size": 128,
"notnull": true,
"summary": "离线包名称"
},
"pkg_version": {
"type": "int",
"notnull": true,
"default": 1,
"summary": "离线包版本"
},
"pkg_json": {
"type": "text",
"default": "",
"summary": "离线包内容JSON自描述schema_version+蓝图+节点+边)"
},
"file_size": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "包体字节数"
},
"checksum": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"summary": "内容校验和sha256"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "ready",
"summary": "离线包状态"
},
"expire_time": {
"type": "datetime",
"summary": "过期时间"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"update_time": {
"type": "datetime",
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_offline_code": {
"fields": [
"tenant_id",
"pkg_code",
"pkg_version"
],
"unique": true,
"summary": "离线包编码+版本唯一"
},
"idx_pbl_offline_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"deleted"
],
"unique": false,
"summary": "按蓝图查离线包"
},
"idx_pbl_offline_status": {
"fields": [
"tenant_id",
"status",
"expire_time"
],
"unique": false,
"summary": "过期包清理扫描"
}
},
"codes": {
"status": {
"summary": "离线包状态",
"items": {
"ready": "可用",
"building": "构建中",
"expired": "已过期",
"invalid": "校验失败"
}
}
}
}

View File

@ -0,0 +1,168 @@
{
"summary": "蓝图发布/投放记录表:蓝图某版本投放到班级/团队/学生的一次发布,记录运行时句柄与编译产物,供 M11 runtime_ext 关联。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "蓝图ID"
},
"version_no": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "发布的版本号"
},
"class_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "投放班级ID"
},
"target_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "class",
"summary": "投放对象类型"
},
"target_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "投放对象ID"
},
"runtime_ref": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"summary": "运行时引用scense_runtime 会话/世界ID"
},
"game_def_json": {
"type": "text",
"default": "",
"summary": "编译产物 Game Definition JSONM3 回写)"
},
"start_time": {
"type": "datetime",
"summary": "开始时间"
},
"end_time": {
"type": "datetime",
"summary": "结束时间"
},
"budget_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"summary": "本次投放预算金额double(18,2)"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "published",
"summary": "投放状态"
},
"remark": {
"type": "str",
"size": 512,
"notnull": true,
"default": "",
"summary": "备注"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"update_time": {
"type": "datetime",
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_publish": {
"fields": [
"tenant_id",
"blueprint_id",
"version_no",
"target_type",
"target_id"
],
"unique": true,
"summary": "同版本同对象只投放一次,保证幂等"
},
"idx_pbl_publish_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"deleted"
],
"unique": false,
"summary": "按蓝图查投放记录"
},
"idx_pbl_publish_class": {
"fields": [
"tenant_id",
"class_id",
"status"
],
"unique": false,
"summary": "按班级查在跑的投放"
}
},
"codes": {
"target_type": {
"summary": "投放对象类型",
"items": {
"class": "班级",
"team": "团队",
"student": "学生",
"preview": "预览"
}
},
"status": {
"summary": "投放状态",
"items": {
"published": "已发布",
"running": "进行中",
"finished": "已结束",
"revoked": "已撤回"
}
}
}
}

View File

@ -1,36 +0,0 @@
{
"summary": "PBL 蓝图子对象-反思规格M1a7类子对象之一。一行=一条反思/自评提示的规格定义触发时机、反思类型、引导问题、量规维度供运行时按节点触发学习者反思并回流评估。统一泛化契约。tenant_id 强制打头。",
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
{"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID"},
{"name": "mission_id", "type": "str", "len": 32, "desc": "关联关卡ID"},
{"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
{"name": "code", "type": "str", "len": 64, "desc": "反思规格编码,蓝图内唯一,缺省自动生成 REF-{seq}"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "反思规格名称"},
{"name": "trigger_point", "type": "str", "len": 32, "notnull": true, "default": "mission_end", "desc": "触发时机,见 codes.trigger_point"},
{"name": "reflection_type", "type": "str", "len": 32, "notnull": true, "default": "self", "desc": "反思类型,见 codes.reflection_type"},
{"name": "description", "type": "text", "desc": "反思说明"},
{"name": "spec", "type": "json", "desc": "反思规格(引导问题数组/作答形式/字数下限)"},
{"name": "rubric_ref", "type": "json", "desc": "关联量规维度引用(供 pbl_assessment 打分)"},
{"name": "weight", "type": "float", "default": "1.0", "desc": "评分权重"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
],
"indexes": [
{"name": "uk_pbl_bpref_tenant_bp_code", "fields": ["tenant_id", "blueprint_id", "code"], "unique": true},
{"name": "idx_pbl_bpref_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
{"name": "idx_pbl_bpref_trigger", "fields": ["tenant_id", "blueprint_id", "trigger_point"], "unique": false}
],
"codes": {
"trigger_point": [["task_start", "任务开始"], ["task_end", "任务结束"], ["mission_end", "关卡结束"], ["milestone", "里程碑"], ["project_end", "项目结题"], ["manual", "手动触发"]],
"reflection_type": [["self", "自我反思"], ["peer", "同伴互评"], ["team", "团队复盘"], ["teacher", "教师点评"]],
"status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
}
}

View File

@ -1,30 +0,0 @@
{
"summary": "PBL 蓝图子对象-角色M1a7类子对象之一。一行=蓝图内定义的一种学习者角色(如队长/记录员/工程师供分组与权限分配使用。统一泛化契约。tenant_id 强制打头。",
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
{"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID为空表示蓝图级角色"},
{"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
{"name": "code", "type": "str", "len": 64, "desc": "角色编码,蓝图内唯一,缺省自动生成 ROLE-{seq}"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "角色名称"},
{"name": "description", "type": "text", "desc": "角色职责说明"},
{"name": "spec", "type": "json", "desc": "角色规格(人数上限/能力标签/可用工具/评分权重)"},
{"name": "permissions", "type": "json", "desc": "角色在运行时的操作权限集合"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
],
"indexes": [
{"name": "uk_pbl_bprole_tenant_bp_code", "fields": ["tenant_id", "blueprint_id", "code"], "unique": true},
{"name": "idx_pbl_bprole_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
{"name": "idx_pbl_bprole_task", "fields": ["tenant_id", "task_id"], "unique": false}
],
"codes": {
"status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
}
}

View File

@ -1,36 +0,0 @@
{
"summary": "PBL 蓝图子对象-任务M1a7类子对象之一。一行=蓝图下的一个学习任务,是关卡/角色/目标的挂载父级。采用统一泛化契约blueprint_id + seq + name + spec + status便于 Compiler 与 Validation 统一遍历。tenant_id 强制打头。",
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
{"name": "parent_id", "type": "str", "len": 32, "desc": "父任务ID支持任务分层顶层为空"},
{"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
{"name": "code", "type": "str", "len": 64, "desc": "任务编码,蓝图内唯一,缺省自动生成 TASK-{seq}"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "任务名称"},
{"name": "description", "type": "text", "desc": "任务描述/驱动性问题"},
{"name": "spec", "type": "json", "desc": "任务规格(时长/难度/分组方式/前置条件/交付要求)"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
],
"indexes": [
{"name": "idx_pbl_bptask_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
{"name": "uk_pbl_bptask_tenant_bp_code", "fields": ["tenant_id", "blueprint_id", "code"], "unique": true},
{"name": "idx_pbl_bptask_parent", "fields": ["tenant_id", "parent_id"], "unique": false}
],
"codes": {
"status": [
["draft", "草稿"],
["active", "生效"],
["disabled", "停用"]
],
"deleted": [
["0", "正常"],
["1", "已删除"]
]
}
}

View File

@ -1,31 +1,191 @@
{
"summary": "PBL 蓝图模板表M1a。一行=一个可复用蓝图模板用于一键实例化生成新蓝图离线兜底时也可由模板生成最小可用蓝图。tenant_id 强制打头,平台内置模板 tenant_id 固定为 platform。",
"summary": "蓝图模板表可复用的蓝图骨架payload_json 存模板树),支持实例化为新蓝图;离线兜底时作为本地模板源。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝);空串=平台公共模板"
},
"code": {
"type": "str",
"size": 64,
"notnull": true,
"summary": "模板编码(租户内唯一)"
},
"name": {
"type": "str",
"size": 128,
"notnull": true,
"summary": "模板名称"
},
"category": {
"type": "str",
"size": 32,
"notnull": true,
"default": "general",
"summary": "模板分类"
},
"subject": {
"type": "str",
"size": 64,
"notnull": true,
"default": "",
"summary": "适用学科"
},
"grade": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "适用年级"
},
"scope": {
"type": "str",
"size": 16,
"notnull": true,
"default": "tenant",
"summary": "可见范围"
},
"payload_json": {
"type": "text",
"default": "",
"summary": "模板树载荷JSONblueprint+nodes+edges"
},
"node_count": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "模板节点数"
},
"edge_count": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "模板边数"
},
"price_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"summary": "模板定价金额double(18,2)0=免费)"
},
"use_count": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "被实例化次数"
},
"status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "enabled",
"summary": "模板状态"
},
"summary": {
"type": "str",
"size": 512,
"notnull": true,
"default": "",
"summary": "模板说明"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"update_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "更新人"
},
"update_time": {
"type": "datetime",
"summary": "更新时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_template_code": {
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头平台内置模板为 platform"},
{"name": "code", "type": "str", "len": 64, "notnull": true, "desc": "模板编码,租户内唯一"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "模板名称"},
{"name": "category", "type": "str", "len": 64, "desc": "模板分类,见 codes.category"},
{"name": "subject", "type": "str", "len": 64, "desc": "适用学科"},
{"name": "grade", "type": "str", "len": 32, "desc": "适用年级"},
{"name": "duration_hours", "type": "int", "desc": "建议课时"},
{"name": "description", "type": "text", "desc": "模板说明"},
{"name": "content", "type": "json", "desc": "模板主体内容(主表默认值+子对象骨架)"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "enabled", "desc": "模板状态,见 codes.status"},
{"name": "use_count", "type": "int", "notnull": true, "default": "0", "desc": "被实例化次数"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"tenant_id",
"code"
],
"indexes": [
{"name": "uk_pbl_bpt_tenant_code", "fields": ["tenant_id", "code"], "unique": true},
{"name": "idx_pbl_bpt_tenant_cat", "fields": ["tenant_id", "category", "status", "deleted"], "unique": false}
"unique": true,
"summary": "租户内模板编码唯一"
},
"idx_pbl_template_list": {
"fields": [
"tenant_id",
"deleted",
"status",
"category"
],
"unique": false,
"summary": "模板列表查询tenant_id 打头)"
},
"idx_pbl_template_subject": {
"fields": [
"tenant_id",
"subject",
"grade"
],
"unique": false,
"summary": "按学科年级筛模板"
}
},
"codes": {
"category": [["stem", "STEM跨学科"], ["humanity", "人文社科"], ["science", "自然科学"], ["engineering", "工程制作"], ["social", "社会服务"], ["general", "通用"]],
"status": [["enabled", "启用"], ["disabled", "停用"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
"scope": {
"summary": "可见范围",
"items": {
"tenant": "本租户",
"platform": "平台公共",
"private": "仅创建人"
}
},
"status": {
"summary": "模板状态",
"items": {
"enabled": "启用",
"disabled": "停用",
"draft": "草稿"
}
},
"category": {
"summary": "模板分类",
"items": {
"stem": "STEM",
"science": "自然科学",
"humanity": "人文社科",
"art": "艺术",
"labor": "劳动实践",
"general": "通用"
}
}
}
}

View File

@ -1,27 +0,0 @@
{
"summary": "PBL 蓝图模板条目表M1a。一行=模板下的一个子对象骨架条目obj_type 取 7 类子对象类型之一;实例化模板时按 seq 顺序批量生成对应子对象记录ref_key/parent_ref 做模板内相对引用。tenant_id 强制打头。",
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "template_id", "type": "str", "len": 32, "notnull": true, "desc": "所属模板ID"},
{"name": "obj_type", "type": "str", "len": 32, "notnull": true, "desc": "子对象类型,见 codes.obj_type"},
{"name": "parent_ref", "type": "str", "len": 64, "desc": "父条目引用键模板内相对引用实例化时解析为真实ID"},
{"name": "ref_key", "type": "str", "len": 64, "desc": "本条目引用键,供子条目 parent_ref 指向"},
{"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "同类型内排序号"},
{"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "条目名称"},
{"name": "spec", "type": "json", "desc": "条目规格(对应子对象 spec 字段默认值)"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
],
"indexes": [
{"name": "idx_pbl_bpti_tenant_tpl", "fields": ["tenant_id", "template_id", "obj_type", "seq"], "unique": false},
{"name": "idx_pbl_bpti_refkey", "fields": ["tenant_id", "template_id", "ref_key"], "unique": false}
],
"codes": {
"obj_type": [["task", "任务"], ["mission", "关卡"], ["role", "角色"], ["learning_goal", "学习目标"], ["evidence_spec", "证据规格"], ["artifact_spec", "产出物规格"], ["reflection_spec", "反思规格"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
}
}

View File

@ -1,28 +1,139 @@
{
"summary": "PBL 蓝图版本快照表M1a。一行=蓝图某版本的完整快照 + 相对上一版本的变更增量 change_delta支撑版本回溯/diff/回滚。snapshot 存全量子对象树change_delta 存 added/updated/removed 三段。tenant_id 强制打头。",
"summary": "蓝图版本表:每次 save_version 生成一个版本快照snapshot_json 存整棵树支持版本回溯、change_delta 对比与回滚。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "所属蓝图ID"
},
"version_no": {
"type": "int",
"notnull": true,
"summary": "版本号(蓝图内递增)"
},
"version_type": {
"type": "str",
"size": 16,
"notnull": true,
"default": "draft",
"summary": "版本类型"
},
"change_summary": {
"type": "str",
"size": 512,
"notnull": true,
"default": "",
"summary": "变更说明"
},
"snapshot_json": {
"type": "text",
"default": "",
"summary": "蓝图整树快照JSON主表字段+节点+边)"
},
"node_count": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "快照节点数"
},
"edge_count": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "快照边数"
},
"quality_status": {
"type": "str",
"size": 16,
"notnull": true,
"default": "q0",
"summary": "该版本质量状态"
},
"score_amount": {
"type": "double",
"size": [
18,
2
],
"notnull": true,
"default": 0,
"summary": "该版本评估得分double(18,2)M6 回写)"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_version": {
"fields": [
{"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键32位无横线UUID"},
{"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID强制打头"},
{"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
{"name": "version_no", "type": "int", "notnull": true, "desc": "版本号,蓝图内递增"},
{"name": "snapshot", "type": "json", "notnull": true, "desc": "该版本蓝图全量快照(主表字段+7类子对象树"},
{"name": "change_delta", "type": "json", "desc": "相对上一版本变更增量 {added:[],updated:[],removed:[]}"},
{"name": "quality_level", "type": "str", "len": 16, "desc": "该版本质量等级快照 L1-L5"},
{"name": "status", "type": "str", "len": 32, "notnull": true, "default": "saved", "desc": "版本状态,见 codes.status"},
{"name": "remark", "type": "str", "len": 500, "desc": "版本说明"},
{"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
{"name": "created_at", "type": "datetime", "desc": "创建时间"},
{"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
{"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
{"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"tenant_id",
"blueprint_id",
"version_no"
],
"indexes": [
{"name": "uk_pbl_bpv_tenant_bp_ver", "fields": ["tenant_id", "blueprint_id", "version_no"], "unique": true},
{"name": "idx_pbl_bpv_tenant_status", "fields": ["tenant_id", "status", "deleted"], "unique": false}
"unique": true,
"summary": "蓝图内版本号唯一"
},
"idx_pbl_version_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"deleted",
"version_no"
],
"unique": false,
"summary": "版本列表(倒序取最新)"
}
},
"codes": {
"status": [["saved", "已保存"], ["published", "已发布"], ["rolled_back", "已回滚"], ["archived", "已归档"]],
"quality_level": [["L1", "L1-初始"], ["L2", "L2-基本完整"], ["L3", "L3-结构合格"], ["L4", "L4-可运行"], ["L5", "L5-优质"]],
"deleted": [["0", "正常"], ["1", "已删除"]]
"version_type": {
"summary": "版本类型",
"items": {
"draft": "草稿版本",
"minor": "小版本",
"major": "大版本",
"publish": "发布版本",
"rollback": "回滚版本"
}
},
"quality_status": {
"summary": "质量状态5 级M2 校验引擎回写)",
"items": {
"q0": "未校验",
"q1": "不合格",
"q2": "基本合格",
"q3": "合格",
"q4": "优秀",
"q5": "标杆"
}
}
}
}

View File

@ -0,0 +1,139 @@
{
"summary": "蓝图版本差异表:记录两个版本之间的 change_delta新增/修改/删除的节点、边与主表字段),供版本对比与回滚审计。",
"primary": [
"id"
],
"fields": {
"id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "主键IDstr32UUID去横线"
},
"tenant_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "租户ID首业务字段所有读写强制打头缺失即 fail-closed 拒绝)"
},
"blueprint_id": {
"type": "str",
"size": 32,
"notnull": true,
"summary": "所属蓝图ID"
},
"from_version": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "基准版本号"
},
"to_version": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "目标版本号"
},
"delta_type": {
"type": "str",
"size": 16,
"notnull": true,
"summary": "差异对象类型"
},
"op_type": {
"type": "str",
"size": 16,
"notnull": true,
"summary": "操作类型"
},
"target_id": {
"type": "str",
"size": 32,
"notnull": true,
"default": "",
"summary": "差异对象ID节点ID/边ID/蓝图ID"
},
"target_name": {
"type": "str",
"size": 128,
"notnull": true,
"default": "",
"summary": "差异对象名称(冗余便于展示)"
},
"before_json": {
"type": "text",
"default": "",
"summary": "变更前JSON"
},
"after_json": {
"type": "text",
"default": "",
"summary": "变更后JSON"
},
"sort_no": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "排序号"
},
"create_user": {
"type": "str",
"size": 32,
"default": "",
"summary": "创建人"
},
"create_time": {
"type": "datetime",
"summary": "创建时间"
},
"deleted": {
"type": "int",
"notnull": true,
"default": 0,
"summary": "逻辑删除标记 0正常 1删除"
}
},
"indexes": {
"uk_pbl_delta": {
"fields": [
"tenant_id",
"blueprint_id",
"from_version",
"to_version",
"delta_type",
"op_type",
"target_id"
],
"unique": true,
"summary": "同一版本对同一对象同一操作唯一,保证幂等写入"
},
"idx_pbl_delta_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"deleted",
"to_version"
],
"unique": false,
"summary": "按蓝图+版本查差异"
}
},
"codes": {
"delta_type": {
"summary": "差异对象类型",
"items": {
"blueprint": "蓝图主表字段",
"node": "节点",
"edge": "边"
}
},
"op_type": {
"summary": "操作类型",
"items": {
"add": "新增",
"update": "修改",
"delete": "删除"
}
}
}
}

1446
pbl_blueprint/service.py Normal file

File diff suppressed because it is too large Load Diff

100
pbl_blueprint/tenant.py Normal file
View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""租户上下文M1atenant_id 强制打头 + fail-closed 校验。
所有对外契约函数第一步必须 require_tenant(...)所有 SQL 条件第一个必须是 tenant_id=?
禁止任何默认租户/全局租户兜底缺失即拒绝
tenant_where() 返回 (TenantWhere, args)TenantWhere str 子类并携带绑定参数
即使调用方漏传 argsdb 层也能从片段上取回参数有占位符无参数的漏绑定缺陷
"""
from .errors import ERR_TENANT_MISSING, ERR_FORBIDDEN, PblBlueprintError
MAX_TENANT_LEN = 32
class TenantWhere(str):
"""WHERE 片段字符串 + 绑定参数(不可变,随片段一起传递)。"""
__slots__ = ("args",)
def __new__(cls, sql, args=None):
obj = str.__new__(cls, sql or "")
obj.args = list(args or [])
return obj
def __repr__(self):
return "TenantWhere(%r, args=%r)" % (str(self), self.args)
def _clean(v):
if v is None:
return ""
if isinstance(v, bytes):
try:
v = v.decode("utf-8")
except Exception:
return ""
return str(v).strip()
def require_tenant(tenant_id, ctx_tenant_id=None):
"""校验并返回规范化 tenant_id。
:param tenant_id: 调用方传入的租户ID必填非空<=32 字符不含通配符
:param ctx_tenant_id: 会话上下文租户ID可选给了就必须一致否则判越权
:raises PblBlueprintError: ERR_TENANT_MISSING / ERR_FORBIDDEN
"""
tid = _clean(tenant_id)
if not tid:
raise PblBlueprintError(ERR_TENANT_MISSING)
if len(tid) > MAX_TENANT_LEN:
raise PblBlueprintError(ERR_TENANT_MISSING, "tenant_id 超长(>%d)" % MAX_TENANT_LEN)
if "*" in tid or "%" in tid:
raise PblBlueprintError(ERR_TENANT_MISSING, "tenant_id 含通配符,拒绝")
if "'" in tid or ";" in tid or "--" in tid:
raise PblBlueprintError(ERR_TENANT_MISSING, "tenant_id 含非法字符,拒绝")
if ctx_tenant_id is not None:
ctx = _clean(ctx_tenant_id)
if ctx and ctx != tid:
raise PblBlueprintError(ERR_FORBIDDEN, "tenant_id 与会话上下文不一致")
return tid
def tenant_where(tenant_id, extra_sql="", extra_args=None):
"""生成 tenant_id 打头的 WHERE 片段。
返回 (TenantWhere, args)片段一定以 `tenant_id=?` 开头杜绝漏租户过滤
"""
tid = require_tenant(tenant_id)
sql = "tenant_id=?"
args = [tid]
if extra_sql:
sql = sql + " and (" + extra_sql + ")"
if extra_args:
args.extend(list(extra_args))
return TenantWhere(sql, args), args
def inject_tenant(tenant_id, data, overwrite=True):
"""把 tenant_id 注入待写入 dict 的首位保持字段顺序tenant_id 打头)。"""
tid = require_tenant(tenant_id)
src = dict(data or {})
if not overwrite and src.get("tenant_id") and _clean(src.get("tenant_id")) != tid:
raise PblBlueprintError(ERR_FORBIDDEN, "data.tenant_id 与上下文不一致")
out = {"tenant_id": tid}
for k, v in src.items():
if k == "tenant_id":
continue
out[k] = v
return out
def assert_tenant_first(fields):
"""校验字段序列中 tenant_id 是首个业务字段(紧跟主键 id 之后)。"""
seq = [str(f) for f in (fields or [])]
if not seq:
return False
if seq[0] == "id":
return len(seq) > 1 and seq[1] == "tenant_id"
return seq[0] == "tenant_id"

View File

@ -5,27 +5,16 @@ build-backend = "setuptools.build_meta"
[project]
name = "pbl_blueprint"
version = "1.0.0"
description = "PBL 蓝图聚合根与子对象、版本、模板模块M1a——蓝图 CRUD/树/fork、7 类子对象泛化契约、版本 change_delta、模板实例化与离线兜底tenant_id 强制打头"
description = "PBL 蓝图聚合根与子对象、版本、模板M1a——蓝图 CRUD/树/fork、7 类子对象泛化契约、版本 change_delta、模板实例化与离线兜底"
readme = "README.md"
requires-python = ">=3.8"
license = { text = "Proprietary" }
authors = [{ name = "sdlc agent.develop" }]
keywords = ["pbl", "blueprint", "sage", "module"]
authors = [{ name = "agent.develop" }]
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=7.0"]
[tool.setuptools]
packages = ["pbl_blueprint"]
include-package-data = true
[tool.setuptools.package-data]
pbl_blueprint = [
"json/*.json",
"models/*.json",
"sql/*.sql",
"wwwroot/*.ui",
"init/*.json",
"skill/*.md",
]
pbl_blueprint = ["models/*.json", "json/*.json", "skill/*.md"]

773
scripts/gen_defs.py Normal file
View File

@ -0,0 +1,773 @@
# -*- 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())

View File

@ -1,132 +1,179 @@
"""pbl_blueprint 模块 RBAC 路径注册脚本。
# -*- coding: utf-8 -*-
"""pbl_blueprint RBAC 路径注册清单(显式枚举,无 %/* 通配符)。
用途把本模块全部新 API 路径 + 页面路径注册到平台 RBAC权限路径表
使 owner.admin / 租户管理员可授权未注册路径一律 403
本文件是 RBAC 路径的唯一声明源init.py RBAC_PATHS 必须与此处 PATHS 完全一致
selfcheck.py 会逐条比对不一致即退出码非 0
执行方式应用部署后一次性 / 幂等可重复执行
python -m pbl_blueprint.scripts.load_path
或在应用 init() 后由部署脚本调用 load_paths(env)
用法
python3 modules/pbl_blueprint/scripts/load_path.py # 打印清单
python3 modules/pbl_blueprint/scripts/load_path.py --check # 与 init.py 比对
"""
import os
import sys
MODULE_NAME = 'pbl_blueprint'
MODULE_NAME = "pbl_blueprint"
# ---- 本模块全部新 API 路径(与 api.py 的 API_PATHS 保持一致,单一事实源)----
API_PATHS = [
('/pbl_blueprint/blueprint/create', 'POST', '新建蓝图'),
('/pbl_blueprint/blueprint/update', 'POST', '更新蓝图'),
('/pbl_blueprint/blueprint/delete', 'POST', '删除蓝图'),
('/pbl_blueprint/blueprint/get', 'GET', '蓝图详情'),
('/pbl_blueprint/blueprint/list', 'GET', '蓝图列表'),
('/pbl_blueprint/blueprint/tree', 'GET', '蓝图子对象树'),
('/pbl_blueprint/blueprint/fork', 'POST', '复制蓝图'),
('/pbl_blueprint/version/save', 'POST', '保存版本快照'),
('/pbl_blueprint/version/list', 'GET', '版本列表'),
('/pbl_blueprint/version/diff', 'GET', '版本对比'),
('/pbl_blueprint/version/rollback', 'POST', '版本回滚'),
('/pbl_blueprint/subobject/create', 'POST', '新建子对象'),
('/pbl_blueprint/subobject/update', 'POST', '更新子对象'),
('/pbl_blueprint/subobject/delete', 'POST', '删除子对象'),
('/pbl_blueprint/subobject/get', 'GET', '子对象详情'),
('/pbl_blueprint/subobject/list', 'GET', '子对象列表'),
('/pbl_blueprint/subobject/batch_upsert', 'POST', '批量写子对象'),
('/pbl_blueprint/subobject/types', 'GET', '子对象类型枚举'),
('/pbl_blueprint/template/create', 'POST', '新建模板'),
('/pbl_blueprint/template/update', 'POST', '更新模板'),
('/pbl_blueprint/template/delete', 'POST', '删除模板'),
('/pbl_blueprint/template/get', 'GET', '模板详情'),
('/pbl_blueprint/template/list', 'GET', '模板列表'),
('/pbl_blueprint/template/instantiate', 'POST', '模板实例化为蓝图'),
('/pbl_blueprint/template/offline_fallback', 'POST', '离线兜底生成蓝图'),
]
# 蓝图主表
PATHS_BLUEPRINT = (
"/api/pbl_blueprint/list.dspy",
"/api/pbl_blueprint/create.dspy",
"/api/pbl_blueprint/get.dspy",
"/api/pbl_blueprint/update.dspy",
"/api/pbl_blueprint/delete.dspy",
"/api/pbl_blueprint/tree.dspy",
"/api/pbl_blueprint/fork.dspy",
"/api/pbl_blueprint/forks.dspy",
"/api/pbl_blueprint/stats.dspy",
)
# ---- 页面路径wwwroot/*.ui 挂载点)----
PAGE_PATHS = [
('/pbl_blueprint/index', 'GET', 'PBL蓝图管理主页'),
('/pbl_blueprint/blueprint/edit', 'GET', '蓝图编辑页'),
('/pbl_blueprint/template/list', 'GET', '蓝图模板库页'),
]
# 子对象节点
PATHS_NODE = (
"/api/pbl_blueprint_node/list.dspy",
"/api/pbl_blueprint_node/create.dspy",
"/api/pbl_blueprint_node/get.dspy",
"/api/pbl_blueprint_node/update.dspy",
"/api/pbl_blueprint_node/delete.dspy",
)
# ---- 表级数据权限路径CRUD 定义 json/*.json 对应)----
TABLE_PATHS = [
('/pbl_blueprint/tbl/pbl_blueprint', 'CRUD', '蓝图主表数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_version', 'CRUD', '蓝图版本表数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_template', 'CRUD', '蓝图模板表数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_template_item', 'CRUD', '蓝图模板条目表数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_task', 'CRUD', '子对象-任务数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_mission', 'CRUD', '子对象-关卡数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_role', 'CRUD', '子对象-角色数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_learning_goal', 'CRUD', '子对象-学习目标数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_evidence_spec', 'CRUD', '子对象-证据规格数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_artifact_spec', 'CRUD', '子对象-产出物规格数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_reflection_spec', 'CRUD', '子对象-反思规格数据权限'),
]
# 关系边
PATHS_EDGE = (
"/api/pbl_blueprint_edge/list.dspy",
"/api/pbl_blueprint_edge/create.dspy",
"/api/pbl_blueprint_edge/get.dspy",
"/api/pbl_blueprint_edge/update.dspy",
"/api/pbl_blueprint_edge/delete.dspy",
)
ALL_PATHS = API_PATHS + PAGE_PATHS + TABLE_PATHS
# 版本与差异
PATHS_VERSION = (
"/api/pbl_blueprint_version/list.dspy",
"/api/pbl_blueprint_version/get.dspy",
"/api/pbl_blueprint_version/save.dspy",
"/api/pbl_blueprint_version/rollback.dspy",
"/api/pbl_blueprint_version_delta/list.dspy",
"/api/pbl_blueprint_version_delta/get.dspy",
)
# 模板
PATHS_TEMPLATE = (
"/api/pbl_blueprint_template/list.dspy",
"/api/pbl_blueprint_template/create.dspy",
"/api/pbl_blueprint_template/get.dspy",
"/api/pbl_blueprint_template/update.dspy",
"/api/pbl_blueprint_template/delete.dspy",
"/api/pbl_blueprint_template/instantiate.dspy",
)
# 发布
PATHS_PUBLISH = (
"/api/pbl_blueprint_publish/list.dspy",
"/api/pbl_blueprint_publish/create.dspy",
"/api/pbl_blueprint_publish/get.dspy",
"/api/pbl_blueprint_publish/revoke.dspy",
)
# 离线包
PATHS_OFFLINE = (
"/api/pbl_blueprint_offline/list.dspy",
"/api/pbl_blueprint_offline/export.dspy",
"/api/pbl_blueprint_offline/import.dspy",
"/api/pbl_blueprint_offline/get.dspy",
)
# 血缘
PATHS_FORK = (
"/api/pbl_blueprint_fork/list.dspy",
"/api/pbl_blueprint_fork/get.dspy",
)
# 编辑锁
PATHS_LOCK = (
"/api/pbl_blueprint_lock/lock.dspy",
"/api/pbl_blueprint_lock/unlock.dspy",
"/api/pbl_blueprint_lock/list.dspy",
"/api/pbl_blueprint_lock/clean_expired.dspy",
)
# 审计(只读)
PATHS_AUDIT = (
"/api/pbl_blueprint_audit/list.dspy",
"/api/pbl_blueprint_audit/get.dspy",
)
PATHS = (
PATHS_BLUEPRINT + PATHS_NODE + PATHS_EDGE + PATHS_VERSION + PATHS_TEMPLATE +
PATHS_PUBLISH + PATHS_OFFLINE + PATHS_FORK + PATHS_LOCK + PATHS_AUDIT
)
# 只读路径RBAC 上归为 read 权限组)
READONLY_PATHS = tuple(p for p in PATHS if p.rsplit("/", 1)[-1].split(".")[0] in
("list", "get", "tree", "forks", "stats"))
WRITE_PATHS = tuple(p for p in PATHS if p not in READONLY_PATHS)
def load_paths(env=None):
"""把 ALL_PATHS 注册进 RBAC幂等存在则更新描述不存在则新增
:return: {'registered': n, 'skipped': m, 'failed': k}
"""
result = {'registered': 0, 'skipped': 0, 'failed': 0, 'paths': []}
if env is None:
try:
from sage import ServerEnv
env = ServerEnv()
except Exception:
env = None
rbac = None
if env is not None:
for attr in ('rbac', 'RBAC'):
rbac = getattr(env, attr, None)
if rbac is not None:
break
if rbac is None:
getter = getattr(env, 'get_rbac', None)
if callable(getter):
try:
rbac = getter()
except Exception:
rbac = None
for path, method, caption in ALL_PATHS:
item = {'path': path, 'method': method, 'caption': caption, 'module': MODULE_NAME}
result['paths'].append(item)
if rbac is None:
result['skipped'] += 1
def validate(paths=None):
"""校验路径合法性:非空、以 / 开头、.dspy 结尾、无通配符、无重复。返回错误列表。"""
ps = list(paths if paths is not None else PATHS)
errs = []
seen = set()
for p in ps:
if not p or not isinstance(p, str):
errs.append("空路径或非字符串: %r" % (p,))
continue
try:
registered = False
for fn_name in ('register_path', 'add_path', 'load_path', 'register'):
fn = getattr(rbac, fn_name, None)
if callable(fn):
try:
fn(path, method, caption)
except TypeError:
fn(item)
registered = True
break
if registered:
result['registered'] += 1
else:
result['skipped'] += 1
except Exception:
result['failed'] += 1
return result
if not p.startswith("/api/"):
errs.append("路径必须以 /api/ 开头: %s" % p)
if not p.endswith(".dspy"):
errs.append("路径必须以 .dspy 结尾: %s" % p)
if "%" in p or "*" in p or "?" in p:
errs.append("路径含通配符(禁止): %s" % p)
if " " in p:
errs.append("路径含空格: %s" % p)
if p in seen:
errs.append("路径重复: %s" % p)
seen.add(p)
return errs
def main():
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
res = load_paths()
print('[pbl_blueprint] load_path done: registered=%d skipped=%d failed=%d total=%d' % (
res['registered'], res['skipped'], res['failed'], len(res['paths'])))
def check_against_init():
"""与 pbl_blueprint/init.py 的 RBAC_PATHS 逐条比对,返回 (ok, diff_msg)。"""
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if root not in sys.path:
sys.path.insert(0, root)
try:
from pbl_blueprint import init as _init # noqa
except Exception as e:
return False, "无法导入 pbl_blueprint.init: %s" % e
a = list(PATHS)
b = list(getattr(_init, "RBAC_PATHS", ()))
if a == b:
return True, "RBAC 路径一致(%d 条,顺序相同)" % len(a)
only_a = [x for x in a if x not in b]
only_b = [x for x in b if x not in a]
return False, "RBAC 路径不一致: 仅 load_path=%s / 仅 init=%s / 顺序差异=%s" % (
only_a, only_b, a != b and not only_a and not only_b)
def main(argv=None):
argv = list(sys.argv[1:] if argv is None else argv)
if "--check" in argv:
errs = validate()
good, msg = check_against_init()
for e in errs:
print("[PATH-ERR] %s" % e)
print("[CHECK] %s" % msg)
print("[STATS] total=%d readonly=%d write=%d" % (len(PATHS), len(READONLY_PATHS), len(WRITE_PATHS)))
return 0 if (good and not errs) else 1
print("# %s RBAC 路径清单(显式枚举,共 %d 条,无通配符)" % (MODULE_NAME, len(PATHS)))
for p in PATHS:
print(p)
errs = validate()
if errs:
for e in errs:
print("[PATH-ERR] %s" % e, file=sys.stderr)
return 1
return 0
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(main())

File diff suppressed because it is too large Load Diff