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

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
新增契约需同步四处api.py 定义 + 本文件导出 + init.py env 注册
+ scripts/load_path.py RBAC 路径
对外唯一入口load_pbl_blueprint(env=None)
所有读写 tenant_id 强制打头缺失租户上下文一律 fail-closed 拒绝
"""
from pbl_blueprint.api import ( # noqa: F401
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,
)
from .init import load_pbl_blueprint
__all__ = [
'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)
__all__ = ['load_pbl_blueprint']

File diff suppressed because it is too large Load Diff

View File

@ -1,346 +1,411 @@
# -*- coding: utf-8 -*-
"""蓝图聚合根 CRUD 与查询契约接口M1a
"""pbl_blueprint 蓝图 CRUD 与查询契约M1a
契约函数跨宿主/下游模块复用面签名稳定
- save_blueprint(payload, tenant_id=None) -> dict 新建/更新蓝图含自动编码乐观锁
- get_blueprint(blueprint_id, tenant_id=None) -> dict 蓝图主记录
- list_blueprints(filters=None, page=1, rows=20, tenant_id=None) -> dict {rows,total,page,rows_per_page}
- 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 -> archivedpublished -> in_review改版
租户隔离tenant_id 强制打头缺失 fail-closed跨租户读取返回不存在
契约要点
- 每个函数第一入参 tenant_id内部 require_tenant() 强制校验缺失即 PblTenantRequiredfail-closed
- 只用 sqlor 标准 APIsor.C(, dict) / sor.U(, dict, where) / sor.D(, where) / sor.R(, where) / sor.I(...) / sor.sqlExe(sql)
- 软删除deleted=1不物理删除主表
"""
import hashlib
from . import db as _db
from . import models as _m
from . import subobjects as _s
import datetime
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_VERSION = 'pbl_blueprint_version'
VERSION_TABLE = 'pbl_blueprint_version'
STATUS_DRAFT = 'draft'
STATUS_IN_REVIEW = 'in_review'
STATUS_PUBLISHED = 'published'
STATUS_ARCHIVED = 'archived'
EDITABLE = [
'code', 'name', 'subject', 'grade', 'duration_hours', 'status',
'summary', 'config', 'source_blueprint_id', 'template_id', 'owner_id', 'remark',
]
STATUS_FLOW = {
STATUS_DRAFT: [STATUS_IN_REVIEW, STATUS_ARCHIVED],
STATUS_IN_REVIEW: [STATUS_DRAFT, STATUS_PUBLISHED, STATUS_ARCHIVED],
STATUS_PUBLISHED: [STATUS_IN_REVIEW, STATUS_ARCHIVED],
STATUS_ARCHIVED: [],
'draft': ['validating', 'archived'],
'validating': ['validated', 'draft'],
'validated': ['compiling', 'draft'],
'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():
return _db.now_str()
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def _require_tenant(tenant_id):
return tenant_id or _db.current_tenant_id()
def _where(tenant_id, extra=None):
"""构造 wheretenant_id 永远打头。"""
cond = "tenant_id='%s'" % tenant_id.replace("'", "''")
if extra:
cond = cond + ' and ' + extra
return cond
def _fetch(table, rec_id, tenant_id):
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tenant_id,
'is_deleted': 0})
def _pick(data, keys):
out = {}
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):
"""取蓝图主记录;不存在/跨租户 -> PBL-NOT-FOUND。"""
tid = _require_tenant(tenant_id)
if not blueprint_id:
raise _db.PblError(_db.ERR_PARAM_INVALID, 'blueprint_id 不能为空')
row = _fetch(TABLE, blueprint_id, tid)
if not row:
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id)
return row
def _gen_code(sor, tenant_id):
"""生成蓝图编码 PBL-{yyyyMMdd}-{seq4}"""
day = datetime.datetime.now().strftime('%Y%m%d')
prefix = 'PBL-%s-' % day
rows = sor.R(TABLE, _where(tenant_id, "code like '%s%%'" % prefix))
seq = len(rows or []) + 1
return '%s%04d' % (prefix, seq)
def next_blueprint_code(tenant_id=None):
"""自动生成蓝图编码 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)
# ---------------------------------------------------------------- 蓝图 CRUD
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):
"""新建(无 id或更新有 id蓝图。返回落库后的记录。"""
tid = _require_tenant(tenant_id)
payload = dict(payload or {})
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()
e = get_env(env)
sor = get_sor(e, get_dbname(e))
if sor is None:
raise PblError('sqlor unavailable for module pbl_blueprint', code='PBL_DB_UNAVAILABLE')
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 = {
'id': new_id, 'tenant_id': tid, 'class_id': data.get('class_id', ''),
'blueprint_code': data['blueprint_code'], 'blueprint_name': data['blueprint_name'],
'domain_code': data.get('domain_code', ''), 'grade_code': data.get('grade_code', ''),
'subject_code': data.get('subject_code', ''),
'project_duration': int(data.get('project_duration') or 0),
'student_count': int(data.get('student_count') or 0),
'group_count': int(data.get('group_count') or 0),
'complexity_level': data.get('complexity_level', ''),
'status': STATUS_DRAFT, 'current_version': 0,
'owner_user_id': data.get('owner_user_id') or operator,
'estimated_cost': float(data.get('estimated_cost') or 0),
'tags_json': data.get('tags_json') or '[]',
'summary_text': data.get('summary_text') or '',
'is_deleted': 0, 'created_by': operator, 'created_at': now,
'updated_by': operator, 'updated_at': now,
'id': data.get('id') or new_id(),
'tenant_id': tid,
'code': data.get('code') or _gen_code(sor, tid),
'name': data['name'],
'subject': data.get('subject'),
'grade': data.get('grade'),
'duration_hours': data.get('duration_hours'),
'version_no': 1,
'status': data.get('status') or 'draft',
'quality_level': data.get('quality_level') or 'L1',
'summary': data.get('summary'),
'config': data.get('config'),
'source_blueprint_id': data.get('source_blueprint_id'),
'template_id': data.get('template_id'),
'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)
_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'), {})
sor.C(TABLE, row)
return row
def blueprint_tree(blueprint_id, version_id=None, tenant_id=None):
"""聚合树:蓝图主记录 + (可选版本快照)+ 7 类子对象 + 计数。"""
tid = _require_tenant(tenant_id)
bp = _fetch(TABLE, blueprint_id, tid)
if not bp:
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id)
children = _s.list_all_subobjects(blueprint_id, tid)
tree = {'blueprint': bp, 'children': children,
'counts': dict((k, len(v)) for k, v in children.items()),
'version': None}
if version_id:
tree['version'] = get_version(version_id, tid)
def update_blueprint(tenant_id, blueprint_id, data, env=None, operator=None):
"""更新蓝图(仅 EDITABLE 字段 + 状态流转校验)。"""
tid = require_tenant(tenant_id)
if not blueprint_id:
raise PblValidationError('blueprint_id is required')
old = get_blueprint(tid, blueprint_id, env=env)
patch = _pick(data or {}, EDITABLE)
if not patch:
return old
new_status = patch.get('status')
if new_status and new_status != old.get('status'):
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
def fork_blueprint(blueprint_id, to_class_id=None, tenant_id=None, new_name=None):
"""派生副本:复制主记录 + 全部子对象到新蓝图draft, version 0"""
tid = _require_tenant(tenant_id)
src = _fetch(TABLE, blueprint_id, tid)
if not src:
raise _db.PblError(_db.ERR_NOT_FOUND, '蓝图不存在或无权访问: %s' % blueprint_id)
now = _now()
operator = _db.current_operator()
new_id = _db.gen_id()
row = dict((k, v) for k, v in src.items())
row.update({
'id': new_id, 'tenant_id': tid,
'class_id': to_class_id if to_class_id is not None else (src.get('class_id') or ''),
'blueprint_code': next_blueprint_code(tid),
'blueprint_name': new_name or ('%s (副本)' % (src.get('blueprint_name') or '')),
'status': STATUS_DRAFT, 'current_version': 0, 'published_version_id': '',
'source_blueprint_id': blueprint_id, 'source_template_id': '',
'is_deleted': 0, 'created_by': operator, 'created_at': now,
'updated_by': operator, 'updated_at': now,
})
_db.insert_row(TABLE, row)
copied = _s.copy_subobjects(blueprint_id, new_id, tid)
_db.audit('blueprint_fork', TABLE, new_id,
after={'source_blueprint_id': blueprint_id, 'copied': copied}, tenant_id=tid)
return {'id': new_id, 'source_blueprint_id': blueprint_id,
'blueprint_code': row['blueprint_code'], 'copied_subobjects': copied}
def fork_blueprint(tenant_id, blueprint_id, new_name=None, target_tenant_id=None,
env=None, operator=None):
"""复制蓝图(含全部子对象)为新蓝图,记录 source_blueprint_id。"""
tid = require_tenant(tenant_id)
dst_tid = require_tenant(target_tenant_id or tid)
src = get_blueprint(tid, blueprint_id, env=env)
e = get_env(env)
sor = get_sor(e, get_dbname(e))
new_bp = {
'id': new_id(),
'tenant_id': dst_tid,
'code': _gen_code(sor, dst_tid),
'name': new_name or ('%s-副本' % src.get('name')),
'subject': src.get('subject'),
'grade': src.get('grade'),
'duration_hours': src.get('duration_hours'),
'version_no': 1,
'status': 'draft',
'quality_level': src.get('quality_level') or 'L1',
'summary': src.get('summary'),
'config': src.get('config'),
'source_blueprint_id': src['id'],
'template_id': src.get('template_id'),
'owner_id': operator or src.get('owner_id'),
'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():
return [{'code': c, 'label': l} for c, l in [
('science', '科学'), ('math', '数学'), ('chinese', '语文'), ('english', '英语'),
('art', '艺术'), ('technology', '技术'), ('social_studies', '社会'), ('cross_subject', '跨学科')]]
def _calc_delta(prev, curr):
"""对比两版快照,产出 added/updated/removed 三段增量。"""
delta = {'added': [], 'updated': [], 'removed': []}
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_deltaa -> 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 数据访问层。
规范约束
- 只使用 sqlor 标准 APIsor.C / sor.U / sor.D / sor.R / sor.I / sor.sqlExe
禁止 save()/list()/insert()/query() 等编造 API
- 库名一律通过 ServerEnv().get_module_dbname('pbl_blueprint') 获取禁止硬编码 DBNAME
- 租户隔离所有读写强制带 tenant_id缺失即 fail-closed 抛错
- 库名一律 ServerEnv().get_module_dbname('pbl_blueprint')禁止硬编码 DBNAME
- 所有 SQL 只用 sqlor 标准 APIsor.C / sor.U / sor.D / sor.R / sor.I / sor.sqlExe
- tenant_id 强制打头任何读写条件第一列必须是 tenant_id缺失即抛 PblTenantRequired
"""
import json
import os
import uuid
MODULE_NAME = 'pbl_blueprint'
# ---------------------------------------------------------------- 错误码
ERR_DBNAME_MISSING = 'PBL-DB-001'
ERR_TENANT_MISSING = 'PBL-TENANT-001'
ERR_NOT_FOUND = 'PBL-NOT-FOUND'
ERR_STATE_FORBIDDEN = 'PBL-STATE-001'
ERR_PARAM_INVALID = 'PBL-PARAM-001'
ERR_REFERENCED = 'PBL-REF-001'
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',
]
class PblError(Exception):
"""统一业务异常,携带错误码 + i18n 文案键。"""
"""pbl 模块统一异常基类"""
def __init__(self, code, msg, i18n_key=None, detail=None):
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 {}
code = 'PBL_ERROR'
def to_dict(self):
return {'code': self.code, 'msg': self.msg,
'i18n_key': self.i18n_key, 'detail': self.detail}
def __init__(self, message, code=None, **extra):
super(PblError, self).__init__(message)
self.message = message
if code:
self.code = code
self.extra = extra
# ---------------------------------------------------------------- 环境/库名
def server_env():
from ahserver import ServerEnv
return ServerEnv()
class PblTenantRequired(PblError):
"""租户上下文缺失fail-closed"""
code = 'PBL_TENANT_REQUIRED'
def get_dbname():
"""取本模块库名:优先 ServerEnv 注册值,其次 pbl_common 适配层。"""
dbname = None
class PblNotFound(PblError):
"""记录不存在。"""
code = 'PBL_NOT_FOUND'
class PblValidationError(PblError):
"""入参校验失败。"""
code = 'PBL_VALIDATION_ERROR'
def new_id():
"""32 位无横线 UUID 主键。"""
return uuid.uuid4().hex
def get_env(env=None):
if env is not None:
return env
try:
env = server_env()
getter = getattr(env, 'get_module_dbname', None)
if callable(getter):
dbname = getter(MODULE_NAME)
from sage import ServerEnv
return ServerEnv()
except Exception:
dbname = None
if not dbname:
return None
def get_dbname(env=None):
"""取本模块库名,禁止硬编码。"""
e = get_env(env)
if e is None:
return None
getter = getattr(e, 'get_module_dbname', None)
if callable(getter):
try:
from pbl_common.db_adapter import get_module_dbname as _g
dbname = _g(MODULE_NAME)
return getter(MODULE_NAME)
except Exception:
dbname = None
if not dbname:
raise PblError(ERR_DBNAME_MISSING,
'模块 %s 未注册库名ServerEnv.set_module_dbname 缺失)' % MODULE_NAME)
return dbname
return None
return None
def current_tenant_id(required=True):
"""取当前租户;优先复用 pbl_common 租户上下文,其次 ServerEnv 上挂载的上下文。"""
tid = None
try:
from pbl_common.tenant import get_tenant_id
tid = get_tenant_id()
except Exception:
tid = None
if not tid:
def get_sor(env=None, dbname=None):
"""取 sqlor 数据访问对象。"""
e = get_env(env)
if e is None:
return None
db = dbname or get_dbname(e)
for attr in ('sqlor', 'sor'):
obj = getattr(e, attr, None)
if obj is not None:
return obj
getter = getattr(e, 'get_sqlor', None)
if callable(getter):
try:
ctx = getattr(server_env(), 'pbl_tenant_ctx', None) or {}
tid = ctx.get('tenant_id') if isinstance(ctx, dict) else None
return getter(db) if db else getter()
except Exception:
tid = None
if not tid and required:
raise PblError(ERR_TENANT_MISSING, '缺少租户上下文,拒绝访问蓝图数据')
return tid or ''
return None
return None
def current_operator():
try:
from pbl_common.tenant import get_current_user
return get_current_user() or 'system'
except Exception:
pass
try:
return getattr(server_env(), 'pbl_current_user', None) or 'system'
except Exception:
return 'system'
def require_tenant(tenant_id):
"""tenant_id 强制打头校验:空/非字符串一律拒绝。"""
if tenant_id is None:
raise PblTenantRequired('tenant_id is required (租户上下文缺失,拒绝访问)')
if not isinstance(tenant_id, str) or not tenant_id.strip():
raise PblTenantRequired('tenant_id must be a non-empty string')
return tenant_id.strip()
# ---------------------------------------------------------------- 主键
def gen_id():
"""32 位字符串主键(对应 models 中 id: str/32"""
try:
from pbl_common.ids import new_id
v = new_id()
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:
def ensure_tables(env=None, dbname=None):
"""幂等建表:执行 sql/pbl_blueprint.core.sql 与 sql/pbl_blueprint.subobjects.sql。"""
e = get_env(env)
db = dbname or get_dbname(e)
sor = get_sor(e, db)
if sor is None:
return False
sql_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sql')
executed = []
for fname in ('pbl_blueprint.core.sql', 'pbl_blueprint.subobjects.sql'):
path = os.path.join(sql_dir, fname)
if not os.path.isfile(path):
continue
with open(path, 'r', encoding='utf-8') as fp:
content = fp.read()
for stmt in _split_sql(content):
try:
sor.sqlExe(stmt)
executed.append(stmt[:40])
except Exception:
continue
return executed
def _split_sql(content):
"""按分号切分 DDL剔除注释行与空语句。"""
lines = []
for line in content.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith('--'):
continue
lines.append(line)
stmts = []
for chunk in '\n'.join(lines).split(';'):
chunk = chunk.strip()
if chunk:
stmts.append(chunk)
return stmts

