[M1a] pbl_blueprint 蓝图核心表与CRUD契约: 11表models四段式 + 11个CRUD(tblname/params) + 核心4表与子对象7表DDL + 蓝图CRUD/树/fork + 版本change_delta/diff/rollback + 模板实例化与离线兜底 + tenant_id强制打头 + load_path RBAC 39路径 + SKILL.md

This commit is contained in:
agent.develop 2026-09-15 18:45:46 +08:00
parent a51815075f
commit 3ebd234b2e
50 changed files with 3658 additions and 5379 deletions

23
.gitignore vendored
View File

@ -1,6 +1,21 @@
__pycache__/ __pycache__/
*.pyc *.py[cod]
*.pyo
.pytest_cache/
*.egg-info/ *.egg-info/
.venv/ build/
dist/
.eggs/
*.swp
*.swo
*~
.DS_Store
mysql.ddl.sql
wwwroot/pbl_blueprint/
wwwroot/pbl_blueprint_learning_goal/
wwwroot/pbl_blueprint_role/
wwwroot/pbl_blueprint_mission/
wwwroot/pbl_blueprint_task/
wwwroot/pbl_blueprint_artifact_spec/
wwwroot/pbl_blueprint_evidence_spec/
wwwroot/pbl_blueprint_reflection_spec/
wwwroot/pbl_blueprint_version/
wwwroot/pbl_template/

View File

@ -1,2 +1,95 @@
# pbl_blueprint # pbl_blueprint —— PBL 蓝图聚合根与子对象、版本、模板(M1a)
PBL(Project-Based Learning)平台的**蓝图域基础模块**。蓝图是整个 PBL 系统的
聚合根:一份蓝图定义了一个项目的学习目标、角色分工、驱动问题(Mission)、任务、
产出物规格、证据规格与反思规格,并带完整版本历史(`change_delta` 记录对话式改
模型的每一步)。模板(`pbl_template`)提供蓝图骨架与 LLM 不可用时的离线兜底槽位。
## 特性
- **多租户强制打头**:所有表的第二列即 `tenant_id`,所有读写 SQL 的 WHERE 第一
条件必须是 `tenant_id`;上下文缺失时 **fail-closed 抛 `PblTenantMissing`**,
绝不降级为全租户可见。
- **聚合根 + 7 类子对象**:`learning_goal / role / mission / task /
artifact_spec / evidence_spec / reflection_spec`,统一走
`pbl_blueprint_subobject_save|list|delete` 三个泛化契约,新增子对象类型只需在
`api.py: SUBOBJECTS` 注册一行 + 加 models/json 定义。
- **版本化**:任何主表/子对象变更都会自动升 `version_no` 并追加一条
`pbl_blueprint_version` 记录(含 `change_delta` 与全量 `snapshot_json`),
为 M2 校验引擎与 M3 Compiler 提供可追溯的模型演化证据。
- **模板实例化 + 离线兜底**:`pbl_template_instantiate` 展开
`blueprint_snapshot`,缺失字段用 `offline_slots[].default` 填充(LLM 不可用
时仍能产出可用蓝图)。
- **派生(fork)**:深拷贝聚合根与全部子对象,并重映射子对象之间的引用
(`mission_id / task_id / artifact_spec_id / role_id`)。
- **状态保护**:`status='published'` 的蓝图禁止直接改主表/子对象/删除,必须先
fork 或回退状态(`PblStateConflict`)。
## 数据表(10 张)
| 表 | 说明 |
|----|------|
| `pbl_blueprint` | 蓝图聚合根 |
| `pbl_blueprint_learning_goal` | 子对象-学习目标 |
| `pbl_blueprint_role` | 子对象-PBL 角色 |
| `pbl_blueprint_mission` | 子对象-驱动问题/Mission |
| `pbl_blueprint_task` | 子对象-任务 |
| `pbl_blueprint_artifact_spec` | 子对象-产出物规格 |
| `pbl_blueprint_evidence_spec` | 子对象-证据规格 |
| `pbl_blueprint_reflection_spec` | 子对象-反思规格 |
| `pbl_blueprint_version` | 版本快照 + change_delta |
| `pbl_template` | 模板(含离线兜底槽位) |
DDL 基线:`models/pbl_blueprint.subobjects.sql`;表定义四段式:`models/*.json`
(build.sh 用 `json2ddl mysql .` 生成 `mysql.ddl.sql`)。
## 契约接口(17 个,路径 `/pbl_blueprint/api/<name>.dspy`)
蓝图:`pbl_blueprint_create` / `_read` / `_update` / `_delete` / `_list` /
`_tree` / `_fork` / `_get_contract`
子对象:`pbl_blueprint_subobject_save` / `_list` / `_delete`
版本:`pbl_blueprint_version_create` / `_diff`
模板:`pbl_template_list` / `_instantiate` / `_save` / `_delete`
统一返回:`{"status":"success","data":...,"total":N}` 或
`{"status":"error","code":"PBL_XXX","message":"..."}`。
## 安装与集成(宿主应用 apps/pbls)
```python
# app/pbls.py
from pbl_blueprint.init import load_pbl_blueprint
def init():
env = ServerEnv()
env.get_module_dbname = get_module_dbname # 模块 -> 库名映射,禁止模块内硬编码
load_pbl_common() # 先加载公共内核(tenant 上下文/审计)
load_pbl_blueprint() # 再加载蓝图域
```
```bash
cd apps/pbls/pkgs && git clone <repo>/pbl_blueprint && pip install ./pbl_blueprint
bash modules/pbl_blueprint/build.sh # DDL + CRUD UI + 软链
./py3/bin/python modules/pbl_blueprint/scripts/load_path.py # RBAC 路径注册
```
## 目录
```
pbl_blueprint/
├── pbl_blueprint/ # Python 包:__init__.py / init.py / api.py
├── models/ # 10 张表定义(四段式 JSON)+ DDL 基线 SQL
├── json/ # 10 份 CRUD 定义
├── wwwroot/ # index.ui / menu.ui / api/*.dspy(17 个契约薄包装)
├── init/data.json # appcodes 10 组编码 + 2 份内置模板真实种子
├── scripts/load_path.py # RBAC 显式路径(无通配符)
├── skill/SKILL.md # agent 可读模块规范
├── pyproject.toml / build.sh / README.md
```
## 自测
```bash
python3 -m py_compile pbl_blueprint/*.py scripts/load_path.py
python3 scripts/selftest.py # 无 DB 环境下的契约/租户 fail-closed 静态校验
```

66
build.sh Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env bash
# pbl_blueprint 模块构建脚本(由宿主应用 apps/pbls/build.sh 调用)
# 步骤:① json2ddl 生成 DDL ② xls2ui 生成 CRUD UI ③ 软链 wwwroot 到宿主
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MOD_NAME="pbl_blueprint"
# ---- 定位宿主应用根(不假设相对层级)
APP_ROOT=""
for candidate in "$SCRIPT_DIR/../../apps/pbls" "$SCRIPT_DIR/../.." "$PBL_APP_ROOT" "$SAGE_ROOT"; do
if [ -n "$candidate" ] && [ -d "$candidate/wwwroot" ]; then
APP_ROOT="$(cd "$candidate" && pwd)"
break
fi
done
echo "[${MOD_NAME}] module dir: $SCRIPT_DIR"
echo "[${MOD_NAME}] app root : ${APP_ROOT:-<未找到,仅生成本地产物>}"
# ---- ① 表定义 -> DDL
if [ -d "$SCRIPT_DIR/models" ]; then
if command -v json2ddl >/dev/null 2>&1; then
cd "$SCRIPT_DIR/models"
json2ddl mysql . > "$SCRIPT_DIR/mysql.ddl.sql"
echo "[${MOD_NAME}] mysql.ddl.sql 已生成"
cd "$SCRIPT_DIR"
else
echo "[${MOD_NAME}] WARN: json2ddl 未安装,使用 models/pbl_blueprint.subobjects.sql 作为 DDL 基线"
cp -f "$SCRIPT_DIR/models/pbl_blueprint.subobjects.sql" "$SCRIPT_DIR/mysql.ddl.sql"
fi
fi
# ---- ② CRUD 定义 -> UI
if [ -d "$SCRIPT_DIR/json" ] && command -v xls2ui >/dev/null 2>&1; then
cd "$SCRIPT_DIR/json"
xls2ui -m ../models -o ../wwwroot "${MOD_NAME}" *.json || \
echo "[${MOD_NAME}] WARN: xls2ui 生成失败,CRUD 页面需手工核对"
cd "$SCRIPT_DIR"
fi
# ---- ③ 软链 wwwroot 到宿主应用
if [ -n "$APP_ROOT" ]; then
mkdir -p "$APP_ROOT/wwwroot/${MOD_NAME}"
for f in "$SCRIPT_DIR"/wwwroot/*.ui "$SCRIPT_DIR"/wwwroot/*.js "$SCRIPT_DIR"/wwwroot/*.css; do
[ -e "$f" ] || continue
ln -sf "$f" "$APP_ROOT/wwwroot/${MOD_NAME}/"
done
[ -d "$SCRIPT_DIR/wwwroot/api" ] && {
mkdir -p "$APP_ROOT/wwwroot/${MOD_NAME}/api"
for f in "$SCRIPT_DIR"/wwwroot/api/*.dspy; do
[ -e "$f" ] || continue
ln -sf "$f" "$APP_ROOT/wwwroot/${MOD_NAME}/api/"
done
}
# CRUD 生成子目录整体软链(排除 api/styles/scripts)
for d in "$SCRIPT_DIR"/wwwroot/*/; do
[ -d "$d" ] || continue
bn="$(basename "$d")"
case "$bn" in api|styles|scripts) continue ;; esac
ln -sfn "${d%/}" "$APP_ROOT/wwwroot/${MOD_NAME}/$bn"
done
echo "[${MOD_NAME}] wwwroot 已链接到 $APP_ROOT/wwwroot/${MOD_NAME}"
fi
echo "[${MOD_NAME}] build done"

151
init/data.json Normal file
View File

@ -0,0 +1,151 @@
{
"appcodes": [
{
"parentid": "pbl_quality_status",
"parentname": "PBL蓝图质量状态",
"items": [
{"k": "draft", "v": "草稿"},
{"k": "partial", "v": "部分完整"},
{"k": "valid", "v": "校验通过"},
{"k": "complete", "v": "完备"},
{"k": "published", "v": "已发布"}
]
},
{
"parentid": "pbl_bp_status",
"parentname": "PBL蓝图生命周期状态",
"items": [
{"k": "draft", "v": "草稿"},
{"k": "published", "v": "已发布"},
{"k": "archived", "v": "已归档"},
{"k": "deleted", "v": "已删除"}
]
},
{
"parentid": "pbl_knowledge_type",
"parentname": "PBL知识类型",
"items": [
{"k": "fact", "v": "事实性知识"},
{"k": "concept", "v": "概念性知识"},
{"k": "procedure", "v": "程序性知识"},
{"k": "metacognition", "v": "元认知知识"}
]
},
{
"parentid": "pbl_task_type",
"parentname": "PBL任务类型",
"items": [
{"k": "explore", "v": "探究"},
{"k": "build", "v": "构建"},
{"k": "test", "v": "测试验证"},
{"k": "present", "v": "展示汇报"},
{"k": "reflect", "v": "反思"}
]
},
{
"parentid": "pbl_artifact_type",
"parentname": "PBL产出物类型",
"items": [
{"k": "document", "v": "文档"},
{"k": "model", "v": "三维模型"},
{"k": "code", "v": "代码/脚本"},
{"k": "image", "v": "图像"},
{"k": "video", "v": "视频"},
{"k": "audio", "v": "音频"},
{"k": "data", "v": "数据集"},
{"k": "other", "v": "其他"}
]
},
{
"parentid": "pbl_evidence_type",
"parentname": "PBL证据类型",
"items": [
{"k": "process", "v": "过程证据"},
{"k": "result", "v": "结果证据"},
{"k": "behavior", "v": "行为证据"},
{"k": "log", "v": "系统日志"}
]
},
{
"parentid": "pbl_collect_mode",
"parentname": "PBL证据采集方式",
"items": [
{"k": "auto", "v": "自动采集"},
{"k": "manual", "v": "人工上传"},
{"k": "hybrid", "v": "混合"}
]
},
{
"parentid": "pbl_reflect_type",
"parentname": "PBL反思类型",
"items": [
{"k": "self", "v": "自我反思"},
{"k": "peer", "v": "同伴互评"},
{"k": "team", "v": "团队反思"},
{"k": "teacher", "v": "教师点评"}
]
},
{
"parentid": "pbl_version_source",
"parentname": "PBL版本变更来源",
"items": [
{"k": "manual", "v": "手工编辑"},
{"k": "dialogue", "v": "对话式改模型"},
{"k": "template", "v": "模板实例化"},
{"k": "fork", "v": "蓝图派生"},
{"k": "validation", "v": "校验引擎回写"}
]
},
{
"parentid": "pbl_template_cat",
"parentname": "PBL模板分类",
"items": [
{"k": "general", "v": "通用"},
{"k": "stem", "v": "STEM"},
{"k": "humanities", "v": "人文社科"},
{"k": "art", "v": "艺术设计"},
{"k": "engineering", "v": "工程实践"}
]
}
],
"pbl_template": [
{
"id": "pbltpl_builtin_stem01",
"tenant_id": "0",
"template_code": "TPL_STEM_BRIDGE",
"name": "STEM 桥梁承重挑战(内置)",
"description": "以桥梁承重为主题的 STEM 项目式学习蓝图骨架,含 2 个 Mission、5 个任务、产出物与证据规格示例。",
"category": "stem",
"subject_area": "物理/工程",
"grade_level": "初中",
"is_builtin": "1",
"template_version": "1.0.0",
"status": "active",
"blueprint_snapshot": "{\"title\":\"桥梁承重挑战\",\"subject_area\":\"物理/工程\",\"grade_level\":\"初中\",\"duration_hours\":12,\"driving_question\":\"如何用限定材料设计一座承重最大的桥梁?\",\"summary\":\"学生以工程团队角色完成调研-设计-建造-测试-汇报全流程。\",\"subobjects\":{\"learning_goal\":[{\"goal_code\":\"LG1\",\"title\":\"理解力的传递与结构稳定性\",\"knowledge_type\":\"concept\",\"assessable\":\"1\",\"seq\":1},{\"goal_code\":\"LG2\",\"title\":\"掌握基本工程制图与比例换算\",\"knowledge_type\":\"procedure\",\"assessable\":\"1\",\"seq\":2}],\"role\":[{\"role_code\":\"R_PM\",\"role_name\":\"项目经理\",\"is_mandatory\":\"1\",\"min_members\":1,\"max_members\":1,\"seq\":1},{\"role_code\":\"R_ENG\",\"role_name\":\"结构工程师\",\"is_mandatory\":\"1\",\"min_members\":1,\"max_members\":2,\"seq\":2},{\"role_code\":\"R_DOC\",\"role_name\":\"记录员\",\"is_mandatory\":\"0\",\"min_members\":1,\"max_members\":1,\"seq\":3}],\"mission\":[{\"mission_code\":\"M1\",\"title\":\"调研与方案设计\",\"driving_question\":\"哪些结构形式最省材料又最承重?\",\"duration_hours\":4,\"seq\":1},{\"mission_code\":\"M2\",\"title\":\"建造、测试与汇报\",\"driving_question\":\"我们的桥能承受多少重量?\",\"duration_hours\":8,\"seq\":2}],\"task\":[{\"task_code\":\"T1\",\"title\":\"结构形式调研\",\"task_type\":\"explore\",\"estimated_minutes\":90,\"seq\":1},{\"task_code\":\"T2\",\"title\":\"绘制设计图\",\"task_type\":\"build\",\"estimated_minutes\":120,\"seq\":2},{\"task_code\":\"T3\",\"title\":\"模型搭建\",\"task_type\":\"build\",\"estimated_minutes\":180,\"seq\":3},{\"task_code\":\"T4\",\"title\":\"承重测试\",\"task_type\":\"test\",\"estimated_minutes\":60,\"seq\":4},{\"task_code\":\"T5\",\"title\":\"成果汇报\",\"task_type\":\"present\",\"estimated_minutes\":45,\"seq\":5}],\"artifact_spec\":[{\"artifact_code\":\"A1\",\"name\":\"结构设计图\",\"artifact_type\":\"image\",\"format\":\"png\",\"required\":\"1\",\"seq\":1},{\"artifact_code\":\"A2\",\"name\":\"承重测试数据表\",\"artifact_type\":\"data\",\"format\":\"csv\",\"required\":\"1\",\"seq\":2},{\"artifact_code\":\"A3\",\"name\":\"汇报演示稿\",\"artifact_type\":\"document\",\"format\":\"pdf\",\"required\":\"1\",\"seq\":3}],\"evidence_spec\":[{\"evidence_code\":\"E1\",\"name\":\"测试过程录像\",\"evidence_type\":\"process\",\"collect_mode\":\"manual\",\"required\":\"1\",\"seq\":1},{\"evidence_code\":\"E2\",\"name\":\"最大承重读数\",\"evidence_type\":\"result\",\"collect_mode\":\"auto\",\"idempotency_key_hint\":\"task_id+artifact_code\",\"required\":\"1\",\"seq\":2}],\"reflection_spec\":[{\"reflection_code\":\"RF1\",\"title\":\"设计迭代反思\",\"reflection_type\":\"self\",\"trigger_point\":\"mission_end\",\"required\":\"1\",\"seq\":1},{\"reflection_code\":\"RF2\",\"title\":\"团队协作互评\",\"reflection_type\":\"peer\",\"trigger_point\":\"project_end\",\"required\":\"1\",\"seq\":2}]}}",
"offline_slots": "[{\"path\":\"/driving_question\",\"field\":\"driving_question\",\"default\":\"如何用限定材料设计一座承重最大的桥梁?\"},{\"path\":\"/summary\",\"field\":\"summary\",\"default\":\"学生分组完成调研、设计、建造、测试与汇报的完整工程实践流程。\"},{\"path\":\"/duration_hours\",\"field\":\"duration_hours\",\"default\":12},{\"path\":\"/subject_area\",\"field\":\"subject_area\",\"default\":\"物理/工程\"},{\"path\":\"/grade_level\",\"field\":\"grade_level\",\"default\":\"初中\"}]",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_by": "system",
"updated_at": "2026-01-01 00:00:00"
},
{
"id": "pbltpl_builtin_hum01",
"tenant_id": "0",
"template_code": "TPL_HUM_COMMUNITY",
"name": "社区口述史调查(内置)",
"description": "人文社科类 PBL 骨架:社区访谈-资料整理-策展-公开汇报。",
"category": "humanities",
"subject_area": "历史/社会",
"grade_level": "高中",
"is_builtin": "1",
"template_version": "1.0.0",
"status": "active",
"blueprint_snapshot": "{\"title\":\"社区口述史调查\",\"subject_area\":\"历史/社会\",\"grade_level\":\"高中\",\"duration_hours\":10,\"driving_question\":\"我们社区的记忆如何被记录与传承?\",\"summary\":\"学生完成访谈设计、实地采集、资料编目与策展汇报。\",\"subobjects\":{\"learning_goal\":[{\"goal_code\":\"LG1\",\"title\":\"掌握口述史访谈方法与伦理\",\"knowledge_type\":\"procedure\",\"assessable\":\"1\",\"seq\":1},{\"goal_code\":\"LG2\",\"title\":\"理解地方史与集体记忆的关系\",\"knowledge_type\":\"concept\",\"assessable\":\"1\",\"seq\":2}],\"role\":[{\"role_code\":\"R_INT\",\"role_name\":\"访谈员\",\"is_mandatory\":\"1\",\"min_members\":1,\"max_members\":2,\"seq\":1},{\"role_code\":\"R_ARCH\",\"role_name\":\"资料管理员\",\"is_mandatory\":\"1\",\"min_members\":1,\"max_members\":1,\"seq\":2},{\"role_code\":\"R_CUR\",\"role_name\":\"策展人\",\"is_mandatory\":\"0\",\"min_members\":1,\"max_members\":1,\"seq\":3}],\"mission\":[{\"mission_code\":\"M1\",\"title\":\"访谈设计与采集\",\"driving_question\":\"该问谁、问什么、怎么问?\",\"duration_hours\":5,\"seq\":1},{\"mission_code\":\"M2\",\"title\":\"编目与策展\",\"driving_question\":\"如何把碎片记忆变成可展出的故事?\",\"duration_hours\":5,\"seq\":2}],\"task\":[{\"task_code\":\"T1\",\"title\":\"拟定访谈提纲\",\"task_type\":\"explore\",\"estimated_minutes\":90,\"seq\":1},{\"task_code\":\"T2\",\"title\":\"实地访谈录音\",\"task_type\":\"build\",\"estimated_minutes\":150,\"seq\":2},{\"task_code\":\"T3\",\"title\":\"转写与编目\",\"task_type\":\"build\",\"estimated_minutes\":120,\"seq\":3},{\"task_code\":\"T4\",\"title\":\"策展与汇报\",\"task_type\":\"present\",\"estimated_minutes\":90,\"seq\":4}],\"artifact_spec\":[{\"artifact_code\":\"A1\",\"name\":\"访谈提纲\",\"artifact_type\":\"document\",\"format\":\"md\",\"required\":\"1\",\"seq\":1},{\"artifact_code\":\"A2\",\"name\":\"访谈录音\",\"artifact_type\":\"audio\",\"format\":\"mp3\",\"required\":\"1\",\"seq\":2},{\"artifact_code\":\"A3\",\"name\":\"口述史编目表\",\"artifact_type\":\"data\",\"format\":\"csv\",\"required\":\"1\",\"seq\":3}],\"evidence_spec\":[{\"evidence_code\":\"E1\",\"name\":\"访谈知情同意记录\",\"evidence_type\":\"behavior\",\"collect_mode\":\"manual\",\"required\":\"1\",\"seq\":1},{\"evidence_code\":\"E2\",\"name\":\"编目条目数量\",\"evidence_type\":\"result\",\"collect_mode\":\"auto\",\"idempotency_key_hint\":\"task_id+artifact_code\",\"required\":\"1\",\"seq\":2}],\"reflection_spec\":[{\"reflection_code\":\"RF1\",\"title\":\"访谈伦理反思\",\"reflection_type\":\"self\",\"trigger_point\":\"task_end\",\"required\":\"1\",\"seq\":1},{\"reflection_code\":\"RF2\",\"title\":\"团队分工复盘\",\"reflection_type\":\"team\",\"trigger_point\":\"project_end\",\"required\":\"1\",\"seq\":2}]}}",
"offline_slots": "[{\"path\":\"/driving_question\",\"field\":\"driving_question\",\"default\":\"我们社区的记忆如何被记录与传承?\"},{\"path\":\"/summary\",\"field\":\"summary\",\"default\":\"学生完成访谈设计、实地采集、资料编目与策展汇报。\"},{\"path\":\"/duration_hours\",\"field\":\"duration_hours\",\"default\":10}]",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_by": "system",
"updated_at": "2026-01-01 00:00:00"
}
]
}

View File

