[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:
parent
a51815075f
commit
3ebd234b2e
23
.gitignore
vendored
23
.gitignore
vendored
@ -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/
|
||||
|
||||
95
README.md
95
README.md
@ -1,2 +1,95 @@
|
||||
# pbl_blueprint
|
||||
# pbl_blueprint —— PBL 蓝图聚合根与子对象、版本、模板(M1a)
|
||||
|
||||
PBL(Project-Based Learning)平台的**蓝图域基础模块**。蓝图是整个 PBL 系统的
|
||||
聚合根:一份蓝图定义了一个项目的学习目标、角色分工、驱动问题(Mission)、任务、
|
||||
产出物规格、证据规格与反思规格,并带完整版本历史(`change_delta` 记录对话式改
|
||||
模型的每一步)。模板(`pbl_template`)提供蓝图骨架与 LLM 不可用时的离线兜底槽位。
|
||||
|
||||
## 特性
|
||||
|
||||
- **多租户强制打头**:所有表的第二列即 `tenant_id`,所有读写 SQL 的 WHERE 第一
|
||||
条件必须是 `tenant_id`;上下文缺失时 **fail-closed 抛 `PblTenantMissing`**,
|
||||
绝不降级为全租户可见。
|
||||
- **聚合根 + 7 类子对象**:`learning_goal / role / mission / task /
|
||||
artifact_spec / evidence_spec / reflection_spec`,统一走
|
||||
`pbl_blueprint_subobject_save|list|delete` 三个泛化契约,新增子对象类型只需在
|
||||
`api.py: SUBOBJECTS` 注册一行 + 加 models/json 定义。
|
||||
- **版本化**:任何主表/子对象变更都会自动升 `version_no` 并追加一条
|
||||
`pbl_blueprint_version` 记录(含 `change_delta` 与全量 `snapshot_json`),
|
||||
为 M2 校验引擎与 M3 Compiler 提供可追溯的模型演化证据。
|
||||
- **模板实例化 + 离线兜底**:`pbl_template_instantiate` 展开
|
||||
`blueprint_snapshot`,缺失字段用 `offline_slots[].default` 填充(LLM 不可用
|
||||
时仍能产出可用蓝图)。
|
||||
- **派生(fork)**:深拷贝聚合根与全部子对象,并重映射子对象之间的引用
|
||||
(`mission_id / task_id / artifact_spec_id / role_id`)。
|
||||
- **状态保护**:`status='published'` 的蓝图禁止直接改主表/子对象/删除,必须先
|
||||
fork 或回退状态(`PblStateConflict`)。
|
||||
|
||||
## 数据表(10 张)
|
||||
|
||||
| 表 | 说明 |
|
||||
|----|------|
|
||||
| `pbl_blueprint` | 蓝图聚合根 |
|
||||
| `pbl_blueprint_learning_goal` | 子对象-学习目标 |
|
||||
| `pbl_blueprint_role` | 子对象-PBL 角色 |
|
||||
| `pbl_blueprint_mission` | 子对象-驱动问题/Mission |
|
||||
| `pbl_blueprint_task` | 子对象-任务 |
|
||||
| `pbl_blueprint_artifact_spec` | 子对象-产出物规格 |
|
||||
| `pbl_blueprint_evidence_spec` | 子对象-证据规格 |
|
||||
| `pbl_blueprint_reflection_spec` | 子对象-反思规格 |
|
||||
| `pbl_blueprint_version` | 版本快照 + change_delta |
|
||||
| `pbl_template` | 模板(含离线兜底槽位) |
|
||||
|
||||
DDL 基线:`models/pbl_blueprint.subobjects.sql`;表定义四段式:`models/*.json`
|
||||
(build.sh 用 `json2ddl mysql .` 生成 `mysql.ddl.sql`)。
|
||||
|
||||
## 契约接口(17 个,路径 `/pbl_blueprint/api/<name>.dspy`)
|
||||
|
||||
蓝图:`pbl_blueprint_create` / `_read` / `_update` / `_delete` / `_list` /
|
||||
`_tree` / `_fork` / `_get_contract`
|
||||
子对象:`pbl_blueprint_subobject_save` / `_list` / `_delete`
|
||||
版本:`pbl_blueprint_version_create` / `_diff`
|
||||
模板:`pbl_template_list` / `_instantiate` / `_save` / `_delete`
|
||||
|
||||
统一返回:`{"status":"success","data":...,"total":N}` 或
|
||||
`{"status":"error","code":"PBL_XXX","message":"..."}`。
|
||||
|
||||
## 安装与集成(宿主应用 apps/pbls)
|
||||
|
||||
```python
|
||||
# app/pbls.py
|
||||
from pbl_blueprint.init import load_pbl_blueprint
|
||||
|
||||
def init():
|
||||
env = ServerEnv()
|
||||
env.get_module_dbname = get_module_dbname # 模块 -> 库名映射,禁止模块内硬编码
|
||||
load_pbl_common() # 先加载公共内核(tenant 上下文/审计)
|
||||
load_pbl_blueprint() # 再加载蓝图域
|
||||
```
|
||||
|
||||
```bash
|
||||
cd apps/pbls/pkgs && git clone <repo>/pbl_blueprint && pip install ./pbl_blueprint
|
||||
bash modules/pbl_blueprint/build.sh # DDL + CRUD UI + 软链
|
||||
./py3/bin/python modules/pbl_blueprint/scripts/load_path.py # RBAC 路径注册
|
||||
```
|
||||
|
||||
## 目录
|
||||
|
||||
```
|
||||
pbl_blueprint/
|
||||
├── pbl_blueprint/ # Python 包:__init__.py / init.py / api.py
|
||||
├── models/ # 10 张表定义(四段式 JSON)+ DDL 基线 SQL
|
||||
├── json/ # 10 份 CRUD 定义
|
||||
├── wwwroot/ # index.ui / menu.ui / api/*.dspy(17 个契约薄包装)
|
||||
├── init/data.json # appcodes 10 组编码 + 2 份内置模板真实种子
|
||||
├── scripts/load_path.py # RBAC 显式路径(无通配符)
|
||||
├── skill/SKILL.md # agent 可读模块规范
|
||||
├── pyproject.toml / build.sh / README.md
|
||||
```
|
||||
|
||||
## 自测
|
||||
|
||||
```bash
|
||||
python3 -m py_compile pbl_blueprint/*.py scripts/load_path.py
|
||||
python3 scripts/selftest.py # 无 DB 环境下的契约/租户 fail-closed 静态校验
|
||||
```
|
||||
|
||||
66
build.sh
Normal file
66
build.sh
Normal 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
151
init/data.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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'"}
|
||||
]
|
||||
}
|
||||
|
||||
234
models/pbl_blueprint.subobjects.sql
Normal file
234
models/pbl_blueprint.subobjects.sql
Normal 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模板(含离线兜底槽位)';
|
||||
@ -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']
|
||||
|
||||
1430
pbl_blueprint/api.py
1430
pbl_blueprint/api.py
File diff suppressed because it is too large
Load Diff
@ -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 -> archived;published -> in_review(改版)
|
||||
租户隔离:tenant_id 强制打头,缺失 fail-closed;跨租户读取返回「不存在」。
|
||||
契约要点:
|
||||
- 每个函数第一入参 tenant_id,内部 require_tenant() 强制校验,缺失即 PblTenantRequired(fail-closed)。
|
||||
- 只用 sqlor 标准 API:sor.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):
|
||||
"""构造 where:tenant_id 永远打头。"""
|
||||
cond = "tenant_id='%s'" % tenant_id.replace("'", "''")
|
||||
if extra:
|
||||
cond = cond + ' and ' + extra
|
||||
return cond
|
||||
|
||||
|
||||
def _fetch(table, rec_id, tenant_id):
|
||||
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_delta(a -> b)。"""
|
||||
tid = require_tenant(tenant_id)
|
||||
e = get_env(env)
|
||||
sor = get_sor(e, get_dbname(e))
|
||||
rows_a = sor.R(VERSION_TABLE, _where(
|
||||
tid, "blueprint_id='%s' and version_no=%d and deleted=0" % (blueprint_id, int(version_a))))
|
||||
rows_b = sor.R(VERSION_TABLE, _where(
|
||||
tid, "blueprint_id='%s' and version_no=%d and deleted=0" % (blueprint_id, int(version_b))))
|
||||
if not rows_a or not rows_b:
|
||||
raise PblNotFound('version not found', code='PBL_VERSION_NOT_FOUND')
|
||||
return {
|
||||
'from': int(version_a),
|
||||
'to': int(version_b),
|
||||
'delta': _calc_delta(rows_a[0].get('snapshot') or {}, rows_b[0].get('snapshot') or {}),
|
||||
}
|
||||
|
||||
|
||||
def rollback_version(tenant_id, blueprint_id, version_no, env=None, operator=None):
|
||||
"""回滚蓝图到指定版本:先存当前版本快照,再按目标快照重写主表与子对象。"""
|
||||
tid = require_tenant(tenant_id)
|
||||
e = get_env(env)
|
||||
sor = get_sor(e, get_dbname(e))
|
||||
rows = sor.R(VERSION_TABLE, _where(
|
||||
tid, "blueprint_id='%s' and version_no=%d and deleted=0" % (blueprint_id, int(version_no))))
|
||||
if not rows:
|
||||
raise PblNotFound('version not found: %s' % version_no, code='PBL_VERSION_NOT_FOUND')
|
||||
save_version(tid, blueprint_id, remark='auto-snapshot before rollback', env=env, operator=operator)
|
||||
|
||||
snapshot = rows[0].get('snapshot') or {}
|
||||
bp_snap = snapshot.get('blueprint') or {}
|
||||
patch = _pick(bp_snap, EDITABLE)
|
||||
patch['updated_by'] = operator
|
||||
patch['updated_at'] = _now()
|
||||
if patch:
|
||||
sor.U(TABLE, patch, _where(tid, "id='%s'" % blueprint_id))
|
||||
|
||||
for obj_type in SUBOBJECT_TYPES:
|
||||
sor.D(SUBOBJECT_TABLES[obj_type], _where(tid, "blueprint_id='%s'" % blueprint_id))
|
||||
for obj_type, obj_rows in (snapshot.get('children') or {}).items():
|
||||
if obj_type not in SUBOBJECT_TABLES:
|
||||
continue
|
||||
for row in obj_rows or []:
|
||||
new_row = dict(row)
|
||||
new_row['tenant_id'] = tid
|
||||
new_row['blueprint_id'] = blueprint_id
|
||||
new_row['deleted'] = 0
|
||||
new_row['updated_by'] = operator
|
||||
new_row['updated_at'] = _now()
|
||||
sor.C(SUBOBJECT_TABLES[obj_type], new_row)
|
||||
|
||||
sor.U(VERSION_TABLE, {'status': 'rolled_back', 'updated_by': operator, 'updated_at': _now()},
|
||||
_where(tid, "blueprint_id='%s' and version_no=%d" % (blueprint_id, int(version_no))))
|
||||
return {'blueprint_id': blueprint_id, 'rolled_back_to': int(version_no)}
|
||||
|
||||
@ -1,200 +1,153 @@
|
||||
"""pbl_blueprint 数据访问适配层(唯一允许直接调用 sqlor 的地方)。
|
||||
"""pbl_blueprint 数据访问层。
|
||||
|
||||
规范约束:
|
||||
- 只使用 sqlor 标准 API:sor.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 标准 API:sor.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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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", "已删除"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 蓝图子对象-产出物规格(M1a,7类子对象之一)。一行=一种期望产出物的规格定义(作品/报告/模型/演示),供 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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 蓝图子对象-证据规格(M1a,7类子对象之一)。一行=一条学习证据的采集规格,定义证据类型/采集方式/校验规则,供 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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 蓝图子对象-学习目标(M1a,7类子对象之一)。一行=蓝图的一条学习目标(知识/能力/素养/态度四维),可关联到任务并被评估量规引用。统一泛化契约。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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 蓝图子对象-关卡(M1a,7类子对象之一)。一行=任务下的一个游戏化关卡,是 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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 蓝图子对象-反思规格(M1a,7类子对象之一)。一行=一条反思/自评提示的规格定义(触发时机、反思类型、引导问题、量规维度),供运行时按节点触发学习者反思并回流评估。统一泛化契约。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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 蓝图子对象-角色(M1a,7类子对象之一)。一行=蓝图内定义的一种学习者角色(如队长/记录员/工程师),供分组与权限分配使用。统一泛化契约。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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 蓝图子对象-任务(M1a,7类子对象之一)。一行=蓝图下的一个学习任务,是关卡/角色/目标的挂载父级。采用统一泛化契约(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", "已删除"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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", "已删除"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 核心表 DDL(M1a)
|
||||
-- 模块:pbl_blueprint 库名:由 ServerEnv().get_module_dbname('pbl_blueprint') 解析,禁止硬编码
|
||||
-- 约定:主键 id 一律 varchar(32)(32位无横线UUID);tenant_id 强制打头且进入每个索引首列;
|
||||
-- 软删除 deleted tinyint;JSON 字段用 json 类型;时间用 datetime。
|
||||
-- 表清单(核心 4 张):pbl_blueprint / pbl_blueprint_version / pbl_blueprint_template / pbl_blueprint_template_item
|
||||
-- 子对象 7 张见 pbl_blueprint.subobjects.sql
|
||||
-- =====================================================================
|
||||
|
||||
-- pbl_blueprint: PBL蓝图聚合根
|
||||
CREATE TABLE IF NOT EXISTS pbl_blueprint (
|
||||
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蓝图模板条目表';
|
||||
|
||||
188
pbl_blueprint/sql/pbl_blueprint.subobjects.sql
Normal file
188
pbl_blueprint/sql/pbl_blueprint.subobjects.sql
Normal file
@ -0,0 +1,188 @@
|
||||
-- =====================================================================
|
||||
-- pbl_blueprint 子对象表 DDL(M1a,7 类子对象)
|
||||
-- 统一泛化契约:id / tenant_id / blueprint_id / (task_id|mission_id|learning_goal_id) / seq / code /
|
||||
-- name / description / spec(json) / status / 审计五件套 / deleted
|
||||
-- 所有索引首列 = tenant_id(租户强制打头);主键 varchar(32);软删除 tinyint;不使用 boolean/int(11)。
|
||||
-- =====================================================================
|
||||
|
||||
-- 1/7 pbl_blueprint_task 任务 -------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_task` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
|
||||
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
|
||||
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
|
||||
`parent_id` varchar(32) DEFAULT NULL COMMENT '父任务ID,支持分层',
|
||||
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
|
||||
`code` varchar(64) DEFAULT NULL COMMENT '任务编码,蓝图内唯一',
|
||||
`name` varchar(200) NOT NULL COMMENT '任务名称',
|
||||
`description` text COMMENT '任务描述/驱动性问题',
|
||||
`spec` json DEFAULT NULL COMMENT '任务规格',
|
||||
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
|
||||
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
|
||||
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_pbl_bptask_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
|
||||
KEY `idx_pbl_bptask_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
|
||||
KEY `idx_pbl_bptask_parent` (`tenant_id`, `parent_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-任务';
|
||||
|
||||
-- 2/7 pbl_blueprint_mission 关卡 ---------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_mission` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
|
||||
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
|
||||
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
|
||||
`task_id` varchar(32) NOT NULL COMMENT '所属任务ID',
|
||||
`seq` int NOT NULL DEFAULT 0 COMMENT '任务内排序号',
|
||||
`code` varchar(64) DEFAULT NULL COMMENT '关卡编码,蓝图内唯一',
|
||||
`name` varchar(200) NOT NULL COMMENT '关卡名称',
|
||||
`description` text COMMENT '关卡说明',
|
||||
`spec` json DEFAULT NULL COMMENT '关卡规格',
|
||||
`unlock_condition` json DEFAULT NULL COMMENT '解锁条件',
|
||||
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
|
||||
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
|
||||
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_pbl_bpms_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
|
||||
KEY `idx_pbl_bpms_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
|
||||
KEY `idx_pbl_bpms_task` (`tenant_id`, `task_id`, `seq`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-关卡';
|
||||
|
||||
-- 3/7 pbl_blueprint_role 角色 -------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_role` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
|
||||
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
|
||||
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
|
||||
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID,空=蓝图级角色',
|
||||
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
|
||||
`code` varchar(64) DEFAULT NULL COMMENT '角色编码,蓝图内唯一',
|
||||
`name` varchar(200) NOT NULL COMMENT '角色名称',
|
||||
`description` text COMMENT '角色职责说明',
|
||||
`spec` json DEFAULT NULL COMMENT '角色规格',
|
||||
`permissions` json DEFAULT NULL COMMENT '运行时操作权限集合',
|
||||
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
|
||||
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
|
||||
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_pbl_bprole_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
|
||||
KEY `idx_pbl_bprole_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
|
||||
KEY `idx_pbl_bprole_task` (`tenant_id`, `task_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-角色';
|
||||
|
||||
-- 4/7 pbl_blueprint_learning_goal 学习目标 ------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_learning_goal` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
|
||||
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
|
||||
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
|
||||
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID,空=蓝图级目标',
|
||||
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
|
||||
`code` varchar(64) DEFAULT NULL COMMENT '目标编码,蓝图内唯一',
|
||||
`name` varchar(200) NOT NULL COMMENT '目标名称',
|
||||
`dimension` varchar(32) NOT NULL DEFAULT 'knowledge' COMMENT 'knowledge/ability/literacy/attitude',
|
||||
`description` text COMMENT '目标描述',
|
||||
`spec` json DEFAULT NULL COMMENT '目标规格',
|
||||
`weight` float DEFAULT 1.0 COMMENT '评估权重',
|
||||
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
|
||||
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
|
||||
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_pbl_bplg_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
|
||||
KEY `idx_pbl_bplg_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
|
||||
KEY `idx_pbl_bplg_dim` (`tenant_id`, `blueprint_id`, `dimension`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-学习目标';
|
||||
|
||||
-- 5/7 pbl_blueprint_evidence_spec 证据规格 ------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_evidence_spec` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
|
||||
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
|
||||
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
|
||||
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID',
|
||||
`mission_id` varchar(32) DEFAULT NULL COMMENT '关联关卡ID',
|
||||
`learning_goal_id` varchar(32) DEFAULT NULL COMMENT '关联学习目标ID',
|
||||
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
|
||||
`code` varchar(64) DEFAULT NULL COMMENT '证据规格编码,蓝图内唯一',
|
||||
`name` varchar(200) NOT NULL COMMENT '证据规格名称',
|
||||
`evidence_type` varchar(32) NOT NULL DEFAULT 'auto' COMMENT 'auto/manual/artifact/interaction/assessment',
|
||||
`collect_mode` varchar(32) NOT NULL DEFAULT 'auto' COMMENT 'auto/trigger/manual/scheduled',
|
||||
`description` text COMMENT '证据说明',
|
||||
`spec` json DEFAULT NULL COMMENT '证据规格',
|
||||
`rule` json DEFAULT NULL COMMENT '校验规则',
|
||||
`weight` float DEFAULT 1.0 COMMENT '评分权重',
|
||||
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
|
||||
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
|
||||
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_pbl_bpev_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
|
||||
KEY `idx_pbl_bpev_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
|
||||
KEY `idx_pbl_bpev_goal` (`tenant_id`, `learning_goal_id`),
|
||||
KEY `idx_pbl_bpev_mission` (`tenant_id`, `mission_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-证据规格';
|
||||
|
||||
-- 6/7 pbl_blueprint_artifact_spec 产出物规格 ----------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_artifact_spec` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
|
||||
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
|
||||
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
|
||||
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID',
|
||||
`mission_id` varchar(32) DEFAULT NULL COMMENT '关联关卡ID',
|
||||
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
|
||||
`code` varchar(64) DEFAULT NULL COMMENT '产出物规格编码,蓝图内唯一',
|
||||
`name` varchar(200) NOT NULL COMMENT '产出物名称',
|
||||
`artifact_type` varchar(32) NOT NULL DEFAULT 'document' COMMENT 'document/model/scene/video/audio/image/code/presentation/dataset',
|
||||
`description` text COMMENT '产出物说明与要求',
|
||||
`spec` json DEFAULT NULL COMMENT '产出物规格',
|
||||
`accept_criteria` json DEFAULT NULL COMMENT '验收标准条目数组',
|
||||
`required` tinyint NOT NULL DEFAULT 1 COMMENT '是否必交 0否 1是',
|
||||
`weight` float DEFAULT 1.0 COMMENT '评分权重',
|
||||
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
|
||||
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
|
||||
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_pbl_bpart_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
|
||||
KEY `idx_pbl_bpart_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
|
||||
KEY `idx_pbl_bpart_task` (`tenant_id`, `task_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-产出物规格';
|
||||
|
||||
-- 7/7 pbl_blueprint_reflection_spec 反思规格 ----------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_reflection_spec` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键,32位无横线UUID',
|
||||
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID,强制打头',
|
||||
`blueprint_id` varchar(32) NOT NULL COMMENT '所属蓝图ID',
|
||||
`task_id` varchar(32) DEFAULT NULL COMMENT '关联任务ID',
|
||||
`mission_id` varchar(32) DEFAULT NULL COMMENT '关联关卡ID',
|
||||
`seq` int NOT NULL DEFAULT 0 COMMENT '蓝图内排序号',
|
||||
`code` varchar(64) DEFAULT NULL COMMENT '反思规格编码,蓝图内唯一',
|
||||
`name` varchar(200) NOT NULL COMMENT '反思规格名称',
|
||||
`trigger_point` varchar(32) NOT NULL DEFAULT 'mission_end' COMMENT 'task_start/task_end/mission_end/milestone/project_end/manual',
|
||||
`reflection_type` varchar(32) NOT NULL DEFAULT 'self' COMMENT 'self/peer/team/teacher',
|
||||
`description` text COMMENT '反思说明',
|
||||
`spec` json DEFAULT NULL COMMENT '反思规格(引导问题等)',
|
||||
`rubric_ref` json DEFAULT NULL COMMENT '关联量规维度引用',
|
||||
`weight` float DEFAULT 1.0 COMMENT '评分权重',
|
||||
`status` varchar(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
|
||||
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(32) DEFAULT NULL COMMENT '最后修改人',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '最后修改时间',
|
||||
`deleted` tinyint NOT NULL DEFAULT 0 COMMENT '软删除 0正常 1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_pbl_bpref_tenant_bp_code` (`tenant_id`, `blueprint_id`, `code`),
|
||||
KEY `idx_pbl_bpref_tenant_bp_seq` (`tenant_id`, `blueprint_id`, `seq`),
|
||||
KEY `idx_pbl_bpref_trigger` (`tenant_id`, `blueprint_id`, `trigger_point`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL蓝图子对象-反思规格';
|
||||
@ -1,213 +1,241 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""蓝图子对象契约接口(M1a 落表 + CRUD;M1b 扩展校验/编排)。
|
||||
"""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}
|
||||
|
||||
@ -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
|
||||
- 模板 CRUD(pbl_blueprint_template / pbl_blueprint_template_item)
|
||||
- instantiate_template:模板 -> 新蓝图(含 7 类子对象,ref_key/parent_ref 解析为真实 ID)
|
||||
- build_offline_fallback_blueprint:Agent/网络不可用时的最小可用蓝图兜底
|
||||
|
||||
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):
|
||||
"""把蓝图抽为模板:主表 + 每类子对象一条 item(payload 可覆盖模板字段)。"""
|
||||
tid = tenant_id or _db.current_tenant_id()
|
||||
payload = dict(payload or {})
|
||||
bp = _bp.get_blueprint(blueprint_id, tid)
|
||||
tpl_payload = {
|
||||
'template_name': payload.get('template_name') or ('%s 模板' % (bp.get('blueprint_name') or '')),
|
||||
'template_code': payload.get('template_code') or '',
|
||||
'template_type': payload.get('template_type', 'blueprint'),
|
||||
'domain_code': bp.get('domain_code', ''), 'grade_code': bp.get('grade_code', ''),
|
||||
'subject_code': bp.get('subject_code', ''),
|
||||
'complexity_level': bp.get('complexity_level', ''),
|
||||
'scope': payload.get('scope', 'tenant'),
|
||||
'source_blueprint_id': blueprint_id,
|
||||
'description': payload.get('description', ''),
|
||||
'class_id': payload.get('class_id') or bp.get('class_id', ''),
|
||||
}
|
||||
tpl = save_template(tpl_payload, tid)
|
||||
now = _db.now_str()
|
||||
operator = _db.current_operator()
|
||||
children = _s.list_all_subobjects(blueprint_id, tid)
|
||||
created = 0
|
||||
for stype in sorted(children):
|
||||
seq = 0
|
||||
for obj in children[stype]:
|
||||
payload_json = dict((k, v) for k, v in obj.items()
|
||||
if k not in ('id', 'tenant_id', 'blueprint_id', 'version_id',
|
||||
'created_by', 'created_at', 'updated_by',
|
||||
'updated_at', 'is_deleted'))
|
||||
_db.insert_row(TABLE_ITEM, {
|
||||
'id': _db.gen_id(), 'tenant_id': tid,
|
||||
'class_id': obj.get('class_id') or '', 'template_id': tpl['id'],
|
||||
'subobject_type': stype, 'code': obj.get('code') or '',
|
||||
'name': obj.get('name') or '', 'description': obj.get('description') or '',
|
||||
'item_payload_json': _db.dumps(payload_json), 'seq_no': seq,
|
||||
'required_flag': int(obj.get('required_flag') or 1),
|
||||
'status': 'active', 'is_deleted': 0,
|
||||
'created_by': operator, 'created_at': now,
|
||||
'updated_by': operator, 'updated_at': now,
|
||||
})
|
||||
seq += 1
|
||||
created += 1
|
||||
_db.audit('template_from_blueprint', TABLE, tpl['id'],
|
||||
after={'source_blueprint_id': blueprint_id, 'items': created}, tenant_id=tid)
|
||||
tpl = get_template(tpl['id'], tid)
|
||||
tpl['item_created'] = created
|
||||
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
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
161
skill/SKILL.md
161
skill/SKILL.md
@ -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` 是 PBL(Project-Based Learning)平台的**蓝图域聚合根模块**,负责:
|
||||
- 蓝图主表 `pbl_blueprint` 的 CRUD / 列表 / 树 / fork
|
||||
- 7 类子对象的**泛化契约**(一套 CRUD 覆盖 7 张表)
|
||||
- 版本快照 `pbl_blueprint_version`(含 `change_delta` 增量、diff、rollback)
|
||||
- 模板 `pbl_blueprint_template` / `pbl_blueprint_template_item`(实例化 + 离线兜底)
|
||||
|
||||
## 数据表(10 张)
|
||||
- `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` tinyint(0 正常 / 1 已删除)
|
||||
- 结构化字段用 `json` 类型(`spec` / `config` / `snapshot` / `change_delta` / `rule` / `permissions`)
|
||||
- 审计五件套 `created_by/created_at/updated_by/updated_at/deleted`
|
||||
|
||||
## 4. 子对象泛化契约(重点)
|
||||
7 类子对象共用一套函数,`obj_type` 分派到表:
|
||||
|
||||
| obj_type | 表 | 编码前缀 | 特有字段 |
|
||||
|---|---|---|---|
|
||||
| task | pbl_blueprint_task | TASK | parent_id |
|
||||
| mission | pbl_blueprint_mission | MISSION | task_id(必填), unlock_condition |
|
||||
| role | pbl_blueprint_role | ROLE | task_id, permissions |
|
||||
| learning_goal | pbl_blueprint_learning_goal | GOAL | task_id, dimension, weight |
|
||||
| evidence_spec | pbl_blueprint_evidence_spec | EVI | task_id, mission_id, learning_goal_id, evidence_type, collect_mode, rule, weight |
|
||||
| artifact_spec | pbl_blueprint_artifact_spec | ART | task_id, mission_id, artifact_type, accept_criteria, required, weight |
|
||||
| reflection_spec | pbl_blueprint_reflection_spec | REF | task_id, mission_id, trigger_point, reflection_type, rubric_ref, weight |
|
||||
|
||||
```python
|
||||
from pbl_blueprint import load_pbl_blueprint
|
||||
contract = load_pbl_blueprint(env)
|
||||
contract['create_subobject'](tenant_id, 'mission', {'blueprint_id': bp_id, 'task_id': t_id, 'name': '关卡1'}, env=env)
|
||||
contract['list_subobjects'](tenant_id, 'task', bp_id, env=env)
|
||||
contract['batch_upsert_subobjects'](tenant_id, 'task', bp_id, rows, env=env) # Designer Agent 整树写入
|
||||
```
|
||||
|
||||
## 5. 蓝图契约
|
||||
```python
|
||||
create_blueprint(tenant_id, data, env, operator) # 自动生成 code PBL-{yyyyMMdd}-{seq4}
|
||||
update_blueprint(tenant_id, blueprint_id, data, ...) # 状态流转校验 STATUS_FLOW
|
||||
delete_blueprint(tenant_id, blueprint_id, ...) # 软删(连带子对象+版本)
|
||||
get_blueprint(tenant_id, blueprint_id, with_children) # with_children=True 附带 7 类子树
|
||||
list_blueprints(tenant_id, filters, page, page_size) # 分页 + keyword/status/subject 过滤
|
||||
get_blueprint_tree(tenant_id, blueprint_id) # {obj_type: [rows]}
|
||||
fork_blueprint(tenant_id, blueprint_id, new_name, target_tenant_id) # 跨租户复制,ID 重映射
|
||||
```
|
||||
状态机:`draft → validating → validated → compiling → compiled → published → archived`(非法流转抛 `PblValidationError`)。
|
||||
|
||||
## 6. 版本契约
|
||||
```python
|
||||
save_version(tenant_id, blueprint_id, remark) # 存全量 snapshot + 计算 change_delta,主表 version_no 自增
|
||||
list_versions(tenant_id, blueprint_id, page, page_size)
|
||||
diff_versions(tenant_id, blueprint_id, version_a, version_b) # {added, updated, removed}
|
||||
rollback_version(tenant_id, blueprint_id, version_no) # 先自动存当前版本再回滚
|
||||
```
|
||||
`change_delta` 结构:`{"added":[{"obj_type","id","name"}], "updated":[{"obj_type","id","fields"}], "removed":[...]}`
|
||||
|
||||
## 7. 模板与离线兜底
|
||||
```python
|
||||
create_template(tenant_id, {'name':..., 'items':[{'obj_type':'task','ref_key':'T1','name':...}, ...]})
|
||||
instantiate_template(tenant_id, template_id, name, overrides) # ref_key/parent_ref 解析为真实 ID,use_count 自增
|
||||
build_offline_fallback_blueprint(tenant_id, name) # Agent/网络不可用时的最小可用蓝图(9 条子对象)
|
||||
```
|
||||
模板条目用 `ref_key` / `parent_ref` 做**模板内相对引用**,实例化时按 `SUBOBJECT_TYPES` 顺序解析为真实 ID(父先于子)。
|
||||
|
||||
## 8. tenant_id 强制打头(铁律)
|
||||
- 每个契约函数第一入参 `tenant_id`,内部 `db.require_tenant()` 校验
|
||||
- 缺失/空/非字符串 → 抛 `PblTenantRequired`(code=`PBL_TENANT_REQUIRED`),**fail-closed 不放行**
|
||||
- 所有 where 条件由 `_where(tenant_id, extra)` 构造,`tenant_id=` 永远在最前
|
||||
- API 层 `_tid(params)` 从入参或 `context.tenant_id` 取,取不到直接失败
|
||||
- 平台内置模板 `tenant_id='platform'`,`list_templates(include_platform=True)` 才并入
|
||||
|
||||
## 9. 异常码
|
||||
| code | 含义 |
|
||||
|---|---|
|
||||
| PBL_TENANT_REQUIRED | 租户上下文缺失 |
|
||||
| PBL_VALIDATION_ERROR | 入参校验失败 |
|
||||
| PBL_NOT_FOUND / PBL_BLUEPRINT_NOT_FOUND / PBL_SUBOBJECT_NOT_FOUND / PBL_VERSION_NOT_FOUND / PBL_TEMPLATE_NOT_FOUND | 记录不存在 |
|
||||
| PBL_UNKNOWN_OBJ_TYPE | 未知子对象类型 |
|
||||
| PBL_DB_UNAVAILABLE | sqlor 不可用 |
|
||||
|
||||
## 10. 数据访问约束
|
||||
- 库名:`ServerEnv().get_module_dbname('pbl_blueprint')`,**禁止硬编码 DBNAME**
|
||||
- SQL 只用 sqlor 标准 API:`sor.C / sor.U / sor.D / sor.R / sor.sqlExe`,禁编造 save/list/insert
|
||||
- 建表:`db.ensure_tables()` 幂等执行 `sql/*.sql`(`CREATE TABLE IF NOT EXISTS`)
|
||||
|
||||
## 11. API 路径(25 个,见 api.py API_PATHS)
|
||||
前缀 `/pbl_blueprint/`,分四组:`blueprint/*`(7)、`version/*`(4)、`subobject/*`(7)、`template/*`(7)。
|
||||
RBAC 注册:`python -m pbl_blueprint.scripts.load_path`(幂等,注册 API + 页面 + 表权限共 39 条路径)。
|
||||
|
||||
## 12. 下游模块依赖
|
||||
- `pbl_validation`(M2)读 `get_blueprint_tree` 做 14 维校验,回写 `quality_level`
|
||||
- `pbl_compiler`(M3)读蓝图树生成 Game Definition
|
||||
- `pbl_evidence`(M5)按 `evidence_spec` / `artifact_spec` 采集
|
||||
- `pbl_assessment`(M6)按 `learning_goal.weight` / `reflection_spec.rubric_ref` 评分
|
||||
- `pbl_agent_runtime`(M4)用 `batch_upsert_subobjects` 写 Designer 产出,fail-closed 裁决
|
||||
|
||||
12
wwwroot/api/pbl_blueprint_create.dspy
Normal file
12
wwwroot/api/pbl_blueprint_create.dspy
Normal 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)}
|
||||
12
wwwroot/api/pbl_blueprint_delete.dspy
Normal file
12
wwwroot/api/pbl_blueprint_delete.dspy
Normal file
@ -0,0 +1,12 @@
|
||||
# pbl_blueprint_delete.dspy —— 删除蓝图(默认软删 status=deleted;hard=1 物理级联删)
|
||||
debug('pbl_blueprint_delete.dspy: START params_kw=%s' % dict(params_kw))
|
||||
try:
|
||||
tenant_id = params_kw.get('tenant_id') or None
|
||||
res = await pbl_blueprint_delete(dict(params_kw), request=request, tenant_id=tenant_id)
|
||||
return res
|
||||
except PblBlueprintError as e:
|
||||
debug('pbl_blueprint_delete.dspy: BIZ_ERR %s' % e.to_dict())
|
||||
return e.to_dict()
|
||||
except Exception as e:
|
||||
exception('pbl_blueprint_delete.dspy: %s' % format_exc())
|
||||
return {'status': 'error', 'code': 'PBL_INTERNAL', 'message': str(e)}
|
||||
12
wwwroot/api/pbl_blueprint_fork.dspy
Normal file
12
wwwroot/api/pbl_blueprint_fork.dspy
Normal 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)}
|
||||
13
wwwroot/api/pbl_blueprint_list.dspy
Normal file
13
wwwroot/api/pbl_blueprint_list.dspy
Normal 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': []}
|
||||
12
wwwroot/api/pbl_blueprint_read.dspy
Normal file
12
wwwroot/api/pbl_blueprint_read.dspy
Normal 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)}
|
||||
12
wwwroot/api/pbl_blueprint_subobject_delete.dspy
Normal file
12
wwwroot/api/pbl_blueprint_subobject_delete.dspy
Normal 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)}
|
||||
13
wwwroot/api/pbl_blueprint_subobject_list.dspy
Normal file
13
wwwroot/api/pbl_blueprint_subobject_list.dspy
Normal 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': []}
|
||||
12
wwwroot/api/pbl_blueprint_subobject_save.dspy
Normal file
12
wwwroot/api/pbl_blueprint_subobject_save.dspy
Normal 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)}
|
||||
12
wwwroot/api/pbl_blueprint_tree.dspy
Normal file
12
wwwroot/api/pbl_blueprint_tree.dspy
Normal 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)}
|
||||
12
wwwroot/api/pbl_blueprint_update.dspy
Normal file
12
wwwroot/api/pbl_blueprint_update.dspy
Normal 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)}
|
||||
Loading…
x
Reference in New Issue
Block a user