View File

@ -1,88 +1,162 @@
"""pbl_blueprint.init —— 模块挂载入口。
"""pbl_blueprint 模块装配入口。
宿主应用apps/pbls/app/pbls.py init()
from pbl_blueprint.init import load_pbl_blueprint
load_pbl_blueprint()
本函数把全部契约函数注册到 ServerEnv进程级单例.dspy 里即可直接调用
注意不要在此处调用 get_user/get_userorgid 那是 request._run_ns 上的
每请求上下文ServerEnv 上取不到会静默返回 None
三处同步注册module-development-spec
1) 函数定义本文件 load_pbl_blueprint()
2) 包导出 __init__.py -> from .init import load_pbl_blueprint
3) env 注册ServerEnv().register_module('pbl_blueprint', load_pbl_blueprint)
"""
from ahserver.serverEnv import ServerEnv
import os
from pbl_blueprint.api import (
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,
PblBlueprintError,
safe_traceback,
from .db import ensure_tables, get_dbname
from .blueprint_crud import (
create_blueprint,
update_blueprint,
delete_blueprint,
get_blueprint,
list_blueprints,
get_blueprint_tree,
fork_blueprint,
save_version,
list_versions,
diff_versions,
rollback_version,
)
from .subobjects import (
SUBOBJECT_TYPES,
create_subobject,
update_subobject,
delete_subobject,
get_subobject,
list_subobjects,
batch_upsert_subobjects,
)
from .templates import (
create_template,
update_template,
delete_template,
list_templates,
get_template,
instantiate_template,
build_offline_fallback_blueprint,
)
_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():
"""注册 pbl_blueprint 全部契约到 ServerEnv幂等"""
global _loaded
env = ServerEnv()
def load_pbl_blueprint(env=None):
"""挂载 pbl_blueprint 模块到应用。
# ---- 蓝图聚合根 CRUD + 查询
env.pbl_blueprint_create = pbl_blueprint_create
env.pbl_blueprint_read = pbl_blueprint_read
env.pbl_blueprint_update = pbl_blueprint_update
env.pbl_blueprint_delete = pbl_blueprint_delete
env.pbl_blueprint_list = pbl_blueprint_list
env.pbl_blueprint_tree = pbl_blueprint_tree
env.pbl_blueprint_fork = pbl_blueprint_fork
:param env: ServerEnv 实例应用 init() 里逐个 load 时传入为空则自建
:return: 模块契约字典供应用/其他模块按契约调用
"""
if env is None:
try:
from sage import ServerEnv
env = ServerEnv()
except Exception:
env = None
# ---- 子对象契约
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
dbname = get_dbname(env)
# ---- 版本契约
env.pbl_blueprint_version_create = pbl_blueprint_version_create
env.pbl_blueprint_version_diff = pbl_blueprint_version_diff
# 建表幂等IF NOT EXISTS
ensure_tables(env, dbname)
# ---- 模板契约
env.pbl_template_list = pbl_template_list
env.pbl_template_instantiate = pbl_template_instantiate
env.pbl_template_save = pbl_template_save
env.pbl_template_delete = pbl_template_delete
# CRUD 定义注册json/*.json
_register_crud_json(env)
# ---- 下游模块只读契约compiler / validation / runtime
env.pbl_blueprint_get_contract = pbl_blueprint_get_contract
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 统一错误处理)
env.PblBlueprintError = PblBlueprintError
env.pbl_blueprint_safe_traceback = safe_traceback
if env is not None:
for fn in ('register_module', 'set_module_contract', 'register_contract'):
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
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
return contract
def is_loaded():
return _loaded
def _register_crud_json(env):
"""把 json/*.json 的 CRUD 定义注册到 env若平台提供注册钩子"""
json_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'json')
if not os.path.isdir(json_dir):
return
defs = {}
for fname in sorted(os.listdir(json_dir)):
if not fname.endswith('.json'):
continue
path = os.path.join(json_dir, fname)
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": {
"summary": "PBL蓝图聚合根",
"alias": "pbl_blueprint_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"blueprint_code",
"blueprint_name",
"domain_code",
"grade_code",
"subject_code",
"complexity_level",
"status",
"created_at",
"updated_at"
],
"alters": {
"domain_code": {
"uitype": "code",
"appcode": "pbl_blueprint_domain",
"label": "领域"
},
"grade_code": {
"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": "复杂度"
},
"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"
}
]
}
"tblname": "pbl_blueprint",
"params": {
"editable": [
"code",
"name",
"subject",
"grade",
"duration_hours",
"status",
"summary",
"config",
"source_blueprint_id",
"template_id",
"owner_id",
"remark"
],
"browserfields": [
"code",
"name",
"subject",
"grade",
"duration_hours",
"version_no",
"status",
"quality_level",
"owner_id",
"template_id",
"published_at",
"updated_at"
],
"searchfields": [
"tenant_id",
"code",
"name",
"subject",
"status",
"owner_id"
],
"orderby": "updated_at desc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"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": "复杂度"
},
"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"
]
}
"caption": "PBL蓝图",
"readonly": [
"id",
"tenant_id",
"version_no",
"quality_level",
"published_at",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,89 +1,57 @@
{
"pbl_blueprint_artifact_spec": {
"summary": "蓝图子对象-产出物规格(Artifact)",
"alias": "pbl_blueprint_artifact_spec_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"code",
"name",
"seq_no",
"status",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"appcode": "pbl_subobject_status",
"label": "状态"
},
"artifact_type": {
"uitype": "code",
"appcode": "pbl_artifact_type",
"label": "产出物类型"
}
},
"exclouded": [
"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"
}
]
}
"tblname": "pbl_blueprint_artifact_spec",
"params": {
"editable": [
"blueprint_id",
"task_id",
"mission_id",
"seq",
"code",
"name",
"artifact_type",
"description",
"spec",
"accept_criteria",
"required",
"weight",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"mission_id",
"seq",
"code",
"name",
"artifact_type",
"required",
"weight",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"mission_id",
"artifact_type",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"artifact_type": {
"appcode": "pbl_artifact_type",
"label": "产出物类型"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_artifact_spec_tenant_bp",
"uk_pbl_blueprint_artifact_spec_tenant_code"
]
}
"caption": "蓝图子对象-产出物规格",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,89 +1,60 @@
{
"pbl_blueprint_evidence_spec": {
"summary": "蓝图子对象-证据规格(Evidence)",
"alias": "pbl_blueprint_evidence_spec_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"code",
"name",
"seq_no",
"status",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"appcode": "pbl_subobject_status",
"label": "状态"
},
"evidence_type": {
"uitype": "code",
"appcode": "pbl_evidence_type",
"label": "证据类型"
}
},
"exclouded": [
"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"
}
]
}
"tblname": "pbl_blueprint_evidence_spec",
"params": {
"editable": [
"blueprint_id",
"task_id",
"mission_id",
"learning_goal_id",
"seq",
"code",
"name",
"evidence_type",
"collect_mode",
"description",
"spec",
"rule",
"weight",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"mission_id",
"learning_goal_id",
"seq",
"code",
"name",
"evidence_type",
"collect_mode",
"weight",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"mission_id",
"learning_goal_id",
"evidence_type",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"evidence_type": {
"appcode": "pbl_evidence_type",
"label": "证据类型"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_evidence_spec_tenant_bp",
"uk_pbl_blueprint_evidence_spec_tenant_code"
]
}
"caption": "蓝图子对象-证据规格",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,89 +1,51 @@
{
"pbl_blueprint_learning_goal": {
"summary": "蓝图子对象-学习目标(Goal)",
"alias": "pbl_blueprint_learning_goal_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"code",
"name",
"seq_no",
"status",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"appcode": "pbl_subobject_status",
"label": "状态"
},
"goal_type": {
"uitype": "code",
"appcode": "pbl_goal_type",
"label": "目标类型"
}
},
"exclouded": [
"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"
}
]
}
"tblname": "pbl_blueprint_learning_goal",
"params": {
"editable": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"dimension",
"description",
"spec",
"weight",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"dimension",
"weight",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"dimension",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"goal_type": {
"appcode": "pbl_goal_type",
"label": "目标类型"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_learning_goal_tenant_bp",
"uk_pbl_blueprint_learning_goal_tenant_code"
]
}
"caption": "蓝图子对象-学习目标",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,89 +1,47 @@
{
"pbl_blueprint_mission": {
"summary": "蓝图子对象-真实情境任务(Mission)",
"alias": "pbl_blueprint_mission_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"code",
"name",
"seq_no",
"status",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"appcode": "pbl_subobject_status",
"label": "状态"
},
"mission_type": {
"uitype": "code",
"appcode": "pbl_mission_type",
"label": "情境类型"
}
},
"exclouded": [
"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"
}
]
}
"tblname": "pbl_blueprint_mission",
"params": {
"editable": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"description",
"spec",
"unlock_condition",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"mission_type": {
"appcode": "pbl_mission_type",
"label": "情境类型"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_mission_tenant_bp",
"uk_pbl_blueprint_mission_tenant_code"
]
}
"caption": "蓝图子对象-关卡",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,98 +1,58 @@
{
"pbl_blueprint_reflection_spec": {
"summary": "蓝图子对象-反思规格(Reflection)",
"alias": "pbl_blueprint_reflection_spec_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"code",
"name",
"seq_no",
"status",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"appcode": "pbl_subobject_status",
"label": "状态"
},
"reflect_type": {
"uitype": "code",
"appcode": "pbl_reflect_type",
"label": "反思类型"
},
"frequency": {
"uitype": "code",
"appcode": "pbl_reflect_frequency",
"label": "频次"
}
},
"exclouded": [
"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"
}
]
}
"tblname": "pbl_blueprint_reflection_spec",
"params": {
"editable": [
"blueprint_id",
"task_id",
"mission_id",
"seq",
"code",
"name",
"trigger_point",
"reflection_type",
"description",
"spec",
"rubric_ref",
"weight",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"mission_id",
"seq",
"code",
"name",
"trigger_point",
"reflection_type",
"weight",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"mission_id",
"trigger_point",
"reflection_type",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"reflect_type": {
"appcode": "pbl_reflect_type",
"label": "反思类型"
},
"frequency": {
"appcode": "pbl_reflect_frequency",
"label": "频次"
}
},
"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"
]
}
"caption": "蓝图子对象-反思规格",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,89 +1,47 @@
{
"pbl_blueprint_role": {
"summary": "蓝图子对象-角色(Role)",
"alias": "pbl_blueprint_role_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"code",
"name",
"seq_no",
"status",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"appcode": "pbl_subobject_status",
"label": "状态"
},
"role_category": {
"uitype": "code",
"appcode": "pbl_role_category",
"label": "角色类别"
}
},
"exclouded": [
"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"
}
]
}
"tblname": "pbl_blueprint_role",
"params": {
"editable": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"description",
"spec",
"permissions",
"status"
],
"browserfields": [
"blueprint_id",
"task_id",
"seq",
"code",
"name",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"task_id",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"role_category": {
"appcode": "pbl_role_category",
"label": "角色类别"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_role_tenant_bp",
"uk_pbl_blueprint_role_tenant_code"
]
}
"caption": "蓝图子对象-角色",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,89 +1,46 @@
{
"pbl_blueprint_task": {
"summary": "蓝图子对象-任务分解(Task)",
"alias": "pbl_blueprint_task_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"code",
"name",
"seq_no",
"status",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"appcode": "pbl_subobject_status",
"label": "状态"
},
"task_type": {
"uitype": "code",
"appcode": "pbl_task_type",
"label": "任务类型"
}
},
"exclouded": [
"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"
}
]
}
"tblname": "pbl_blueprint_task",
"params": {
"editable": [
"blueprint_id",
"parent_id",
"seq",
"code",
"name",
"description",
"spec",
"status"
],
"browserfields": [
"blueprint_id",
"seq",
"code",
"name",
"parent_id",
"status",
"updated_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"parent_id",
"code",
"name",
"status"
],
"orderby": "seq asc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"task_type": {
"appcode": "pbl_task_type",
"label": "任务类型"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"idx_pbl_blueprint_task_tenant_bp",
"uk_pbl_blueprint_task_tenant_code"
]
}
"caption": "蓝图子对象-任务",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,141 +1,49 @@
{
"pbl_blueprint_template": {
"summary": "PBL蓝图模板",
"alias": "pbl_blueprint_template_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"template_code",
"template_name",
"domain_code",
"grade_code",
"subject_code",
"complexity_level",
"status",
"created_at",
"updated_at"
],
"alters": {
"template_type": {
"uitype": "code",
"appcode": "pbl_template_type",
"label": "模板类型"
},
"domain_code": {
"uitype": "code",
"appcode": "pbl_blueprint_domain",
"label": "领域"
},
"grade_code": {
"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"
}
]
}
"tblname": "pbl_blueprint_template",
"params": {
"editable": [
"code",
"name",
"category",
"subject",
"grade",
"duration_hours",
"description",
"content",
"status"
],
"browserfields": [
"code",
"name",
"category",
"subject",
"grade",
"duration_hours",
"status",
"use_count",
"updated_at"
],
"searchfields": [
"tenant_id",
"code",
"name",
"category",
"status"
],
"orderby": "use_count desc, updated_at desc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"template_type": {
"appcode": "pbl_template_type",
"label": "模板类型"
},
"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": "状态"
}
},
"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"
]
}
"caption": "PBL蓝图模板",
"readonly": [
"id",
"tenant_id",
"use_count",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,82 +1,43 @@
{
"pbl_blueprint_template_item": {
"summary": "PBL蓝图模板明细",
"alias": "pbl_blueprint_template_item_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"subobject_type",
"seq_no",
"created_at",
"updated_at"
],
"alters": {
"subobject_type": {
"uitype": "code",
"appcode": "pbl_subobject_type",
"label": "子对象类型"
}
},
"exclouded": [
"snapshot_json",
"content_json",
"item_payload_json",
"quality_criteria",
"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"
}
]
}
"tblname": "pbl_blueprint_template_item",
"params": {
"editable": [
"template_id",
"obj_type",
"parent_ref",
"ref_key",
"seq",
"name",
"spec"
],
"browserfields": [
"template_id",
"obj_type",
"seq",
"name",
"ref_key",
"parent_ref",
"updated_at"
],
"searchfields": [
"tenant_id",
"template_id",
"obj_type",
"name"
],
"orderby": "obj_type asc, seq asc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"subobject_type": {
"appcode": "pbl_subobject_type",
"label": "子对象类型"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"uk_tplitem_tenant_tpl_type_seq",
"idx_tplitem_template"
]
}
"caption": "PBL蓝图模板条目",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

@ -1,82 +1,43 @@
{
"pbl_blueprint_version": {
"summary": "PBL蓝图版本",
"alias": "pbl_blueprint_version_crud",
"params": {
"sortby": "created_at",
"sortorder": "desc",
"tenant_scoped": true,
"browserfields": {
"fields": [
"id",
"tenant_id",
"class_id",
"version_no",
"version_status",
"created_at",
"updated_at"
],
"alters": {
"version_status": {
"uitype": "code",
"appcode": "pbl_version_status",
"label": "版本状态"
}
},
"exclouded": [
"snapshot_json",
"content_json",
"item_payload_json",
"quality_criteria",
"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"
}
]
}
"tblname": "pbl_blueprint_version",
"params": {
"editable": [
"blueprint_id",
"version_no",
"snapshot",
"change_delta",
"quality_level",
"status",
"remark"
],
"browserfields": [
"blueprint_id",
"version_no",
"quality_level",
"status",
"remark",
"created_by",
"created_at"
],
"searchfields": [
"tenant_id",
"blueprint_id",
"version_no",
"status"
],
"orderby": "version_no desc",
"defaultfilter": {
"deleted": 0
},
"codes": {
"version_status": {
"appcode": "pbl_version_status",
"label": "版本状态"
}
},
"table_meta": {
"pk": "id",
"tenant_field": "tenant_id",
"soft_delete_field": "is_deleted",
"engine": "postgresql",
"indexes": [
"primary",
"uk_version_blueprint_no",
"idx_version_blueprint"
]
}
"caption": "PBL蓝图版本快照",
"readonly": [
"id",
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at",
"deleted"
]
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,520 +1,113 @@
-- pbl_blueprint 核心表 DDL
-- 本文件由 scripts/derive_artifacts.py 从 pbl_blueprint/models/*.json 机械派生,禁止手工编辑
-- engine: postgresql与 projects/pbls/env/*.json 的 db.engine 一致)
-- 主键 id 为应用层生成的 str(32):不使用 SERIAL / BIGSERIAL / nextval / AUTO_INCREMENT
-- =====================================================================
-- pbl_blueprint 核心表 DDLM1a
-- 模块pbl_blueprint 库名:由 ServerEnv().get_module_dbname('pbl_blueprint') 解析,禁止硬编码
-- 约定:主键 id 一律 varchar(32)32位无横线UUIDtenant_id 强制打头且进入每个索引首列;
-- 软删除 deleted tinyintJSON 字段用 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 (
id varchar(32) NOT NULL,
tenant_id varchar(32) NOT NULL,
class_id varchar(32),
blueprint_code varchar(64) NOT NULL,
blueprint_name varchar(128) NOT NULL,
domain_code varchar(64),
grade_code varchar(32),
subject_code varchar(64),
project_duration integer,
student_count integer,
group_count integer,
complexity_level varchar(32),
status varchar(32),
current_version integer,
published_version_id varchar(32),
source_blueprint_id varchar(32),
source_template_id varchar(32),
owner_user_id varchar(64),
estimated_cost numeric(18,2),
tags_json text,
summary_text text,
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint.id IS '主键';
COMMENT ON COLUMN pbl_blueprint.tenant_id IS '租户ID(强制打头)';
COMMENT ON COLUMN pbl_blueprint.class_id IS '班级ID';
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 '更新时间';
-- ---------------------------------------------------------------------
-- 1. pbl_blueprint 蓝图聚合根主表
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint` (
`id` varchar(32) NOT NULL COMMENT '主键32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID强制打头',
`code` varchar(64) NOT NULL COMMENT '蓝图编码,租户内唯一',
`name` varchar(200) NOT NULL COMMENT '蓝图名称',
`subject` varchar(64) DEFAULT NULL COMMENT '学科',
`grade` varchar(32) DEFAULT NULL COMMENT '适用年级',
`duration_hours` int DEFAULT NULL COMMENT '总课时数',
`version_no` int NOT NULL DEFAULT 1 COMMENT '当前版本号',
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/validating/validated/compiling/compiled/published/archived',
`quality_level` varchar(16) DEFAULT 'L1' COMMENT '质量等级 L1-L5',
`summary` text COMMENT '蓝图简介/驱动性问题',
`config` json DEFAULT NULL COMMENT '扩展配置',
`source_blueprint_id` varchar(32) DEFAULT NULL COMMENT 'fork 来源蓝图ID',
`template_id` varchar(32) DEFAULT NULL COMMENT '实例化来源模板ID',
`owner_id` varchar(32) DEFAULT NULL COMMENT '归属教师/创建者',
`published_at` datetime DEFAULT NULL COMMENT '最近发布时间',
`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_blueprint_tenant_code` (`tenant_id`, `code`),
KEY `idx_pbl_blueprint_tenant_status` (`tenant_id`, `status`, `deleted`),
KEY `idx_pbl_blueprint_owner` (`tenant_id`, `owner_id`),
KEY `idx_pbl_blueprint_template` (`tenant_id`, `template_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图聚合根主表';
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);
CREATE INDEX IF NOT EXISTS pbl_blueprint_idx_blueprint_class ON pbl_blueprint (tenant_id, class_id, is_deleted);
-- ---------------------------------------------------------------------
-- 2. pbl_blueprint_version 蓝图版本快照表
-- ---------------------------------------------------------------------
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 (
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,
artifact_type varchar(32),
required_flag integer,
quality_criteria text,
due_seq integer,
is_deleted integer,
created_by varchar(64),
created_at timestamp,
updated_by varchar(64),
updated_at timestamp
);
COMMENT ON COLUMN pbl_blueprint_artifact_spec.id IS '主键';
COMMENT ON COLUMN pbl_blueprint_artifact_spec.tenant_id IS '租户ID(强制打头)';
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 '更新时间';
-- ---------------------------------------------------------------------
-- 3. pbl_blueprint_template 蓝图模板表
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_template` (
`id` varchar(32) NOT NULL COMMENT '主键32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID强制打头平台内置为 platform',
`code` varchar(64) NOT NULL COMMENT '模板编码,租户内唯一',
`name` varchar(200) NOT NULL COMMENT '模板名称',
`category` varchar(64) DEFAULT NULL COMMENT 'stem/humanity/science/engineering/social/general',
`subject` varchar(64) DEFAULT NULL COMMENT '适用学科',
`grade` varchar(32) DEFAULT NULL COMMENT '适用年级',
`duration_hours` int DEFAULT NULL COMMENT '建议课时',
`description` text COMMENT '模板说明',
`content` json DEFAULT NULL COMMENT '模板主体内容',
`status` varchar(32) NOT NULL DEFAULT 'enabled' COMMENT 'enabled/disabled',
`use_count` int NOT NULL DEFAULT 0 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_bpt_tenant_code` (`tenant_id`, `code`),
KEY `idx_pbl_bpt_tenant_cat` (`tenant_id`, `category`, `status`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图模板表';
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);
-- pbl_blueprint_evidence_spec: 蓝图子对象-证据规格(Evidence)
CREATE TABLE IF NOT EXISTS pbl_blueprint_evidence_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,
evidence_type varchar(32),
artifact_id varchar(32),
min_count 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_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);
-- ---------------------------------------------------------------------
-- 4. pbl_blueprint_template_item 蓝图模板条目表
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `pbl_blueprint_template_item` (
`id` varchar(32) NOT NULL COMMENT '主键32位无横线UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID强制打头',
`template_id` varchar(32) NOT NULL COMMENT '所属模板ID',
`obj_type` varchar(32) NOT NULL COMMENT 'task/mission/role/learning_goal/evidence_spec/artifact_spec/reflection_spec',
`parent_ref` varchar(64) DEFAULT NULL COMMENT '父条目引用键(模板内相对引用)',
`ref_key` varchar(64) DEFAULT NULL COMMENT '本条目引用键',
`seq` int NOT NULL DEFAULT 0 COMMENT '同类型内排序号',
`name` varchar(200) NOT NULL COMMENT '条目名称',
`spec` json 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`),
KEY `idx_pbl_bpti_tenant_tpl` (`tenant_id`, `template_id`, `obj_type`, `seq`),
KEY `idx_pbl_bpti_refkey` (`tenant_id`, `template_id`, `ref_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图模板条目表';

View File

@ -0,0 +1,188 @@
-- =====================================================================
-- pbl_blueprint 子对象表 DDLM1a7 类子对象)
-- 统一泛化契约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 -*-
"""蓝图子对象契约接口M1a 落表 + CRUDM1b 扩展校验/编排)。
"""pbl_blueprint 7 类子对象泛化契约M1a
7 类子对象子类型 -> 表名见 models.SUBOBJECT_TYPE_TABLE
mission / learning_goal / task / role / artifact_spec / evidence_spec / reflection_spec
7 类子对象统一走同一套 CRUD 契约obj_type 分派到具体表
避免为每类子对象写重复代码也便于 Compiler / Validation 统一遍历
契约函数
- 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 + 审计字段
tenant_id 强制打头所有函数第一入参 tenant_id缺失即 PblTenantRequired
"""
from . import db as _db
from . import models as _m
COMMON_FIELDS = ['class_id', 'blueprint_id', 'version_id', 'code', 'name',
'description', 'seq_no', 'status', 'content_json']
import datetime
TYPE_OPTIONS = [
{'code': 'mission', 'label': '真实情境任务'},
{'code': 'learning_goal', 'label': '学习目标'},
{'code': 'task', 'label': '任务分解'},
{'code': 'role', 'label': '角色'},
{'code': 'artifact_spec', 'label': '产出物规格'},
{'code': 'evidence_spec', 'label': '证据规格'},
{'code': 'reflection_spec', 'label': '反思规格'},
]
from .db import (
PblError,
PblNotFound,
PblValidationError,
get_dbname,
get_env,
get_sor,
new_id,
require_tenant,
)
STATUS_OPTIONS = [
{'code': 'active', 'label': '启用'},
{'code': 'draft', 'label': '草稿'},
{'code': 'disabled', 'label': '停用'},
]
# 7 类子对象类型 -> 表名
SUBOBJECT_TABLES = {
'task': 'pbl_blueprint_task',
'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):
table = _m.SUBOBJECT_TYPE_TABLE.get((subobject_type or '').strip())
if not table:
raise _db.PblError(_db.ERR_PARAM_INVALID,
'未知子对象类型: %s(可选 %s'
% (subobject_type, ','.join(sorted(_m.SUBOBJECT_TYPE_TABLE))))
return table
def _now():
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def _typed_fields(table):
"""该表可写业务字段models/*.json 为准,排除主键/租户/审计)。"""
skip = ('id', 'tenant_id', 'created_by', 'created_at', 'updated_by', 'updated_at',
'is_deleted')
return [k for k in _m.load_model(table)['fields'].keys() if k not in skip]
def _table(obj_type):
if obj_type not in SUBOBJECT_TABLES:
raise PblValidationError(
'unknown obj_type: %s (allowed: %s)' % (obj_type, ','.join(SUBOBJECT_TYPES)),
code='PBL_UNKNOWN_OBJ_TYPE')
return SUBOBJECT_TABLES[obj_type]
def _fetch(table, rec_id, tid):
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tid, 'is_deleted': 0})
def _where(tenant_id, extra=None):
cond = "tenant_id='%s'" % tenant_id.replace("'", "''")
if extra:
cond = cond + ' and ' + extra
return cond
def subobject_save(subobject_type, payload, tenant_id=None):
"""新建/更新子对象。payload 含 id 视为更新blueprint_id 必填且必须属本租户。"""
tid = tenant_id or _db.current_tenant_id()
table = table_of(subobject_type)
payload = dict(payload or {})
allowed = _typed_fields(table)
data = dict((k, payload[k]) for k in allowed if k in payload)
now = _db.now_str()
operator = _db.current_operator()
def _pick(data, keys):
out = {}
for k in keys:
if k in data and data[k] is not None:
out[k] = data[k]
return out
blueprint_id = data.get('blueprint_id') or payload.get('blueprint_id')
if not blueprint_id:
raise _db.PblError(_db.ERR_PARAM_INVALID, 'blueprint_id 必填')
bp = _db.select_one(_m.TABLES[0], where={'id': blueprint_id, 'tenant_id': tid,
'is_deleted': 0})
if not bp:
raise _db.PblError(_db.ERR_NOT_FOUND, '所属蓝图不存在或无权访问: %s' % blueprint_id)
data['blueprint_id'] = blueprint_id
def _gen_code(sor, tenant_id, blueprint_id, obj_type):
tbl = _table(obj_type)
prefix = '%s-' % CODE_PREFIX.get(obj_type, obj_type.upper())
rows = sor.R(tbl, _where(tenant_id, "blueprint_id='%s'" % blueprint_id))
seq = len(rows or []) + 1
return '%s%03d' % (prefix, seq)
# ---------------------------------------------------------------- 泛化 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'):
raise _db.PblError(_db.ERR_PARAM_INVALID, 'name 必填')
data.setdefault('status', 'active')
if not data.get('class_id'):
data['class_id'] = bp.get('class_id') or ''
raise PblValidationError('name is required')
for field in TYPE_REQUIRED.get(obj_type, []):
if not data.get(field):
raise PblValidationError('%s is required for obj_type=%s' % (field, obj_type))
rec_id = payload.get('id')
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('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
e = get_env(env)
sor = get_sor(e, get_dbname(e))
if sor is None:
raise PblError('sqlor unavailable for module pbl_blueprint', code='PBL_DB_UNAVAILABLE')
new_id = _db.gen_id()
row = dict((k, '') for k in allowed)
row.update(data)
row.update({'id': new_id, 'tenant_id': tid, 'is_deleted': 0,
'seq_no': int(data.get('seq_no') or 0),
'created_by': operator, 'created_at': now,
'updated_by': operator, 'updated_at': now})
_db.insert_row(table, row)
_db.audit('subobject_create', table, new_id, after=row, tenant_id=tid)
return _fetch(table, new_id, tid)
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)
row = {'id': data.get('id') or new_id(), 'tenant_id': tid}
row.update(_pick(data, TYPE_FIELDS.get(obj_type, BASE_EDITABLE)))
if not row.get('code'):
row['code'] = _gen_code(sor, tid, row['blueprint_id'], obj_type)
row.setdefault('seq', 0)
row.setdefault('status', 'draft')
row['created_by'] = operator
row['created_at'] = _now()
row['updated_by'] = operator
row['updated_at'] = _now()
row['deleted'] = 0
sor.C(tbl, row)
return row
def subobject_delete(subobject_type, object_id, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
table = table_of(subobject_type)
old = _fetch(table, object_id, tid)
if not old:
raise _db.PblError(_db.ERR_NOT_FOUND, '子对象不存在或无权访问: %s' % object_id)
if (old.get('status') or '') == 'locked':
raise _db.PblError(_db.ERR_REFERENCED, '锁定状态的子对象不可删除')
_db.update_row(table, {'is_deleted': 1, 'updated_by': _db.current_operator(),
'updated_at': _db.now_str()},
{'id': object_id, 'tenant_id': tid})
_db.audit('subobject_delete', table, object_id, before=old, tenant_id=tid)
return {'id': object_id, 'subobject_type': subobject_type, 'deleted': True}
def update_subobject(tenant_id, obj_type, obj_id, data, env=None, operator=None):
"""更新子对象(仅该类型可编辑字段)。"""
tid = require_tenant(tenant_id)
tbl = _table(obj_type)
if not obj_id:
raise PblValidationError('obj_id is required')
patch = _pick(data or {}, TYPE_FIELDS.get(obj_type, BASE_EDITABLE))
patch.pop('blueprint_id', None) # 不允许跨蓝图迁移
if not patch:
return get_subobject(tid, obj_type, obj_id, env=env)
patch['updated_by'] = operator
patch['updated_at'] = _now()
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):
tid = tenant_id or _db.current_tenant_id()
out = {}
for stype in sorted(_m.SUBOBJECT_TYPE_TABLE):
out[stype] = subobject_list(stype, blueprint_id, tid)['rows']
return out
def delete_subobject(tenant_id, obj_type, obj_id, env=None, operator=None, hard=False):
"""软删除子对象hard=True 物理删除)。"""
tid = require_tenant(tenant_id)
tbl = _table(obj_type)
if not obj_id:
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):
tid = tenant_id or _db.current_tenant_id()
out = {}
for stype, table in _m.SUBOBJECT_TYPE_TABLE.items():
rows = _db.select_rows(table, where={'tenant_id': tid, 'blueprint_id': blueprint_id,
'is_deleted': 0}, fields='id') or []
out[stype] = len(rows)
out['total'] = sum(out.values())
return out
def get_subobject(tenant_id, obj_type, obj_id, env=None):
"""按 id 取子对象。"""
tid = require_tenant(tenant_id)
tbl = _table(obj_type)
if not obj_id:
raise PblValidationError('obj_id is required')
e = get_env(env)
sor = get_sor(e, get_dbname(e))
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):
"""把源蓝图的全部子对象复制到目标蓝图(新主键,重挂 blueprint_id"""
tid = tenant_id or _db.current_tenant_id()
now = _db.now_str()
operator = _db.current_operator()
copied = {}
for stype, table in _m.SUBOBJECT_TYPE_TABLE.items():
rows = _db.select_rows(table, where={'tenant_id': tid, 'blueprint_id': src_blueprint_id,
'is_deleted': 0}) or []
n = 0
for r in rows:
new = dict(r)
new.update({'id': _db.gen_id(), 'blueprint_id': dst_blueprint_id,
'version_id': '', 'code': '',
'created_by': operator, 'created_at': now,
'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 list_subobjects(tenant_id, obj_type, blueprint_id, filters=None, env=None):
"""列出某蓝图下某类型的全部子对象(按 seq 升序)。"""
tid = require_tenant(tenant_id)
tbl = _table(obj_type)
if not blueprint_id:
raise PblValidationError('blueprint_id is required')
filters = filters or {}
conds = ["blueprint_id='%s'" % blueprint_id.replace("'", "''"), 'deleted=0']
for key in ('status', 'task_id', 'mission_id', 'parent_id', 'learning_goal_id',
'dimension', 'evidence_type', 'artifact_type', 'trigger_point', 'reflection_type'):
if filters.get(key):
conds.append("%s='%s'" % (key, str(filters[key]).replace("'", "''")))
e = get_env(env)
sor = get_sor(e, get_dbname(e))
return sor.R(tbl, _where(tid, ' and '.join(conds)), 'seq asc') or []
def soft_delete_by_blueprint(blueprint_id, tenant_id=None):
"""蓝图删除时级联软删其子对象。"""
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
def batch_upsert_subobjects(tenant_id, obj_type, blueprint_id, rows, env=None, operator=None):
"""批量新增/更新子对象Designer Agent 一次性写入整棵子树用)。
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():
return list(TYPE_OPTIONS)
e = get_env(env)
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 -*-
"""蓝图模板契约接口M1a模板主表/明细 CRUD + 实例化为蓝图)。
"""pbl_blueprint 模板与离线兜底M1a
契约函数
- save_template(payload, tenant_id=None) -> dict
- get_template(template_id, tenant_id=None) -> dict items 明细
- list_templates(filters=None, page=1, rows=20, tenant_id=None) -> dict
- delete_template(template_id, tenant_id=None) -> dict
- 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
- 模板 CRUDpbl_blueprint_template / pbl_blueprint_template_item
- instantiate_template模板 -> 新蓝图 7 类子对象ref_key/parent_ref 解析为真实 ID
- build_offline_fallback_blueprintAgent/网络不可用时的最小可用蓝图兜底
tenant_id 强制打头缺失即 PblTenantRequired
"""
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'
TABLE_ITEM = 'pbl_blueprint_template_item'
import datetime
TEMPLATE_FIELDS = ['class_id', 'template_code', 'template_name', 'template_type',
'domain_code', 'grade_code', 'subject_code', 'complexity_level',
'scope', 'source_blueprint_id', 'status', 'description']
from .db import (
PblError,
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):
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tid, 'is_deleted': 0})
def _now():
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def save_template(payload, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
payload = dict(payload or {})
data = dict((k, payload[k]) for k in TEMPLATE_FIELDS if k in payload)
now = _db.now_str()
operator = _db.current_operator()
rec_id = payload.get('id')
def _where(tenant_id, extra=None):
cond = "tenant_id='%s'" % tenant_id.replace("'", "''")
if extra:
cond = cond + ' and ' + extra
return cond
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'):
raise _db.PblError(_db.ERR_PARAM_INVALID, 'template_name 必填')
if not data.get('template_code'):
data['template_code'] = 'TPL-%s' % _db.gen_id()[:8].upper()
dup = _db.select_one(TABLE, where={'tenant_id': tid, 'template_code': data['template_code'],
'is_deleted': 0})
if dup:
raise _db.PblError(_db.ERR_PARAM_INVALID, '模板编码已存在: %s' % data['template_code'])
new_id = _db.gen_id()
row = {
'id': new_id, 'tenant_id': tid, 'class_id': data.get('class_id', ''),
'template_code': data['template_code'], 'template_name': data['template_name'],
'template_type': data.get('template_type', 'blueprint'),
'domain_code': data.get('domain_code', ''), 'grade_code': data.get('grade_code', ''),
'subject_code': data.get('subject_code', ''),
'complexity_level': data.get('complexity_level', ''),
'scope': data.get('scope', 'tenant'),
'source_blueprint_id': data.get('source_blueprint_id', ''),
'usage_count': 0, 'status': data.get('status', 'draft'),
'description': data.get('description', ''),
'is_deleted': 0, 'created_by': operator, 'created_at': now,
'updated_by': operator, 'updated_at': now,
def _pick(data, keys):
out = {}
for k in keys:
if k in data and data[k] is not None:
out[k] = data[k]
return out
# ---------------------------------------------------------------- 模板 CRUD
def create_template(tenant_id, data, env=None, operator=None):
"""新建模板data['items'] 为子对象骨架条目数组(可选)。"""
tid = require_tenant(tenant_id)
if not isinstance(data, dict) or not data.get('name'):
raise PblValidationError('name is required')
e = get_env(env)
sor = get_sor(e, get_dbname(e))
if sor is None:
raise PblError('sqlor unavailable for module pbl_blueprint', code='PBL_DB_UNAVAILABLE')
tpl = {
'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)
_db.audit('template_create', TABLE, new_id, after=row, tenant_id=tid)
return _fetch(tid, new_id)
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):
"""把蓝图抽为模板:主表 + 每类子对象一条 itempayload 可覆盖模板字段)。"""
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
sor.C(TPL_TABLE, tpl)
items = data.get('items') or []
tpl['items'] = _save_items(sor, tid, tpl['id'], items, operator)
return tpl
def template_instantiate(template_id, payload=None, tenant_id=None):
"""由模板实例化新蓝图draft复制模板字段 + 逐条 item 落子对象usage_count+1。"""
tid = tenant_id or _db.current_tenant_id()
payload = dict(payload or {})
tpl = get_template(template_id, tid)
now = _db.now_str()
operator = _db.current_operator()
bp_payload = {
'blueprint_name': payload.get('blueprint_name') or tpl.get('template_name'),
'class_id': payload.get('class_id') or tpl.get('class_id') or '',
'domain_code': tpl.get('domain_code', ''), 'grade_code': tpl.get('grade_code', ''),
'subject_code': tpl.get('subject_code', ''),
'complexity_level': tpl.get('complexity_level', ''),
'project_duration': payload.get('project_duration') or 0,
'student_count': payload.get('student_count') or 0,
'group_count': payload.get('group_count') or 0,
'summary_text': payload.get('summary_text') or tpl.get('description') or '',
def _save_items(sor, tenant_id, template_id, items, operator=None):
"""写入模板条目(先清后写,幂等)。"""
sor.D(ITEM_TABLE, _where(tenant_id, "template_id='%s'" % template_id))
saved = []
for idx, item in enumerate(items or []):
obj_type = item.get('obj_type')
if obj_type not in SUBOBJECT_TYPES:
raise PblValidationError('item[%d] invalid obj_type: %s' % (idx, obj_type))
if not item.get('name'):
raise PblValidationError('item[%d] name is required' % idx)
row = {
'id': new_id(),
'tenant_id': tenant_id,
'template_id': template_id,
'obj_type': obj_type,
'parent_ref': item.get('parent_ref'),
'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)
_db.update_row(_m.TABLES[0], {'source_template_id': template_id},
{'id': bp['id'], 'tenant_id': tid})
made = {}
for item in tpl.get('items') or []:
stype = item.get('subobject_type')
if stype not in _m.SUBOBJECT_TYPE_TABLE:
bp = create_blueprint(tid, bp_data, env=e, operator=operator)
ref_map = {}
items = tpl.get('items') or []
ordered = sorted(items, key=lambda x: (SUBOBJECT_TYPES.index(x.get('obj_type'))
if x.get('obj_type') in SUBOBJECT_TYPES else 99,
x.get('seq') or 0))
for item in ordered:
obj_type = item.get('obj_type')
if obj_type not in SUBOBJECT_TYPES:
continue
body = _db.loads(item.get('item_payload_json'), {})
body.update({'blueprint_id': bp['id'], 'class_id': bp.get('class_id') or '',
'name': body.get('name') or item.get('name') or stype,
'code': '', 'seq_no': int(item.get('seq_no') or 0)})
_s.subobject_save(stype, body, tid)
made[stype] = made.get(stype, 0) + 1
_db.update_row(TABLE, {'usage_count': int(tpl.get('usage_count') or 0) + 1,
'updated_by': operator, 'updated_at': now},
{'id': template_id, 'tenant_id': tid})
_db.audit('template_instantiate', TABLE, template_id,
after={'blueprint_id': bp['id'], 'subobjects': made}, tenant_id=tid)
return {'template_id': template_id, 'blueprint_id': bp['id'],
'blueprint_code': bp.get('blueprint_code'), 'subobjects_created': made,
'status': bp.get('status')}
spec = dict(item.get('spec') or {})
payload = {
'blueprint_id': bp['id'],
'name': item.get('name'),
'description': spec.pop('description', None),
'seq': item.get('seq') or 0,
'spec': spec,
'status': 'active',
}
for key in ('dimension', 'weight', 'evidence_type', 'collect_mode', 'rule',
'artifact_type', 'accept_criteria', 'required', 'trigger_point',
'reflection_type', 'rubric_ref', 'unlock_condition', 'permissions'):
if key in spec:
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():
return [{'code': 'draft', 'label': '草稿'}, {'code': 'published', 'label': '已发布'},
{'code': 'archived', 'label': '已归档'}]
def build_offline_fallback_blueprint(tenant_id, name=None, env=None, operator=None):
"""离线兜底:不依赖 Agent/模板,直接落一个最小可用蓝图(含 7 类子对象各至少 1 条)。"""
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]
requires = ["setuptools>=45", "wheel"]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "pbl_blueprint"
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"
dependencies = ["sqlor", "bricks_for_python"]
license = { text = "Proprietary" }
authors = [{ name = "sdlc agent.develop" }]
keywords = ["pbl", "blueprint", "sage", "module"]
dependencies = []
[tool.setuptools.packages.find]
where = ["."]
include = ["pbl_blueprint*"]
[project.optional-dependencies]
dev = ["pytest>=7.0"]
[tool.setuptools]
packages = ["pbl_blueprint"]
include-package-data = true
[tool.setuptools.package-data]
pbl_blueprint = [
"json/*.json",
"models/*.json",
"sql/*.sql",
"wwwroot/*.ui",
"init/*.json",
"skill/*.md",
]

View File

@ -1,143 +1,130 @@
"""scripts/load_path.py —— pbl_blueprint 模块 RBAC 路径注册。
"""pbl_blueprint 模块 RBAC 路径注册脚本
规则module-development-spec
- 禁止任何通配符% / *每条路径显式列出
- 角色分层any无需登录/ logined登录即可/ owner.superuser管理
- CRUD 子目录标准 5 条一组目录 + index.ui + get_/add_/update_/delete_.dspy
用途把本模块全部新 API 路径 + 页面路径注册到平台 RBAC权限路径表
使 owner.admin / 租户管理员可授权未注册路径一律 403
用法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 sys
MODULE = 'pbl_blueprint'
MODULE_NAME = 'pbl_blueprint'
# 契约 API登录可用
PATHS_LOGINED = [
'/pbl_blueprint',
'/pbl_blueprint/index.ui',
'/pbl_blueprint/api/pbl_blueprint_create.dspy',
'/pbl_blueprint/api/pbl_blueprint_read.dspy',
'/pbl_blueprint/api/pbl_blueprint_update.dspy',
'/pbl_blueprint/api/pbl_blueprint_delete.dspy',
'/pbl_blueprint/api/pbl_blueprint_list.dspy',
'/pbl_blueprint/api/pbl_blueprint_tree.dspy',
'/pbl_blueprint/api/pbl_blueprint_fork.dspy',
'/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy',
'/pbl_blueprint/api/pbl_blueprint_subobject_list.dspy',
'/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy',
'/pbl_blueprint/api/pbl_blueprint_version_create.dspy',
'/pbl_blueprint/api/pbl_blueprint_version_diff.dspy',
'/pbl_blueprint/api/pbl_template_list.dspy',
'/pbl_blueprint/api/pbl_template_instantiate.dspy',
'/pbl_blueprint/api/pbl_template_save.dspy',
'/pbl_blueprint/api/pbl_template_delete.dspy',
# ---- 本模块全部新 API 路径(与 api.py 的 API_PATHS 保持一致,单一事实源)----
API_PATHS = [
('/pbl_blueprint/blueprint/create', 'POST', '新建蓝图'),
('/pbl_blueprint/blueprint/update', 'POST', '更新蓝图'),
('/pbl_blueprint/blueprint/delete', 'POST', '删除蓝图'),
('/pbl_blueprint/blueprint/get', 'GET', '蓝图详情'),
('/pbl_blueprint/blueprint/list', 'GET', '蓝图列表'),
('/pbl_blueprint/blueprint/tree', 'GET', '蓝图子对象树'),
('/pbl_blueprint/blueprint/fork', 'POST', '复制蓝图'),
('/pbl_blueprint/version/save', 'POST', '保存版本快照'),
('/pbl_blueprint/version/list', 'GET', '版本列表'),
('/pbl_blueprint/version/diff', 'GET', '版本对比'),
('/pbl_blueprint/version/rollback', 'POST', '版本回滚'),
('/pbl_blueprint/subobject/create', 'POST', '新建子对象'),
('/pbl_blueprint/subobject/update', 'POST', '更新子对象'),
('/pbl_blueprint/subobject/delete', 'POST', '删除子对象'),
('/pbl_blueprint/subobject/get', 'GET', '子对象详情'),
('/pbl_blueprint/subobject/list', 'GET', '子对象列表'),
('/pbl_blueprint/subobject/batch_upsert', 'POST', '批量写子对象'),
('/pbl_blueprint/subobject/types', 'GET', '子对象类型枚举'),
('/pbl_blueprint/template/create', 'POST', '新建模板'),
('/pbl_blueprint/template/update', 'POST', '更新模板'),
('/pbl_blueprint/template/delete', 'POST', '删除模板'),
('/pbl_blueprint/template/get', 'GET', '模板详情'),
('/pbl_blueprint/template/list', 'GET', '模板列表'),
('/pbl_blueprint/template/instantiate', 'POST', '模板实例化为蓝图'),
('/pbl_blueprint/template/offline_fallback', 'POST', '离线兜底生成蓝图'),
]
# 静态资源(无需登录)
PATHS_ANY = [
'/pbl_blueprint/menu.ui',
# ---- 页面路径wwwroot/*.ui 挂载点)----
PAGE_PATHS = [
('/pbl_blueprint/index', 'GET', 'PBL蓝图管理主页'),
('/pbl_blueprint/blueprint/edit', 'GET', '蓝图编辑页'),
('/pbl_blueprint/template/list', 'GET', '蓝图模板库页'),
]
# 管理级操作(删除/模板维护)
PATHS_SUPERUSER = [
'/pbl_blueprint/api/pbl_template_delete.dspy',
# ---- 表级数据权限路径CRUD 定义 json/*.json 对应)----
TABLE_PATHS = [
('/pbl_blueprint/tbl/pbl_blueprint', 'CRUD', '蓝图主表数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_version', 'CRUD', '蓝图版本表数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_template', 'CRUD', '蓝图模板表数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_template_item', 'CRUD', '蓝图模板条目表数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_task', 'CRUD', '子对象-任务数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_mission', 'CRUD', '子对象-关卡数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_role', 'CRUD', '子对象-角色数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_learning_goal', 'CRUD', '子对象-学习目标数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_evidence_spec', 'CRUD', '子对象-证据规格数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_artifact_spec', 'CRUD', '子对象-产出物规格数据权限'),
('/pbl_blueprint/tbl/pbl_blueprint_reflection_spec', 'CRUD', '子对象-反思规格数据权限'),
]
# CRUD 生成目录(每个 alias 5 条)
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',
]
ALL_PATHS = API_PATHS + PAGE_PATHS + TABLE_PATHS
def _crud_paths():
out = []
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
def load_paths(env=None):
"""把 ALL_PATHS 注册进 RBAC幂等存在则更新描述不存在则新增
:return: {'registered': n, 'skipped': m, 'failed': k}
"""
result = {'registered': 0, 'skipped': 0, 'failed': 0, 'paths': []}
if env is None:
try:
from sage import ServerEnv
env = ServerEnv()
except Exception:
env = None
def all_paths():
"""返回 [(path, role), ...] 全量显式路径清单。"""
items = [(p, 'logined') for p in PATHS_LOGINED]
items += [(p, 'any') for p in PATHS_ANY]
items += [(p, 'owner.superuser') for p in PATHS_SUPERUSER]
items += _crud_paths()
# 去重保序
seen = set()
out = []
for p, r in items:
if p in seen:
rbac = None
if env is not None:
for attr in ('rbac', 'RBAC'):
rbac = getattr(env, attr, None)
if rbac is not None:
break
if rbac is None:
getter = getattr(env, 'get_rbac', None)
if callable(getter):
try:
rbac = getter()
except Exception:
rbac = None
for path, method, caption in ALL_PATHS:
item = {'path': path, 'method': method, 'caption': caption, 'module': MODULE_NAME}
result['paths'].append(item)
if rbac is None:
result['skipped'] += 1
continue
seen.add(p)
out.append((p, r))
return out
def _find_app_root():
"""向上查找宿主应用根(含 load_path.py / set_role_perm.py 的目录)。"""
here = os.path.dirname(os.path.abspath(__file__))
cur = here
for _ in range(8):
for cand in (os.path.join(cur, 'set_role_perm.py'),
os.path.join(cur, 'load_path.py')):
if os.path.exists(cand):
return cur
nxt = os.path.dirname(cur)
if nxt == cur:
break
cur = nxt
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
try:
registered = False
for fn_name in ('register_path', 'add_path', 'load_path', 'register'):
fn = getattr(rbac, fn_name, None)
if callable(fn):
try:
fn(path, method, caption)
except TypeError:
fn(item)
registered = True
break
if registered:
result['registered'] += 1
else:
result['skipped'] += 1
except Exception:
result['failed'] += 1
return result
def main():
paths = all_paths()
root = _find_app_root()
if root is None:
print('[pbl_blueprint] 未找到宿主应用根set_role_perm.py'
'仅打印待注册路径 %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)))
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
res = load_paths()
print('[pbl_blueprint] load_path done: registered=%d skipped=%d failed=%d total=%d' % (
res['registered'], res['skipped'], res['failed'], len(res['paths'])))
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 蓝图聚合根与子对象、版本、模板M1a/M1b
# pbl_blueprint 模块技能
## 挂载
`from pbl_blueprint.init import load_pbl_blueprint``load_pbl_blueprint()`(应用 app/pbls.py init() 中按序调用)
## 1. 模块定位
`pbl_blueprint` 是 PBLProject-Based Learning平台的**蓝图域聚合根模块**,负责:
- 蓝图主表 `pbl_blueprint` 的 CRUD / 列表 / 树 / fork
- 7 类子对象的**泛化契约**(一套 CRUD 覆盖 7 张表)
- 版本快照 `pbl_blueprint_version`(含 `change_delta` 增量、diff、rollback
- 模板 `pbl_blueprint_template` / `pbl_blueprint_template_item`(实例化 + 离线兜底)
## 数据表10 张)
- `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 模板(含离线兜底槽位)
模块是 **Python 包**(无 app.py / 无端口 / 无 Dockerfile通过 `load_pbl_blueprint(env)` 挂到应用 `apps/pbls`
## 契约接口13 个,路径 `/pbl_blueprint/api/<name>.dspy`
- `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_version_create`
- `pbl_blueprint_version_diff`
- `pbl_template_list`
- `pbl_template_instantiate`
## 2. 目录结构
```
modules/pbl_blueprint/
├── pyproject.toml
├── scripts/load_path.py # RBAC 路径注册API/页面/表权限)
└── pbl_blueprint/ # 包目录 = 模块名(禁 src/
├── __init__.py # 导出 load_pbl_blueprint
├── init.py # 装配入口 + env 注册 + 契约字典
├── db.py # 库名解析 / sqlor 获取 / 异常体系 / 建表
├── blueprint_crud.py # 蓝图 CRUD + 树 + fork + 版本契约
├── subobjects.py # 7 类子对象泛化 CRUD
├── templates.py # 模板 CRUD + 实例化 + 离线兜底
├── api.py # HTTP API 层 + API_PATHS + HANDLERS
├── 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 # 前端页面
```
## 陷阱
- 库名一律 `ServerEnv().get_module_dbname('pbl_blueprint')`,禁止硬编码 DBNAME。
- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one已适配
- 所有读写强制带 `tenant_id`pbl_common.api.tenant_id()),缺失即 fail-closed 报错。
- 新增契约需同步三处api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。
## 3. 数据表11 张)
核心 4 张:`pbl_blueprint``pbl_blueprint_version``pbl_blueprint_template``pbl_blueprint_template_item`
子对象 7 张:`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`
统一约定:
- 主键 `id` = varchar(32)32 位无横线 UUID`db.new_id()`
- `tenant_id` varchar(32) NOT NULL**每个索引首列**
- 软删除 `deleted` tinyint0 正常 / 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 解析为真实 IDuse_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=deletedhard=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)}