@ -1,50 +1,47 @@
{ {
"summary": [ "summary": [
{ {
"table": "pbl_blueprint", "name": "pbl_blueprint",
"name": "PBL蓝图", "title": "PBL蓝图聚合根",
"title": "PBL蓝图", "primary": ["id"],
"subtitle": "PBL蓝图聚合根", "catelog": "entity"
"primary": ["id"], }
"defaultsort": "created_at desc", ],
"orderby": "created_at desc", "fields": [
"caption": "蓝图", {"name": "tenant_id", "title": "租户ID", "type": "str", "length": 32, "nullable": "no", "default": "0"},
"multilingual": 0 {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
} {"name": "blueprint_code", "title": "蓝图编码", "type": "str", "length": 64, "nullable": "no"},
], {"name": "name", "title": "蓝图名称", "type": "str", "length": 255, "nullable": "no"},
"fields": [ {"name": "subject", "title": "学科领域", "type": "str", "length": 64, "default": ""},
{"name": "id", "type": "str(32)", "title": "主键", "notnull": 1, "primary": 1, "readonly": 1}, {"name": "grade", "title": "适用年级", "type": "str", "length": 32, "default": ""},
{"name": "tenant_id", "type": "str(32)", "title": "租户ID", "notnull": 1, "default": "0", "comment": "多租户强制打头字段,所有读写必须带"}, {"name": "description", "title": "蓝图描述", "type": "text"},
{"name": "org_id", "type": "str(32)", "title": "所属机构", "notnull": 1, "default": "0"}, {"name": "status", "title": "蓝图状态", "type": "str", "length": 32, "nullable": "no", "default": "draft"},
{"name": "blueprint_code", "type": "str(64)", "title": "蓝图编码", "notnull": 1}, {"name": "quality_status", "title": "质量状态(L0-L4)", "type": "str", "length": 8, "nullable": "no", "default": "L0"},
{"name": "title", "type": "str(255)", "title": "蓝图标题", "notnull": 1}, {"name": "current_version", "title": "当前版本号", "type": "int", "nullable": "no", "default": "0"},
{"name": "subject_area", "type": "str(64)", "title": "学科领域", "default": ""}, {"name": "published_version", "title": "已发布版本号", "type": "int", "nullable": "no", "default": "0"},
{"name": "grade_level", "type": "str(32)", "title": "适用年级", "default": ""}, {"name": "root_world_id", "title": "根世界ID", "type": "str", "length": 32, "default": ""},
{"name": "duration_hours", "type": "int", "title": "总课时(小时)", "default": 0}, {"name": "template_id", "title": "来源模板ID", "type": "str", "length": 32, "default": ""},
{"name": "driving_question", "type": "text", "title": "驱动问题", "default": ""}, {"name": "fork_from_id", "title": "fork来源蓝图ID", "type": "str", "length": 32, "default": ""},
{"name": "summary", "type": "text", "title": "蓝图摘要", "default": ""}, {"name": "extra_json", "title": "扩展属性JSON", "type": "text"},
{"name": "quality_status", "type": "str(32)", "title": "质量状态", "notnull": 1, "default": "draft", "comment": "5级:draft/partial/valid/complete/published,由 pbl_validation 写入"}, {"name": "owner_id", "title": "归属用户ID", "type": "str", "length": 32, "nullable": "no", "default": "0"},
{"name": "validation_score", "type": "decimal(5,2)", "title": "校验得分", "default": 0}, {"name": "org_id", "title": "归属机构ID", "type": "str", "length": 32, "nullable": "no", "default": "0"},
{"name": "version_no", "type": "int", "title": "当前版本号", "notnull": 1, "default": 1}, {"name": "created_by", "title": "创建人", "type": "str", "length": 32, "default": ""},
{"name": "current_version_id", "type": "str(32)", "title": "当前版本ID", "default": ""}, {"name": "created_at", "title": "创建时间", "type": "timestamp"},
{"name": "template_id", "type": "str(32)", "title": "来源模板ID", "default": ""}, {"name": "updated_by", "title": "最后修改人", "type": "str", "length": 32, "default": ""},
{"name": "forked_from_id", "type": "str(32)", "title": "派生自蓝图ID", "default": ""}, {"name": "updated_at", "title": "最后修改时间", "type": "timestamp"},
{"name": "status", "type": "str(32)", "title": "生命周期状态", "notnull": 1, "default": "draft", "comment": "draft/published/archived/deleted"}, {"name": "deleted", "title": "软删除标记(0正常1删除)", "type": "str", "length": 1, "nullable": "no", "default": "0"}
{"name": "extra_json", "type": "longtext", "title": "扩展属性JSON", "default": ""}, ],
{"name": "created_by", "type": "str(32)", "title": "创建人", "default": ""}, "indexes": [
{"name": "created_at", "type": "datetime", "title": "创建时间", "notnull": 1}, {"name": "uk_pbl_blueprint_code", "idxtype": "unique", "idxfields": ["tenant_id", "blueprint_code"]},
{"name": "updated_by", "type": "str(32)", "title": "更新人", "default": ""}, {"name": "idx_pbl_blueprint_tenant_status", "idxtype": "index", "idxfields": ["tenant_id", "status", "deleted"]},
{"name": "updated_at", "type": "datetime", "title": "更新时间"} {"name": "idx_pbl_blueprint_tenant_owner", "idxtype": "index", "idxfields": ["tenant_id", "owner_id"]},
], {"name": "idx_pbl_blueprint_tenant_updated", "idxtype": "index", "idxfields": ["tenant_id", "deleted", "updated_at"]},
"indexes": [ {"name": "idx_pbl_blueprint_template", "idxtype": "index", "idxfields": ["tenant_id", "template_id"]}
{"name": "uk_pbl_blueprint_code", "fields": ["tenant_id", "blueprint_code"], "unique": 1}, ],
{"name": "idx_pbl_blueprint_tenant", "fields": ["tenant_id", "status"], "unique": 0}, "codes": [
{"name": "idx_pbl_blueprint_org", "fields": ["org_id"], "unique": 0}, {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='pbl_bp_status'"},
{"name": "idx_pbl_blueprint_created", "fields": ["tenant_id", "created_at"], "unique": 0} {"field": "quality_status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='pbl_quality_status'"},
], {"field": "subject", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='pbl_subject'"},
"codes": [ {"field": "grade", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='pbl_grade'"}
{"field": "quality_status", "table": "appcodes_kv", "cond": "parentid='pbl_quality_status'", "valuefield": "k", "textfield": "v"}, ]
{"field": "status", "table": "appcodes_kv", "cond": "parentid='pbl_bp_status'", "valuefield": "k", "textfield": "v"},
{"field": "template_id", "table": "pbl_template", "cond": "", "valuefield": "id", "textfield": "name"}
]
} }

View File

@ -0,0 +1,234 @@
-- pbl_blueprint DDL (MariaDB / MySQL)
-- 由 models/*.json 派生(json2ddl mysql . 等价输出),tenant_id 强制打头。
-- 部署:应用 build.sh 执行 json2ddl 生成;本文件为可核对的落库基线。
CREATE TABLE IF NOT EXISTS `pbl_blueprint` (
`id` VARCHAR(32) NOT NULL COMMENT '主键',
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0' COMMENT '租户ID(强制打头)',
`org_id` VARCHAR(32) NOT NULL DEFAULT '0' COMMENT '所属机构',
`blueprint_code` VARCHAR(64) NOT NULL COMMENT '蓝图编码',
`title` VARCHAR(255) NOT NULL COMMENT '蓝图标题',
`subject_area` VARCHAR(64) DEFAULT '' COMMENT '学科领域',
`grade_level` VARCHAR(32) DEFAULT '' COMMENT '适用年级',
`duration_hours` INT DEFAULT 0 COMMENT '总课时(小时)',
`driving_question` TEXT COMMENT '驱动问题',
`summary` TEXT COMMENT '蓝图摘要',
`quality_status` VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT '5级质量状态',
`validation_score` DECIMAL(5,2) DEFAULT 0 COMMENT '校验得分',
`version_no` INT NOT NULL DEFAULT 1 COMMENT '当前版本号',
`current_version_id` VARCHAR(32) DEFAULT '' COMMENT '当前版本ID',
`template_id` VARCHAR(32) DEFAULT '' COMMENT '来源模板ID',
`forked_from_id` VARCHAR(32) DEFAULT '' COMMENT '派生自蓝图ID',
`status` VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/published/archived/deleted',
`extra_json` LONGTEXT COMMENT '扩展属性JSON',
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_blueprint_code` (`tenant_id`,`blueprint_code`),
KEY `idx_pbl_blueprint_tenant` (`tenant_id`,`status`),
KEY `idx_pbl_blueprint_org` (`org_id`),
KEY `idx_pbl_blueprint_created` (`tenant_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图聚合根';
CREATE TABLE IF NOT EXISTS `pbl_blueprint_learning_goal` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0',
`blueprint_id` VARCHAR(32) NOT NULL,
`seq` INT NOT NULL DEFAULT 0,
`goal_code` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NOT NULL,
`description` TEXT,
`knowledge_type` VARCHAR(32) DEFAULT 'concept',
`competency_tags` TEXT,
`assessable` VARCHAR(1) DEFAULT '1',
`alignment_json` LONGTEXT,
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_goal_code` (`tenant_id`,`blueprint_id`,`goal_code`),
KEY `idx_pbl_goal_bp` (`tenant_id`,`blueprint_id`,`seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蓝图子对象-学习目标';
CREATE TABLE IF NOT EXISTS `pbl_blueprint_role` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0',
`blueprint_id` VARCHAR(32) NOT NULL,
`seq` INT NOT NULL DEFAULT 0,
`role_code` VARCHAR(64) NOT NULL,
`role_name` VARCHAR(128) NOT NULL,
`description` TEXT,
`responsibilities` TEXT,
`required_skills` TEXT,
`min_members` INT DEFAULT 1,
`max_members` INT DEFAULT 1,
`is_mandatory` VARCHAR(1) DEFAULT '0',
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_role_code` (`tenant_id`,`blueprint_id`,`role_code`),
KEY `idx_pbl_role_bp` (`tenant_id`,`blueprint_id`,`seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蓝图子对象-PBL角色';
CREATE TABLE IF NOT EXISTS `pbl_blueprint_mission` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0',
`blueprint_id` VARCHAR(32) NOT NULL,
`seq` INT NOT NULL DEFAULT 0,
`mission_code` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NOT NULL,
`driving_question` TEXT,
`narrative` LONGTEXT,
`success_criteria` TEXT,
`related_goal_ids` TEXT,
`duration_hours` INT DEFAULT 0,
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_mission_code` (`tenant_id`,`blueprint_id`,`mission_code`),
KEY `idx_pbl_mission_bp` (`tenant_id`,`blueprint_id`,`seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蓝图子对象-驱动问题/Mission';
CREATE TABLE IF NOT EXISTS `pbl_blueprint_task` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0',
`blueprint_id` VARCHAR(32) NOT NULL,
`mission_id` VARCHAR(32) DEFAULT '',
`seq` INT NOT NULL DEFAULT 0,
`task_code` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NOT NULL,
`description` TEXT,
`task_type` VARCHAR(32) DEFAULT 'explore',
`role_id` VARCHAR(32) DEFAULT '',
`depends_on_ids` TEXT,
`estimated_minutes` INT DEFAULT 0,
`artifact_spec_ids` TEXT,
`status` VARCHAR(32) DEFAULT 'draft',
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_task_code` (`tenant_id`,`blueprint_id`,`task_code`),
KEY `idx_pbl_task_bp` (`tenant_id`,`blueprint_id`,`seq`),
KEY `idx_pbl_task_mission` (`tenant_id`,`mission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蓝图子对象-任务';
CREATE TABLE IF NOT EXISTS `pbl_blueprint_artifact_spec` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0',
`blueprint_id` VARCHAR(32) NOT NULL,
`task_id` VARCHAR(32) DEFAULT '',
`seq` INT NOT NULL DEFAULT 0,
`artifact_code` VARCHAR(64) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`artifact_type` VARCHAR(32) DEFAULT 'document',
`format` VARCHAR(64) DEFAULT '',
`description` TEXT,
`required` VARCHAR(1) DEFAULT '1',
`max_size_mb` INT DEFAULT 0,
`schema_json` LONGTEXT,
`rubric_id` VARCHAR(32) DEFAULT '',
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_artifact_code` (`tenant_id`,`blueprint_id`,`artifact_code`),
KEY `idx_pbl_artifact_bp` (`tenant_id`,`blueprint_id`,`seq`),
KEY `idx_pbl_artifact_task` (`tenant_id`,`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蓝图子对象-产出物规格';
CREATE TABLE IF NOT EXISTS `pbl_blueprint_evidence_spec` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0',
`blueprint_id` VARCHAR(32) NOT NULL,
`artifact_spec_id` VARCHAR(32) DEFAULT '',
`task_id` VARCHAR(32) DEFAULT '',
`seq` INT NOT NULL DEFAULT 0,
`evidence_code` VARCHAR(64) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`evidence_type` VARCHAR(32) DEFAULT 'result',
`collect_mode` VARCHAR(32) DEFAULT 'auto',
`source_hint` VARCHAR(255) DEFAULT '',
`idempotency_key_hint` VARCHAR(128) DEFAULT '',
`required` VARCHAR(1) DEFAULT '1',
`description` TEXT,
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_evidence_code` (`tenant_id`,`blueprint_id`,`evidence_code`),
KEY `idx_pbl_evidence_bp` (`tenant_id`,`blueprint_id`,`seq`),
KEY `idx_pbl_evidence_artifact` (`tenant_id`,`artifact_spec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蓝图子对象-证据规格';
CREATE TABLE IF NOT EXISTS `pbl_blueprint_reflection_spec` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0',
`blueprint_id` VARCHAR(32) NOT NULL,
`seq` INT NOT NULL DEFAULT 0,
`reflection_code` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NOT NULL,
`prompt` TEXT,
`reflection_type` VARCHAR(32) DEFAULT 'self',
`trigger_point` VARCHAR(64) DEFAULT 'task_end',
`required` VARCHAR(1) DEFAULT '1',
`rubric_hint` TEXT,
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_reflection_code` (`tenant_id`,`blueprint_id`,`reflection_code`),
KEY `idx_pbl_reflection_bp` (`tenant_id`,`blueprint_id`,`seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蓝图子对象-反思规格';
CREATE TABLE IF NOT EXISTS `pbl_blueprint_version` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0',
`blueprint_id` VARCHAR(32) NOT NULL,
`version_no` INT NOT NULL DEFAULT 1,
`parent_version_id` VARCHAR(32) DEFAULT '',
`change_delta` LONGTEXT COMMENT '{op,path,before,after} 列表',
`snapshot_json` LONGTEXT,
`change_reason` VARCHAR(500) DEFAULT '',
`source` VARCHAR(32) DEFAULT 'manual',
`quality_status` VARCHAR(32) DEFAULT 'draft',
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_version_no` (`tenant_id`,`blueprint_id`,`version_no`),
KEY `idx_pbl_version_bp` (`tenant_id`,`blueprint_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蓝图版本(change_delta 证明对话式改模型)';
CREATE TABLE IF NOT EXISTS `pbl_template` (
`id` VARCHAR(32) NOT NULL,
`tenant_id` VARCHAR(32) NOT NULL DEFAULT '0' COMMENT '0=平台内置全局模板',
`template_code` VARCHAR(64) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
`category` VARCHAR(64) DEFAULT 'general',
`subject_area` VARCHAR(64) DEFAULT '',
`grade_level` VARCHAR(32) DEFAULT '',
`blueprint_snapshot` LONGTEXT,
`offline_slots` LONGTEXT COMMENT 'LLM 不可用时降级填充槽位',
`is_builtin` VARCHAR(1) DEFAULT '0',
`template_version` VARCHAR(32) DEFAULT '1.0.0',
`status` VARCHAR(32) DEFAULT 'active',
`created_by` VARCHAR(32) DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_by` VARCHAR(32) DEFAULT '',
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_template_code` (`tenant_id`,`template_code`),
KEY `idx_pbl_template_cat` (`tenant_id`,`category`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL模板(含离线兜底槽位)';

View File

@ -1,64 +1,9 @@
"""pbl_blueprint —— PBL 蓝图聚合根与子对象、版本、模板(M1a)。 """pbl_blueprint —— PBL 蓝图聚合根与子对象、版本、模板模块(M1a)。
所有 async 契约函数必须在此导出,否则 .dspy 调用会 NameError。 对外唯一入口:load_pbl_blueprint(env=None)
新增契约需同步四处:api.py 定义 + 本文件导出 + init.py env 注册 所有读写 tenant_id 强制打头,缺失租户上下文一律 fail-closed 拒绝。
+ scripts/load_path.py RBAC 路径。
""" """
from pbl_blueprint.api import ( # noqa: F401 from .init import load_pbl_blueprint
MODULE_NAME,
SUBOBJECTS,
BLUEPRINT_TABLE,
VERSION_TABLE,
TEMPLATE_TABLE,
QUALITY_STATUS_ORDER,
CONTRACT_FUNCTIONS,
PblBlueprintError,
PblTenantMissing,
PblNotFound,
PblDuplicate,
PblInvalidParam,
PblSubobjectUnknown,
PblStateConflict,
pbl_blueprint_create,
pbl_blueprint_read,
pbl_blueprint_update,
pbl_blueprint_delete,
pbl_blueprint_list,
pbl_blueprint_tree,
pbl_blueprint_fork,
pbl_blueprint_subobject_save,
pbl_blueprint_subobject_list,
pbl_blueprint_subobject_delete,
pbl_blueprint_version_create,
pbl_blueprint_version_diff,
pbl_template_list,
pbl_template_instantiate,
pbl_template_save,
pbl_template_delete,
pbl_blueprint_get_contract,
safe_traceback,
)
__all__ = [ __all__ = ['load_pbl_blueprint']
'MODULE_NAME', 'SUBOBJECTS', 'BLUEPRINT_TABLE', 'VERSION_TABLE',
'TEMPLATE_TABLE', 'QUALITY_STATUS_ORDER', 'CONTRACT_FUNCTIONS',
'PblBlueprintError', 'PblTenantMissing', 'PblNotFound', 'PblDuplicate',
'PblInvalidParam', 'PblSubobjectUnknown', 'PblStateConflict',
'load_pbl_blueprint',
'pbl_blueprint_create', 'pbl_blueprint_read', 'pbl_blueprint_update',
'pbl_blueprint_delete', 'pbl_blueprint_list', 'pbl_blueprint_tree',
'pbl_blueprint_fork', 'pbl_blueprint_subobject_save',
'pbl_blueprint_subobject_list', 'pbl_blueprint_subobject_delete',
'pbl_blueprint_version_create', 'pbl_blueprint_version_diff',
'pbl_template_list', 'pbl_template_instantiate', 'pbl_template_save',
'pbl_template_delete', 'pbl_blueprint_get_contract', 'safe_traceback',
]
def __getattr__(name):
# 延迟导入 init,避免循环依赖(init.py 会 from pbl_blueprint import ...)
if name == 'load_pbl_blueprint':
from pbl_blueprint.init import load_pbl_blueprint
return load_pbl_blueprint
raise AttributeError(name)

File diff suppressed because it is too large Load Diff

View File

@ -1,346 +1,411 @@
# -*- coding: utf-8 -*- """pbl_blueprint 蓝图 CRUD 与查询契约(M1a)。
"""蓝图聚合根 CRUD 与查询契约接口(M1a)。
契约函数(跨宿主/下游模块复用面,签名稳定): 契约要点:
- save_blueprint(payload, tenant_id=None) -> dict 新建/更新蓝图(含自动编码、乐观锁) - 每个函数第一入参 tenant_id,内部 require_tenant() 强制校验,缺失即 PblTenantRequired(fail-closed)。
- get_blueprint(blueprint_id, tenant_id=None) -> dict 蓝图主记录 - 只用 sqlor 标准 API:sor.C(表, dict) / sor.U(表, dict, where) / sor.D(表, where) / sor.R(表, where) / sor.I(...) / sor.sqlExe(sql)。
- list_blueprints(filters=None, page=1, rows=20, tenant_id=None) -> dict {rows,total,page,rows_per_page} - 软删除:deleted=1,不物理删除主表。
- delete_blueprint(blueprint_id, tenant_id=None) -> dict 软删除(已发布需先撤回)
- blueprint_tree(blueprint_id, version_id=None, tenant_id=None) -> dict 聚合树(蓝图+版本+7类子对象)
- change_blueprint_status(blueprint_id, to_status, tenant_id=None, note='') -> dict 状态机流转
- publish_blueprint(blueprint_id, tenant_id=None, note='') -> dict 发布版本快照(version_no 自增)
- fork_blueprint(blueprint_id, to_class_id=None, tenant_id=None) -> dict 派生副本(含子对象)
- count_subobjects(blueprint_id, tenant_id=None) -> dict 各子对象计数(校验引擎 M2 复用)
状态机:draft -> in_review -> published -> archived;published -> in_review(改版)
租户隔离:tenant_id 强制打头,缺失 fail-closed;跨租户读取返回「不存在」。
""" """
import hashlib
from . import db as _db import datetime
from . import models as _m
from . import subobjects as _s from .db import (
PblError,
PblNotFound,
PblValidationError,
get_dbname,
get_env,
get_sor,
new_id,
require_tenant,
)
from .subobjects import SUBOBJECT_TABLES, SUBOBJECT_TYPES, list_subobjects
TABLE = 'pbl_blueprint' TABLE = 'pbl_blueprint'
TABLE_VERSION = 'pbl_blueprint_version' VERSION_TABLE = 'pbl_blueprint_version'
STATUS_DRAFT = 'draft' EDITABLE = [
STATUS_IN_REVIEW = 'in_review' 'code', 'name', 'subject', 'grade', 'duration_hours', 'status',
STATUS_PUBLISHED = 'published' 'summary', 'config', 'source_blueprint_id', 'template_id', 'owner_id', 'remark',
STATUS_ARCHIVED = 'archived' ]
STATUS_FLOW = { STATUS_FLOW = {
STATUS_DRAFT: [STATUS_IN_REVIEW, STATUS_ARCHIVED], 'draft': ['validating', 'archived'],
STATUS_IN_REVIEW: [STATUS_DRAFT, STATUS_PUBLISHED, STATUS_ARCHIVED], 'validating': ['validated', 'draft'],
STATUS_PUBLISHED: [STATUS_IN_REVIEW, STATUS_ARCHIVED], 'validated': ['compiling', 'draft'],
STATUS_ARCHIVED: [], 'compiling': ['compiled', 'validated'],
'compiled': ['published', 'draft'],
'published': ['archived', 'draft'],
'archived': ['draft'],
} }
EDITABLE_FIELDS = [
'class_id', 'blueprint_code', 'blueprint_name', 'domain_code', 'grade_code',
'subject_code', 'project_duration', 'student_count', 'group_count',
'complexity_level', 'owner_user_id', 'estimated_cost', 'tags_json',
'summary_text', 'status', 'current_version',
]
def _now(): def _now():
return _db.now_str() return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def _require_tenant(tenant_id): def _where(tenant_id, extra=None):
return tenant_id or _db.current_tenant_id() """构造 where:tenant_id 永远打头。"""
cond = "tenant_id='%s'" % tenant_id.replace("'", "''")
if extra:
cond = cond + ' and ' + extra
return cond
def _fetch(table, rec_id, tenant_id): def _pick(data, keys):
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tenant_id, out = {}
'is_deleted': 0}) for k in keys:
if k in data and data[k] is not None:
out[k] = data[k]
return out
def get_blueprint(blueprint_id, tenant_id=None): def _gen_code(sor, tenant_id):
"""取蓝图主记录;不存在/跨租户 -> PBL-NOT-FOUND。""" """生成蓝图编码 PBL-{yyyyMMdd}-{seq4}。"""
tid = _require_tenant(tenant_id) day = datetime.datetime.now().strftime('%Y%m%d')
if not blueprint_id: prefix = 'PBL-%s-' % day
raise _db.PblError(_db.ERR_PARAM_INVALID, 'blueprint_id 不能为空') rows = sor.R(TABLE, _where(tenant_id, "code like '%s%%'" % prefix))
row = _fetch(TABLE, blueprint_id, tid) seq = len(rows or []) + 1
if not row: return '%s%04d' % (prefix, seq)
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id)
return row
def next_blueprint_code(tenant_id=None): # ---------------------------------------------------------------- 蓝图 CRUD
"""自动生成蓝图编码 PBL-{年份}-{4位序号}(同租户内递增)。"""
tid = _require_tenant(tenant_id)
import datetime
year = datetime.datetime.now().strftime('%Y')
prefix = 'PBL-%s-' % year
rows = _db.select_rows(TABLE, where={'tenant_id': tid, 'blueprint_code~': prefix + '%'},
fields='blueprint_code', orderby='blueprint_code desc') or []
max_seq = 0
for r in rows:
code = (r.get('blueprint_code') or '')[len(prefix):]
try:
max_seq = max(max_seq, int(code))
except (TypeError, ValueError):
continue
return '%s%04d' % (prefix, max_seq + 1)
def create_blueprint(tenant_id, data, env=None, operator=None):
"""新建蓝图。:return: 蓝图记录 dict"""
tid = require_tenant(tenant_id)
if not isinstance(data, dict):
raise PblValidationError('data must be a dict')
if not data.get('name'):
raise PblValidationError('name is required')
def save_blueprint(payload, tenant_id=None): e = get_env(env)
"""新建(无 id)或更新(有 id)蓝图。返回落库后的记录。""" sor = get_sor(e, get_dbname(e))
tid = _require_tenant(tenant_id) if sor is None:
payload = dict(payload or {}) raise PblError('sqlor unavailable for module pbl_blueprint', code='PBL_DB_UNAVAILABLE')
data = dict((k, payload[k]) for k in EDITABLE_FIELDS if k in payload)
rec_id = payload.get('id')
now = _now()
operator = _db.current_operator()
if rec_id:
old = _fetch(TABLE, rec_id, tid)
if not old:
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % rec_id)
if old.get('status') == STATUS_PUBLISHED:
raise _db.PblError(_db.ERR_STATE_FORBIDDEN,
'已发布蓝图不可直接编辑,请先撤回或派生新版本')
code = data.get('blueprint_code') or old.get('blueprint_code')
dup = _db.select_one(TABLE, where={'tenant_id': tid, 'blueprint_code': code,
'id!': rec_id, 'is_deleted': 0})
if dup:
raise _db.PblError(_db.ERR_PARAM_INVALID, '蓝图编码已存在: %s' % code)
data.update({'updated_by': operator, 'updated_at': now})
_db.update_row(TABLE, data, {'id': rec_id, 'tenant_id': tid})
result = _fetch(TABLE, rec_id, tid)
_db.audit('blueprint_update', TABLE, rec_id, before=old, after=result, tenant_id=tid)
return result
if not data.get('blueprint_name'):
raise _db.PblError(_db.ERR_PARAM_INVALID, 'blueprint_name 必填')
if not data.get('blueprint_code'):
data['blueprint_code'] = next_blueprint_code(tid)
dup = _db.select_one(TABLE, where={'tenant_id': tid,
'blueprint_code': data['blueprint_code'],
'is_deleted': 0})
if dup:
raise _db.PblError(_db.ERR_PARAM_INVALID, '蓝图编码已存在: %s' % data['blueprint_code'])
new_id = _db.gen_id()
row = { row = {
'id': new_id, 'tenant_id': tid, 'class_id': data.get('class_id', ''), 'id': data.get('id') or new_id(),
'blueprint_code': data['blueprint_code'], 'blueprint_name': data['blueprint_name'], 'tenant_id': tid,
'domain_code': data.get('domain_code', ''), 'grade_code': data.get('grade_code', ''), 'code': data.get('code') or _gen_code(sor, tid),
'subject_code': data.get('subject_code', ''), 'name': data['name'],
'project_duration': int(data.get('project_duration') or 0), 'subject': data.get('subject'),
'student_count': int(data.get('student_count') or 0), 'grade': data.get('grade'),
'group_count': int(data.get('group_count') or 0), 'duration_hours': data.get('duration_hours'),
'complexity_level': data.get('complexity_level', ''), 'version_no': 1,
'status': STATUS_DRAFT, 'current_version': 0, 'status': data.get('status') or 'draft',
'owner_user_id': data.get('owner_user_id') or operator, 'quality_level': data.get('quality_level') or 'L1',
'estimated_cost': float(data.get('estimated_cost') or 0), 'summary': data.get('summary'),
'tags_json': data.get('tags_json') or '[]', 'config': data.get('config'),
'summary_text': data.get('summary_text') or '', 'source_blueprint_id': data.get('source_blueprint_id'),
'is_deleted': 0, 'created_by': operator, 'created_at': now, 'template_id': data.get('template_id'),
'updated_by': operator, 'updated_at': now, 'owner_id': data.get('owner_id') or operator,
'remark': data.get('remark'),
'created_by': operator,
'created_at': _now(),
'updated_by': operator,
'updated_at': _now(),
'deleted': 0,
} }
_db.insert_row(TABLE, row) sor.C(TABLE, row)
_db.audit('blueprint_create', TABLE, new_id, after=row, tenant_id=tid)
return _fetch(TABLE, new_id, tid)
def list_blueprints(filters=None, page=1, rows=20, tenant_id=None):
"""分页查询(租户强制过滤 + 软删除过滤 + 排序白名单)。"""
tid = _require_tenant(tenant_id)
filters = dict(filters or {})
where = {'tenant_id': tid, 'is_deleted': 0}
allow_eq = ('status', 'domain_code', 'grade_code', 'subject_code',
'class_id', 'complexity_level')
for k in allow_eq:
if filters.get(k):
where[k] = filters[k]
allow_like = ('blueprint_name', 'blueprint_code', 'owner_user_id')
for k in allow_like:
if filters.get(k):
where['%s~' % k] = '%%%s%%' % filters[k]
page = max(1, int(page or 1))
rows = min(200, max(1, int(rows or 20)))
allowed_sort = ('created_at', 'updated_at', 'blueprint_code', 'status',
'current_version', 'project_duration')
sortby = filters.get('sortby') if filters.get('sortby') in allowed_sort else 'created_at'
order = 'desc' if str(filters.get('sortorder', 'desc')).lower() == 'desc' else 'asc'
fields = ('id, tenant_id, class_id, blueprint_code, blueprint_name, domain_code, '
'grade_code, subject_code, project_duration, student_count, group_count, '
'complexity_level, status, current_version, owner_user_id, estimated_cost, '
'created_at, updated_at')
all_rows = _db.select_rows(TABLE, where=where, fields=fields,
orderby='%s %s' % (sortby, order)) or []
total = len(all_rows)
start = (page - 1) * rows
return {'rows': all_rows[start:start + rows], 'total': total,
'page': page, 'rows_per_page': rows}
def delete_blueprint(blueprint_id, tenant_id=None):
"""软删除;已发布蓝图须先撤回/归档。级联软删子对象。"""
tid = _require_tenant(tenant_id)
old = _fetch(TABLE, blueprint_id, tid)
if not old:
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id)
if old.get('status') == STATUS_PUBLISHED:
raise _db.PblError(_db.ERR_REFERENCED, '已发布蓝图不可删除,请先归档')
now = _now()
_db.update_row(TABLE, {'is_deleted': 1, 'updated_by': _db.current_operator(),
'updated_at': now}, {'id': blueprint_id, 'tenant_id': tid})
_s.soft_delete_by_blueprint(blueprint_id, tid)
_db.audit('blueprint_delete', TABLE, blueprint_id, before=old, tenant_id=tid)
return {'id': blueprint_id, 'deleted': True}
def change_blueprint_status(blueprint_id, to_status, tenant_id=None, note=''):
"""状态机流转(非法流转 -> PBL-STATE-001)。"""
tid = _require_tenant(tenant_id)
row = _fetch(TABLE, blueprint_id, tid)
if not row:
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id)
cur = row.get('status') or STATUS_DRAFT
if to_status not in STATUS_FLOW:
raise _db.PblError(_db.ERR_STATE_FORBIDDEN, '未知目标状态: %s' % to_status)
if to_status not in STATUS_FLOW.get(cur, []):
raise _db.PblError(_db.ERR_STATE_FORBIDDEN,
'状态不允许 %s -> %s' % (cur, to_status))
if to_status == STATUS_PUBLISHED:
return publish_blueprint(blueprint_id, tenant_id=tid, note=note)
now = _now()
_db.update_row(TABLE, {'status': to_status, 'updated_by': _db.current_operator(),
'updated_at': now}, {'id': blueprint_id, 'tenant_id': tid})
_db.audit('blueprint_status', TABLE, blueprint_id,
before={'status': cur}, after={'status': to_status, 'note': note}, tenant_id=tid)
return {'id': blueprint_id, 'status': to_status, 'from_status': cur}
def _snapshot(blueprint_id, tid):
bp = _fetch(TABLE, blueprint_id, tid)
snap = {'blueprint': dict((k, v) for k, v in (bp or {}).items()
if k not in ('is_deleted',))}
snap['subobjects'] = {}
for stype, table in _m.SUBOBJECT_TYPE_TABLE.items():
snap['subobjects'][stype] = _db.select_rows(
table, where={'tenant_id': tid, 'blueprint_id': blueprint_id, 'is_deleted': 0},
orderby='seq_no asc') or []
return snap
def _hash(text):
return hashlib.sha256(text.encode('utf-8')).hexdigest()[:64]
def publish_blueprint(blueprint_id, tenant_id=None, note=''):
"""发布:生成版本快照(version_no 自增)并把蓝图置 published。"""
tid = _require_tenant(tenant_id)
bp = _fetch(TABLE, blueprint_id, tid)
if not bp:
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id)
if bp.get('status') not in (STATUS_IN_REVIEW, STATUS_DRAFT, STATUS_PUBLISHED):
raise _db.PblError(_db.ERR_STATE_FORBIDDEN,
'当前状态不可发布: %s' % bp.get('status'))
counts = _s.count_subobjects(blueprint_id, tid)
if not counts.get('learning_goal'):
raise _db.PblError(_db.ERR_STATE_FORBIDDEN, '缺少学习目标子对象,无法发布')
if not counts.get('mission'):
raise _db.PblError(_db.ERR_STATE_FORBIDDEN, '缺少真实情境任务,无法发布')
snap = _snapshot(blueprint_id, tid)
snap_text = _db.dumps(snap)
version_no = int(bp.get('current_version') or 0) + 1
now = _now()
operator = _db.current_operator()
vid = _db.gen_id()
_db.insert_row(TABLE_VERSION, {
'id': vid, 'tenant_id': tid, 'class_id': bp.get('class_id') or '',
'blueprint_id': blueprint_id, 'version_no': version_no,
'version_name': 'v%d' % version_no, 'version_status': 'published',
'snapshot_json': snap_text, 'content_hash': _hash(snap_text),
'published_by': operator, 'published_at': now, 'change_note': note or '',
'base_version_id': bp.get('published_version_id') or '',
'is_deleted': 0, 'created_by': operator, 'created_at': now,
'updated_by': operator, 'updated_at': now,
})
_db.update_row(TABLE, {'status': STATUS_PUBLISHED, 'current_version': version_no,
'published_version_id': vid, 'updated_by': operator,
'updated_at': now}, {'id': blueprint_id, 'tenant_id': tid})
_db.audit('blueprint_publish', TABLE, blueprint_id,
after={'version_no': version_no, 'version_id': vid,
'content_hash': _hash(snap_text)}, tenant_id=tid)
return {'id': blueprint_id, 'version_id': vid, 'version_no': version_no,
'status': STATUS_PUBLISHED, 'content_hash': _hash(snap_text)}
def list_versions(blueprint_id, tenant_id=None):
tid = _require_tenant(tenant_id)
rows = _db.select_rows(TABLE_VERSION,
where={'tenant_id': tid, 'blueprint_id': blueprint_id,
'is_deleted': 0},
fields='id, blueprint_id, version_no, version_name, '
'version_status, content_hash, published_by, published_at, '
'change_note, created_at',
orderby='version_no desc') or []
return {'rows': rows, 'total': len(rows)}
def get_version(version_id, tenant_id=None):
tid = _require_tenant(tenant_id)
row = _fetch(TABLE_VERSION, version_id, tid)
if not row:
raise _db.PblError(_db.ERR_NOT_FOUND, '版本不存在或无权访问: %s' % version_id)
row['snapshot'] = _db.loads(row.get('snapshot_json'), {})
return row return row
def blueprint_tree(blueprint_id, version_id=None, tenant_id=None): def update_blueprint(tenant_id, blueprint_id, data, env=None, operator=None):
"""聚合树:蓝图主记录 + (可选版本快照)+ 7 类子对象 + 计数。""" """更新蓝图(仅 EDITABLE 字段 + 状态流转校验)。"""
tid = _require_tenant(tenant_id) tid = require_tenant(tenant_id)
bp = _fetch(TABLE, blueprint_id, tid) if not blueprint_id:
if not bp: raise PblValidationError('blueprint_id is required')
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id) old = get_blueprint(tid, blueprint_id, env=env)
children = _s.list_all_subobjects(blueprint_id, tid) patch = _pick(data or {}, EDITABLE)
tree = {'blueprint': bp, 'children': children, if not patch:
'counts': dict((k, len(v)) for k, v in children.items()), return old
'version': None} new_status = patch.get('status')
if version_id: if new_status and new_status != old.get('status'):
tree['version'] = get_version(version_id, tid) allowed = STATUS_FLOW.get(old.get('status') or 'draft', [])
if new_status not in allowed:
raise PblValidationError(
'illegal status transition %s -> %s' % (old.get('status'), new_status))
if new_status == 'published':
patch['published_at'] = _now()
patch['updated_by'] = operator
patch['updated_at'] = _now()
e = get_env(env)
sor = get_sor(e, get_dbname(e))
sor.U(TABLE, patch, _where(tid, "id='%s'" % blueprint_id))
merged = dict(old)
merged.update(patch)
return merged
def delete_blueprint(tenant_id, blueprint_id, env=None, operator=None, hard=False):
"""软删除蓝图(连带子对象与版本一并软删)。"""
tid = require_tenant(tenant_id)
if not blueprint_id:
raise PblValidationError('blueprint_id is required')
e = get_env(env)
sor = get_sor(e, get_dbname(e))
patch = {'deleted': 1, 'updated_by': operator, 'updated_at': _now()}
if hard:
sor.D(TABLE, _where(tid, "id='%s'" % blueprint_id))
else:
sor.U(TABLE, patch, _where(tid, "id='%s'" % blueprint_id))
for tbl in list(SUBOBJECT_TABLES.values()) + [VERSION_TABLE]:
if hard:
sor.D(tbl, _where(tid, "blueprint_id='%s'" % blueprint_id))
else:
sor.U(tbl, patch, _where(tid, "blueprint_id='%s'" % blueprint_id))
return {'id': blueprint_id, 'deleted': 1}
def get_blueprint(tenant_id, blueprint_id, env=None, with_children=False):
"""按 id 取蓝图;with_children=True 时附带 7 类子对象树。"""
tid = require_tenant(tenant_id)
if not blueprint_id:
raise PblValidationError('blueprint_id is required')
e = get_env(env)
sor = get_sor(e, get_dbname(e))
rows = sor.R(TABLE, _where(tid, "id='%s' and deleted=0" % blueprint_id))
if not rows:
raise PblNotFound('blueprint not found: %s' % blueprint_id, code='PBL_BLUEPRINT_NOT_FOUND')
row = rows[0]
if with_children:
row['children'] = get_blueprint_tree(tid, blueprint_id, env=env)
return row
def list_blueprints(tenant_id, filters=None, page=1, page_size=20, env=None):
"""分页查询蓝图列表。filters 支持 status/subject/grade/owner_id/keyword/template_id。"""
tid = require_tenant(tenant_id)
filters = filters or {}
conds = ["deleted=0"]
for key in ('status', 'subject', 'grade', 'owner_id', 'template_id', 'quality_level'):
if filters.get(key):
conds.append("%s='%s'" % (key, str(filters[key]).replace("'", "''")))
if filters.get('keyword'):
kw = str(filters['keyword']).replace("'", "''")
conds.append("(name like '%%%s%%' or code like '%%%s%%')" % (kw, kw))
where = _where(tid, ' and '.join(conds))
e = get_env(env)
sor = get_sor(e, get_dbname(e))
page = max(int(page or 1), 1)
page_size = min(max(int(page_size or 20), 1), 200)
offset = (page - 1) * page_size
sql = ("select * from %s where %s order by updated_at desc limit %d offset %d"
% (TABLE, where, page_size, offset))
rows = sor.sqlExe(sql)
count_sql = "select count(1) as total from %s where %s" % (TABLE, where)
total_rows = sor.sqlExe(count_sql)
total = 0
if total_rows:
first = total_rows[0]
total = first.get('total') if isinstance(first, dict) else first
return {'total': int(total or 0), 'page': page, 'page_size': page_size, 'rows': rows or []}
def get_blueprint_tree(tenant_id, blueprint_id, env=None):
"""返回蓝图 7 类子对象树:{obj_type: [rows...]}。"""
tid = require_tenant(tenant_id)
tree = {}
for obj_type in SUBOBJECT_TYPES:
tree[obj_type] = list_subobjects(tid, obj_type, blueprint_id, env=env)
return tree return tree
def fork_blueprint(blueprint_id, to_class_id=None, tenant_id=None, new_name=None): def fork_blueprint(tenant_id, blueprint_id, new_name=None, target_tenant_id=None,
"""派生副本:复制主记录 + 全部子对象到新蓝图(draft, version 0)。""" env=None, operator=None):
tid = _require_tenant(tenant_id) """复制蓝图(含全部子对象)为新蓝图,记录 source_blueprint_id。"""
src = _fetch(TABLE, blueprint_id, tid) tid = require_tenant(tenant_id)
if not src: dst_tid = require_tenant(target_tenant_id or tid)
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id) src = get_blueprint(tid, blueprint_id, env=env)
now = _now() e = get_env(env)
operator = _db.current_operator() sor = get_sor(e, get_dbname(e))
new_id = _db.gen_id()
row = dict((k, v) for k, v in src.items()) new_bp = {
row.update({ 'id': new_id(),
'id': new_id, 'tenant_id': tid, 'tenant_id': dst_tid,
'class_id': to_class_id if to_class_id is not None else (src.get('class_id') or ''), 'code': _gen_code(sor, dst_tid),
'blueprint_code': next_blueprint_code(tid), 'name': new_name or ('%s-副本' % src.get('name')),
'blueprint_name': new_name or ('%s (副本)' % (src.get('blueprint_name') or '')), 'subject': src.get('subject'),
'status': STATUS_DRAFT, 'current_version': 0, 'published_version_id': '', 'grade': src.get('grade'),
'source_blueprint_id': blueprint_id, 'source_template_id': '', 'duration_hours': src.get('duration_hours'),
'is_deleted': 0, 'created_by': operator, 'created_at': now, 'version_no': 1,
'updated_by': operator, 'updated_at': now, 'status': 'draft',
}) 'quality_level': src.get('quality_level') or 'L1',
_db.insert_row(TABLE, row) 'summary': src.get('summary'),
copied = _s.copy_subobjects(blueprint_id, new_id, tid) 'config': src.get('config'),
_db.audit('blueprint_fork', TABLE, new_id, 'source_blueprint_id': src['id'],
after={'source_blueprint_id': blueprint_id, 'copied': copied}, tenant_id=tid) 'template_id': src.get('template_id'),
return {'id': new_id, 'source_blueprint_id': blueprint_id, 'owner_id': operator or src.get('owner_id'),
'blueprint_code': row['blueprint_code'], 'copied_subobjects': copied} 'remark': src.get('remark'),
'created_by': operator,
'created_at': _now(),
'updated_by': operator,
'updated_at': _now(),
'deleted': 0,
}
sor.C(TABLE, new_bp)
id_map = {}
for obj_type in SUBOBJECT_TYPES:
rows = list_subobjects(tid, obj_type, blueprint_id, env=env)
for row in rows or []:
old_id = row.get('id')
new_oid = new_id()
id_map[old_id] = new_oid
child = dict(row)
child['id'] = new_oid
child['tenant_id'] = dst_tid
child['blueprint_id'] = new_bp['id']
for fk in ('task_id', 'parent_id', 'mission_id', 'learning_goal_id'):
if child.get(fk) and child[fk] in id_map:
child[fk] = id_map[child[fk]]
child['created_by'] = operator
child['created_at'] = _now()
child['updated_by'] = operator
child['updated_at'] = _now()
child['deleted'] = 0
sor.C(SUBOBJECT_TABLES[obj_type], child)
new_bp['children_count'] = len(id_map)
return new_bp
def status_options(): # ---------------------------------------------------------------- 版本契约
return [{'code': STATUS_DRAFT, 'label': '草稿'}, {'code': STATUS_IN_REVIEW, 'label': '评审中'},
{'code': STATUS_PUBLISHED, 'label': '已发布'}, {'code': STATUS_ARCHIVED, 'label': '已归档'}] def save_version(tenant_id, blueprint_id, remark=None, env=None, operator=None):
"""保存当前蓝图为一个新版本快照,计算 change_delta,主表 version_no 自增。"""
tid = require_tenant(tenant_id)
bp = get_blueprint(tid, blueprint_id, env=env, with_children=True)
e = get_env(env)
sor = get_sor(e, get_dbname(e))
snapshot = {
'blueprint': {k: v for k, v in bp.items() if k != 'children'},
'children': bp.get('children') or {},
}
prev_no = int(bp.get('version_no') or 1) - 1
change_delta = {'added': [], 'updated': [], 'removed': []}
if prev_no >= 1:
prev_rows = sor.R(VERSION_TABLE, _where(
tid, "blueprint_id='%s' and version_no=%d and deleted=0" % (blueprint_id, prev_no)))
if prev_rows:
change_delta = _calc_delta(prev_rows[0].get('snapshot') or {}, snapshot)
new_no = int(bp.get('version_no') or 0) + 1
version = {
'id': new_id(),
'tenant_id': tid,
'blueprint_id': blueprint_id,
'version_no': new_no,
'snapshot': snapshot,
'change_delta': change_delta,
'quality_level': bp.get('quality_level'),
'status': 'saved',
'remark': remark,
'created_by': operator,
'created_at': _now(),
'updated_by': operator,
'updated_at': _now(),
'deleted': 0,
}
sor.C(VERSION_TABLE, version)
sor.U(TABLE, {'version_no': new_no, 'updated_by': operator, 'updated_at': _now()},
_where(tid, "id='%s'" % blueprint_id))
return version
def domain_options(): def _calc_delta(prev, curr):
return [{'code': c, 'label': l} for c, l in [ """对比两版快照,产出 added/updated/removed 三段增量。"""
('science', '科学'), ('math', '数学'), ('chinese', '语文'), ('english', '英语'), delta = {'added': [], 'updated': [], 'removed': []}
('art', '艺术'), ('technology', '技术'), ('social_studies', '社会'), ('cross_subject', '跨学科')]] prev_ids, curr_ids = {}, {}
for obj_type, rows in (prev.get('children') or {}).items():
for row in rows or []:
prev_ids[row.get('id')] = (obj_type, row)
for obj_type, rows in (curr.get('children') or {}).items():
for row in rows or []:
curr_ids[row.get('id')] = (obj_type, row)
for oid, (obj_type, row) in curr_ids.items():
if oid not in prev_ids:
delta['added'].append({'obj_type': obj_type, 'id': oid, 'name': row.get('name')})
else:
old_row = prev_ids[oid][1]
changed = [k for k in row if k in old_row and row[k] != old_row[k]
and k not in ('updated_at', 'updated_by')]
if changed:
delta['updated'].append({'obj_type': obj_type, 'id': oid, 'fields': changed})
for oid, (obj_type, row) in prev_ids.items():
if oid not in curr_ids:
delta['removed'].append({'obj_type': obj_type, 'id': oid, 'name': row.get('name')})
return delta
def list_versions(tenant_id, blueprint_id, page=1, page_size=20, env=None):
"""列出蓝图版本(不含 snapshot 大字段)。"""
tid = require_tenant(tenant_id)
e = get_env(env)
sor = get_sor(e, get_dbname(e))
page = max(int(page or 1), 1)
page_size = min(max(int(page_size or 20), 1), 200)
offset = (page - 1) * page_size
where = _where(tid, "blueprint_id='%s' and deleted=0" % blueprint_id)
sql = ("select id,tenant_id,blueprint_id,version_no,quality_level,status,remark,"
"created_by,created_at from %s where %s order by version_no desc limit %d offset %d"
% (VERSION_TABLE, where, page_size, offset))
return {'rows': sor.sqlExe(sql) or [], 'page': page, 'page_size': page_size}
def diff_versions(tenant_id, blueprint_id, version_a, version_b, env=None):
"""对比两个版本,返回 change_delta(a -> b)。"""
tid = require_tenant(tenant_id)
e = get_env(env)
sor = get_sor(e, get_dbname(e))
rows_a = sor.R(VERSION_TABLE, _where(
tid, "blueprint_id='%s' and version_no=%d and deleted=0" % (blueprint_id, int(version_a))))
rows_b = sor.R(VERSION_TABLE, _where(
tid, "blueprint_id='%s' and version_no=%d and deleted=0" % (blueprint_id, int(version_b))))
if not rows_a or not rows_b:
raise PblNotFound('version not found', code='PBL_VERSION_NOT_FOUND')
return {
'from': int(version_a),
'to': int(version_b),
'delta': _calc_delta(rows_a[0].get('snapshot') or {}, rows_b[0].get('snapshot') or {}),
}
def rollback_version(tenant_id, blueprint_id, version_no, env=None, operator=None):
"""回滚蓝图到指定版本:先存当前版本快照,再按目标快照重写主表与子对象。"""
tid = require_tenant(tenant_id)
e = get_env(env)
sor = get_sor(e, get_dbname(e))
rows = sor.R(VERSION_TABLE, _where(
tid, "blueprint_id='%s' and version_no=%d and deleted=0" % (blueprint_id, int(version_no))))
if not rows:
raise PblNotFound('version not found: %s' % version_no, code='PBL_VERSION_NOT_FOUND')
save_version(tid, blueprint_id, remark='auto-snapshot before rollback', env=env, operator=operator)
snapshot = rows[0].get('snapshot') or {}
bp_snap = snapshot.get('blueprint') or {}
patch = _pick(bp_snap, EDITABLE)
patch['updated_by'] = operator
patch['updated_at'] = _now()
if patch:
sor.U(TABLE, patch, _where(tid, "id='%s'" % blueprint_id))
for obj_type in SUBOBJECT_TYPES:
sor.D(SUBOBJECT_TABLES[obj_type], _where(tid, "blueprint_id='%s'" % blueprint_id))
for obj_type, obj_rows in (snapshot.get('children') or {}).items():
if obj_type not in SUBOBJECT_TABLES:
continue
for row in obj_rows or []:
new_row = dict(row)
new_row['tenant_id'] = tid
new_row['blueprint_id'] = blueprint_id
new_row['deleted'] = 0
new_row['updated_by'] = operator
new_row['updated_at'] = _now()
sor.C(SUBOBJECT_TABLES[obj_type], new_row)
sor.U(VERSION_TABLE, {'status': 'rolled_back', 'updated_by': operator, 'updated_at': _now()},
_where(tid, "blueprint_id='%s' and version_no=%d" % (blueprint_id, int(version_no))))
return {'blueprint_id': blueprint_id, 'rolled_back_to': int(version_no)}

View File

@ -1,200 +1,153 @@
"""pbl_blueprint 数据访问适配层(唯一允许直接调用 sqlor 的地方)。 """pbl_blueprint 数据访问层。
规范约束: - 库名一律 ServerEnv().get_module_dbname('pbl_blueprint'),禁止硬编码 DBNAME。
- 只使用 sqlor 标准 API:sor.C / sor.U / sor.D / sor.R / sor.I / sor.sqlExe - 所有 SQL 只用 sqlor 标准 API:sor.C / sor.U / sor.D / sor.R / sor.I / sor.sqlExe。
(禁止 save()/list()/insert()/query() 等编造 API)。 - tenant_id 强制打头:任何读写条件第一列必须是 tenant_id,缺失即抛 PblTenantRequired。
- 库名一律通过 ServerEnv().get_module_dbname('pbl_blueprint') 获取,禁止硬编码 DBNAME。
- 租户隔离:所有读写强制带 tenant_id,缺失即 fail-closed 抛错。
""" """
import json
import os
import uuid import uuid
MODULE_NAME = 'pbl_blueprint' MODULE_NAME = 'pbl_blueprint'
# ---------------------------------------------------------------- 错误码 TABLES = [
ERR_DBNAME_MISSING = 'PBL-DB-001' 'pbl_blueprint',
ERR_TENANT_MISSING = 'PBL-TENANT-001' 'pbl_blueprint_version',
ERR_NOT_FOUND = 'PBL-NOT-FOUND' 'pbl_blueprint_template',
ERR_STATE_FORBIDDEN = 'PBL-STATE-001' 'pbl_blueprint_template_item',
ERR_PARAM_INVALID = 'PBL-PARAM-001' 'pbl_blueprint_task',
ERR_REFERENCED = 'PBL-REF-001' 'pbl_blueprint_mission',
'pbl_blueprint_role',
'pbl_blueprint_learning_goal',
'pbl_blueprint_evidence_spec',
'pbl_blueprint_artifact_spec',
'pbl_blueprint_reflection_spec',
]
class PblError(Exception): class PblError(Exception):
"""统一业务异常,携带错误码 + i18n 文案键。""" """pbl 模块统一异常基类。"""
def __init__(self, code, msg, i18n_key=None, detail=None): code = 'PBL_ERROR'
Exception.__init__(self, '[%s] %s' % (code, msg))
self.code = code
self.msg = msg
self.i18n_key = i18n_key or ('pbl_blueprint.err.%s' % code.lower())
self.detail = detail or {}
def to_dict(self): def __init__(self, message, code=None, **extra):
return {'code': self.code, 'msg': self.msg, super(PblError, self).__init__(message)
'i18n_key': self.i18n_key, 'detail': self.detail} self.message = message
if code:
self.code = code
self.extra = extra
# ---------------------------------------------------------------- 环境/库名 class PblTenantRequired(PblError):
def server_env(): """租户上下文缺失(fail-closed)。"""
from ahserver import ServerEnv
return ServerEnv() code = 'PBL_TENANT_REQUIRED'
def get_dbname(): class PblNotFound(PblError):
"""取本模块库名:优先 ServerEnv 注册值,其次 pbl_common 适配层。""" """记录不存在。"""
dbname = None
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
try: try:
env = server_env() from sage import ServerEnv
getter = getattr(env, 'get_module_dbname', None) return ServerEnv()
if callable(getter):
dbname = getter(MODULE_NAME)
except Exception: except Exception:
dbname = None return None
if not dbname:
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):
try: try:
from pbl_common.db_adapter import get_module_dbname as _g return getter(MODULE_NAME)
dbname = _g(MODULE_NAME)
except Exception: except Exception:
dbname = None return None
if not dbname: return None
raise PblError(ERR_DBNAME_MISSING,
'模块 %s 未注册库名(ServerEnv.set_module_dbname 缺失)' % MODULE_NAME)
return dbname
def current_tenant_id(required=True): def get_sor(env=None, dbname=None):
"""取当前租户;优先复用 pbl_common 租户上下文,其次 ServerEnv 上挂载的上下文。""" """取 sqlor 数据访问对象。"""
tid = None e = get_env(env)
try: if e is None:
from pbl_common.tenant import get_tenant_id return None
tid = get_tenant_id() db = dbname or get_dbname(e)
except Exception: for attr in ('sqlor', 'sor'):
tid = None obj = getattr(e, attr, None)
if not tid: if obj is not None:
return obj
getter = getattr(e, 'get_sqlor', None)
if callable(getter):
try: try:
ctx = getattr(server_env(), 'pbl_tenant_ctx', None) or {} return getter(db) if db else getter()
tid = ctx.get('tenant_id') if isinstance(ctx, dict) else None
except Exception: except Exception:
tid = None return None
if not tid and required: return None
raise PblError(ERR_TENANT_MISSING, '缺少租户上下文,拒绝访问蓝图数据')
return tid or ''
def current_operator(): def require_tenant(tenant_id):
try: """tenant_id 强制打头校验:空/非字符串一律拒绝。"""
from pbl_common.tenant import get_current_user if tenant_id is None:
return get_current_user() or 'system' raise PblTenantRequired('tenant_id is required (租户上下文缺失,拒绝访问)')
except Exception: if not isinstance(tenant_id, str) or not tenant_id.strip():
pass raise PblTenantRequired('tenant_id must be a non-empty string')
try: return tenant_id.strip()
return getattr(server_env(), 'pbl_current_user', None) or 'system'
except Exception:
return 'system'
# ---------------------------------------------------------------- 主键 def ensure_tables(env=None, dbname=None):
def gen_id(): """幂等建表:执行 sql/pbl_blueprint.core.sql 与 sql/pbl_blueprint.subobjects.sql。"""
"""32 位字符串主键(对应 models 中 id: str/32)。""" e = get_env(env)
try: db = dbname or get_dbname(e)
from pbl_common.ids import new_id sor = get_sor(e, db)
v = new_id() if sor is None:
if v:
return str(v)[:32]
except Exception:
pass
return uuid.uuid4().hex[:32]
# ---------------------------------------------------------------- sqlor 封装
def _sor():
import sqlor
return sqlor
def _call(func_name, *args, **kw):
sor = _sor()
fn = getattr(sor, func_name, None)
if fn is None:
raise PblError(ERR_DBNAME_MISSING, 'sqlor 缺少 API: %s' % func_name)
kw = dict(kw)
kw.setdefault('dbname', get_dbname())
try:
return fn(*args, **kw)
except TypeError:
# 兼容位置参数签名(dbname 位置化)的 sqlor 版本
kw.pop('dbname', None)
return fn(*args, **kw)
def insert_row(table, data):
return _call('C', table, data)
def insert_rows(table, rows):
return _call('I', table, rows)
def update_row(table, data, where):
return _call('U', table, data, where)
def delete_row(table, where):
return _call('D', table, where)
def select_rows(table, where=None, fields='*', orderby=None, limit=None):
return _call('R', table, where=where, fields=fields,
orderby=orderby, limit=limit)
def select_one(table, where=None, fields='*'):
rows = select_rows(table, where=where, fields=fields, limit=1) or []
return rows[0] if rows else None
def sql_exe(sql, params=None):
return _call('sqlExe', sql, params if params is not None else {})
# ---------------------------------------------------------------- 通用工具
def dumps(obj):
return json.dumps(obj if obj is not None else {}, ensure_ascii=False, sort_keys=False)
def loads(text, default=None):
if text in (None, ''):
return {} if default is None else default
if isinstance(text, (dict, list)):
return text
try:
return json.loads(text)
except Exception:
return {} if default is None else default
def now_str():
import datetime
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def tenant_where(tenant_id, extra=None):
"""构造强制打头的租户条件。"""
where = {'tenant_id': tenant_id or current_tenant_id()}
if extra:
where.update(extra)
return where
def audit(action, table, record_id, before=None, after=None, tenant_id=None):
"""审计写入委托 pbl_common;不可用时静默降级(不阻塞主流程)。"""
try:
from pbl_common.audit import write_audit
write_audit(module=MODULE_NAME, action=action, table=table,
record_id=record_id, before=before, after=after,
tenant_id=tenant_id or current_tenant_id(required=False),
operator=current_operator())
return True
except Exception:
return False 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

View File

@ -1,88 +1,162 @@
"""pbl_blueprint.init —— 模块挂载入口。 """pbl_blueprint 模块装配入口。
宿主应用(apps/pbls/app/pbls.py)在 init() 中: 三处同步注册(module-development-spec):
from pbl_blueprint.init import load_pbl_blueprint 1) 函数定义:本文件 load_pbl_blueprint()
load_pbl_blueprint() 2) 包导出: __init__.py -> from .init import load_pbl_blueprint
3) env 注册:ServerEnv().register_module('pbl_blueprint', load_pbl_blueprint)
本函数把全部契约函数注册到 ServerEnv(进程级单例),.dspy 里即可直接调用。
注意:不要在此处调用 get_user/get_userorgid —— 那是 request._run_ns 上的
每请求上下文,ServerEnv 上取不到(会静默返回 None)。
""" """
from ahserver.serverEnv import ServerEnv import os
from pbl_blueprint.api import ( from .db import ensure_tables, get_dbname
pbl_blueprint_create, from .blueprint_crud import (
pbl_blueprint_read, create_blueprint,
pbl_blueprint_update, update_blueprint,
pbl_blueprint_delete, delete_blueprint,
pbl_blueprint_list, get_blueprint,
pbl_blueprint_tree, list_blueprints,
pbl_blueprint_fork, get_blueprint_tree,
pbl_blueprint_subobject_save, fork_blueprint,
pbl_blueprint_subobject_list, save_version,
pbl_blueprint_subobject_delete, list_versions,
pbl_blueprint_version_create, diff_versions,
pbl_blueprint_version_diff, rollback_version,
pbl_template_list, )
pbl_template_instantiate, from .subobjects import (
pbl_template_save, SUBOBJECT_TYPES,
pbl_template_delete, create_subobject,
pbl_blueprint_get_contract, update_subobject,
PblBlueprintError, delete_subobject,
safe_traceback, 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,
) )
_loaded = False MODULE_NAME = 'pbl_blueprint'
# 本模块全部表(核心 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',
]
def load_pbl_blueprint(): def load_pbl_blueprint(env=None):
"""注册 pbl_blueprint 全部契约到 ServerEnv(幂等)。""" """挂载 pbl_blueprint 模块到应用。
global _loaded
env = ServerEnv()
# ---- 蓝图聚合根 CRUD + 查询 :param env: ServerEnv 实例(应用 init() 里逐个 load 时传入);为空则自建。
env.pbl_blueprint_create = pbl_blueprint_create :return: 模块契约字典(供应用/其他模块按契约调用)
env.pbl_blueprint_read = pbl_blueprint_read """
env.pbl_blueprint_update = pbl_blueprint_update if env is None:
env.pbl_blueprint_delete = pbl_blueprint_delete try:
env.pbl_blueprint_list = pbl_blueprint_list from sage import ServerEnv
env.pbl_blueprint_tree = pbl_blueprint_tree env = ServerEnv()
env.pbl_blueprint_fork = pbl_blueprint_fork except Exception:
env = None
# ---- 子对象契约 dbname = get_dbname(env)
env.pbl_blueprint_subobject_save = pbl_blueprint_subobject_save
env.pbl_blueprint_subobject_list = pbl_blueprint_subobject_list
env.pbl_blueprint_subobject_delete = pbl_blueprint_subobject_delete
# ---- 版本契约 # 建表(幂等,IF NOT EXISTS)
env.pbl_blueprint_version_create = pbl_blueprint_version_create ensure_tables(env, dbname)
env.pbl_blueprint_version_diff = pbl_blueprint_version_diff
# ---- 模板契约 # CRUD 定义注册(json/*.json)
env.pbl_template_list = pbl_template_list _register_crud_json(env)
env.pbl_template_instantiate = pbl_template_instantiate
env.pbl_template_save = pbl_template_save
env.pbl_template_delete = pbl_template_delete
# ---- 下游模块只读契约(compiler / validation / runtime) contract = {
env.pbl_blueprint_get_contract = pbl_blueprint_get_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,
}
# ---- 异常类型与工具(供 dspy 统一错误处理) if env is not None:
env.PblBlueprintError = PblBlueprintError for fn in ('register_module', 'set_module_contract', 'register_contract'):
env.pbl_blueprint_safe_traceback = safe_traceback reg = getattr(env, fn, None)
if callable(reg):
try:
reg(MODULE_NAME, contract)
except TypeError:
try:
reg(contract)
except Exception:
pass
except Exception:
pass
break
# xls2ui 生成的 CRUD 包装用复数表名,需同时注册(否则新增按钮 500) return contract
env.create_pbl_blueprints = pbl_blueprint_create
env.update_pbl_blueprints = pbl_blueprint_update
env.delete_pbl_blueprints = pbl_blueprint_delete
env.create_pbl_templates = pbl_template_save
env.update_pbl_templates = pbl_template_save
env.delete_pbl_templates = pbl_template_delete
_loaded = True
return env
def is_loaded(): def _register_crud_json(env):
return _loaded """把 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)
try:
import json as _json
with open(path, 'r', encoding='utf-8') as fp:
defs[fname[:-5]] = _json.load(fp)
except Exception:
continue
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):
try:
reg(defs)
except Exception:
pass
break
return defs

View File

@ -1,124 +1,58 @@
{ {
"pbl_blueprint": { "tblname": "pbl_blueprint",
"summary": "PBL蓝图聚合根", "params": {
"alias": "pbl_blueprint_crud", "editable": [
"params": { "code",
"sortby": "created_at", "name",
"sortorder": "desc", "subject",
"tenant_scoped": true, "grade",
"browserfields": { "duration_hours",
"fields": [ "status",
"id", "summary",
"tenant_id", "config",
"class_id", "source_blueprint_id",
"blueprint_code", "template_id",
"blueprint_name", "owner_id",
"domain_code", "remark"
"grade_code", ],
"subject_code", "browserfields": [
"complexity_level", "code",
"status", "name",
"created_at", "subject",
"updated_at" "grade",
], "duration_hours",
"alters": { "version_no",
"domain_code": { "status",
"uitype": "code", "quality_level",
"appcode": "pbl_blueprint_domain", "owner_id",
"label": "领域" "template_id",
}, "published_at",
"grade_code": { "updated_at"
"uitype": "code", ],
"appcode": "pbl_grade_code", "searchfields": [
"label": "学段" "tenant_id",
}, "code",
"subject_code": { "name",
"uitype": "code", "subject",
"appcode": "pbl_subject_code", "status",
"label": "学科" "owner_id"
}, ],
"complexity_level": { "orderby": "updated_at desc",
"uitype": "code", "defaultfilter": {
"appcode": "pbl_complexity_level", "deleted": 0
"label": "复杂度"
},
"status": {
"uitype": "code",
"appcode": "pbl_blueprint_status",
"label": "蓝图状态"
}
},
"exclouded": [
"snapshot_json",
"content_json",
"item_payload_json",
"quality_criteria",
"knowledge_points",
"tags_json"
]
},
"editable": {
"new_data_url": "{{entire_url('../api/blueprint_save.dspy')}}",
"update_data_url": "{{entire_url('../api/blueprint_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/blueprint_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/blueprint_list.dspy')}}",
"get_data_url": "{{entire_url('../api/blueprint_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_code",
"blueprint_name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "PBL蓝图",
"domain_code": { "readonly": [
"appcode": "pbl_blueprint_domain", "id",
"label": "领域" "tenant_id",
}, "version_no",
"grade_code": { "quality_level",
"appcode": "pbl_grade_code", "published_at",
"label": "学段" "created_by",
}, "created_at",
"subject_code": { "updated_by",
"appcode": "pbl_subject_code", "updated_at",
"label": "学科" "deleted"
}, ]
"complexity_level": {
"appcode": "pbl_complexity_level",
"label": "复杂度"
},
"status": {
"appcode": "pbl_blueprint_status",
"label": "蓝图状态"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"uk_blueprint_tenant_code",
"idx_blueprint_tenant_status",
"idx_blueprint_class"
]
}
} }
} }

View File

@ -1,89 +1,57 @@
{ {
"pbl_blueprint_artifact_spec": { "tblname": "pbl_blueprint_artifact_spec",
"summary": "蓝图子对象-产出物规格(Artifact)", "params": {
"alias": "pbl_blueprint_artifact_spec_crud", "editable": [
"params": { "blueprint_id",
"sortby": "created_at", "task_id",
"sortorder": "desc", "mission_id",
"tenant_scoped": true, "seq",
"browserfields": { "code",
"fields": [ "name",
"id", "artifact_type",
"tenant_id", "description",
"class_id", "spec",
"code", "accept_criteria",
"name", "required",
"seq_no", "weight",
"status", "status"
"created_at", ],
"updated_at" "browserfields": [
], "blueprint_id",
"alters": { "task_id",
"status": { "mission_id",
"uitype": "code", "seq",
"appcode": "pbl_subobject_status", "code",
"label": "状态" "name",
}, "artifact_type",
"artifact_type": { "required",
"uitype": "code", "weight",
"appcode": "pbl_artifact_type", "status",
"label": "产出物类型" "updated_at"
} ],
}, "searchfields": [
"exclouded": [ "tenant_id",
"snapshot_json", "blueprint_id",
"content_json", "task_id",
"item_payload_json", "mission_id",
"quality_criteria", "artifact_type",
"knowledge_points", "code",
"tags_json" "name",
] "status"
}, ],
"editable": { "orderby": "seq asc",
"new_data_url": "{{entire_url('../api/subobject_save.dspy')}}", "defaultfilter": {
"update_data_url": "{{entire_url('../api/subobject_save.dspy')}}", "deleted": 0
"delete_data_url": "{{entire_url('../api/subobject_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/subobject_list.dspy')}}",
"get_data_url": "{{entire_url('../api/subobject_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_id",
"name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "蓝图子对象-产出物规格",
"artifact_type": { "readonly": [
"appcode": "pbl_artifact_type", "id",
"label": "产出物类型" "tenant_id",
} "created_by",
}, "created_at",
"table_meta": { "updated_by",
"pk": "id", "updated_at",
"tenant_field": "tenant_id", "deleted"
"soft_delete_field": "is_deleted", ]
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_artifact_spec_tenant_bp",
"uk_pbl_blueprint_artifact_spec_tenant_code"
]
}
} }
} }

View File

@ -1,89 +1,60 @@
{ {
"pbl_blueprint_evidence_spec": { "tblname": "pbl_blueprint_evidence_spec",
"summary": "蓝图子对象-证据规格(Evidence)", "params": {
"alias": "pbl_blueprint_evidence_spec_crud", "editable": [
"params": { "blueprint_id",
"sortby": "created_at", "task_id",
"sortorder": "desc", "mission_id",
"tenant_scoped": true, "learning_goal_id",
"browserfields": { "seq",
"fields": [ "code",
"id", "name",
"tenant_id", "evidence_type",
"class_id", "collect_mode",
"code", "description",
"name", "spec",
"seq_no", "rule",
"status", "weight",
"created_at", "status"
"updated_at" ],
], "browserfields": [
"alters": { "blueprint_id",
"status": { "task_id",
"uitype": "code", "mission_id",
"appcode": "pbl_subobject_status", "learning_goal_id",
"label": "状态" "seq",
}, "code",
"evidence_type": { "name",
"uitype": "code", "evidence_type",
"appcode": "pbl_evidence_type", "collect_mode",
"label": "证据类型" "weight",
} "status",
}, "updated_at"
"exclouded": [ ],
"snapshot_json", "searchfields": [
"content_json", "tenant_id",
"item_payload_json", "blueprint_id",
"quality_criteria", "task_id",
"knowledge_points", "mission_id",
"tags_json" "learning_goal_id",
] "evidence_type",
}, "code",
"editable": { "name",
"new_data_url": "{{entire_url('../api/subobject_save.dspy')}}", "status"
"update_data_url": "{{entire_url('../api/subobject_save.dspy')}}", ],
"delete_data_url": "{{entire_url('../api/subobject_delete.dspy')}}", "orderby": "seq asc",
"list_data_url": "{{entire_url('../api/subobject_list.dspy')}}", "defaultfilter": {
"get_data_url": "{{entire_url('../api/subobject_get.dspy')}}", "deleted": 0
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_id",
"name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "蓝图子对象-证据规格",
"evidence_type": { "readonly": [
"appcode": "pbl_evidence_type", "id",
"label": "证据类型" "tenant_id",
} "created_by",
}, "created_at",
"table_meta": { "updated_by",
"pk": "id", "updated_at",
"tenant_field": "tenant_id", "deleted"
"soft_delete_field": "is_deleted", ]
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_evidence_spec_tenant_bp",
"uk_pbl_blueprint_evidence_spec_tenant_code"
]
}
} }
} }

View File

@ -1,89 +1,51 @@
{ {
"pbl_blueprint_learning_goal": { "tblname": "pbl_blueprint_learning_goal",
"summary": "蓝图子对象-学习目标(Goal)", "params": {
"alias": "pbl_blueprint_learning_goal_crud", "editable": [
"params": { "blueprint_id",
"sortby": "created_at", "task_id",
"sortorder": "desc", "seq",
"tenant_scoped": true, "code",
"browserfields": { "name",
"fields": [ "dimension",
"id", "description",
"tenant_id", "spec",
"class_id", "weight",
"code", "status"
"name", ],
"seq_no", "browserfields": [
"status", "blueprint_id",
"created_at", "task_id",
"updated_at" "seq",
], "code",
"alters": { "name",
"status": { "dimension",
"uitype": "code", "weight",
"appcode": "pbl_subobject_status", "status",
"label": "状态" "updated_at"
}, ],
"goal_type": { "searchfields": [
"uitype": "code", "tenant_id",
"appcode": "pbl_goal_type", "blueprint_id",
"label": "目标类型" "task_id",
} "dimension",
}, "code",
"exclouded": [ "name",
"snapshot_json", "status"
"content_json", ],
"item_payload_json", "orderby": "seq asc",
"quality_criteria", "defaultfilter": {
"knowledge_points", "deleted": 0
"tags_json"
]
},
"editable": {
"new_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"update_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/subobject_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/subobject_list.dspy')}}",
"get_data_url": "{{entire_url('../api/subobject_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_id",
"name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "蓝图子对象-学习目标",
"goal_type": { "readonly": [
"appcode": "pbl_goal_type", "id",
"label": "目标类型" "tenant_id",
} "created_by",
}, "created_at",
"table_meta": { "updated_by",
"pk": "id", "updated_at",
"tenant_field": "tenant_id", "deleted"
"soft_delete_field": "is_deleted", ]
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_learning_goal_tenant_bp",
"uk_pbl_blueprint_learning_goal_tenant_code"
]
}
} }
} }

View File

@ -1,89 +1,47 @@
{ {
"pbl_blueprint_mission": { "tblname": "pbl_blueprint_mission",
"summary": "蓝图子对象-真实情境任务(Mission)", "params": {
"alias": "pbl_blueprint_mission_crud", "editable": [
"params": { "blueprint_id",
"sortby": "created_at", "task_id",
"sortorder": "desc", "seq",
"tenant_scoped": true, "code",
"browserfields": { "name",
"fields": [ "description",
"id", "spec",
"tenant_id", "unlock_condition",
"class_id", "status"
"code", ],
"name", "browserfields": [
"seq_no", "blueprint_id",
"status", "task_id",
"created_at", "seq",
"updated_at" "code",
], "name",
"alters": { "status",
"status": { "updated_at"
"uitype": "code", ],
"appcode": "pbl_subobject_status", "searchfields": [
"label": "状态" "tenant_id",
}, "blueprint_id",
"mission_type": { "task_id",
"uitype": "code", "code",
"appcode": "pbl_mission_type", "name",
"label": "情境类型" "status"
} ],
}, "orderby": "seq asc",
"exclouded": [ "defaultfilter": {
"snapshot_json", "deleted": 0
"content_json",
"item_payload_json",
"quality_criteria",
"knowledge_points",
"tags_json"
]
},
"editable": {
"new_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"update_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/subobject_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/subobject_list.dspy')}}",
"get_data_url": "{{entire_url('../api/subobject_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_id",
"name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "蓝图子对象-关卡",
"mission_type": { "readonly": [
"appcode": "pbl_mission_type", "id",
"label": "情境类型" "tenant_id",
} "created_by",
}, "created_at",
"table_meta": { "updated_by",
"pk": "id", "updated_at",
"tenant_field": "tenant_id", "deleted"
"soft_delete_field": "is_deleted", ]
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_mission_tenant_bp",
"uk_pbl_blueprint_mission_tenant_code"
]
}
} }
} }

View File

@ -1,98 +1,58 @@
{ {
"pbl_blueprint_reflection_spec": { "tblname": "pbl_blueprint_reflection_spec",
"summary": "蓝图子对象-反思规格(Reflection)", "params": {
"alias": "pbl_blueprint_reflection_spec_crud", "editable": [
"params": { "blueprint_id",
"sortby": "created_at", "task_id",
"sortorder": "desc", "mission_id",
"tenant_scoped": true, "seq",
"browserfields": { "code",
"fields": [ "name",
"id", "trigger_point",
"tenant_id", "reflection_type",
"class_id", "description",
"code", "spec",
"name", "rubric_ref",
"seq_no", "weight",
"status", "status"
"created_at", ],
"updated_at" "browserfields": [
], "blueprint_id",
"alters": { "task_id",
"status": { "mission_id",
"uitype": "code", "seq",
"appcode": "pbl_subobject_status", "code",
"label": "状态" "name",
}, "trigger_point",
"reflect_type": { "reflection_type",
"uitype": "code", "weight",
"appcode": "pbl_reflect_type", "status",
"label": "反思类型" "updated_at"
}, ],
"frequency": { "searchfields": [
"uitype": "code", "tenant_id",
"appcode": "pbl_reflect_frequency", "blueprint_id",
"label": "频次" "task_id",
} "mission_id",
}, "trigger_point",
"exclouded": [ "reflection_type",
"snapshot_json", "code",
"content_json", "name",
"item_payload_json", "status"
"quality_criteria", ],
"knowledge_points", "orderby": "seq asc",
"tags_json" "defaultfilter": {
] "deleted": 0
},
"editable": {
"new_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"update_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/subobject_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/subobject_list.dspy')}}",
"get_data_url": "{{entire_url('../api/subobject_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_id",
"name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "蓝图子对象-反思规格",
"reflect_type": { "readonly": [
"appcode": "pbl_reflect_type", "id",
"label": "反思类型" "tenant_id",
}, "created_by",
"frequency": { "created_at",
"appcode": "pbl_reflect_frequency", "updated_by",
"label": "频次" "updated_at",
} "deleted"
}, ]
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_reflection_spec_tenant_bp",
"uk_pbl_blueprint_reflection_spec_tenant_code"
]
}
} }
} }

View File

@ -1,89 +1,47 @@
{ {
"pbl_blueprint_role": { "tblname": "pbl_blueprint_role",
"summary": "蓝图子对象-角色(Role)", "params": {
"alias": "pbl_blueprint_role_crud", "editable": [
"params": { "blueprint_id",
"sortby": "created_at", "task_id",
"sortorder": "desc", "seq",
"tenant_scoped": true, "code",
"browserfields": { "name",
"fields": [ "description",
"id", "spec",
"tenant_id", "permissions",
"class_id", "status"
"code", ],
"name", "browserfields": [
"seq_no", "blueprint_id",
"status", "task_id",
"created_at", "seq",
"updated_at" "code",
], "name",
"alters": { "status",
"status": { "updated_at"
"uitype": "code", ],
"appcode": "pbl_subobject_status", "searchfields": [
"label": "状态" "tenant_id",
}, "blueprint_id",
"role_category": { "task_id",
"uitype": "code", "code",
"appcode": "pbl_role_category", "name",
"label": "角色类别" "status"
} ],
}, "orderby": "seq asc",
"exclouded": [ "defaultfilter": {
"snapshot_json", "deleted": 0
"content_json",
"item_payload_json",
"quality_criteria",
"knowledge_points",
"tags_json"
]
},
"editable": {
"new_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"update_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/subobject_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/subobject_list.dspy')}}",
"get_data_url": "{{entire_url('../api/subobject_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_id",
"name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "蓝图子对象-角色",
"role_category": { "readonly": [
"appcode": "pbl_role_category", "id",
"label": "角色类别" "tenant_id",
} "created_by",
}, "created_at",
"table_meta": { "updated_by",
"pk": "id", "updated_at",
"tenant_field": "tenant_id", "deleted"
"soft_delete_field": "is_deleted", ]
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_role_tenant_bp",
"uk_pbl_blueprint_role_tenant_code"
]
}
} }
} }

View File

@ -1,89 +1,46 @@
{ {
"pbl_blueprint_task": { "tblname": "pbl_blueprint_task",
"summary": "蓝图子对象-任务分解(Task)", "params": {
"alias": "pbl_blueprint_task_crud", "editable": [
"params": { "blueprint_id",
"sortby": "created_at", "parent_id",
"sortorder": "desc", "seq",
"tenant_scoped": true, "code",
"browserfields": { "name",
"fields": [ "description",
"id", "spec",
"tenant_id", "status"
"class_id", ],
"code", "browserfields": [
"name", "blueprint_id",
"seq_no", "seq",
"status", "code",
"created_at", "name",
"updated_at" "parent_id",
], "status",
"alters": { "updated_at"
"status": { ],
"uitype": "code", "searchfields": [
"appcode": "pbl_subobject_status", "tenant_id",
"label": "状态" "blueprint_id",
}, "parent_id",
"task_type": { "code",
"uitype": "code", "name",
"appcode": "pbl_task_type", "status"
"label": "任务类型" ],
} "orderby": "seq asc",
}, "defaultfilter": {
"exclouded": [ "deleted": 0
"snapshot_json",
"content_json",
"item_payload_json",
"quality_criteria",
"knowledge_points",
"tags_json"
]
},
"editable": {
"new_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"update_data_url": "{{entire_url('../api/subobject_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/subobject_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/subobject_list.dspy')}}",
"get_data_url": "{{entire_url('../api/subobject_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_id",
"name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "蓝图子对象-任务",
"task_type": { "readonly": [
"appcode": "pbl_task_type", "id",
"label": "任务类型" "tenant_id",
} "created_by",
}, "created_at",
"table_meta": { "updated_by",
"pk": "id", "updated_at",
"tenant_field": "tenant_id", "deleted"
"soft_delete_field": "is_deleted", ]
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_task_tenant_bp",
"uk_pbl_blueprint_task_tenant_code"
]
}
} }
} }

View File

@ -1,141 +1,49 @@
{ {
"pbl_blueprint_template": { "tblname": "pbl_blueprint_template",
"summary": "PBL蓝图模板", "params": {
"alias": "pbl_blueprint_template_crud", "editable": [
"params": { "code",
"sortby": "created_at", "name",
"sortorder": "desc", "category",
"tenant_scoped": true, "subject",
"browserfields": { "grade",
"fields": [ "duration_hours",
"id", "description",
"tenant_id", "content",
"class_id", "status"
"template_code", ],
"template_name", "browserfields": [
"domain_code", "code",
"grade_code", "name",
"subject_code", "category",
"complexity_level", "subject",
"status", "grade",
"created_at", "duration_hours",
"updated_at" "status",
], "use_count",
"alters": { "updated_at"
"template_type": { ],
"uitype": "code", "searchfields": [
"appcode": "pbl_template_type", "tenant_id",
"label": "模板类型" "code",
}, "name",
"domain_code": { "category",
"uitype": "code", "status"
"appcode": "pbl_blueprint_domain", ],
"label": "领域" "orderby": "use_count desc, updated_at desc",
}, "defaultfilter": {
"grade_code": { "deleted": 0
"uitype": "code",
"appcode": "pbl_grade_code",
"label": "学段"
},
"subject_code": {
"uitype": "code",
"appcode": "pbl_subject_code",
"label": "学科"
},
"complexity_level": {
"uitype": "code",
"appcode": "pbl_complexity_level",
"label": "复杂度"
},
"scope": {
"uitype": "code",
"appcode": "pbl_template_scope",
"label": "可见范围"
},
"status": {
"uitype": "code",
"appcode": "pbl_template_status",
"label": "状态"
}
},
"exclouded": [
"snapshot_json",
"content_json",
"item_payload_json",
"quality_criteria",
"knowledge_points",
"tags_json"
]
},
"editable": {
"new_data_url": "{{entire_url('../api/template_save.dspy')}}",
"update_data_url": "{{entire_url('../api/template_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/template_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/template_list.dspy')}}",
"get_data_url": "{{entire_url('../api/template_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"template_code",
"template_name"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "PBL蓝图模板",
"template_type": { "readonly": [
"appcode": "pbl_template_type", "id",
"label": "模板类型" "tenant_id",
}, "use_count",
"domain_code": { "created_by",
"appcode": "pbl_blueprint_domain", "created_at",
"label": "领域" "updated_by",
}, "updated_at",
"grade_code": { "deleted"
"appcode": "pbl_grade_code", ]
"label": "学段"
},
"subject_code": {
"appcode": "pbl_subject_code",
"label": "学科"
},
"complexity_level": {
"appcode": "pbl_complexity_level",
"label": "复杂度"
},
"scope": {
"appcode": "pbl_template_scope",
"label": "可见范围"
},
"status": {
"appcode": "pbl_template_status",
"label": "状态"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"uk_template_tenant_code",
"idx_template_tenant_status"
]
}
} }
} }

View File

@ -1,82 +1,43 @@
{ {
"pbl_blueprint_template_item": { "tblname": "pbl_blueprint_template_item",
"summary": "PBL蓝图模板明细", "params": {
"alias": "pbl_blueprint_template_item_crud", "editable": [
"params": { "template_id",
"sortby": "created_at", "obj_type",
"sortorder": "desc", "parent_ref",
"tenant_scoped": true, "ref_key",
"browserfields": { "seq",
"fields": [ "name",
"id", "spec"
"tenant_id", ],
"class_id", "browserfields": [
"subobject_type", "template_id",
"seq_no", "obj_type",
"created_at", "seq",
"updated_at" "name",
], "ref_key",
"alters": { "parent_ref",
"subobject_type": { "updated_at"
"uitype": "code", ],
"appcode": "pbl_subobject_type", "searchfields": [
"label": "子对象类型" "tenant_id",
} "template_id",
}, "obj_type",
"exclouded": [ "name"
"snapshot_json", ],
"content_json", "orderby": "obj_type asc, seq asc",
"item_payload_json", "defaultfilter": {
"quality_criteria", "deleted": 0
"knowledge_points",
"tags_json"
]
},
"editable": {
"new_data_url": "{{entire_url('../api/template_item_save.dspy')}}",
"update_data_url": "{{entire_url('../api/template_item_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/template_item_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/template_item_list.dspy')}}",
"get_data_url": "{{entire_url('../api/template_item_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"template_id",
"subobject_type"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "PBL蓝图模板条目",
"subobject_type": { "readonly": [
"appcode": "pbl_subobject_type", "id",
"label": "子对象类型" "tenant_id",
} "created_by",
}, "created_at",
"table_meta": { "updated_by",
"pk": "id", "updated_at",
"tenant_field": "tenant_id", "deleted"
"soft_delete_field": "is_deleted", ]
"engine": "postgresql",
"indexes": [
"primary",
"uk_tplitem_tenant_tpl_type_seq",
"idx_tplitem_template"
]
}
} }
} }

View File

@ -1,82 +1,43 @@
{ {
"pbl_blueprint_version": { "tblname": "pbl_blueprint_version",
"summary": "PBL蓝图版本", "params": {
"alias": "pbl_blueprint_version_crud", "editable": [
"params": { "blueprint_id",
"sortby": "created_at", "version_no",
"sortorder": "desc", "snapshot",
"tenant_scoped": true, "change_delta",
"browserfields": { "quality_level",
"fields": [ "status",
"id", "remark"
"tenant_id", ],
"class_id", "browserfields": [
"version_no", "blueprint_id",
"version_status", "version_no",
"created_at", "quality_level",
"updated_at" "status",
], "remark",
"alters": { "created_by",
"version_status": { "created_at"
"uitype": "code", ],
"appcode": "pbl_version_status", "searchfields": [
"label": "版本状态" "tenant_id",
} "blueprint_id",
}, "version_no",
"exclouded": [ "status"
"snapshot_json", ],
"content_json", "orderby": "version_no desc",
"item_payload_json", "defaultfilter": {
"quality_criteria", "deleted": 0
"knowledge_points",
"tags_json"
]
},
"editable": {
"new_data_url": "{{entire_url('../api/version_save.dspy')}}",
"update_data_url": "{{entire_url('../api/version_save.dspy')}}",
"delete_data_url": "{{entire_url('../api/version_delete.dspy')}}",
"list_data_url": "{{entire_url('../api/version_list.dspy')}}",
"get_data_url": "{{entire_url('../api/version_get.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"created_by",
"created_at"
]
},
"required": [
"id",
"tenant_id",
"blueprint_id",
"version_no"
],
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
}
]
}
}, },
"codes": { "caption": "PBL蓝图版本快照",
"version_status": { "readonly": [
"appcode": "pbl_version_status", "id",
"label": "版本状态" "tenant_id",
} "created_by",
}, "created_at",
"table_meta": { "updated_by",
"pk": "id", "updated_at",
"tenant_field": "tenant_id", "deleted"
"soft_delete_field": "is_deleted", ]
"engine": "postgresql",
"indexes": [
"primary",
"uk_version_blueprint_no",
"idx_version_blueprint"
]
}
} }
} }

View File

@ -1,185 +1,55 @@
{ {
"summary": { "summary": "PBL 蓝图聚合根主表(M1a)。一行=一个项目式学习蓝图,承载学科/年级/课时与质量状态;所有子对象(task/mission/role/learning_goal/evidence_spec/artifact_spec/reflection_spec)通过 blueprint_id 挂靠。tenant_id 为强制打头字段,任何读写必须带租户条件,缺失即拒绝。",
"name": "pbl_blueprint", "fields": [
"label": "PBL蓝图聚合根", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "PBL蓝图聚合根(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头,所有查询第一过滤条件"},
"module": "pbl_blueprint", {"name": "code", "type": "str", "len": 64, "notnull": true, "desc": "蓝图编码,租户内唯一,形如 PBL-{yyyyMMdd}-{seq4}"},
"owner": "agent.develop", {"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "蓝图名称"},
"version": "1.0", {"name": "subject", "type": "str", "len": 64, "desc": "学科"},
"engine": "postgresql", {"name": "grade", "type": "str", "len": 32, "desc": "适用年级"},
"tenant_scoped": true {"name": "duration_hours", "type": "int", "desc": "总课时数"},
}, {"name": "version_no", "type": "int", "notnull": true, "default": "1", "desc": "当前版本号,每次发布快照自增"},
"fields": { {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "蓝图状态,见 codes.status"},
"id": { {"name": "quality_level", "type": "str", "len": 16, "default": "L1", "desc": "质量等级 L1-L5,由 pbl_validation 回写"},
"type": "str(32)", {"name": "summary", "type": "text", "desc": "蓝图简介/驱动性问题"},
"label": "主键", {"name": "config", "type": "json", "desc": "扩展配置(分组策略/评分权重/运行时参数)"},
"pk": true, {"name": "source_blueprint_id", "type": "str", "len": 32, "desc": "fork 来源蓝图ID,非fork为空"},
"required": true {"name": "template_id", "type": "str", "len": 32, "desc": "实例化来源模板ID,非模板创建为空"},
}, {"name": "owner_id", "type": "str", "len": 32, "desc": "归属教师/创建者用户ID"},
"tenant_id": { {"name": "published_at", "type": "datetime", "desc": "最近发布时间"},
"type": "str(32)", {"name": "remark", "type": "str", "len": 500, "desc": "备注"},
"label": "租户ID(强制打头)", {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"required": true {"name": "created_at", "type": "datetime", "desc": "创建时间"},
}, {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
"class_id": { {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
"type": "str(32)", {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"label": "班级ID" ],
}, "indexes": [
"blueprint_code": { {"name": "uk_pbl_blueprint_tenant_code", "fields": ["tenant_id", "code"], "unique": true},
"type": "str(64)", {"name": "idx_pbl_blueprint_tenant_status", "fields": ["tenant_id", "status", "deleted"], "unique": false},
"label": "蓝图编码", {"name": "idx_pbl_blueprint_owner", "fields": ["tenant_id", "owner_id"], "unique": false},
"required": true {"name": "idx_pbl_blueprint_template", "fields": ["tenant_id", "template_id"], "unique": false}
}, ],
"blueprint_name": {
"type": "str(128)",
"label": "蓝图名称",
"required": true
},
"domain_code": {
"type": "str(64)",
"label": "领域",
"appcode": "pbl_blueprint_domain"
},
"grade_code": {
"type": "str(32)",
"label": "学段",
"appcode": "pbl_grade_code"
},
"subject_code": {
"type": "str(64)",
"label": "学科",
"appcode": "pbl_subject_code"
},
"project_duration": {
"type": "int",
"label": "项目时长(课时)"
},
"student_count": {
"type": "int",
"label": "参与学生数"
},
"group_count": {
"type": "int",
"label": "小组数"
},
"complexity_level": {
"type": "str(32)",
"label": "复杂度",
"appcode": "pbl_complexity_level"
},
"status": {
"type": "str(32)",
"label": "蓝图状态",
"appcode": "pbl_blueprint_status",
"default": "draft"
},
"current_version": {
"type": "int",
"label": "当前版本号",
"default": 0
},
"published_version_id": {
"type": "str(32)",
"label": "已发布版本ID"
},
"source_blueprint_id": {
"type": "str(32)",
"label": "派生来源蓝图ID(fork)"
},
"source_template_id": {
"type": "str(32)",
"label": "来源模板ID"
},
"owner_user_id": {
"type": "str(64)",
"label": "负责人"
},
"estimated_cost": {
"type": "double(18,2)",
"label": "预估成本(元)",
"precision": 2
},
"tags_json": {
"type": "text",
"label": "标签(JSON数组)"
},
"summary_text": {
"type": "text",
"label": "蓝图摘要"
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"uk_blueprint_tenant_code": {
"fields": [
"tenant_id",
"blueprint_code"
],
"unique": true
},
"idx_blueprint_tenant_status": {
"fields": [
"tenant_id",
"status",
"is_deleted"
],
"unique": false
},
"idx_blueprint_class": {
"fields": [
"tenant_id",
"class_id",
"is_deleted"
],
"unique": false
}
},
"codes": { "codes": {
"domain_code": { "status": [
"appcode": "pbl_blueprint_domain", ["draft", "草稿"],
"label": "领域" ["validating", "校验中"],
}, ["validated", "校验通过"],
"grade_code": { ["compiling", "编译中"],
"appcode": "pbl_grade_code", ["compiled", "已编译"],
"label": "学段" ["published", "已发布"],
}, ["archived", "已归档"]
"subject_code": { ],
"appcode": "pbl_subject_code", "quality_level": [
"label": "学科" ["L1", "L1-初始"],
}, ["L2", "L2-基本完整"],
"complexity_level": { ["L3", "L3-结构合格"],
"appcode": "pbl_complexity_level", ["L4", "L4-可运行"],
"label": "复杂度" ["L5", "L5-优质"]
}, ],
"status": { "deleted": [
"appcode": "pbl_blueprint_status", ["0", "正常"],
"label": "蓝图状态" ["1", "已删除"]
} ]
} }
} }

View File

@ -1,133 +1,36 @@
{ {
"summary": { "summary": "PBL 蓝图子对象-产出物规格(M1a,7类子对象之一)。一行=一种期望产出物的规格定义(作品/报告/模型/演示),供 pbl_evidence 归档产出物与 pbl_compiler 生成任务交付要求。统一泛化契约。tenant_id 强制打头。",
"name": "pbl_blueprint_artifact_spec", "fields": [
"label": "蓝图子对象-产出物规格(Artifact)", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "蓝图子对象-产出物规格(Artifact)(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头"},
"module": "pbl_blueprint", {"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
"owner": "agent.develop", {"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID"},
"version": "1.0", {"name": "mission_id", "type": "str", "len": 32, "desc": "关联关卡ID"},
"engine": "postgresql", {"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
"tenant_scoped": true {"name": "code", "type": "str", "len": 64, "desc": "产出物规格编码,蓝图内唯一,缺省自动生成 ART-{seq}"},
}, {"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "产出物名称"},
"fields": { {"name": "artifact_type", "type": "str", "len": 32, "notnull": true, "default": "document", "desc": "产出物类型,见 codes.artifact_type"},
"id": { {"name": "description", "type": "text", "desc": "产出物说明与要求"},
"type": "str(32)", {"name": "spec", "type": "json", "desc": "产出物规格(格式/尺寸/时长/字数/文件类型白名单)"},
"label": "主键", {"name": "accept_criteria", "type": "json", "desc": "验收标准条目数组"},
"pk": true, {"name": "required", "type": "int", "notnull": true, "default": "1", "desc": "是否必交 0否 1是"},
"required": true {"name": "weight", "type": "float", "default": "1.0", "desc": "评分权重"},
}, {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
"tenant_id": { {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"type": "str(32)", {"name": "created_at", "type": "datetime", "desc": "创建时间"},
"label": "租户ID(强制打头)", {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
"required": true {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
}, {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"class_id": { ],
"type": "str(32)", "indexes": [
"label": "班级ID" {"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},
"blueprint_id": { {"name": "idx_pbl_bpart_task", "fields": ["tenant_id", "task_id"], "unique": false}
"type": "str(32)", ],
"label": "所属蓝图ID",
"required": true
},
"version_id": {
"type": "str(32)",
"label": "所属版本ID"
},
"code": {
"type": "str(64)",
"label": "对象编码"
},
"name": {
"type": "str(128)",
"label": "对象名称",
"required": true
},
"description": {
"type": "text",
"label": "描述"
},
"seq_no": {
"type": "int",
"label": "排序号",
"default": 0
},
"status": {
"type": "str(32)",
"label": "状态",
"appcode": "pbl_subobject_status"
},
"content_json": {
"type": "text",
"label": "扩展内容(JSON)"
},
"artifact_type": {
"type": "str(32)",
"label": "产出物类型",
"appcode": "pbl_artifact_type"
},
"required_flag": {
"type": "int",
"label": "是否必需(0/1)",
"default": 1
},
"quality_criteria": {
"type": "text",
"label": "质量标准(JSON)"
},
"due_seq": {
"type": "int",
"label": "交付次序"
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"idx_pbl_blueprint_artifact_spec_tenant_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"is_deleted"
],
"unique": false
},
"uk_pbl_blueprint_artifact_spec_tenant_code": {
"fields": [
"tenant_id",
"code"
],
"unique": true
}
},
"codes": { "codes": {
"artifact_type": { "artifact_type": [["document", "文档报告"], ["model", "三维模型"], ["scene", "场景作品"], ["video", "视频"], ["audio", "音频"], ["image", "图片"], ["code", "程序脚本"], ["presentation", "演示汇报"], ["dataset", "数据集"]],
"appcode": "pbl_artifact_type", "required": [["0", "选交"], ["1", "必交"]],
"label": "产出物类型" "status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
} "deleted": [["0", "正常"], ["1", "已删除"]]
} }
} }

View File

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

View File

@ -1,138 +1,32 @@
{ {
"summary": { "summary": "PBL 蓝图子对象-学习目标(M1a,7类子对象之一)。一行=蓝图的一条学习目标(知识/能力/素养/态度四维),可关联到任务并被评估量规引用。统一泛化契约。tenant_id 强制打头。",
"name": "pbl_blueprint_learning_goal", "fields": [
"label": "蓝图子对象-学习目标(Goal)", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "蓝图子对象-学习目标(Goal)(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头"},
"module": "pbl_blueprint", {"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
"owner": "agent.develop", {"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID,为空表示蓝图级目标"},
"version": "1.0", {"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
"engine": "postgresql", {"name": "code", "type": "str", "len": 64, "desc": "目标编码,蓝图内唯一,缺省自动生成 GOAL-{seq}"},
"tenant_scoped": true {"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "目标名称"},
}, {"name": "dimension", "type": "str", "len": 32, "notnull": true, "default": "knowledge", "desc": "目标维度,见 codes.dimension"},
"fields": { {"name": "description", "type": "text", "desc": "目标描述(可观测行为动词表述)"},
"id": { {"name": "spec", "type": "json", "desc": "目标规格(课标对应/掌握层级/评价方式)"},
"type": "str(32)", {"name": "weight", "type": "float", "default": "1.0", "desc": "评估权重,供 pbl_assessment Rubric 使用"},
"label": "主键", {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
"pk": true, {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"required": true {"name": "created_at", "type": "datetime", "desc": "创建时间"},
}, {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
"tenant_id": { {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
"type": "str(32)", {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"label": "租户ID(强制打头)", ],
"required": true "indexes": [
}, {"name": "uk_pbl_bplg_tenant_bp_code", "fields": ["tenant_id", "blueprint_id", "code"], "unique": true},
"class_id": { {"name": "idx_pbl_bplg_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
"type": "str(32)", {"name": "idx_pbl_bplg_dim", "fields": ["tenant_id", "blueprint_id", "dimension"], "unique": false}
"label": "班级ID" ],
},
"blueprint_id": {
"type": "str(32)",
"label": "所属蓝图ID",
"required": true
},
"version_id": {
"type": "str(32)",
"label": "所属版本ID"
},
"code": {
"type": "str(64)",
"label": "对象编码"
},
"name": {
"type": "str(128)",
"label": "对象名称",
"required": true
},
"description": {
"type": "text",
"label": "描述"
},
"seq_no": {
"type": "int",
"label": "排序号",
"default": 0
},
"status": {
"type": "str(32)",
"label": "状态",
"appcode": "pbl_subobject_status"
},
"content_json": {
"type": "text",
"label": "扩展内容(JSON)"
},
"goal_type": {
"type": "str(32)",
"label": "目标类型",
"appcode": "pbl_goal_type"
},
"competency_code": {
"type": "str(64)",
"label": "素养/课标编码"
},
"knowledge_points": {
"type": "text",
"label": "知识点(JSON数组)"
},
"measurable_flag": {
"type": "int",
"label": "是否可测(0/1)",
"default": 0
},
"weight_value": {
"type": "double(18,2)",
"label": "权重",
"precision": 2
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"idx_pbl_blueprint_learning_goal_tenant_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"is_deleted"
],
"unique": false
},
"uk_pbl_blueprint_learning_goal_tenant_code": {
"fields": [
"tenant_id",
"code"
],
"unique": true
}
},
"codes": { "codes": {
"goal_type": { "dimension": [["knowledge", "知识"], ["ability", "能力"], ["literacy", "素养"], ["attitude", "态度"]],
"appcode": "pbl_goal_type", "status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"label": "目标类型" "deleted": [["0", "正常"], ["1", "已删除"]]
}
} }
} }

View File

@ -1,138 +1,30 @@
{ {
"summary": { "summary": "PBL 蓝图子对象-关卡(M1a,7类子对象之一)。一行=任务下的一个游戏化关卡,是 Compiler 生成 Game Definition 的直接来源。统一泛化契约(blueprint_id + task_id + seq + code + name + spec + status)。tenant_id 强制打头。",
"name": "pbl_blueprint_mission", "fields": [
"label": "蓝图子对象-真实情境任务(Mission)", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "蓝图子对象-真实情境任务(Mission)(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头"},
"module": "pbl_blueprint", {"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
"owner": "agent.develop", {"name": "task_id", "type": "str", "len": 32, "notnull": true, "desc": "所属任务ID"},
"version": "1.0", {"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "任务内排序号"},
"engine": "postgresql", {"name": "code", "type": "str", "len": 64, "desc": "关卡编码,蓝图内唯一,缺省自动生成 MISSION-{seq}"},
"tenant_scoped": true {"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "关卡名称"},
}, {"name": "description", "type": "text", "desc": "关卡说明"},
"fields": { {"name": "spec", "type": "json", "desc": "关卡规格(目标/规则/场景引用/通关条件/奖励)"},
"id": { {"name": "unlock_condition", "type": "json", "desc": "解锁条件(前置关卡/分数门槛)"},
"type": "str(32)", {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
"label": "主键", {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"pk": true, {"name": "created_at", "type": "datetime", "desc": "创建时间"},
"required": true {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
}, {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
"tenant_id": { {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"type": "str(32)", ],
"label": "租户ID(强制打头)", "indexes": [
"required": true {"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},
"class_id": { {"name": "idx_pbl_bpms_task", "fields": ["tenant_id", "task_id", "seq"], "unique": false}
"type": "str(32)", ],
"label": "班级ID"
},
"blueprint_id": {
"type": "str(32)",
"label": "所属蓝图ID",
"required": true
},
"version_id": {
"type": "str(32)",
"label": "所属版本ID"
},
"code": {
"type": "str(64)",
"label": "对象编码"
},
"name": {
"type": "str(128)",
"label": "对象名称",
"required": true
},
"description": {
"type": "text",
"label": "描述"
},
"seq_no": {
"type": "int",
"label": "排序号",
"default": 0
},
"status": {
"type": "str(32)",
"label": "状态",
"appcode": "pbl_subobject_status"
},
"content_json": {
"type": "text",
"label": "扩展内容(JSON)"
},
"mission_type": {
"type": "str(32)",
"label": "情境类型",
"appcode": "pbl_mission_type"
},
"scenario_text": {
"type": "text",
"label": "情境描述"
},
"audience": {
"type": "str(256)",
"label": "受众/服务对象"
},
"duration_hours": {
"type": "double(18,2)",
"label": "预计课时",
"precision": 2
},
"authenticity_score": {
"type": "double(18,2)",
"label": "真实性评分",
"precision": 2
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"idx_pbl_blueprint_mission_tenant_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"is_deleted"
],
"unique": false
},
"uk_pbl_blueprint_mission_tenant_code": {
"fields": [
"tenant_id",
"code"
],
"unique": true
}
},
"codes": { "codes": {
"mission_type": { "status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"appcode": "pbl_mission_type", "deleted": [["0", "正常"], ["1", "已删除"]]
"label": "情境类型"
}
} }
} }

View File

@ -1,137 +1,36 @@
{ {
"summary": { "summary": "PBL 蓝图子对象-反思规格(M1a,7类子对象之一)。一行=一条反思/自评提示的规格定义(触发时机、反思类型、引导问题、量规维度),供运行时按节点触发学习者反思并回流评估。统一泛化契约。tenant_id 强制打头。",
"name": "pbl_blueprint_reflection_spec", "fields": [
"label": "蓝图子对象-反思规格(Reflection)", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "蓝图子对象-反思规格(Reflection)(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头"},
"module": "pbl_blueprint", {"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
"owner": "agent.develop", {"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID"},
"version": "1.0", {"name": "mission_id", "type": "str", "len": 32, "desc": "关联关卡ID"},
"engine": "postgresql", {"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
"tenant_scoped": true {"name": "code", "type": "str", "len": 64, "desc": "反思规格编码,蓝图内唯一,缺省自动生成 REF-{seq}"},
}, {"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "反思规格名称"},
"fields": { {"name": "trigger_point", "type": "str", "len": 32, "notnull": true, "default": "mission_end", "desc": "触发时机,见 codes.trigger_point"},
"id": { {"name": "reflection_type", "type": "str", "len": 32, "notnull": true, "default": "self", "desc": "反思类型,见 codes.reflection_type"},
"type": "str(32)", {"name": "description", "type": "text", "desc": "反思说明"},
"label": "主键", {"name": "spec", "type": "json", "desc": "反思规格(引导问题数组/作答形式/字数下限)"},
"pk": true, {"name": "rubric_ref", "type": "json", "desc": "关联量规维度引用(供 pbl_assessment 打分)"},
"required": true {"name": "weight", "type": "float", "default": "1.0", "desc": "评分权重"},
}, {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
"tenant_id": { {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"type": "str(32)", {"name": "created_at", "type": "datetime", "desc": "创建时间"},
"label": "租户ID(强制打头)", {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
"required": true {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
}, {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"class_id": { ],
"type": "str(32)", "indexes": [
"label": "班级ID" {"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},
"blueprint_id": { {"name": "idx_pbl_bpref_trigger", "fields": ["tenant_id", "blueprint_id", "trigger_point"], "unique": false}
"type": "str(32)", ],
"label": "所属蓝图ID",
"required": true
},
"version_id": {
"type": "str(32)",
"label": "所属版本ID"
},
"code": {
"type": "str(64)",
"label": "对象编码"
},
"name": {
"type": "str(128)",
"label": "对象名称",
"required": true
},
"description": {
"type": "text",
"label": "描述"
},
"seq_no": {
"type": "int",
"label": "排序号",
"default": 0
},
"status": {
"type": "str(32)",
"label": "状态",
"appcode": "pbl_subobject_status"
},
"content_json": {
"type": "text",
"label": "扩展内容(JSON)"
},
"reflect_type": {
"type": "str(32)",
"label": "反思类型",
"appcode": "pbl_reflect_type"
},
"frequency": {
"type": "str(32)",
"label": "频次",
"appcode": "pbl_reflect_frequency"
},
"prompt_text": {
"type": "text",
"label": "反思引导语"
},
"duration_minutes": {
"type": "int",
"label": "时长(分钟)"
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"idx_pbl_blueprint_reflection_spec_tenant_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"is_deleted"
],
"unique": false
},
"uk_pbl_blueprint_reflection_spec_tenant_code": {
"fields": [
"tenant_id",
"code"
],
"unique": true
}
},
"codes": { "codes": {
"reflect_type": { "trigger_point": [["task_start", "任务开始"], ["task_end", "任务结束"], ["mission_end", "关卡结束"], ["milestone", "里程碑"], ["project_end", "项目结题"], ["manual", "手动触发"]],
"appcode": "pbl_reflect_type", "reflection_type": [["self", "自我反思"], ["peer", "同伴互评"], ["team", "团队复盘"], ["teacher", "教师点评"]],
"label": "反思类型" "status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
}, "deleted": [["0", "正常"], ["1", "已删除"]]
"frequency": {
"appcode": "pbl_reflect_frequency",
"label": "频次"
}
} }
} }

View File

@ -1,132 +1,30 @@
{ {
"summary": { "summary": "PBL 蓝图子对象-角色(M1a,7类子对象之一)。一行=蓝图内定义的一种学习者角色(如队长/记录员/工程师),供分组与权限分配使用。统一泛化契约。tenant_id 强制打头。",
"name": "pbl_blueprint_role", "fields": [
"label": "蓝图子对象-角色(Role)", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "蓝图子对象-角色(Role)(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头"},
"module": "pbl_blueprint", {"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
"owner": "agent.develop", {"name": "task_id", "type": "str", "len": 32, "desc": "关联任务ID,为空表示蓝图级角色"},
"version": "1.0", {"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
"engine": "postgresql", {"name": "code", "type": "str", "len": 64, "desc": "角色编码,蓝图内唯一,缺省自动生成 ROLE-{seq}"},
"tenant_scoped": true {"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "角色名称"},
}, {"name": "description", "type": "text", "desc": "角色职责说明"},
"fields": { {"name": "spec", "type": "json", "desc": "角色规格(人数上限/能力标签/可用工具/评分权重)"},
"id": { {"name": "permissions", "type": "json", "desc": "角色在运行时的操作权限集合"},
"type": "str(32)", {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
"label": "主键", {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"pk": true, {"name": "created_at", "type": "datetime", "desc": "创建时间"},
"required": true {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
}, {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
"tenant_id": { {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"type": "str(32)", ],
"label": "租户ID(强制打头)", "indexes": [
"required": true {"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},
"class_id": { {"name": "idx_pbl_bprole_task", "fields": ["tenant_id", "task_id"], "unique": false}
"type": "str(32)", ],
"label": "班级ID"
},
"blueprint_id": {
"type": "str(32)",
"label": "所属蓝图ID",
"required": true
},
"version_id": {
"type": "str(32)",
"label": "所属版本ID"
},
"code": {
"type": "str(64)",
"label": "对象编码"
},
"name": {
"type": "str(128)",
"label": "对象名称",
"required": true
},
"description": {
"type": "text",
"label": "描述"
},
"seq_no": {
"type": "int",
"label": "排序号",
"default": 0
},
"status": {
"type": "str(32)",
"label": "状态",
"appcode": "pbl_subobject_status"
},
"content_json": {
"type": "text",
"label": "扩展内容(JSON)"
},
"role_category": {
"type": "str(32)",
"label": "角色类别",
"appcode": "pbl_role_category"
},
"member_count": {
"type": "int",
"label": "角色人数"
},
"responsibility": {
"type": "text",
"label": "职责说明"
},
"competency_tags": {
"type": "text",
"label": "能力标签(JSON数组)"
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"idx_pbl_blueprint_role_tenant_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"is_deleted"
],
"unique": false
},
"uk_pbl_blueprint_role_tenant_code": {
"fields": [
"tenant_id",
"code"
],
"unique": true
}
},
"codes": { "codes": {
"role_category": { "status": [["draft", "草稿"], ["active", "生效"], ["disabled", "停用"]],
"appcode": "pbl_role_category", "deleted": [["0", "正常"], ["1", "已删除"]]
"label": "角色类别"
}
} }
} }

View File

@ -1,137 +1,36 @@
{ {
"summary": { "summary": "PBL 蓝图子对象-任务(M1a,7类子对象之一)。一行=蓝图下的一个学习任务,是关卡/角色/目标的挂载父级。采用统一泛化契约(blueprint_id + seq + name + spec + status),便于 Compiler 与 Validation 统一遍历。tenant_id 强制打头。",
"name": "pbl_blueprint_task", "fields": [
"label": "蓝图子对象-任务分解(Task)", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "蓝图子对象-任务分解(Task)(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头"},
"module": "pbl_blueprint", {"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
"owner": "agent.develop", {"name": "parent_id", "type": "str", "len": 32, "desc": "父任务ID,支持任务分层,顶层为空"},
"version": "1.0", {"name": "seq", "type": "int", "notnull": true, "default": "0", "desc": "蓝图内排序号"},
"engine": "postgresql", {"name": "code", "type": "str", "len": 64, "desc": "任务编码,蓝图内唯一,缺省自动生成 TASK-{seq}"},
"tenant_scoped": true {"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "任务名称"},
}, {"name": "description", "type": "text", "desc": "任务描述/驱动性问题"},
"fields": { {"name": "spec", "type": "json", "desc": "任务规格(时长/难度/分组方式/前置条件/交付要求)"},
"id": { {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "draft", "desc": "子对象状态,见 codes.status"},
"type": "str(32)", {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"label": "主键", {"name": "created_at", "type": "datetime", "desc": "创建时间"},
"pk": true, {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
"required": true {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
}, {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"tenant_id": { ],
"type": "str(32)", "indexes": [
"label": "租户ID(强制打头)", {"name": "idx_pbl_bptask_tenant_bp_seq", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": false},
"required": true {"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}
"class_id": { ],
"type": "str(32)",
"label": "班级ID"
},
"blueprint_id": {
"type": "str(32)",
"label": "所属蓝图ID",
"required": true
},
"version_id": {
"type": "str(32)",
"label": "所属版本ID"
},
"code": {
"type": "str(64)",
"label": "对象编码"
},
"name": {
"type": "str(128)",
"label": "对象名称",
"required": true
},
"description": {
"type": "text",
"label": "描述"
},
"seq_no": {
"type": "int",
"label": "排序号",
"default": 0
},
"status": {
"type": "str(32)",
"label": "状态",
"appcode": "pbl_subobject_status"
},
"content_json": {
"type": "text",
"label": "扩展内容(JSON)"
},
"parent_task_id": {
"type": "str(32)",
"label": "父任务ID"
},
"task_type": {
"type": "str(32)",
"label": "任务类型",
"appcode": "pbl_task_type"
},
"tool_resources": {
"type": "text",
"label": "工具与资源(JSON数组)"
},
"planned_hours": {
"type": "double(18,2)",
"label": "计划课时",
"precision": 2
},
"assessment_rule": {
"type": "text",
"label": "评价规则(JSON)"
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"idx_pbl_blueprint_task_tenant_bp": {
"fields": [
"tenant_id",
"blueprint_id",
"is_deleted"
],
"unique": false
},
"uk_pbl_blueprint_task_tenant_code": {
"fields": [
"tenant_id",
"code"
],
"unique": true
}
},
"codes": { "codes": {
"task_type": { "status": [
"appcode": "pbl_task_type", ["draft", "草稿"],
"label": "任务类型" ["active", "生效"],
} ["disabled", "停用"]
],
"deleted": [
["0", "正常"],
["1", "已删除"]
]
} }
} }

View File

@ -1,159 +1,31 @@
{ {
"summary": { "summary": "PBL 蓝图模板表(M1a)。一行=一个可复用蓝图模板,用于一键实例化生成新蓝图;离线兜底时也可由模板生成最小可用蓝图。tenant_id 强制打头,平台内置模板 tenant_id 固定为 platform。",
"name": "pbl_blueprint_template", "fields": [
"label": "PBL蓝图模板", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "PBL蓝图模板(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头;平台内置模板为 platform"},
"module": "pbl_blueprint", {"name": "code", "type": "str", "len": 64, "notnull": true, "desc": "模板编码,租户内唯一"},
"owner": "agent.develop", {"name": "name", "type": "str", "len": 200, "notnull": true, "desc": "模板名称"},
"version": "1.0", {"name": "category", "type": "str", "len": 64, "desc": "模板分类,见 codes.category"},
"engine": "postgresql", {"name": "subject", "type": "str", "len": 64, "desc": "适用学科"},
"tenant_scoped": true {"name": "grade", "type": "str", "len": 32, "desc": "适用年级"},
}, {"name": "duration_hours", "type": "int", "desc": "建议课时"},
"fields": { {"name": "description", "type": "text", "desc": "模板说明"},
"id": { {"name": "content", "type": "json", "desc": "模板主体内容(主表默认值+子对象骨架)"},
"type": "str(32)", {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "enabled", "desc": "模板状态,见 codes.status"},
"label": "主键", {"name": "use_count", "type": "int", "notnull": true, "default": "0", "desc": "被实例化次数"},
"pk": true, {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"required": true {"name": "created_at", "type": "datetime", "desc": "创建时间"},
}, {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
"tenant_id": { {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
"type": "str(32)", {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
"label": "租户ID(强制打头)", ],
"required": true "indexes": [
}, {"name": "uk_pbl_bpt_tenant_code", "fields": ["tenant_id", "code"], "unique": true},
"class_id": { {"name": "idx_pbl_bpt_tenant_cat", "fields": ["tenant_id", "category", "status", "deleted"], "unique": false}
"type": "str(32)", ],
"label": "班级ID"
},
"template_code": {
"type": "str(64)",
"label": "模板编码",
"required": true
},
"template_name": {
"type": "str(128)",
"label": "模板名称",
"required": true
},
"template_type": {
"type": "str(32)",
"label": "模板类型",
"appcode": "pbl_template_type"
},
"domain_code": {
"type": "str(64)",
"label": "领域",
"appcode": "pbl_blueprint_domain"
},
"grade_code": {
"type": "str(32)",
"label": "学段",
"appcode": "pbl_grade_code"
},
"subject_code": {
"type": "str(64)",
"label": "学科",
"appcode": "pbl_subject_code"
},
"complexity_level": {
"type": "str(32)",
"label": "复杂度",
"appcode": "pbl_complexity_level"
},
"scope": {
"type": "str(32)",
"label": "可见范围",
"appcode": "pbl_template_scope",
"default": "tenant"
},
"source_blueprint_id": {
"type": "str(32)",
"label": "来源蓝图ID"
},
"usage_count": {
"type": "int",
"label": "引用次数",
"default": 0
},
"status": {
"type": "str(32)",
"label": "状态",
"appcode": "pbl_template_status",
"default": "draft"
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"uk_template_tenant_code": {
"fields": [
"tenant_id",
"template_code"
],
"unique": true
},
"idx_template_tenant_status": {
"fields": [
"tenant_id",
"status",
"is_deleted"
],
"unique": false
}
},
"codes": { "codes": {
"template_type": { "category": [["stem", "STEM跨学科"], ["humanity", "人文社科"], ["science", "自然科学"], ["engineering", "工程制作"], ["social", "社会服务"], ["general", "通用"]],
"appcode": "pbl_template_type", "status": [["enabled", "启用"], ["disabled", "停用"]],
"label": "模板类型" "deleted": [["0", "正常"], ["1", "已删除"]]
},
"domain_code": {
"appcode": "pbl_blueprint_domain",
"label": "领域"
},
"grade_code": {
"appcode": "pbl_grade_code",
"label": "学段"
},
"subject_code": {
"appcode": "pbl_subject_code",
"label": "学科"
},
"complexity_level": {
"appcode": "pbl_complexity_level",
"label": "复杂度"
},
"scope": {
"appcode": "pbl_template_scope",
"label": "可见范围"
},
"status": {
"appcode": "pbl_template_status",
"label": "状态"
}
} }
} }

View File

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

View File

@ -1,124 +1,28 @@
{ {
"summary": { "summary": "PBL 蓝图版本快照表(M1a)。一行=蓝图某版本的完整快照 + 相对上一版本的变更增量 change_delta,支撑版本回溯/diff/回滚。snapshot 存全量子对象树,change_delta 存 added/updated/removed 三段。tenant_id 强制打头。",
"name": "pbl_blueprint_version", "fields": [
"label": "PBL蓝图版本", {"name": "id", "type": "str", "len": 32, "primary": true, "notnull": true, "desc": "主键,32位无横线UUID"},
"desc": "PBL蓝图版本(pbl_blueprint 模块 M1a)", {"name": "tenant_id", "type": "str", "len": 32, "notnull": true, "desc": "租户ID,强制打头"},
"module": "pbl_blueprint", {"name": "blueprint_id", "type": "str", "len": 32, "notnull": true, "desc": "所属蓝图ID"},
"owner": "agent.develop", {"name": "version_no", "type": "int", "notnull": true, "desc": "版本号,蓝图内递增"},
"version": "1.0", {"name": "snapshot", "type": "json", "notnull": true, "desc": "该版本蓝图全量快照(主表字段+7类子对象树)"},
"engine": "postgresql", {"name": "change_delta", "type": "json", "desc": "相对上一版本变更增量 {added:[],updated:[],removed:[]}"},
"tenant_scoped": true {"name": "quality_level", "type": "str", "len": 16, "desc": "该版本质量等级快照 L1-L5"},
}, {"name": "status", "type": "str", "len": 32, "notnull": true, "default": "saved", "desc": "版本状态,见 codes.status"},
"fields": { {"name": "remark", "type": "str", "len": 500, "desc": "版本说明"},
"id": { {"name": "created_by", "type": "str", "len": 32, "desc": "创建人"},
"type": "str(32)", {"name": "created_at", "type": "datetime", "desc": "创建时间"},
"label": "主键", {"name": "updated_by", "type": "str", "len": 32, "desc": "最后修改人"},
"pk": true, {"name": "updated_at", "type": "datetime", "desc": "最后修改时间"},
"required": true {"name": "deleted", "type": "int", "notnull": true, "default": "0", "desc": "软删除标记 0正常 1已删除"}
}, ],
"tenant_id": { "indexes": [
"type": "str(32)", {"name": "uk_pbl_bpv_tenant_bp_ver", "fields": ["tenant_id", "blueprint_id", "version_no"], "unique": true},
"label": "租户ID(强制打头)", {"name": "idx_pbl_bpv_tenant_status", "fields": ["tenant_id", "status", "deleted"], "unique": false}
"required": true ],
},
"class_id": {
"type": "str(32)",
"label": "班级ID"
},
"blueprint_id": {
"type": "str(32)",
"label": "所属蓝图ID",
"required": true
},
"version_no": {
"type": "int",
"label": "版本号",
"required": true
},
"version_name": {
"type": "str(128)",
"label": "版本名称"
},
"version_status": {
"type": "str(32)",
"label": "版本状态",
"appcode": "pbl_version_status",
"default": "draft"
},
"snapshot_json": {
"type": "text",
"label": "蓝图快照(JSON)"
},
"content_hash": {
"type": "str(64)",
"label": "快照内容哈希"
},
"published_by": {
"type": "str(64)",
"label": "发布人"
},
"published_at": {
"type": "datetime",
"label": "发布时间"
},
"change_note": {
"type": "text",
"label": "变更说明"
},
"base_version_id": {
"type": "str(32)",
"label": "基线版本ID"
},
"is_deleted": {
"type": "int",
"label": "删除标记(0未删/1已删)",
"default": 0
},
"created_by": {
"type": "str(64)",
"label": "创建人"
},
"created_at": {
"type": "datetime",
"label": "创建时间"
},
"updated_by": {
"type": "str(64)",
"label": "更新人"
},
"updated_at": {
"type": "datetime",
"label": "更新时间"
}
},
"indexes": {
"primary": {
"fields": [
"id"
],
"unique": true
},
"uk_version_blueprint_no": {
"fields": [
"tenant_id",
"blueprint_id",
"version_no"
],
"unique": true
},
"idx_version_blueprint": {
"fields": [
"tenant_id",
"blueprint_id",
"is_deleted"
],
"unique": false
}
},
"codes": { "codes": {
"version_status": { "status": [["saved", "已保存"], ["published", "已发布"], ["rolled_back", "已回滚"], ["archived", "已归档"]],
"appcode": "pbl_version_status", "quality_level": [["L1", "L1-初始"], ["L2", "L2-基本完整"], ["L3", "L3-结构合格"], ["L4", "L4-可运行"], ["L5", "L5-优质"]],
"label": "版本状态" "deleted": [["0", "正常"], ["1", "已删除"]]
}
} }
} }

View File

@ -1,520 +1,113 @@
-- pbl_blueprint 核心表 DDL -- =====================================================================
-- 本文件由 scripts/derive_artifacts.py 从 pbl_blueprint/models/*.json 机械派生,禁止手工编辑 -- pbl_blueprint 核心表 DDL(M1a)
-- engine: postgresql(与 projects/pbls/env/*.json 的 db.engine 一致) -- 模块:pbl_blueprint 库名:由 ServerEnv().get_module_dbname('pbl_blueprint') 解析,禁止硬编码
-- 主键 id 为应用层生成的 str(32):不使用 SERIAL / BIGSERIAL / nextval / AUTO_INCREMENT -- 约定:主键 id 一律 varchar(32)(32位无横线UUID);tenant_id 强制打头且进入每个索引首列;
-- 软删除 deleted tinyint;JSON 字段用 json 类型;时间用 datetime。
-- 表清单(核心 4 张):pbl_blueprint / pbl_blueprint_version / pbl_blueprint_template / pbl_blueprint_template_item
-- 子对象 7 张见 pbl_blueprint.subobjects.sql
-- =====================================================================
-- pbl_blueprint: PBL蓝图聚合根 -- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS pbl_blueprint ( -- 1. pbl_blueprint 蓝图聚合根主表
id varchar(32) NOT NULL, -- ---------------------------------------------------------------------
tenant_id varchar(32) NOT NULL, CREATE TABLE IF NOT EXISTS `pbl_blueprint` (
class_id varchar(32), `id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
blueprint_code varchar(64) NOT NULL, `tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
blueprint_name varchar(128) NOT NULL, `code` varchar(64) NOT NULL COMMENT '蓝图编码,租户内唯一',
domain_code varchar(64), `name` varchar(200) NOT NULL COMMENT '蓝图名称',
grade_code varchar(32), `subject` varchar(64) DEFAULT NULL COMMENT '学科',
subject_code varchar(64), `grade` varchar(32) DEFAULT NULL COMMENT '适用年级',
project_duration integer, `duration_hours` int DEFAULT NULL COMMENT '总课时数',
student_count integer, `version_no` int NOT NULL DEFAULT 1 COMMENT '当前版本号',
group_count integer, `status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/validating/validated/compiling/compiled/published/archived',
complexity_level varchar(32), `quality_level` varchar(16) DEFAULT 'L1' COMMENT '质量等级 L1-L5',
status varchar(32), `summary` text COMMENT '蓝图简介/驱动性问题',
current_version integer, `config` json DEFAULT NULL COMMENT '扩展配置',
published_version_id varchar(32), `source_blueprint_id` varchar(32) DEFAULT NULL COMMENT 'fork 来源蓝图ID',
source_blueprint_id varchar(32), `template_id` varchar(32) DEFAULT NULL COMMENT '实例化来源模板ID',
source_template_id varchar(32), `owner_id` varchar(32) DEFAULT NULL COMMENT '归属教师/创建者',
owner_user_id varchar(64), `published_at` datetime DEFAULT NULL COMMENT '最近发布时间',
estimated_cost numeric(18,2), `remark` varchar(500) DEFAULT NULL COMMENT '备注',
tags_json text, `created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
summary_text text, `created_at` datetime DEFAULT NULL COMMENT '创建时间',
is_deleted integer, `updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
created_by varchar(64), `updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
created_at timestamp, `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
updated_by varchar(64), PRIMARY KEY (`id`),
updated_at timestamp UNIQUE KEY `uk_pbl_blueprint_tenant_code` (`tenant_id`, `code`),
); KEY `idx_pbl_blueprint_tenant_status` (`tenant_id`, `status`, `deleted`),
COMMENT ON COLUMN pbl_blueprint.id IS '主键'; KEY `idx_pbl_blueprint_owner` (`tenant_id`, `owner_id`),
COMMENT ON COLUMN pbl_blueprint.tenant_id IS '租户ID(强制打头)'; KEY `idx_pbl_blueprint_template` (`tenant_id`, `template_id`)
COMMENT ON COLUMN pbl_blueprint.class_id IS '班级ID'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图聚合根主表';
COMMENT ON COLUMN pbl_blueprint.blueprint_code IS '蓝图编码';
COMMENT ON COLUMN pbl_blueprint.blueprint_name IS '蓝图名称';
COMMENT ON COLUMN pbl_blueprint.domain_code IS '领域';
COMMENT ON COLUMN pbl_blueprint.grade_code IS '学段';
COMMENT ON COLUMN pbl_blueprint.subject_code IS '学科';
COMMENT ON COLUMN pbl_blueprint.project_duration IS '项目时长(课时)';
COMMENT ON COLUMN pbl_blueprint.student_count IS '参与学生数';
COMMENT ON COLUMN pbl_blueprint.group_count IS '小组数';
COMMENT ON COLUMN pbl_blueprint.complexity_level IS '复杂度';
COMMENT ON COLUMN pbl_blueprint.status IS '蓝图状态';
COMMENT ON COLUMN pbl_blueprint.current_version IS '当前版本号';
COMMENT ON COLUMN pbl_blueprint.published_version_id IS '已发布版本ID';
COMMENT ON COLUMN pbl_blueprint.source_blueprint_id IS '派生来源蓝图ID(fork)';
COMMENT ON COLUMN pbl_blueprint.source_template_id IS '来源模板ID';
COMMENT ON COLUMN pbl_blueprint.owner_user_id IS '负责人';
COMMENT ON COLUMN pbl_blueprint.estimated_cost IS '预估成本(元)';
COMMENT ON COLUMN pbl_blueprint.tags_json IS '标签(JSON数组)';
COMMENT ON COLUMN pbl_blueprint.summary_text IS '蓝图摘要';
COMMENT ON COLUMN pbl_blueprint.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint.updated_at IS '更新时间';
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_uk_blueprint_tenant_code ON pbl_blueprint (tenant_id, blueprint_code); -- ---------------------------------------------------------------------
CREATE INDEX IF NOT EXISTS pbl_blueprint_idx_blueprint_tenant_status ON pbl_blueprint (tenant_id, status, is_deleted); -- 2. pbl_blueprint_version 蓝图版本快照表
CREATE INDEX IF NOT EXISTS pbl_blueprint_idx_blueprint_class ON pbl_blueprint (tenant_id, class_id, is_deleted); -- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_version` (
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
`version_no` int NOT NULL COMMENT '版本号,蓝图内递增',
`snapshot` json NOT NULL COMMENT '该版本蓝图全量快照',
`change_delta` json DEFAULT NULL COMMENT '变更增量 {added,updated,removed}',
`quality_level` varchar(16) DEFAULT NULL COMMENT '该版本质量等级快照',
`status` varchar(32) NOT NULL DEFAULT 'saved' COMMENT 'saved/published/rolled_back/archived',
`remark` varchar(500) DEFAULT NULL COMMENT '版本说明',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_bpv_tenant_bp_ver` (`tenant_id`, `blueprint_id`, `version_no`),
KEY `idx_pbl_bpv_tenant_status` (`tenant_id`, `status`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图版本快照表';
-- pbl_blueprint_artifact_spec: 蓝图子对象-产出物规格(Artifact) -- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS pbl_blueprint_artifact_spec ( -- 3. pbl_blueprint_template 蓝图模板表
id varchar(32) NOT NULL, -- ---------------------------------------------------------------------
tenant_id varchar(32) NOT NULL, CREATE TABLE IF NOT EXISTS `pbl_blueprint_template` (
class_id varchar(32), `id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
blueprint_id varchar(32) NOT NULL, `tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头;平台内置为 platform',
version_id varchar(32), `code` varchar(64) NOT NULL COMMENT '模板编码,租户内唯一',
code varchar(64), `name` varchar(200) NOT NULL COMMENT '模板名称',
name varchar(128) NOT NULL, `category` varchar(64) DEFAULT NULL COMMENT 'stem/humanity/science/engineering/social/general',
description text, `subject` varchar(64) DEFAULT NULL COMMENT '适用学科',
seq_no integer, `grade` varchar(32) DEFAULT NULL COMMENT '适用年级',
status varchar(32), `duration_hours` int DEFAULT NULL COMMENT '建议课时',
content_json text, `description` text COMMENT '模板说明',
artifact_type varchar(32), `content` json DEFAULT NULL COMMENT '模板主体内容',
required_flag integer, `status` varchar(32) NOT NULL DEFAULT 'enabled' COMMENT 'enabled/disabled',
quality_criteria text, `use_count` int NOT NULL DEFAULT 0 COMMENT '被实例化次数',
due_seq integer, `created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
is_deleted integer, `created_at` datetime DEFAULT NULL COMMENT '创建时间',
created_by varchar(64), `updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
created_at timestamp, `updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
updated_by varchar(64), `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
updated_at timestamp PRIMARY KEY (`id`),
); UNIQUE KEY `uk_pbl_bpt_tenant_code` (`tenant_id`, `code`),
COMMENT ON COLUMN pbl_blueprint_artifact_spec.id IS '主键'; KEY `idx_pbl_bpt_tenant_cat` (`tenant_id`, `category`, `status`, `deleted`)
COMMENT ON COLUMN pbl_blueprint_artifact_spec.tenant_id IS '租户ID(强制打头)'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图模板表';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.blueprint_id IS '所属蓝图ID';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.version_id IS '所属版本ID';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.code IS '对象编码';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.name IS '对象名称';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.description IS '描述';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.seq_no IS '排序号';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.status IS '状态';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.content_json IS '扩展内容(JSON)';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.artifact_type IS '产出物类型';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.required_flag IS '是否必需(0/1)';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.quality_criteria IS '质量标准(JSON)';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.due_seq IS '交付次序';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS pbl_blueprint_artifact_spec_idx_pbl_blueprint_artifact_spec_tenant_bp ON pbl_blueprint_artifact_spec (tenant_id, blueprint_id, is_deleted); -- ---------------------------------------------------------------------
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_artifact_spec_uk_pbl_blueprint_artifact_spec_tenant_code ON pbl_blueprint_artifact_spec (tenant_id, code); -- 4. pbl_blueprint_template_item 蓝图模板条目表
-- ---------------------------------------------------------------------
-- pbl_blueprint_evidence_spec: 蓝图子对象-证据规格(Evidence) CREATE TABLE IF NOT EXISTS `pbl_blueprint_template_item` (
CREATE TABLE IF NOT EXISTS pbl_blueprint_evidence_spec ( `id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
id varchar(32) NOT NULL, `tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
tenant_id varchar(32) NOT NULL, `template_id` varchar(32) NOT NULL COMMENT '所属模板ID',
class_id varchar(32), `obj_type` varchar(32) NOT NULL COMMENT 'task/mission/role/learning_goal/evidence_spec/artifact_spec/reflection_spec',
blueprint_id varchar(32) NOT NULL, `parent_ref` varchar(64) DEFAULT NULL COMMENT '父条目引用键(模板内相对引用)',
version_id varchar(32), `ref_key` varchar(64) DEFAULT NULL COMMENT '本条目引用键',
code varchar(64), `seq` int NOT NULL DEFAULT 0 COMMENT '同类型内排序号',
name varchar(128) NOT NULL, `name` varchar(200) NOT NULL COMMENT '条目名称',
description text, `spec` json DEFAULT NULL COMMENT '条目规格默认值',
seq_no integer, `created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
status varchar(32), `created_at` datetime DEFAULT NULL COMMENT '创建时间',
content_json text, `updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
evidence_type varchar(32), `updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
artifact_id varchar(32), `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
min_count integer, PRIMARY KEY (`id`),
weight_value numeric(18,2), KEY `idx_pbl_bpti_tenant_tpl` (`tenant_id`, `template_id`, `obj_type`, `seq`),
is_deleted integer, KEY `idx_pbl_bpti_refkey` (`tenant_id`, `template_id`, `ref_key`)
created_by varchar(64), ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图模板条目表';
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_evidence_spec.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.blueprint_id IS '所属蓝图ID';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.version_id IS '所属版本ID';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.code IS '对象编码';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.name IS '对象名称';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.description IS '描述';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.seq_no IS '排序号';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.status IS '状态';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.content_json IS '扩展内容(JSON)';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.evidence_type IS '证据类型';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.artifact_id IS '关联产出物ID';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.min_count IS '最少提交数';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.weight_value IS '证据权重';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_evidence_spec.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS pbl_blueprint_evidence_spec_idx_pbl_blueprint_evidence_spec_tenant_bp ON pbl_blueprint_evidence_spec (tenant_id, blueprint_id, is_deleted);
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_evidence_spec_uk_pbl_blueprint_evidence_spec_tenant_code ON pbl_blueprint_evidence_spec (tenant_id, code);
-- pbl_blueprint_learning_goal: 蓝图子对象-学习目标(Goal)
CREATE TABLE IF NOT EXISTS pbl_blueprint_learning_goal (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
blueprint_id varchar(32) NOT NULL,
version_id varchar(32),
code varchar(64),
name varchar(128) NOT NULL,
description text,
seq_no integer,
status varchar(32),
content_json text,
goal_type varchar(32),
competency_code varchar(64),
knowledge_points text,
measurable_flag integer,
weight_value numeric(18,2),
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_learning_goal.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_learning_goal.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_learning_goal.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_learning_goal.blueprint_id IS '所属蓝图ID';
COMMENT ON COLUMN pbl_blueprint_learning_goal.version_id IS '所属版本ID';
COMMENT ON COLUMN pbl_blueprint_learning_goal.code IS '对象编码';
COMMENT ON COLUMN pbl_blueprint_learning_goal.name IS '对象名称';
COMMENT ON COLUMN pbl_blueprint_learning_goal.description IS '描述';
COMMENT ON COLUMN pbl_blueprint_learning_goal.seq_no IS '排序号';
COMMENT ON COLUMN pbl_blueprint_learning_goal.status IS '状态';
COMMENT ON COLUMN pbl_blueprint_learning_goal.content_json IS '扩展内容(JSON)';
COMMENT ON COLUMN pbl_blueprint_learning_goal.goal_type IS '目标类型';
COMMENT ON COLUMN pbl_blueprint_learning_goal.competency_code IS '素养/课标编码';
COMMENT ON COLUMN pbl_blueprint_learning_goal.knowledge_points IS '知识点(JSON数组)';
COMMENT ON COLUMN pbl_blueprint_learning_goal.measurable_flag IS '是否可测(0/1)';
COMMENT ON COLUMN pbl_blueprint_learning_goal.weight_value IS '权重';
COMMENT ON COLUMN pbl_blueprint_learning_goal.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_learning_goal.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_learning_goal.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_learning_goal.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_learning_goal.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS pbl_blueprint_learning_goal_idx_pbl_blueprint_learning_goal_tenant_bp ON pbl_blueprint_learning_goal (tenant_id, blueprint_id, is_deleted);
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_learning_goal_uk_pbl_blueprint_learning_goal_tenant_code ON pbl_blueprint_learning_goal (tenant_id, code);
-- pbl_blueprint_mission: 蓝图子对象-真实情境任务(Mission)
CREATE TABLE IF NOT EXISTS pbl_blueprint_mission (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
blueprint_id varchar(32) NOT NULL,
version_id varchar(32),
code varchar(64),
name varchar(128) NOT NULL,
description text,
seq_no integer,
status varchar(32),
content_json text,
mission_type varchar(32),
scenario_text text,
audience varchar(256),
duration_hours numeric(18,2),
authenticity_score numeric(18,2),
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_mission.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_mission.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_mission.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_mission.blueprint_id IS '所属蓝图ID';
COMMENT ON COLUMN pbl_blueprint_mission.version_id IS '所属版本ID';
COMMENT ON COLUMN pbl_blueprint_mission.code IS '对象编码';
COMMENT ON COLUMN pbl_blueprint_mission.name IS '对象名称';
COMMENT ON COLUMN pbl_blueprint_mission.description IS '描述';
COMMENT ON COLUMN pbl_blueprint_mission.seq_no IS '排序号';
COMMENT ON COLUMN pbl_blueprint_mission.status IS '状态';
COMMENT ON COLUMN pbl_blueprint_mission.content_json IS '扩展内容(JSON)';
COMMENT ON COLUMN pbl_blueprint_mission.mission_type IS '情境类型';
COMMENT ON COLUMN pbl_blueprint_mission.scenario_text IS '情境描述';
COMMENT ON COLUMN pbl_blueprint_mission.audience IS '受众/服务对象';
COMMENT ON COLUMN pbl_blueprint_mission.duration_hours IS '预计课时';
COMMENT ON COLUMN pbl_blueprint_mission.authenticity_score IS '真实性评分';
COMMENT ON COLUMN pbl_blueprint_mission.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_mission.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_mission.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_mission.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_mission.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS pbl_blueprint_mission_idx_pbl_blueprint_mission_tenant_bp ON pbl_blueprint_mission (tenant_id, blueprint_id, is_deleted);
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_mission_uk_pbl_blueprint_mission_tenant_code ON pbl_blueprint_mission (tenant_id, code);
-- pbl_blueprint_reflection_spec: 蓝图子对象-反思规格(Reflection)
CREATE TABLE IF NOT EXISTS pbl_blueprint_reflection_spec (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
blueprint_id varchar(32) NOT NULL,
version_id varchar(32),
code varchar(64),
name varchar(128) NOT NULL,
description text,
seq_no integer,
status varchar(32),
content_json text,
reflect_type varchar(32),
frequency varchar(32),
prompt_text text,
duration_minutes integer,
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_reflection_spec.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.blueprint_id IS '所属蓝图ID';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.version_id IS '所属版本ID';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.code IS '对象编码';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.name IS '对象名称';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.description IS '描述';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.seq_no IS '排序号';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.status IS '状态';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.content_json IS '扩展内容(JSON)';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.reflect_type IS '反思类型';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.frequency IS '频次';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.prompt_text IS '反思引导语';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.duration_minutes IS '时长(分钟)';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_reflection_spec.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS pbl_blueprint_reflection_spec_idx_pbl_blueprint_reflection_spec_tenant_bp ON pbl_blueprint_reflection_spec (tenant_id, blueprint_id, is_deleted);
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_reflection_spec_uk_pbl_blueprint_reflection_spec_tenant_code ON pbl_blueprint_reflection_spec (tenant_id, code);
-- pbl_blueprint_role: 蓝图子对象-角色(Role)
CREATE TABLE IF NOT EXISTS pbl_blueprint_role (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
blueprint_id varchar(32) NOT NULL,
version_id varchar(32),
code varchar(64),
name varchar(128) NOT NULL,
description text,
seq_no integer,
status varchar(32),
content_json text,
role_category varchar(32),
member_count integer,
responsibility text,
competency_tags text,
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_role.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_role.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_role.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_role.blueprint_id IS '所属蓝图ID';
COMMENT ON COLUMN pbl_blueprint_role.version_id IS '所属版本ID';
COMMENT ON COLUMN pbl_blueprint_role.code IS '对象编码';
COMMENT ON COLUMN pbl_blueprint_role.name IS '对象名称';
COMMENT ON COLUMN pbl_blueprint_role.description IS '描述';
COMMENT ON COLUMN pbl_blueprint_role.seq_no IS '排序号';
COMMENT ON COLUMN pbl_blueprint_role.status IS '状态';
COMMENT ON COLUMN pbl_blueprint_role.content_json IS '扩展内容(JSON)';
COMMENT ON COLUMN pbl_blueprint_role.role_category IS '角色类别';
COMMENT ON COLUMN pbl_blueprint_role.member_count IS '角色人数';
COMMENT ON COLUMN pbl_blueprint_role.responsibility IS '职责说明';
COMMENT ON COLUMN pbl_blueprint_role.competency_tags IS '能力标签(JSON数组)';
COMMENT ON COLUMN pbl_blueprint_role.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_role.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_role.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_role.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_role.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS pbl_blueprint_role_idx_pbl_blueprint_role_tenant_bp ON pbl_blueprint_role (tenant_id, blueprint_id, is_deleted);
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_role_uk_pbl_blueprint_role_tenant_code ON pbl_blueprint_role (tenant_id, code);
-- pbl_blueprint_task: 蓝图子对象-任务分解(Task)
CREATE TABLE IF NOT EXISTS pbl_blueprint_task (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
blueprint_id varchar(32) NOT NULL,
version_id varchar(32),
code varchar(64),
name varchar(128) NOT NULL,
description text,
seq_no integer,
status varchar(32),
content_json text,
parent_task_id varchar(32),
task_type varchar(32),
tool_resources text,
planned_hours numeric(18,2),
assessment_rule text,
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_task.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_task.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_task.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_task.blueprint_id IS '所属蓝图ID';
COMMENT ON COLUMN pbl_blueprint_task.version_id IS '所属版本ID';
COMMENT ON COLUMN pbl_blueprint_task.code IS '对象编码';
COMMENT ON COLUMN pbl_blueprint_task.name IS '对象名称';
COMMENT ON COLUMN pbl_blueprint_task.description IS '描述';
COMMENT ON COLUMN pbl_blueprint_task.seq_no IS '排序号';
COMMENT ON COLUMN pbl_blueprint_task.status IS '状态';
COMMENT ON COLUMN pbl_blueprint_task.content_json IS '扩展内容(JSON)';
COMMENT ON COLUMN pbl_blueprint_task.parent_task_id IS '父任务ID';
COMMENT ON COLUMN pbl_blueprint_task.task_type IS '任务类型';
COMMENT ON COLUMN pbl_blueprint_task.tool_resources IS '工具与资源(JSON数组)';
COMMENT ON COLUMN pbl_blueprint_task.planned_hours IS '计划课时';
COMMENT ON COLUMN pbl_blueprint_task.assessment_rule IS '评价规则(JSON)';
COMMENT ON COLUMN pbl_blueprint_task.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_task.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_task.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_task.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_task.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS pbl_blueprint_task_idx_pbl_blueprint_task_tenant_bp ON pbl_blueprint_task (tenant_id, blueprint_id, is_deleted);
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_task_uk_pbl_blueprint_task_tenant_code ON pbl_blueprint_task (tenant_id, code);
-- pbl_blueprint_template: PBL蓝图模板
CREATE TABLE IF NOT EXISTS pbl_blueprint_template (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
template_code varchar(64) NOT NULL,
template_name varchar(128) NOT NULL,
template_type varchar(32),
domain_code varchar(64),
grade_code varchar(32),
subject_code varchar(64),
complexity_level varchar(32),
scope varchar(32),
source_blueprint_id varchar(32),
usage_count integer,
status varchar(32),
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_template.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_template.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_template.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_template.template_code IS '模板编码';
COMMENT ON COLUMN pbl_blueprint_template.template_name IS '模板名称';
COMMENT ON COLUMN pbl_blueprint_template.template_type IS '模板类型';
COMMENT ON COLUMN pbl_blueprint_template.domain_code IS '领域';
COMMENT ON COLUMN pbl_blueprint_template.grade_code IS '学段';
COMMENT ON COLUMN pbl_blueprint_template.subject_code IS '学科';
COMMENT ON COLUMN pbl_blueprint_template.complexity_level IS '复杂度';
COMMENT ON COLUMN pbl_blueprint_template.scope IS '可见范围';
COMMENT ON COLUMN pbl_blueprint_template.source_blueprint_id IS '来源蓝图ID';
COMMENT ON COLUMN pbl_blueprint_template.usage_count IS '引用次数';
COMMENT ON COLUMN pbl_blueprint_template.status IS '状态';
COMMENT ON COLUMN pbl_blueprint_template.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_template.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_template.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_template.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_template.updated_at IS '更新时间';
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_template_uk_template_tenant_code ON pbl_blueprint_template (tenant_id, template_code);
CREATE INDEX IF NOT EXISTS pbl_blueprint_template_idx_template_tenant_status ON pbl_blueprint_template (tenant_id, status, is_deleted);
-- pbl_blueprint_template_item: PBL蓝图模板明细
CREATE TABLE IF NOT EXISTS pbl_blueprint_template_item (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
template_id varchar(32) NOT NULL,
subobject_type varchar(32) NOT NULL,
item_payload_json text,
seq_no integer,
required_flag integer,
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_template_item.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_template_item.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_template_item.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_template_item.template_id IS '所属模板ID';
COMMENT ON COLUMN pbl_blueprint_template_item.subobject_type IS '子对象类型';
COMMENT ON COLUMN pbl_blueprint_template_item.item_payload_json IS '模板内容(JSON)';
COMMENT ON COLUMN pbl_blueprint_template_item.seq_no IS '排序号';
COMMENT ON COLUMN pbl_blueprint_template_item.required_flag IS '是否必需(0/1)';
COMMENT ON COLUMN pbl_blueprint_template_item.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_template_item.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_template_item.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_template_item.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_template_item.updated_at IS '更新时间';
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_template_item_uk_tplitem_tenant_tpl_type_seq ON pbl_blueprint_template_item (tenant_id, template_id, subobject_type, seq_no);
CREATE INDEX IF NOT EXISTS pbl_blueprint_template_item_idx_tplitem_template ON pbl_blueprint_template_item (tenant_id, template_id, is_deleted);
-- pbl_blueprint_version: PBL蓝图版本
CREATE TABLE IF NOT EXISTS pbl_blueprint_version (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
blueprint_id varchar(32) NOT NULL,
version_no integer NOT NULL,
version_name varchar(128),
version_status varchar(32),
snapshot_json text,
content_hash varchar(64),
published_by varchar(64),
published_at timestamp,
change_note text,
base_version_id varchar(32),
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_version.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_version.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint_version.class_id IS '班级ID';
COMMENT ON COLUMN pbl_blueprint_version.blueprint_id IS '所属蓝图ID';
COMMENT ON COLUMN pbl_blueprint_version.version_no IS '版本号';
COMMENT ON COLUMN pbl_blueprint_version.version_name IS '版本名称';
COMMENT ON COLUMN pbl_blueprint_version.version_status IS '版本状态';
COMMENT ON COLUMN pbl_blueprint_version.snapshot_json IS '蓝图快照(JSON)';
COMMENT ON COLUMN pbl_blueprint_version.content_hash IS '快照内容哈希';
COMMENT ON COLUMN pbl_blueprint_version.published_by IS '发布人';
COMMENT ON COLUMN pbl_blueprint_version.published_at IS '发布时间';
COMMENT ON COLUMN pbl_blueprint_version.change_note IS '变更说明';
COMMENT ON COLUMN pbl_blueprint_version.base_version_id IS '基线版本ID';
COMMENT ON COLUMN pbl_blueprint_version.is_deleted IS '删除标记(0未删/1已删)';
COMMENT ON COLUMN pbl_blueprint_version.created_by IS '创建人';
COMMENT ON COLUMN pbl_blueprint_version.created_at IS '创建时间';
COMMENT ON COLUMN pbl_blueprint_version.updated_by IS '更新人';
COMMENT ON COLUMN pbl_blueprint_version.updated_at IS '更新时间';
CREATE UNIQUE INDEX IF NOT EXISTS pbl_blueprint_version_uk_version_blueprint_no ON pbl_blueprint_version (tenant_id, blueprint_id, version_no);
CREATE INDEX IF NOT EXISTS pbl_blueprint_version_idx_version_blueprint ON pbl_blueprint_version (tenant_id, blueprint_id, is_deleted);

View File

@ -0,0 +1,188 @@
-- =====================================================================
-- pbl_blueprint 子对象表 DDL(M1a,7 类子对象)
-- 统一泛化契约:id / tenant_id / blueprint_id / (task_id|mission_id|learning_goal_id) / seq / code /
-- name / description / spec(json) / status / 审计五件套 / deleted
-- 所有索引首列 = tenant_id(租户强制打头);主键 varchar(32);软删除 tinyint;不使用 boolean/int(11)。
-- =====================================================================
-- 1/7 pbl_blueprint_task 任务 -------------------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_task` (
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
`parent_id` varchar(32) DEFAULT NULL COMMENT '父任务ID,支持分层',
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
`code` varchar(64) DEFAULT NULL COMMENT '任务编码,蓝图内唯一',
`name` varchar(200) NOT NULL COMMENT '任务名称',
`description` text COMMENT '任务描述/驱动性问题',
`spec` json DEFAULT NULL COMMENT '任务规格',
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_bptask_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
KEY `idx_pbl_bptask_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
KEY `idx_pbl_bptask_parent` (`tenant_id`, `parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-任务';
-- 2/7 pbl_blueprint_mission 关卡 ---------------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_mission` (
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
`task_id` varchar(32) NOT NULL COMMENT '所属任务ID',
`seq` int NOT NULL DEFAULT 0 COMMENT '任务内排序号',
`code` varchar(64) DEFAULT NULL COMMENT '关卡编码,蓝图内唯一',
`name` varchar(200) NOT NULL COMMENT '关卡名称',
`description` text COMMENT '关卡说明',
`spec` json DEFAULT NULL COMMENT '关卡规格',
`unlock_condition` json DEFAULT NULL COMMENT '解锁条件',
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_bpms_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
KEY `idx_pbl_bpms_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
KEY `idx_pbl_bpms_task` (`tenant_id`, `task_id`, `seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-关卡';
-- 3/7 pbl_blueprint_role 角色 -------------------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_role` (
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID,空=蓝图级角色',
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
`code` varchar(64) DEFAULT NULL COMMENT '角色编码,蓝图内唯一',
`name` varchar(200) NOT NULL COMMENT '角色名称',
`description` text COMMENT '角色职责说明',
`spec` json DEFAULT NULL COMMENT '角色规格',
`permissions` json DEFAULT NULL COMMENT '运行时操作权限集合',
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_bprole_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
KEY `idx_pbl_bprole_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
KEY `idx_pbl_bprole_task` (`tenant_id`, `task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-角色';
-- 4/7 pbl_blueprint_learning_goal 学习目标 ------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_learning_goal` (
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID,空=蓝图级目标',
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
`code` varchar(64) DEFAULT NULL COMMENT '目标编码,蓝图内唯一',
`name` varchar(200) NOT NULL COMMENT '目标名称',
`dimension` varchar(32) NOT NULL DEFAULT 'knowledge' COMMENT 'knowledge/ability/literacy/attitude',
`description` text COMMENT '目标描述',
`spec` json DEFAULT NULL COMMENT '目标规格',
`weight` float DEFAULT 1.0 COMMENT '评估权重',
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_bplg_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
KEY `idx_pbl_bplg_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
KEY `idx_pbl_bplg_dim` (`tenant_id`, `blueprint_id`, `dimension`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-学习目标';
-- 5/7 pbl_blueprint_evidence_spec 证据规格 ------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_evidence_spec` (
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID',
`mission_id` varchar(32) DEFAULT NULL COMMENT '关联关卡ID',
`learning_goal_id` varchar(32) DEFAULT NULL COMMENT '关联学习目标ID',
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
`code` varchar(64) DEFAULT NULL COMMENT '证据规格编码,蓝图内唯一',
`name` varchar(200) NOT NULL COMMENT '证据规格名称',
`evidence_type` varchar(32) NOT NULL DEFAULT 'auto' COMMENT 'auto/manual/artifact/interaction/assessment',
`collect_mode` varchar(32) NOT NULL DEFAULT 'auto' COMMENT 'auto/trigger/manual/scheduled',
`description` text COMMENT '证据说明',
`spec` json DEFAULT NULL COMMENT '证据规格',
`rule` json DEFAULT NULL COMMENT '校验规则',
`weight` float DEFAULT 1.0 COMMENT '评分权重',
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_bpev_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
KEY `idx_pbl_bpev_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
KEY `idx_pbl_bpev_goal` (`tenant_id`, `learning_goal_id`),
KEY `idx_pbl_bpev_mission` (`tenant_id`, `mission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-证据规格';
-- 6/7 pbl_blueprint_artifact_spec 产出物规格 ----------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_artifact_spec` (
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID',
`mission_id` varchar(32) DEFAULT NULL COMMENT '关联关卡ID',
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
`code` varchar(64) DEFAULT NULL COMMENT '产出物规格编码,蓝图内唯一',
`name` varchar(200) NOT NULL COMMENT '产出物名称',
`artifact_type` varchar(32) NOT NULL DEFAULT 'document' COMMENT 'document/model/scene/video/audio/image/code/presentation/dataset',
`description` text COMMENT '产出物说明与要求',
`spec` json DEFAULT NULL COMMENT '产出物规格',
`accept_criteria` json DEFAULT NULL COMMENT '验收标准条目数组',
`required` tinyint NOT NULL DEFAULT 1 COMMENT '是否必交 0否 1是',
`weight` float DEFAULT 1.0 COMMENT '评分权重',
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_bpart_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
KEY `idx_pbl_bpart_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
KEY `idx_pbl_bpart_task` (`tenant_id`, `task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-产出物规格';
-- 7/7 pbl_blueprint_reflection_spec 反思规格 ----------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_reflection_spec` (
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID',
`mission_id` varchar(32) DEFAULT NULL COMMENT '关联关卡ID',
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
`code` varchar(64) DEFAULT NULL COMMENT '反思规格编码,蓝图内唯一',
`name` varchar(200) NOT NULL COMMENT '反思规格名称',
`trigger_point` varchar(32) NOT NULL DEFAULT 'mission_end' COMMENT 'task_start/task_end/mission_end/milestone/project_end/manual',
`reflection_type` varchar(32) NOT NULL DEFAULT 'self' COMMENT 'self/peer/team/teacher',
`description` text COMMENT '反思说明',
`spec` json DEFAULT NULL COMMENT '反思规格(引导问题等)',
`rubric_ref` json DEFAULT NULL COMMENT '关联量规维度引用',
`weight` float DEFAULT 1.0 COMMENT '评分权重',
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pbl_bpref_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
KEY `idx_pbl_bpref_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
KEY `idx_pbl_bpref_trigger` (`tenant_id`, `blueprint_id`, `trigger_point`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-反思规格';

View File

@ -1,213 +1,241 @@
# -*- coding: utf-8 -*- """pbl_blueprint 7 类子对象泛化契约(M1a)。
"""蓝图子对象契约接口(M1a 落表 + CRUD;M1b 扩展校验/编排)。
7 类子对象表(子类型 -> 表名见 models.SUBOBJECT_TYPE_TABLE): 7 类子对象统一走同一套 CRUD 契约(obj_type 分派到具体表),
mission / learning_goal / task / role / artifact_spec / evidence_spec / reflection_spec 避免为每类子对象写重复代码,也便于 Compiler / Validation 统一遍历。
契约函数: tenant_id 强制打头:所有函数第一入参 tenant_id,缺失即 PblTenantRequired。
- subobject_save(subobject_type, payload, tenant_id=None) -> dict
- subobject_list(subobject_type, blueprint_id, tenant_id=None) -> dict {rows,total}
- subobject_get(subobject_type, object_id, tenant_id=None) -> dict
- subobject_delete(subobject_type, object_id, tenant_id=None) -> dict
- list_all_subobjects(blueprint_id, tenant_id=None) -> dict {type: [rows]}
- count_subobjects(blueprint_id, tenant_id=None) -> dict {type: n}(M2 校验引擎复用)
- copy_subobjects(src_blueprint_id, dst_blueprint_id, tenant_id=None) -> dict
公共字段(所有子对象表一致):tenant_id / class_id / blueprint_id / version_id / code /
name / description / seq_no / status / content_json + 审计字段。
""" """
from . import db as _db
from . import models as _m
COMMON_FIELDS = ['class_id', 'blueprint_id', 'version_id', 'code', 'name', import datetime
'description', 'seq_no', 'status', 'content_json']
TYPE_OPTIONS = [ from .db import (
{'code': 'mission', 'label': '真实情境任务'}, PblError,
{'code': 'learning_goal', 'label': '学习目标'}, PblNotFound,
{'code': 'task', 'label': '任务分解'}, PblValidationError,
{'code': 'role', 'label': '角色'}, get_dbname,
{'code': 'artifact_spec', 'label': '产出物规格'}, get_env,
{'code': 'evidence_spec', 'label': '证据规格'}, get_sor,
{'code': 'reflection_spec', 'label': '反思规格'}, new_id,
] require_tenant,
)
STATUS_OPTIONS = [ # 7 类子对象类型 -> 表名
{'code': 'active', 'label': '启用'}, SUBOBJECT_TABLES = {
{'code': 'draft', 'label': '草稿'}, 'task': 'pbl_blueprint_task',
{'code': 'disabled', 'label': '停用'}, 'mission': 'pbl_blueprint_mission',
] 'role': 'pbl_blueprint_role',
'learning_goal': 'pbl_blueprint_learning_goal',
'evidence_spec': 'pbl_blueprint_evidence_spec',
'artifact_spec': 'pbl_blueprint_artifact_spec',
'reflection_spec': 'pbl_blueprint_reflection_spec',
}
SUBOBJECT_TYPES = list(SUBOBJECT_TABLES.keys())
# 各类型编码前缀(自动生成 code 用)
CODE_PREFIX = {
'task': 'TASK',
'mission': 'MISSION',
'role': 'ROLE',
'learning_goal': 'GOAL',
'evidence_spec': 'EVI',
'artifact_spec': 'ART',
'reflection_spec': 'REF',
}
# 各类型可编辑字段(泛化契约 + 类型特有字段)
BASE_EDITABLE = ['blueprint_id', 'seq', 'code', 'name', 'description', 'spec', 'status']
TYPE_FIELDS = {
'task': BASE_EDITABLE + ['parent_id'],
'mission': BASE_EDITABLE + ['task_id', 'unlock_condition'],
'role': BASE_EDITABLE + ['task_id', 'permissions'],
'learning_goal': BASE_EDITABLE + ['task_id', 'dimension', 'weight'],
'evidence_spec': BASE_EDITABLE + ['task_id', 'mission_id', 'learning_goal_id',
'evidence_type', 'collect_mode', 'rule', 'weight'],
'artifact_spec': BASE_EDITABLE + ['task_id', 'mission_id', 'artifact_type',
'accept_criteria', 'required', 'weight'],
'reflection_spec': BASE_EDITABLE + ['task_id', 'mission_id', 'trigger_point',
'reflection_type', 'rubric_ref', 'weight'],
}
# 类型特有必填字段
TYPE_REQUIRED = {
'mission': ['task_id'],
}
def table_of(subobject_type): def _now():
table = _m.SUBOBJECT_TYPE_TABLE.get((subobject_type or '').strip()) return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if not table:
raise _db.PblError(_db.ERR_PARAM_INVALID,
'未知子对象类型: %s(可选 %s)'
% (subobject_type, ','.join(sorted(_m.SUBOBJECT_TYPE_TABLE))))
return table
def _typed_fields(table): def _table(obj_type):
"""该表可写业务字段(models/*.json 为准,排除主键/租户/审计)。""" if obj_type not in SUBOBJECT_TABLES:
skip = ('id', 'tenant_id', 'created_by', 'created_at', 'updated_by', 'updated_at', raise PblValidationError(
'is_deleted') 'unknown obj_type: %s (allowed: %s)' % (obj_type, ','.join(SUBOBJECT_TYPES)),
return [k for k in _m.load_model(table)['fields'].keys() if k not in skip] code='PBL_UNKNOWN_OBJ_TYPE')
return SUBOBJECT_TABLES[obj_type]
def _fetch(table, rec_id, tid): def _where(tenant_id, extra=None):
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tid, 'is_deleted': 0}) cond = "tenant_id='%s'" % tenant_id.replace("'", "''")
if extra:
cond = cond + ' and ' + extra
return cond
def subobject_save(subobject_type, payload, tenant_id=None): def _pick(data, keys):
"""新建/更新子对象。payload 含 id 视为更新;blueprint_id 必填且必须属本租户。""" out = {}
tid = tenant_id or _db.current_tenant_id() for k in keys:
table = table_of(subobject_type) if k in data and data[k] is not None:
payload = dict(payload or {}) out[k] = data[k]
allowed = _typed_fields(table) return out
data = dict((k, payload[k]) for k in allowed if k in payload)
now = _db.now_str()
operator = _db.current_operator()
blueprint_id = data.get('blueprint_id') or payload.get('blueprint_id')
if not blueprint_id: def _gen_code(sor, tenant_id, blueprint_id, obj_type):
raise _db.PblError(_db.ERR_PARAM_INVALID, 'blueprint_id 必填') tbl = _table(obj_type)
bp = _db.select_one(_m.TABLES[0], where={'id': blueprint_id, 'tenant_id': tid, prefix = '%s-' % CODE_PREFIX.get(obj_type, obj_type.upper())
'is_deleted': 0}) rows = sor.R(tbl, _where(tenant_id, "blueprint_id='%s'" % blueprint_id))
if not bp: seq = len(rows or []) + 1
raise _db.PblError(_db.ERR_NOT_FOUND, '所属蓝图不存在或无权访问: %s' % blueprint_id) return '%s%03d' % (prefix, seq)
data['blueprint_id'] = blueprint_id
# ---------------------------------------------------------------- 泛化 CRUD
def create_subobject(tenant_id, obj_type, data, env=None, operator=None):
"""新建子对象。:param obj_type: 7 类之一 :param data: 字段字典(须含 blueprint_id/name)"""
tid = require_tenant(tenant_id)
tbl = _table(obj_type)
if not isinstance(data, dict):
raise PblValidationError('data must be a dict')
if not data.get('blueprint_id'):
raise PblValidationError('blueprint_id is required')
if not data.get('name'): if not data.get('name'):
raise _db.PblError(_db.ERR_PARAM_INVALID, 'name 必填') raise PblValidationError('name is required')
data.setdefault('status', 'active') for field in TYPE_REQUIRED.get(obj_type, []):
if not data.get('class_id'): if not data.get(field):
data['class_id'] = bp.get('class_id') or '' raise PblValidationError('%s is required for obj_type=%s' % (field, obj_type))
rec_id = payload.get('id') e = get_env(env)
if rec_id: sor = get_sor(e, get_dbname(e))
old = _fetch(table, rec_id, tid) if sor is None:
if not old: raise PblError('sqlor unavailable for module pbl_blueprint', code='PBL_DB_UNAVAILABLE')
raise _db.PblError(_db.ERR_NOT_FOUND, '子对象不存在或无权访问: %s' % rec_id)
if old.get('blueprint_id') != blueprint_id:
raise _db.PblError(_db.ERR_REFERENCED, '子对象不允许跨蓝图迁移')
code = data.get('code') or old.get('code')
if code:
dup = _db.select_one(table, where={'tenant_id': tid, 'code': code,
'id!': rec_id, 'is_deleted': 0})
if dup:
raise _db.PblError(_db.ERR_PARAM_INVALID, '子对象编码已存在: %s' % code)
data['code'] = code
data.update({'updated_by': operator, 'updated_at': now})
_db.update_row(table, data, {'id': rec_id, 'tenant_id': tid})
result = _fetch(table, rec_id, tid)
_db.audit('subobject_update', table, rec_id, before=old, after=result, tenant_id=tid)
return result
new_id = _db.gen_id() row = {'id': data.get('id') or new_id(), 'tenant_id': tid}
row = dict((k, '') for k in allowed) row.update(_pick(data, TYPE_FIELDS.get(obj_type, BASE_EDITABLE)))
row.update(data) if not row.get('code'):
row.update({'id': new_id, 'tenant_id': tid, 'is_deleted': 0, row['code'] = _gen_code(sor, tid, row['blueprint_id'], obj_type)
'seq_no': int(data.get('seq_no') or 0), row.setdefault('seq', 0)
'created_by': operator, 'created_at': now, row.setdefault('status', 'draft')
'updated_by': operator, 'updated_at': now}) row['created_by'] = operator
_db.insert_row(table, row) row['created_at'] = _now()
_db.audit('subobject_create', table, new_id, after=row, tenant_id=tid) row['updated_by'] = operator
return _fetch(table, new_id, tid) row['updated_at'] = _now()
row['deleted'] = 0
sor.C(tbl, row)
def subobject_list(subobject_type, blueprint_id, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
table = table_of(subobject_type)
if not blueprint_id:
raise _db.PblError(_db.ERR_PARAM_INVALID, 'blueprint_id 必填')
rows = _db.select_rows(table, where={'tenant_id': tid, 'blueprint_id': blueprint_id,
'is_deleted': 0}, orderby='seq_no asc, created_at asc') or []
return {'subobject_type': subobject_type, 'table': table,
'rows': rows, 'total': len(rows)}
def subobject_get(subobject_type, object_id, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
table = table_of(subobject_type)
row = _fetch(table, object_id, tid)
if not row:
raise _db.PblError(_db.ERR_NOT_FOUND, '子对象不存在或无权访问: %s' % object_id)
return row return row
def subobject_delete(subobject_type, object_id, tenant_id=None): def update_subobject(tenant_id, obj_type, obj_id, data, env=None, operator=None):
tid = tenant_id or _db.current_tenant_id() """更新子对象(仅该类型可编辑字段)。"""
table = table_of(subobject_type) tid = require_tenant(tenant_id)
old = _fetch(table, object_id, tid) tbl = _table(obj_type)
if not old: if not obj_id:
raise _db.PblError(_db.ERR_NOT_FOUND, '子对象不存在或无权访问: %s' % object_id) raise PblValidationError('obj_id is required')
if (old.get('status') or '') == 'locked': patch = _pick(data or {}, TYPE_FIELDS.get(obj_type, BASE_EDITABLE))
raise _db.PblError(_db.ERR_REFERENCED, '锁定状态的子对象不可删除') patch.pop('blueprint_id', None) # 不允许跨蓝图迁移
_db.update_row(table, {'is_deleted': 1, 'updated_by': _db.current_operator(), if not patch:
'updated_at': _db.now_str()}, return get_subobject(tid, obj_type, obj_id, env=env)
{'id': object_id, 'tenant_id': tid}) patch['updated_by'] = operator
_db.audit('subobject_delete', table, object_id, before=old, tenant_id=tid) patch['updated_at'] = _now()
return {'id': object_id, 'subobject_type': subobject_type, 'deleted': True} e = get_env(env)
sor = get_sor(e, get_dbname(e))
sor.U(tbl, patch, _where(tid, "id='%s'" % obj_id))
row = get_subobject(tid, obj_type, obj_id, env=env)
return row
def list_all_subobjects(blueprint_id, tenant_id=None): def delete_subobject(tenant_id, obj_type, obj_id, env=None, operator=None, hard=False):
tid = tenant_id or _db.current_tenant_id() """软删除子对象(hard=True 物理删除)。"""
out = {} tid = require_tenant(tenant_id)
for stype in sorted(_m.SUBOBJECT_TYPE_TABLE): tbl = _table(obj_type)
out[stype] = subobject_list(stype, blueprint_id, tid)['rows'] if not obj_id:
return out raise PblValidationError('obj_id is required')
e = get_env(env)
sor = get_sor(e, get_dbname(e))
cond = _where(tid, "id='%s'" % obj_id)
if hard:
sor.D(tbl, cond)
else:
sor.U(tbl, {'deleted': 1, 'updated_by': operator, 'updated_at': _now()}, cond)
return {'id': obj_id, 'obj_type': obj_type, 'deleted': 1}
def count_subobjects(blueprint_id, tenant_id=None): def get_subobject(tenant_id, obj_type, obj_id, env=None):
tid = tenant_id or _db.current_tenant_id() """按 id 取子对象。"""
out = {} tid = require_tenant(tenant_id)
for stype, table in _m.SUBOBJECT_TYPE_TABLE.items(): tbl = _table(obj_type)
rows = _db.select_rows(table, where={'tenant_id': tid, 'blueprint_id': blueprint_id, if not obj_id:
'is_deleted': 0}, fields='id') or [] raise PblValidationError('obj_id is required')
out[stype] = len(rows) e = get_env(env)
out['total'] = sum(out.values()) sor = get_sor(e, get_dbname(e))
return out rows = sor.R(tbl, _where(tid, "id='%s' and deleted=0" % obj_id))
if not rows:
raise PblNotFound('%s not found: %s' % (obj_type, obj_id), code='PBL_SUBOBJECT_NOT_FOUND')
return rows[0]
def copy_subobjects(src_blueprint_id, dst_blueprint_id, tenant_id=None): def list_subobjects(tenant_id, obj_type, blueprint_id, filters=None, env=None):
"""把源蓝图的全部子对象复制到目标蓝图(新主键,重挂 blueprint_id)。""" """列出某蓝图下某类型的全部子对象(按 seq 升序)。"""
tid = tenant_id or _db.current_tenant_id() tid = require_tenant(tenant_id)
now = _db.now_str() tbl = _table(obj_type)
operator = _db.current_operator() if not blueprint_id:
copied = {} raise PblValidationError('blueprint_id is required')
for stype, table in _m.SUBOBJECT_TYPE_TABLE.items(): filters = filters or {}
rows = _db.select_rows(table, where={'tenant_id': tid, 'blueprint_id': src_blueprint_id, conds = ["blueprint_id='%s'" % blueprint_id.replace("'", "''"), 'deleted=0']
'is_deleted': 0}) or [] for key in ('status', 'task_id', 'mission_id', 'parent_id', 'learning_goal_id',
n = 0 'dimension', 'evidence_type', 'artifact_type', 'trigger_point', 'reflection_type'):
for r in rows: if filters.get(key):
new = dict(r) conds.append("%s='%s'" % (key, str(filters[key]).replace("'", "''")))
new.update({'id': _db.gen_id(), 'blueprint_id': dst_blueprint_id, e = get_env(env)
'version_id': '', 'code': '', sor = get_sor(e, get_dbname(e))
'created_by': operator, 'created_at': now, return sor.R(tbl, _where(tid, ' and '.join(conds)), 'seq asc') or []
'updated_by': operator, 'updated_at': now, 'is_deleted': 0})
_db.insert_row(table, new)
n += 1
copied[stype] = n
copied['total'] = sum(v for k, v in copied.items() if k != 'total')
return copied
def soft_delete_by_blueprint(blueprint_id, tenant_id=None): def batch_upsert_subobjects(tenant_id, obj_type, blueprint_id, rows, env=None, operator=None):
"""蓝图删除时级联软删其子对象。""" """批量新增/更新子对象(Designer Agent 一次性写入整棵子树用)。
tid = tenant_id or _db.current_tenant_id()
now = _db.now_str()
operator = _db.current_operator()
affected = 0
for table in _m.SUBOBJECT_TYPE_TABLE.values():
rows = _db.select_rows(table, where={'tenant_id': tid, 'blueprint_id': blueprint_id,
'is_deleted': 0}, fields='id') or []
for r in rows:
_db.update_row(table, {'is_deleted': 1, 'updated_by': operator,
'updated_at': now},
{'id': r.get('id'), 'tenant_id': tid})
affected += 1
return affected
rows: [{'id': 可选, ...字段}];带 id 且存在则更新,否则新建。
:return: {'created': n, 'updated': m, 'ids': [...]}
"""
tid = require_tenant(tenant_id)
_table(obj_type)
if not blueprint_id:
raise PblValidationError('blueprint_id is required')
if not isinstance(rows, list):
raise PblValidationError('rows must be a list')
def subobject_type_options(): e = get_env(env)
return list(TYPE_OPTIONS) sor = get_sor(e, get_dbname(e))
created, updated, ids = 0, 0, []
for idx, row in enumerate(rows):
item = dict(row or {})
item['blueprint_id'] = blueprint_id
item.setdefault('seq', idx)
oid = item.get('id')
exists = False
if oid:
try:
get_subobject(tid, obj_type, oid, env=e)
exists = True
except PblNotFound:
exists = False
if exists:
update_subobject(tid, obj_type, oid, item, env=e, operator=operator)
updated += 1
ids.append(oid)
else:
item.pop('id', None)
new_row = create_subobject(tid, obj_type, item, env=e, operator=operator)
created += 1
ids.append(new_row['id'])
return {'created': created, 'updated': updated, 'ids': ids}

View File

@ -1,238 +1,349 @@
# -*- coding: utf-8 -*- """pbl_blueprint 模板与离线兜底(M1a)。
"""蓝图模板契约接口(M1a:模板主表/明细 CRUD + 实例化为蓝图)。
契约函数: - 模板 CRUD(pbl_blueprint_template / pbl_blueprint_template_item)
- save_template(payload, tenant_id=None) -> dict - instantiate_template:模板 -> 新蓝图(含 7 类子对象,ref_key/parent_ref 解析为真实 ID)
- get_template(template_id, tenant_id=None) -> dict(含 items 明细) - build_offline_fallback_blueprint:Agent/网络不可用时的最小可用蓝图兜底
- list_templates(filters=None, page=1, rows=20, tenant_id=None) -> dict
- delete_template(template_id, tenant_id=None) -> dict tenant_id 强制打头,缺失即 PblTenantRequired。
- create_template_from_blueprint(blueprint_id, payload=None, tenant_id=None) -> dict
把蓝图(含子对象)抽为模板:写 template 主表 + template_item 明细
- template_instantiate(template_id, payload=None, tenant_id=None) -> dict
由模板实例化出新蓝图(draft),并回写 usage_count
""" """
from . import db as _db
from . import models as _m
from . import blueprint_crud as _bp
from . import subobjects as _s
TABLE = 'pbl_blueprint_template' import datetime
TABLE_ITEM = 'pbl_blueprint_template_item'
TEMPLATE_FIELDS = ['class_id', 'template_code', 'template_name', 'template_type', from .db import (
'domain_code', 'grade_code', 'subject_code', 'complexity_level', PblError,
'scope', 'source_blueprint_id', 'status', 'description'] PblNotFound,
PblValidationError,
get_dbname,
get_env,
get_sor,
new_id,
require_tenant,
)
from .subobjects import SUBOBJECT_TYPES, create_subobject
TPL_TABLE = 'pbl_blueprint_template'
ITEM_TABLE = 'pbl_blueprint_template_item'
TPL_EDITABLE = ['code', 'name', 'category', 'subject', 'grade', 'duration_hours',
'description', 'content', 'status']
# 离线兜底最小蓝图骨架(无 Agent、无模板时也能产出可编译蓝图)
OFFLINE_FALLBACK = {
'name': '离线兜底蓝图',
'summary': '由 pbl_blueprint 离线兜底生成的最小可用蓝图(1 任务 / 1 关卡 / 2 角色 / 2 目标 / 1 证据 / 1 产出物 / 1 反思)。',
'duration_hours': 4,
'items': [
{'obj_type': 'task', 'ref_key': 'T1', 'name': '主任务:完成项目挑战',
'spec': {'duration_hours': 4, 'difficulty': 'medium'}},
{'obj_type': 'mission', 'parent_ref': 'T1', 'ref_key': 'M1', 'name': '关卡1:需求分析与方案设计',
'spec': {'goal': '产出方案初稿'}},
{'obj_type': 'role', 'parent_ref': 'T1', 'ref_key': 'R1', 'name': '队长',
'spec': {'max_members': 1}},
{'obj_type': 'role', 'parent_ref': 'T1', 'ref_key': 'R2', 'name': '记录员',
'spec': {'max_members': 1}},
{'obj_type': 'learning_goal', 'parent_ref': 'T1', 'ref_key': 'G1', 'name': '掌握项目核心知识',
'dimension': 'knowledge', 'weight': 1.0},
{'obj_type': 'learning_goal', 'parent_ref': 'T1', 'ref_key': 'G2', 'name': '具备协作与表达能力',
'dimension': 'ability', 'weight': 1.0},
{'obj_type': 'evidence_spec', 'parent_ref': 'T1', 'ref_key': 'E1', 'name': '方案设计过程证据',
'evidence_type': 'auto', 'collect_mode': 'auto'},
{'obj_type': 'artifact_spec', 'parent_ref': 'T1', 'ref_key': 'A1', 'name': '项目成果报告',
'artifact_type': 'document', 'required': 1},
{'obj_type': 'reflection_spec', 'parent_ref': 'T1', 'ref_key': 'F1', 'name': '结题反思',
'trigger_point': 'project_end', 'reflection_type': 'self'},
],
}
def _fetch(tid, rec_id, table=TABLE): def _now():
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tid, 'is_deleted': 0}) return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def save_template(payload, tenant_id=None): def _where(tenant_id, extra=None):
tid = tenant_id or _db.current_tenant_id() cond = "tenant_id='%s'" % tenant_id.replace("'", "''")
payload = dict(payload or {}) if extra:
data = dict((k, payload[k]) for k in TEMPLATE_FIELDS if k in payload) cond = cond + ' and ' + extra
now = _db.now_str() return cond
operator = _db.current_operator()
rec_id = payload.get('id')
if rec_id:
old = _fetch(tid, rec_id)
if not old:
raise _db.PblError(_db.ERR_NOT_FOUND, '模板不存在或无权访问: %s' % rec_id)
code = data.get('template_code') or old.get('template_code')
dup = _db.select_one(TABLE, where={'tenant_id': tid, 'template_code': code,
'id!': rec_id, 'is_deleted': 0})
if dup:
raise _db.PblError(_db.ERR_PARAM_INVALID, '模板编码已存在: %s' % code)
data['template_code'] = code
data.update({'updated_by': operator, 'updated_at': now})
_db.update_row(TABLE, data, {'id': rec_id, 'tenant_id': tid})
_db.audit('template_update', TABLE, rec_id, before=old,
after=_fetch(tid, rec_id), tenant_id=tid)
return _fetch(tid, rec_id)
if not data.get('template_name'): def _pick(data, keys):
raise _db.PblError(_db.ERR_PARAM_INVALID, 'template_name 必填') out = {}
if not data.get('template_code'): for k in keys:
data['template_code'] = 'TPL-%s' % _db.gen_id()[:8].upper() if k in data and data[k] is not None:
dup = _db.select_one(TABLE, where={'tenant_id': tid, 'template_code': data['template_code'], out[k] = data[k]
'is_deleted': 0}) return out
if dup:
raise _db.PblError(_db.ERR_PARAM_INVALID, '模板编码已存在: %s' % data['template_code'])
new_id = _db.gen_id() # ---------------------------------------------------------------- 模板 CRUD
row = {
'id': new_id, 'tenant_id': tid, 'class_id': data.get('class_id', ''), def create_template(tenant_id, data, env=None, operator=None):
'template_code': data['template_code'], 'template_name': data['template_name'], """新建模板;data['items'] 为子对象骨架条目数组(可选)。"""
'template_type': data.get('template_type', 'blueprint'), tid = require_tenant(tenant_id)
'domain_code': data.get('domain_code', ''), 'grade_code': data.get('grade_code', ''), if not isinstance(data, dict) or not data.get('name'):
'subject_code': data.get('subject_code', ''), raise PblValidationError('name is required')
'complexity_level': data.get('complexity_level', ''), e = get_env(env)
'scope': data.get('scope', 'tenant'), sor = get_sor(e, get_dbname(e))
'source_blueprint_id': data.get('source_blueprint_id', ''), if sor is None:
'usage_count': 0, 'status': data.get('status', 'draft'), raise PblError('sqlor unavailable for module pbl_blueprint', code='PBL_DB_UNAVAILABLE')
'description': data.get('description', ''),
'is_deleted': 0, 'created_by': operator, 'created_at': now, tpl = {
'updated_by': operator, 'updated_at': now, 'id': data.get('id') or new_id(),
'tenant_id': tid,
'code': data.get('code') or ('TPL-%s' % datetime.datetime.now().strftime('%Y%m%d%H%M%S')),
'name': data['name'],
'category': data.get('category') or 'general',
'subject': data.get('subject'),
'grade': data.get('grade'),
'duration_hours': data.get('duration_hours'),
'description': data.get('description'),
'content': data.get('content'),
'status': data.get('status') or 'enabled',
'use_count': 0,
'created_by': operator,
'created_at': _now(),
'updated_by': operator,
'updated_at': _now(),
'deleted': 0,
} }
_db.insert_row(TABLE, row) sor.C(TPL_TABLE, tpl)
_db.audit('template_create', TABLE, new_id, after=row, tenant_id=tid) items = data.get('items') or []
return _fetch(tid, new_id) tpl['items'] = _save_items(sor, tid, tpl['id'], items, operator)
def list_template_items(template_id, tid):
rows = _db.select_rows(TABLE_ITEM, where={'tenant_id': tid, 'template_id': template_id,
'is_deleted': 0},
orderby='subobject_type asc, seq_no asc') or []
return rows
def get_template(template_id, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
row = _fetch(tid, template_id)
if not row:
raise _db.PblError(_db.ERR_NOT_FOUND, '模板不存在或无权访问: %s' % template_id)
row['items'] = list_template_items(template_id, tid)
row['item_count'] = len(row['items'])
return row
def list_templates(filters=None, page=1, rows=20, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
filters = dict(filters or {})
where = {'tenant_id': tid, 'is_deleted': 0}
for k in ('status', 'template_type', 'domain_code', 'grade_code', 'subject_code',
'complexity_level', 'scope'):
if filters.get(k):
where[k] = filters[k]
for k in ('template_name', 'template_code'):
if filters.get(k):
where['%s~' % k] = '%%%s%%' % filters[k]
page = max(1, int(page or 1))
rows = min(200, max(1, int(rows or 20)))
allowed = ('created_at', 'updated_at', 'template_code', 'usage_count', 'status')
sortby = filters.get('sortby') if filters.get('sortby') in allowed else 'created_at'
order = 'asc' if str(filters.get('sortorder', 'desc')).lower() == 'asc' else 'desc'
all_rows = _db.select_rows(TABLE, where=where,
fields='id, tenant_id, class_id, template_code, template_name, '
'template_type, domain_code, grade_code, subject_code, '
'complexity_level, scope, usage_count, status, created_at, '
'updated_at',
orderby='%s %s' % (sortby, order)) or []
total = len(all_rows)
start = (page - 1) * rows
return {'rows': all_rows[start:start + rows], 'total': total,
'page': page, 'rows_per_page': rows}
def delete_template(template_id, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
old = _fetch(tid, template_id)
if not old:
raise _db.PblError(_db.ERR_NOT_FOUND, '模板不存在或无权访问: %s' % template_id)
now = _db.now_str()
operator = _db.current_operator()
_db.update_row(TABLE, {'is_deleted': 1, 'updated_by': operator, 'updated_at': now},
{'id': template_id, 'tenant_id': tid})
for it in list_template_items(template_id, tid):
_db.update_row(TABLE_ITEM, {'is_deleted': 1, 'updated_by': operator,
'updated_at': now},
{'id': it.get('id'), 'tenant_id': tid})
_db.audit('template_delete', TABLE, template_id, before=old, tenant_id=tid)
return {'id': template_id, 'deleted': True}
def create_template_from_blueprint(blueprint_id, payload=None, tenant_id=None):
"""把蓝图抽为模板:主表 + 每类子对象一条 item(payload 可覆盖模板字段)。"""
tid = tenant_id or _db.current_tenant_id()
payload = dict(payload or {})
bp = _bp.get_blueprint(blueprint_id, tid)
tpl_payload = {
'template_name': payload.get('template_name') or ('%s 模板' % (bp.get('blueprint_name') or '')),
'template_code': payload.get('template_code') or '',
'template_type': payload.get('template_type', 'blueprint'),
'domain_code': bp.get('domain_code', ''), 'grade_code': bp.get('grade_code', ''),
'subject_code': bp.get('subject_code', ''),
'complexity_level': bp.get('complexity_level', ''),
'scope': payload.get('scope', 'tenant'),
'source_blueprint_id': blueprint_id,
'description': payload.get('description', ''),
'class_id': payload.get('class_id') or bp.get('class_id', ''),
}
tpl = save_template(tpl_payload, tid)
now = _db.now_str()
operator = _db.current_operator()
children = _s.list_all_subobjects(blueprint_id, tid)
created = 0
for stype in sorted(children):
seq = 0
for obj in children[stype]:
payload_json = dict((k, v) for k, v in obj.items()
if k not in ('id', 'tenant_id', 'blueprint_id', 'version_id',
'created_by', 'created_at', 'updated_by',
'updated_at', 'is_deleted'))
_db.insert_row(TABLE_ITEM, {
'id': _db.gen_id(), 'tenant_id': tid,
'class_id': obj.get('class_id') or '', 'template_id': tpl['id'],
'subobject_type': stype, 'code': obj.get('code') or '',
'name': obj.get('name') or '', 'description': obj.get('description') or '',
'item_payload_json': _db.dumps(payload_json), 'seq_no': seq,
'required_flag': int(obj.get('required_flag') or 1),
'status': 'active', 'is_deleted': 0,
'created_by': operator, 'created_at': now,
'updated_by': operator, 'updated_at': now,
})
seq += 1
created += 1
_db.audit('template_from_blueprint', TABLE, tpl['id'],
after={'source_blueprint_id': blueprint_id, 'items': created}, tenant_id=tid)
tpl = get_template(tpl['id'], tid)
tpl['item_created'] = created
return tpl return tpl
def template_instantiate(template_id, payload=None, tenant_id=None): def _save_items(sor, tenant_id, template_id, items, operator=None):
"""由模板实例化新蓝图(draft):复制模板字段 + 逐条 item 落子对象;usage_count+1。""" """写入模板条目(先清后写,幂等)。"""
tid = tenant_id or _db.current_tenant_id() sor.D(ITEM_TABLE, _where(tenant_id, "template_id='%s'" % template_id))
payload = dict(payload or {}) saved = []
tpl = get_template(template_id, tid) for idx, item in enumerate(items or []):
now = _db.now_str() obj_type = item.get('obj_type')
operator = _db.current_operator() if obj_type not in SUBOBJECT_TYPES:
bp_payload = { raise PblValidationError('item[%d] invalid obj_type: %s' % (idx, obj_type))
'blueprint_name': payload.get('blueprint_name') or tpl.get('template_name'), if not item.get('name'):
'class_id': payload.get('class_id') or tpl.get('class_id') or '', raise PblValidationError('item[%d] name is required' % idx)
'domain_code': tpl.get('domain_code', ''), 'grade_code': tpl.get('grade_code', ''), row = {
'subject_code': tpl.get('subject_code', ''), 'id': new_id(),
'complexity_level': tpl.get('complexity_level', ''), 'tenant_id': tenant_id,
'project_duration': payload.get('project_duration') or 0, 'template_id': template_id,
'student_count': payload.get('student_count') or 0, 'obj_type': obj_type,
'group_count': payload.get('group_count') or 0, 'parent_ref': item.get('parent_ref'),
'summary_text': payload.get('summary_text') or tpl.get('description') or '', 'ref_key': item.get('ref_key') or ('%s%d' % (obj_type[:2].upper(), idx + 1)),
'seq': item.get('seq', idx),
'name': item['name'],
'spec': _merge_spec(obj_type, item),
'created_by': operator,
'created_at': _now(),
'updated_by': operator,
'updated_at': _now(),
'deleted': 0,
}
sor.C(ITEM_TABLE, row)
saved.append(row)
return saved
def _merge_spec(obj_type, item):
"""把条目上的类型特有字段收进 spec,保持条目表结构统一。"""
spec = dict(item.get('spec') or {})
for key in ('dimension', 'weight', 'evidence_type', 'collect_mode', 'rule',
'artifact_type', 'accept_criteria', 'required', 'trigger_point',
'reflection_type', 'rubric_ref', 'unlock_condition', 'permissions',
'description'):
if item.get(key) is not None:
spec[key] = item[key]
return spec
def update_template(tenant_id, template_id, data, env=None, operator=None):
"""更新模板;data['items'] 存在时整体重写条目。"""
tid = require_tenant(tenant_id)
if not template_id:
raise PblValidationError('template_id is required')
e = get_env(env)
sor = get_sor(e, get_dbname(e))
patch = _pick(data or {}, TPL_EDITABLE)
if patch:
patch['updated_by'] = operator
patch['updated_at'] = _now()
sor.U(TPL_TABLE, patch, _where(tid, "id='%s'" % template_id))
if 'items' in (data or {}):
_save_items(sor, tid, template_id, data['items'], operator)
return get_template(tid, template_id, env=e)
def delete_template(tenant_id, template_id, env=None, operator=None, hard=False):
"""软删除模板(连带条目)。"""
tid = require_tenant(tenant_id)
if not template_id:
raise PblValidationError('template_id is required')
e = get_env(env)
sor = get_sor(e, get_dbname(e))
patch = {'deleted': 1, 'updated_by': operator, 'updated_at': _now()}
for tbl, cond in ((TPL_TABLE, "id='%s'" % template_id),
(ITEM_TABLE, "template_id='%s'" % template_id)):
if hard:
sor.D(tbl, _where(tid, cond))
else:
sor.U(tbl, patch, _where(tid, cond))
return {'id': template_id, 'deleted': 1}
def get_template(tenant_id, template_id, env=None, with_items=True):
"""取模板详情(含条目)。"""
tid = require_tenant(tenant_id)
if not template_id:
raise PblValidationError('template_id is required')
e = get_env(env)
sor = get_sor(e, get_dbname(e))
rows = sor.R(TPL_TABLE, _where(tid, "id='%s' and deleted=0" % template_id))
if not rows:
raise PblNotFound('template not found: %s' % template_id, code='PBL_TEMPLATE_NOT_FOUND')
tpl = rows[0]
if with_items:
tpl['items'] = sor.R(ITEM_TABLE, _where(
tid, "template_id='%s' and deleted=0" % template_id), 'obj_type asc, seq asc') or []
return tpl
def list_templates(tenant_id, filters=None, page=1, page_size=20, env=None,
include_platform=True):
"""分页查询模板列表;include_platform=True 时并入 tenant_id='platform' 的内置模板。"""
tid = require_tenant(tenant_id)
filters = filters or {}
conds = ['deleted=0']
for key in ('category', 'subject', 'grade', 'status'):
if filters.get(key):
conds.append("%s='%s'" % (key, str(filters[key]).replace("'", "''")))
if filters.get('keyword'):
kw = str(filters['keyword']).replace("'", "''")
conds.append("(name like '%%%s%%' or code like '%%%s%%')" % (kw, kw))
tenant_cond = "tenant_id='%s'" % tid.replace("'", "''")
if include_platform and tid != 'platform':
tenant_cond = "(%s or tenant_id='platform')" % tenant_cond
e = get_env(env)
sor = get_sor(e, get_dbname(e))
page = max(int(page or 1), 1)
page_size = min(max(int(page_size or 20), 1), 200)
offset = (page - 1) * page_size
where = '%s and %s' % (tenant_cond, ' and '.join(conds))
sql = ("select * from %s where %s order by use_count desc, updated_at desc limit %d offset %d"
% (TPL_TABLE, where, page_size, offset))
rows = sor.sqlExe(sql)
total_rows = sor.sqlExe("select count(1) as total from %s where %s" % (TPL_TABLE, where))
total = 0
if total_rows:
first = total_rows[0]
total = first.get('total') if isinstance(first, dict) else first
return {'total': int(total or 0), 'page': page, 'page_size': page_size, 'rows': rows or []}
# ---------------------------------------------------------------- 实例化 / 兜底
def instantiate_template(tenant_id, template_id, name=None, env=None, operator=None,
overrides=None):
"""由模板实例化一个新蓝图(含全部子对象),模板 use_count 自增。"""
from .blueprint_crud import TABLE as BP_TABLE, create_blueprint, _gen_code
tid = require_tenant(tenant_id)
tpl = get_template(tid, template_id, env=env)
overrides = overrides or {}
e = get_env(env)
sor = get_sor(e, get_dbname(e))
bp_data = {
'name': name or overrides.get('name') or tpl.get('name'),
'subject': overrides.get('subject', tpl.get('subject')),
'grade': overrides.get('grade', tpl.get('grade')),
'duration_hours': overrides.get('duration_hours', tpl.get('duration_hours')),
'summary': overrides.get('summary') or (tpl.get('content') or {}).get('summary')
if isinstance(tpl.get('content'), dict) else overrides.get('summary'),
'template_id': template_id,
'status': 'draft',
} }
bp = _bp.save_blueprint(bp_payload, tid) bp = create_blueprint(tid, bp_data, env=e, operator=operator)
_db.update_row(_m.TABLES[0], {'source_template_id': template_id},
{'id': bp['id'], 'tenant_id': tid}) ref_map = {}
made = {} items = tpl.get('items') or []
for item in tpl.get('items') or []: ordered = sorted(items, key=lambda x: (SUBOBJECT_TYPES.index(x.get('obj_type'))
stype = item.get('subobject_type') if x.get('obj_type') in SUBOBJECT_TYPES else 99,
if stype not in _m.SUBOBJECT_TYPE_TABLE: x.get('seq') or 0))
for item in ordered:
obj_type = item.get('obj_type')
if obj_type not in SUBOBJECT_TYPES:
continue continue
body = _db.loads(item.get('item_payload_json'), {}) spec = dict(item.get('spec') or {})
body.update({'blueprint_id': bp['id'], 'class_id': bp.get('class_id') or '', payload = {
'name': body.get('name') or item.get('name') or stype, 'blueprint_id': bp['id'],
'code': '', 'seq_no': int(item.get('seq_no') or 0)}) 'name': item.get('name'),
_s.subobject_save(stype, body, tid) 'description': spec.pop('description', None),
made[stype] = made.get(stype, 0) + 1 'seq': item.get('seq') or 0,
_db.update_row(TABLE, {'usage_count': int(tpl.get('usage_count') or 0) + 1, 'spec': spec,
'updated_by': operator, 'updated_at': now}, 'status': 'active',
{'id': template_id, 'tenant_id': tid}) }
_db.audit('template_instantiate', TABLE, template_id, for key in ('dimension', 'weight', 'evidence_type', 'collect_mode', 'rule',
after={'blueprint_id': bp['id'], 'subobjects': made}, tenant_id=tid) 'artifact_type', 'accept_criteria', 'required', 'trigger_point',
return {'template_id': template_id, 'blueprint_id': bp['id'], 'reflection_type', 'rubric_ref', 'unlock_condition', 'permissions'):
'blueprint_code': bp.get('blueprint_code'), 'subobjects_created': made, if key in spec:
'status': bp.get('status')} payload[key] = spec.pop(key)
parent_ref = item.get('parent_ref')
if parent_ref and parent_ref in ref_map:
parent_id = ref_map[parent_ref]
if obj_type == 'task':
payload['parent_id'] = parent_id
else:
payload['task_id'] = parent_id
row = create_subobject(tid, obj_type, payload, env=e, operator=operator)
if item.get('ref_key'):
ref_map[item['ref_key']] = row['id']
sor.U(TPL_TABLE, {'use_count': int(tpl.get('use_count') or 0) + 1,
'updated_by': operator, 'updated_at': _now()},
_where(tid, "id='%s'" % template_id))
bp['from_template'] = template_id
bp['children_count'] = len(ref_map) or len(ordered)
return bp
def template_status_options(): def build_offline_fallback_blueprint(tenant_id, name=None, env=None, operator=None):
return [{'code': 'draft', 'label': '草稿'}, {'code': 'published', 'label': '已发布'}, """离线兜底:不依赖 Agent/模板,直接落一个最小可用蓝图(含 7 类子对象各至少 1 条)。"""
{'code': 'archived', 'label': '已归档'}] from .blueprint_crud import create_blueprint
tid = require_tenant(tenant_id)
skeleton = dict(OFFLINE_FALLBACK)
bp = create_blueprint(tid, {
'name': name or skeleton['name'],
'summary': skeleton['summary'],
'duration_hours': skeleton['duration_hours'],
'status': 'draft',
'remark': 'offline-fallback',
}, env=env, operator=operator)
ref_map = {}
created = 0
for item in skeleton['items']:
obj_type = item['obj_type']
payload = {
'blueprint_id': bp['id'],
'name': item['name'],
'seq': created,
'spec': item.get('spec') or {},
'status': 'active',
}
for key in ('dimension', 'weight', 'evidence_type', 'collect_mode', 'artifact_type',
'required', 'trigger_point', 'reflection_type'):
if key in item:
payload[key] = item[key]
parent_ref = item.get('parent_ref')
if parent_ref and parent_ref in ref_map:
if obj_type == 'task':
payload['parent_id'] = ref_map[parent_ref]
else:
payload['task_id'] = ref_map[parent_ref]
row = create_subobject(tid, obj_type, payload, env=env, operator=operator)
if item.get('ref_key'):
ref_map[item['ref_key']] = row['id']
created += 1
bp['children_count'] = created
bp['offline_fallback'] = True
return bp

View File

@ -1,14 +1,31 @@
[build-system] [build-system]
requires = ["setuptools>=45", "wheel"] requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "pbl_blueprint" name = "pbl_blueprint"
version = "1.0.0" version = "1.0.0"
description = "PBL 蓝图聚合根与子对象、版本、模板(M1a/M1b)" description = "PBL 蓝图聚合根与子对象、版本、模板模块(M1a)——蓝图 CRUD/树/fork、7 类子对象泛化契约、版本 change_delta、模板实例化与离线兜底;tenant_id 强制打头"
readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"] license = { text = "Proprietary" }
authors = [{ name = "sdlc agent.develop" }]
keywords = ["pbl", "blueprint", "sage", "module"]
dependencies = []
[tool.setuptools.packages.find] [project.optional-dependencies]
where = ["."] dev = ["pytest>=7.0"]
include = ["pbl_blueprint*"]
[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",
]

View File

@ -1,143 +1,130 @@
"""scripts/load_path.py —— pbl_blueprint 模块 RBAC 路径注册。 """pbl_blueprint 模块 RBAC 路径注册脚本。
规则(module-development-spec): 用途:把本模块全部新 API 路径 + 页面路径注册到平台 RBAC(权限路径表),
- 禁止任何通配符(% / *),每条路径显式列出; 使 owner.admin / 租户管理员可授权,未注册路径一律 403。
- 角色分层:any(无需登录)/ logined(登录即可)/ owner.superuser(管理);
- CRUD 子目录标准 5 条一组:目录 + index.ui + get_/add_/update_/delete_.dspy。
用法:cd <宿主应用根> && ./py3/bin/python <repo>/modules/pbl_blueprint/scripts/load_path.py 执行方式(应用部署后一次性 / 幂等可重复执行):
python -m pbl_blueprint.scripts.load_path
或在应用 init() 后由部署脚本调用 load_paths(env)。
""" """
import os import os
import sys import sys
MODULE = 'pbl_blueprint' MODULE_NAME = 'pbl_blueprint'
# 契约 API(登录可用) # ---- 本模块全部新 API 路径(与 api.py 的 API_PATHS 保持一致,单一事实源)----
PATHS_LOGINED = [ API_PATHS = [
'/pbl_blueprint', ('/pbl_blueprint/blueprint/create', 'POST', '新建蓝图'),
'/pbl_blueprint/index.ui', ('/pbl_blueprint/blueprint/update', 'POST', '更新蓝图'),
'/pbl_blueprint/api/pbl_blueprint_create.dspy', ('/pbl_blueprint/blueprint/delete', 'POST', '删除蓝图'),
'/pbl_blueprint/api/pbl_blueprint_read.dspy', ('/pbl_blueprint/blueprint/get', 'GET', '蓝图详情'),
'/pbl_blueprint/api/pbl_blueprint_update.dspy', ('/pbl_blueprint/blueprint/list', 'GET', '蓝图列表'),
'/pbl_blueprint/api/pbl_blueprint_delete.dspy', ('/pbl_blueprint/blueprint/tree', 'GET', '蓝图子对象树'),
'/pbl_blueprint/api/pbl_blueprint_list.dspy', ('/pbl_blueprint/blueprint/fork', 'POST', '复制蓝图'),
'/pbl_blueprint/api/pbl_blueprint_tree.dspy', ('/pbl_blueprint/version/save', 'POST', '保存版本快照'),
'/pbl_blueprint/api/pbl_blueprint_fork.dspy', ('/pbl_blueprint/version/list', 'GET', '版本列表'),
'/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy', ('/pbl_blueprint/version/diff', 'GET', '版本对比'),
'/pbl_blueprint/api/pbl_blueprint_subobject_list.dspy', ('/pbl_blueprint/version/rollback', 'POST', '版本回滚'),
'/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy', ('/pbl_blueprint/subobject/create', 'POST', '新建子对象'),
'/pbl_blueprint/api/pbl_blueprint_version_create.dspy', ('/pbl_blueprint/subobject/update', 'POST', '更新子对象'),
'/pbl_blueprint/api/pbl_blueprint_version_diff.dspy', ('/pbl_blueprint/subobject/delete', 'POST', '删除子对象'),
'/pbl_blueprint/api/pbl_template_list.dspy', ('/pbl_blueprint/subobject/get', 'GET', '子对象详情'),
'/pbl_blueprint/api/pbl_template_instantiate.dspy', ('/pbl_blueprint/subobject/list', 'GET', '子对象列表'),
'/pbl_blueprint/api/pbl_template_save.dspy', ('/pbl_blueprint/subobject/batch_upsert', 'POST', '批量写子对象'),
'/pbl_blueprint/api/pbl_template_delete.dspy', ('/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', '离线兜底生成蓝图'),
] ]
# 静态资源(无需登录) # ---- 页面路径(wwwroot/*.ui 挂载点)----
PATHS_ANY = [ PAGE_PATHS = [
'/pbl_blueprint/menu.ui', ('/pbl_blueprint/index', 'GET', 'PBL蓝图管理主页'),
('/pbl_blueprint/blueprint/edit', 'GET', '蓝图编辑页'),
('/pbl_blueprint/template/list', 'GET', '蓝图模板库页'),
] ]
# 管理级操作(删除/模板维护) # ---- 表级数据权限路径(CRUD 定义 json/*.json 对应)----
PATHS_SUPERUSER = [ TABLE_PATHS = [
'/pbl_blueprint/api/pbl_template_delete.dspy', ('/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', '子对象-反思规格数据权限'),
] ]
# CRUD 生成目录(每个 alias 5 条) ALL_PATHS = API_PATHS + PAGE_PATHS + TABLE_PATHS
CRUD_ALIASES = [
'pbl_blueprint',
'pbl_blueprint_learning_goal',
'pbl_blueprint_role',
'pbl_blueprint_mission',
'pbl_blueprint_task',
'pbl_blueprint_artifact_spec',
'pbl_blueprint_evidence_spec',
'pbl_blueprint_reflection_spec',
'pbl_blueprint_version',
'pbl_template',
]
def _crud_paths(): def load_paths(env=None):
out = [] """把 ALL_PATHS 注册进 RBAC(幂等:存在则更新描述,不存在则新增)。
for alias in CRUD_ALIASES:
base = '/%s/%s' % (MODULE, alias)
out.append((base, 'logined'))
out.append((base + '/index.ui', 'logined'))
out.append((base + '/get_%s.dspy' % alias, 'logined'))
out.append((base + '/add_%s.dspy' % alias, 'logined'))
out.append((base + '/update_%s.dspy' % alias, 'logined'))
out.append((base + '/delete_%s.dspy' % alias, 'logined'))
return out
: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
def all_paths(): rbac = None
"""返回 [(path, role), ...] 全量显式路径清单。""" if env is not None:
items = [(p, 'logined') for p in PATHS_LOGINED] for attr in ('rbac', 'RBAC'):
items += [(p, 'any') for p in PATHS_ANY] rbac = getattr(env, attr, None)
items += [(p, 'owner.superuser') for p in PATHS_SUPERUSER] if rbac is not None:
items += _crud_paths() break
# 去重保序 if rbac is None:
seen = set() getter = getattr(env, 'get_rbac', None)
out = [] if callable(getter):
for p, r in items: try:
if p in seen: 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
continue continue
seen.add(p) try:
out.append((p, r)) registered = False
return out for fn_name in ('register_path', 'add_path', 'load_path', 'register'):
fn = getattr(rbac, fn_name, None)
if callable(fn):
def _find_app_root(): try:
"""向上查找宿主应用根(含 load_path.py / set_role_perm.py 的目录)。""" fn(path, method, caption)
here = os.path.dirname(os.path.abspath(__file__)) except TypeError:
cur = here fn(item)
for _ in range(8): registered = True
for cand in (os.path.join(cur, 'set_role_perm.py'), break
os.path.join(cur, 'load_path.py')): if registered:
if os.path.exists(cand): result['registered'] += 1
return cur else:
nxt = os.path.dirname(cur) result['skipped'] += 1
if nxt == cur: except Exception:
break result['failed'] += 1
cur = nxt return result
for env_key in ('SAGE_ROOT', 'PBL_APP_ROOT'):
v = os.environ.get(env_key)
if v and os.path.isdir(v):
return v
return None
def main(): def main():
paths = all_paths() sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
root = _find_app_root() res = load_paths()
if root is None: print('[pbl_blueprint] load_path done: registered=%d skipped=%d failed=%d total=%d' % (
print('[pbl_blueprint] 未找到宿主应用根(set_role_perm.py),' res['registered'], res['skipped'], res['failed'], len(res['paths'])))
'仅打印待注册路径 %d 条:' % len(paths))
for p, r in paths:
print('%s %s' % (p, r))
return 0
sys.path.insert(0, root)
try:
import set_role_perm
except Exception as e:
print('[pbl_blueprint] 导入 set_role_perm 失败: %s' % e)
for p, r in paths:
print('%s %s' % (p, r))
return 1
fn = getattr(set_role_perm, 'set_role_perm', None) or \
getattr(set_role_perm, 'main', None)
ok = 0
for p, r in paths:
try:
if fn is not None:
fn(p, r)
ok += 1
except Exception as e:
print('[pbl_blueprint] 注册失败 %s %s: %s' % (p, r, e))
print('[pbl_blueprint] RBAC 路径注册完成 %d/%d' % (ok, len(paths)))
return 0 return 0

View File

@ -1,40 +1,131 @@
# pbl_blueprint 模块技能(自动生成骨架 + 人工补充) ---
name: pbl_blueprint
description: PBL 蓝图聚合根与子对象、版本、模板(M1a)。蓝图 CRUD/树/fork、7 类子对象泛化契约、版本 change_delta、模板实例化与离线兜底。所有读写 tenant_id 强制打头,缺失即 fail-closed 拒绝。
---
## 定位 # pbl_blueprint 模块技能
PBL 蓝图聚合根与子对象、版本、模板(M1a/M1b)
## 挂载 ## 1. 模块定位
`from pbl_blueprint.init import load_pbl_blueprint` → `load_pbl_blueprint()`(应用 app/pbls.py init() 中按序调用) `pbl_blueprint` 是 PBL(Project-Based Learning)平台的**蓝图域聚合根模块**,负责:
- 蓝图主表 `pbl_blueprint` 的 CRUD / 列表 / 树 / fork
- 7 类子对象的**泛化契约**(一套 CRUD 覆盖 7 张表)
- 版本快照 `pbl_blueprint_version`(含 `change_delta` 增量、diff、rollback)
- 模板 `pbl_blueprint_template` / `pbl_blueprint_template_item`(实例化 + 离线兜底)
## 数据表(10 张) 模块是 **Python 包**(无 app.py / 无端口 / 无 Dockerfile),通过 `load_pbl_blueprint(env)` 挂到应用 `apps/pbls`。
- `pbl_blueprint`:PBL 蓝图聚合根(第8章 schema 落库)
- `pbl_blueprint_learning_goal`:蓝图子对象 - 学习目标
- `pbl_blueprint_role`:蓝图子对象 - PBL 角色
- `pbl_blueprint_mission`:蓝图子对象 - 驱动问题/Mission
- `pbl_blueprint_task`:蓝图子对象 - 任务
- `pbl_blueprint_artifact_spec`:蓝图子对象 - 产出物规格
- `pbl_blueprint_evidence_spec`:蓝图子对象 - 证据规格
- `pbl_blueprint_reflection_spec`:蓝图子对象 - 反思规格
- `pbl_blueprint_version`:蓝图版本(change_delta 证明对话式改模型)
- `pbl_template`:PBL 模板(含离线兜底槽位)
## 契约接口(13 个,路径 `/pbl_blueprint/api/<name>.dspy`) ## 2. 目录结构
- `pbl_blueprint_create` ```
- `pbl_blueprint_read` modules/pbl_blueprint/
- `pbl_blueprint_update` ├── pyproject.toml
- `pbl_blueprint_delete` ├── scripts/load_path.py # RBAC 路径注册(API/页面/表权限)
- `pbl_blueprint_list` └── pbl_blueprint/ # 包目录 = 模块名(禁 src/)
- `pbl_blueprint_tree` ├── __init__.py # 导出 load_pbl_blueprint
- `pbl_blueprint_fork` ├── init.py # 装配入口 + env 注册 + 契约字典
- `pbl_blueprint_subobject_save` ├── db.py # 库名解析 / sqlor 获取 / 异常体系 / 建表
- `pbl_blueprint_subobject_list` ├── blueprint_crud.py # 蓝图 CRUD + 树 + fork + 版本契约
- `pbl_blueprint_version_create` ├── subobjects.py # 7 类子对象泛化 CRUD
- `pbl_blueprint_version_diff` ├── templates.py # 模板 CRUD + 实例化 + 离线兜底
- `pbl_template_list` ├── api.py # HTTP API 层 + API_PATHS + HANDLERS
- `pbl_template_instantiate` ├── models/*.json # 表定义(四段式 summary/fields/indexes/codes)
├── json/*.json # CRUD 定义(根键 tblname + params.editable/browserfields)
├── sql/pbl_blueprint.core.sql # 核心 4 表 DDL
├── sql/pbl_blueprint.subobjects.sql # 子对象 7 表 DDL
├── init/data.json # 初始化数据(内置模板)
└── wwwroot/index.ui # 前端页面
```
## 陷阱 ## 3. 数据表(11 张)
- 库名一律 `ServerEnv().get_module_dbname('pbl_blueprint')`,禁止硬编码 DBNAME。 核心 4 张:`pbl_blueprint`、`pbl_blueprint_version`、`pbl_blueprint_template`、`pbl_blueprint_template_item`
- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one(已适配)。 子对象 7 张:`pbl_blueprint_task`、`pbl_blueprint_mission`、`pbl_blueprint_role`、
- 所有读写强制带 `tenant_id`(pbl_common.api.tenant_id()),缺失即 fail-closed 报错。 `pbl_blueprint_learning_goal`、`pbl_blueprint_evidence_spec`、`pbl_blueprint_artifact_spec`、
- 新增契约需同步三处:api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。 `pbl_blueprint_reflection_spec`
统一约定:
- 主键 `id` = varchar(32),32 位无横线 UUID(`db.new_id()`)
- `tenant_id` varchar(32) NOT NULL,**每个索引首列**
- 软删除 `deleted` tinyint(0 正常 / 1 已删除)
- 结构化字段用 `json` 类型(`spec` / `config` / `snapshot` / `change_delta` / `rule` / `permissions`)
- 审计五件套 `created_by/created_at/updated_by/updated_at/deleted`
## 4. 子对象泛化契约(重点)
7 类子对象共用一套函数,`obj_type` 分派到表:
| obj_type | 表 | 编码前缀 | 特有字段 |
|---|---|---|---|
| task | pbl_blueprint_task | TASK | parent_id |
| mission | pbl_blueprint_mission | MISSION | task_id(必填), unlock_condition |
| role | pbl_blueprint_role | ROLE | task_id, permissions |
| learning_goal | pbl_blueprint_learning_goal | GOAL | task_id, dimension, weight |
| evidence_spec | pbl_blueprint_evidence_spec | EVI | task_id, mission_id, learning_goal_id, evidence_type, collect_mode, rule, weight |
| artifact_spec | pbl_blueprint_artifact_spec | ART | task_id, mission_id, artifact_type, accept_criteria, required, weight |
| reflection_spec | pbl_blueprint_reflection_spec | REF | task_id, mission_id, trigger_point, reflection_type, rubric_ref, weight |
```python
from pbl_blueprint import load_pbl_blueprint
contract = load_pbl_blueprint(env)
contract['create_subobject'](tenant_id, 'mission', {'blueprint_id': bp_id, 'task_id': t_id, 'name': '关卡1'}, env=env)
contract['list_subobjects'](tenant_id, 'task', bp_id, env=env)
contract['batch_upsert_subobjects'](tenant_id, 'task', bp_id, rows, env=env) # Designer Agent 整树写入
```
## 5. 蓝图契约
```python
create_blueprint(tenant_id, data, env, operator) # 自动生成 code PBL-{yyyyMMdd}-{seq4}
update_blueprint(tenant_id, blueprint_id, data, ...) # 状态流转校验 STATUS_FLOW
delete_blueprint(tenant_id, blueprint_id, ...) # 软删(连带子对象+版本)
get_blueprint(tenant_id, blueprint_id, with_children) # with_children=True 附带 7 类子树
list_blueprints(tenant_id, filters, page, page_size) # 分页 + keyword/status/subject 过滤
get_blueprint_tree(tenant_id, blueprint_id) # {obj_type: [rows]}
fork_blueprint(tenant_id, blueprint_id, new_name, target_tenant_id) # 跨租户复制,ID 重映射
```
状态机:`draft → validating → validated → compiling → compiled → published → archived`(非法流转抛 `PblValidationError`)。
## 6. 版本契约
```python
save_version(tenant_id, blueprint_id, remark) # 存全量 snapshot + 计算 change_delta,主表 version_no 自增
list_versions(tenant_id, blueprint_id, page, page_size)
diff_versions(tenant_id, blueprint_id, version_a, version_b) # {added, updated, removed}
rollback_version(tenant_id, blueprint_id, version_no) # 先自动存当前版本再回滚
```
`change_delta` 结构:`{"added":[{"obj_type","id","name"}], "updated":[{"obj_type","id","fields"}], "removed":[...]}`
## 7. 模板与离线兜底
```python
create_template(tenant_id, {'name':..., 'items':[{'obj_type':'task','ref_key':'T1','name':...}, ...]})
instantiate_template(tenant_id, template_id, name, overrides) # ref_key/parent_ref 解析为真实 ID,use_count 自增
build_offline_fallback_blueprint(tenant_id, name) # Agent/网络不可用时的最小可用蓝图(9 条子对象)
```
模板条目用 `ref_key` / `parent_ref` 做**模板内相对引用**,实例化时按 `SUBOBJECT_TYPES` 顺序解析为真实 ID(父先于子)。
## 8. tenant_id 强制打头(铁律)
- 每个契约函数第一入参 `tenant_id`,内部 `db.require_tenant()` 校验
- 缺失/空/非字符串 → 抛 `PblTenantRequired`(code=`PBL_TENANT_REQUIRED`),**fail-closed 不放行**
- 所有 where 条件由 `_where(tenant_id, extra)` 构造,`tenant_id=` 永远在最前
- API 层 `_tid(params)` 从入参或 `context.tenant_id` 取,取不到直接失败
- 平台内置模板 `tenant_id='platform'`,`list_templates(include_platform=True)` 才并入
## 9. 异常码
| code | 含义 |
|---|---|
| PBL_TENANT_REQUIRED | 租户上下文缺失 |
| PBL_VALIDATION_ERROR | 入参校验失败 |
| PBL_NOT_FOUND / PBL_BLUEPRINT_NOT_FOUND / PBL_SUBOBJECT_NOT_FOUND / PBL_VERSION_NOT_FOUND / PBL_TEMPLATE_NOT_FOUND | 记录不存在 |
| PBL_UNKNOWN_OBJ_TYPE | 未知子对象类型 |
| PBL_DB_UNAVAILABLE | sqlor 不可用 |
## 10. 数据访问约束
- 库名:`ServerEnv().get_module_dbname('pbl_blueprint')`,**禁止硬编码 DBNAME**
- SQL 只用 sqlor 标准 API:`sor.C / sor.U / sor.D / sor.R / sor.sqlExe`,禁编造 save/list/insert
- 建表:`db.ensure_tables()` 幂等执行 `sql/*.sql`(`CREATE TABLE IF NOT EXISTS`)
## 11. API 路径(25 个,见 api.py API_PATHS)
前缀 `/pbl_blueprint/`,分四组:`blueprint/*`(7)、`version/*`(4)、`subobject/*`(7)、`template/*`(7)。
RBAC 注册:`python -m pbl_blueprint.scripts.load_path`(幂等,注册 API + 页面 + 表权限共 39 条路径)。
## 12. 下游模块依赖
- `pbl_validation`(M2)读 `get_blueprint_tree` 做 14 维校验,回写 `quality_level`
- `pbl_compiler`(M3)读蓝图树生成 Game Definition
- `pbl_evidence`(M5)按 `evidence_spec` / `artifact_spec` 采集
- `pbl_assessment`(M6)按 `learning_goal.weight` / `reflection_spec.rubric_ref` 评分
- `pbl_agent_runtime`(M4)用 `batch_upsert_subobjects` 写 Designer 产出,fail-closed 裁决

View File

@ -0,0 +1,12 @@
# pbl_blueprint_create.dspy —— 创建蓝图聚合根(薄包装,逻辑在 pbl_blueprint.api)
debug('pbl_blueprint_create.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_create(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_create.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_create.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}

View File

@ -0,0 +1,12 @@
# pbl_blueprint_delete.dspy —— 删除蓝图(默认软删 status=deleted;hard=1 物理级联删)
debug('pbl_blueprint_delete.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_delete(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_delete.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_delete.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}

View File

@ -0,0 +1,12 @@
# pbl_blueprint_fork.dspy —— 派生蓝图(深拷贝聚合根+子对象并重映射内部引用)
debug('pbl_blueprint_fork.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_fork(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_fork.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_fork.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}

View File

@ -0,0 +1,13 @@
# pbl_blueprint_list.dspy —— 蓝图列表(分页 + 租户隔离 + 状态/学科/关键词过滤)
debug('pbl_blueprint_list.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_list(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_list.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_list.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e),
'total': 0, 'data': []}

View File

@ -0,0 +1,12 @@
# pbl_blueprint_read.dspy —— 读取单个蓝图(with_children=1 返回全部子对象)
debug('pbl_blueprint_read.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_read(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_read.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_read.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}

View File

@ -0,0 +1,12 @@
# pbl_blueprint_subobject_delete.dspy —— 子对象删除(蓝图未发布时)
debug('pbl_blueprint_subobject_delete.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_subobject_delete(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_subobject_delete.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_subobject_delete.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}

View File

@ -0,0 +1,13 @@
# pbl_blueprint_subobject_list.dspy —— 子对象列表(subobject + blueprint_id)
debug('pbl_blueprint_subobject_list.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_subobject_list(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_subobject_list.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_subobject_list.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e),
'total': 0, 'data': []}

View File

@ -0,0 +1,12 @@
# pbl_blueprint_subobject_save.dspy —— 子对象新增/更新(subobject 指定类型,有 id 走更新)
debug('pbl_blueprint_subobject_save.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_subobject_save(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_subobject_save.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_subobject_save.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}

View File

@ -0,0 +1,12 @@
# pbl_blueprint_tree.dspy —— 蓝图聚合树(蓝图→mission→task→artifact/evidence)
debug('pbl_blueprint_tree.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_tree(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_tree.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_tree.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}

View File

@ -0,0 +1,12 @@
# pbl_blueprint_update.dspy —— 更新蓝图主表(白名单字段,自动升版本)
debug('pbl_blueprint_update.dspy: START params_kw=%s' % dict(params_kw))
try:
tenant_id = params_kw.get('tenant_id') or None
res = await pbl_blueprint_update(dict(params_kw), request=request, tenant_id=tenant_id)
return res
except PblBlueprintError as e:
debug('pbl_blueprint_update.dspy: BIZ_ERR %s' % e.to_dict())
return e.to_dict()
except Exception as e:
exception('pbl_blueprint_update.dspy: %s' % format_exc())
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}