M1a: pbl_blueprint 蓝图核心表与CRUD契约(本地开发快照,接入远程仓库)

This commit is contained in:
pipeline 2026-09-15 17:18:54 +08:00
parent 4d5dc65538
commit a51815075f
95 changed files with 8458 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/
*.egg-info/
.venv/

31
json/pbl_blueprint.json Normal file
View File

@ -0,0 +1,31 @@
{
"tblname": "pbl_blueprint",
"params": {
"logined_userorgid": "org_id",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_create.dspy')}}",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_update.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_delete.dspy')}}",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_create.dspy')}}",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_update.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_delete.dspy')}}"
},
"browserfields": {
"main": {
"fields": ["blueprint_code", "title", "subject_area", "grade_level", "duration_hours", "quality_status", "version_no", "status", "created_at"],
"title": "蓝图列表"
}
},
"editexclouded": ["id", "tenant_id", "org_id", "created_by", "created_at", "updated_by", "updated_at", "quality_status", "validation_score", "version_no", "current_version_id"],
"subtables": [
{"name": "learning_goal", "label": "学习目标", "url": "{{entire_url('../pbl_blueprint_learning_goal')}}"},
{"name": "role", "label": "PBL角色", "url": "{{entire_url('../pbl_blueprint_role')}}"},
{"name": "mission", "label": "驱动问题", "url": "{{entire_url('../pbl_blueprint_mission')}}"},
{"name": "task", "label": "任务", "url": "{{entire_url('../pbl_blueprint_task')}}"},
{"name": "artifact_spec", "label": "产出物规格", "url": "{{entire_url('../pbl_blueprint_artifact_spec')}}"},
{"name": "evidence_spec", "label": "证据规格", "url": "{{entire_url('../pbl_blueprint_evidence_spec')}}"},
{"name": "reflection_spec", "label": "反思规格", "url": "{{entire_url('../pbl_blueprint_reflection_spec')}}"},
{"name": "version", "label": "版本历史", "url": "{{entire_url('../pbl_blueprint_version')}}"}
]
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_blueprint_artifact_spec",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=artifact_spec",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=artifact_spec",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=artifact_spec",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=artifact_spec",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=artifact_spec",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=artifact_spec"
},
"browserfields": {
"main": {
"fields": ["seq", "artifact_code", "name", "artifact_type", "format", "required"],
"title": "产出物规格"
}
},
"editexclouded": ["id", "tenant_id", "created_by", "created_at", "updated_by", "updated_at"]
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_blueprint_evidence_spec",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=evidence_spec",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=evidence_spec",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=evidence_spec",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=evidence_spec",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=evidence_spec",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=evidence_spec"
},
"browserfields": {
"main": {
"fields": ["seq", "evidence_code", "name", "evidence_type", "collect_mode", "required"],
"title": "证据规格"
}
},
"editexclouded": ["id", "tenant_id", "created_by", "created_at", "updated_by", "updated_at"]
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_blueprint_learning_goal",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=learning_goal",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=learning_goal",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=learning_goal",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=learning_goal",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=learning_goal",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=learning_goal"
},
"browserfields": {
"main": {
"fields": ["seq", "goal_code", "title", "knowledge_type", "assessable", "created_at"],
"title": "学习目标"
}
},
"editexclouded": ["id", "tenant_id", "created_by", "created_at", "updated_by", "updated_at"]
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_blueprint_mission",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=mission",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=mission",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=mission",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=mission",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=mission",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=mission"
},
"browserfields": {
"main": {
"fields": ["seq", "mission_code", "title", "duration_hours", "created_at"],
"title": "驱动问题/Mission"
}
},
"editexclouded": ["id", "tenant_id", "created_by", "created_at", "updated_by", "updated_at"]
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_blueprint_reflection_spec",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=reflection_spec",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=reflection_spec",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=reflection_spec",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=reflection_spec",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=reflection_spec",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=reflection_spec"
},
"browserfields": {
"main": {
"fields": ["seq", "reflection_code", "title", "reflection_type", "trigger_point", "required"],
"title": "反思规格"
}
},
"editexclouded": ["id", "tenant_id", "created_by", "created_at", "updated_by", "updated_at"]
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_blueprint_role",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=role",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=role",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=role",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=role",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=role",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=role"
},
"browserfields": {
"main": {
"fields": ["seq", "role_code", "role_name", "min_members", "max_members", "is_mandatory"],
"title": "PBL角色"
}
},
"editexclouded": ["id", "tenant_id", "created_by", "created_at", "updated_by", "updated_at"]
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_blueprint_task",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=task",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=task",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=task",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=task",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_save.dspy')}}?subobject=task",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_subobject_delete.dspy')}}?subobject=task"
},
"browserfields": {
"main": {
"fields": ["seq", "task_code", "title", "task_type", "estimated_minutes", "status"],
"title": "任务"
}
},
"editexclouded": ["id", "tenant_id", "created_by", "created_at", "updated_by", "updated_at"]
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_blueprint_version",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_version_create.dspy')}}",
"update_data_url": "",
"delete_data_url": "",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_blueprint_version_create.dspy')}}",
"update_data_url": "",
"delete_data_url": ""
},
"browserfields": {
"main": {
"fields": ["version_no", "source", "change_reason", "quality_status", "created_by", "created_at"],
"title": "版本历史"
}
},
"editexclouded": ["id", "tenant_id", "blueprint_id", "parent_version_id", "change_delta", "snapshot_json", "created_by", "created_at"]
}
}

21
json/pbl_template.json Normal file
View File

@ -0,0 +1,21 @@
{
"tblname": "pbl_template",
"params": {
"logined_userorgid": "",
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_template_save.dspy')}}",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_template_save.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_template_delete.dspy')}}",
"editable": {
"new_data_url": "{{entire_url('/pbl_blueprint/api/pbl_template_save.dspy')}}",
"update_data_url": "{{entire_url('/pbl_blueprint/api/pbl_template_save.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_blueprint/api/pbl_template_delete.dspy')}}"
},
"browserfields": {
"main": {
"fields": ["template_code", "name", "category", "subject_area", "grade_level", "template_version", "is_builtin", "status"],
"title": "PBL模板"
}
},
"editexclouded": ["id", "tenant_id", "created_by", "created_at", "updated_by", "updated_at"]
}
}

50
models/pbl_blueprint.json Normal file
View File

@ -0,0 +1,50 @@
{
"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"}
]
}

View File

@ -0,0 +1,44 @@
{
"summary": [
{
"table": "pbl_blueprint_artifact_spec",
"name": "蓝图产出物规格",
"title": "产出物规格",
"subtitle": "蓝图子对象-产出物规格",
"primary": ["id"],
"defaultsort": "seq asc",
"orderby": "seq asc",
"caption": "产出物规格"
}
],
"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"},
{"name": "blueprint_id", "type": "str(32)", "title": "所属蓝图", "notnull": 1},
{"name": "task_id", "type": "str(32)", "title": "所属任务", "default": ""},
{"name": "seq", "type": "int", "title": "序号", "notnull": 1, "default": 0},
{"name": "artifact_code", "type": "str(64)", "title": "产出物编码", "notnull": 1},
{"name": "name", "type": "str(255)", "title": "产出物名称", "notnull": 1},
{"name": "artifact_type", "type": "str(32)", "title": "产出物类型", "default": "document", "comment": "document/model/code/image/video/audio/data/other"},
{"name": "format", "type": "str(64)", "title": "文件格式", "default": ""},
{"name": "description", "type": "text", "title": "规格说明", "default": ""},
{"name": "required", "type": "str(1)", "title": "是否必需", "default": "1"},
{"name": "max_size_mb", "type": "int", "title": "大小上限(MB)", "default": 0},
{"name": "schema_json", "type": "longtext", "title": "结构约束JSON Schema", "default": ""},
{"name": "rubric_id", "type": "str(32)", "title": "评分量规ID", "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_artifact_code", "fields": ["tenant_id", "blueprint_id", "artifact_code"], "unique": 1},
{"name": "idx_pbl_artifact_bp", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": 0},
{"name": "idx_pbl_artifact_task", "fields": ["tenant_id", "task_id"], "unique": 0}
],
"codes": [
{"field": "artifact_type", "table": "appcodes_kv", "cond": "parentid='pbl_artifact_type'", "valuefield": "k", "textfield": "v"},
{"field": "blueprint_id", "table": "pbl_blueprint", "cond": "", "valuefield": "id", "textfield": "title"},
{"field": "task_id", "table": "pbl_blueprint_task", "cond": "", "valuefield": "id", "textfield": "title"}
]
}

View File

@ -0,0 +1,45 @@
{
"summary": [
{
"table": "pbl_blueprint_evidence_spec",
"name": "蓝图证据规格",
"title": "证据规格",
"subtitle": "蓝图子对象-证据规格",
"primary": ["id"],
"defaultsort": "seq asc",
"orderby": "seq asc",
"caption": "证据规格"
}
],
"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"},
{"name": "blueprint_id", "type": "str(32)", "title": "所属蓝图", "notnull": 1},
{"name": "artifact_spec_id", "type": "str(32)", "title": "关联产出物规格", "default": ""},
{"name": "task_id", "type": "str(32)", "title": "关联任务", "default": ""},
{"name": "seq", "type": "int", "title": "序号", "notnull": 1, "default": 0},
{"name": "evidence_code", "type": "str(64)", "title": "证据编码", "notnull": 1},
{"name": "name", "type": "str(255)", "title": "证据名称", "notnull": 1},
{"name": "evidence_type", "type": "str(32)", "title": "证据类型", "default": "result", "comment": "process/result/behavior/log"},
{"name": "collect_mode", "type": "str(32)", "title": "采集方式", "default": "auto", "comment": "auto/manual/hybrid"},
{"name": "source_hint", "type": "str(255)", "title": "采集来源提示", "default": ""},
{"name": "idempotency_key_hint", "type": "str(128)", "title": "幂等键规则", "default": "", "comment": "供 pbl_evidence 幂等采集使用"},
{"name": "required", "type": "str(1)", "title": "是否必需", "default": "1"},
{"name": "description", "type": "text", "title": "说明", "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_evidence_code", "fields": ["tenant_id", "blueprint_id", "evidence_code"], "unique": 1},
{"name": "idx_pbl_evidence_bp", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": 0},
{"name": "idx_pbl_evidence_artifact", "fields": ["tenant_id", "artifact_spec_id"], "unique": 0}
],
"codes": [
{"field": "evidence_type", "table": "appcodes_kv", "cond": "parentid='pbl_evidence_type'", "valuefield": "k", "textfield": "v"},
{"field": "collect_mode", "table": "appcodes_kv", "cond": "parentid='pbl_collect_mode'", "valuefield": "k", "textfield": "v"},
{"field": "blueprint_id", "table": "pbl_blueprint", "cond": "", "valuefield": "id", "textfield": "title"},
{"field": "artifact_spec_id", "table": "pbl_blueprint_artifact_spec", "cond": "", "valuefield": "id", "textfield": "name"}
]
}

View File

@ -0,0 +1,39 @@
{
"summary": [
{
"table": "pbl_blueprint_learning_goal",
"name": "蓝图学习目标",
"title": "学习目标",
"subtitle": "蓝图子对象-学习目标",
"primary": ["id"],
"defaultsort": "seq asc",
"orderby": "seq asc",
"caption": "学习目标"
}
],
"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"},
{"name": "blueprint_id", "type": "str(32)", "title": "所属蓝图", "notnull": 1},
{"name": "seq", "type": "int", "title": "序号", "notnull": 1, "default": 0},
{"name": "goal_code", "type": "str(64)", "title": "目标编码", "notnull": 1},
{"name": "title", "type": "str(255)", "title": "目标标题", "notnull": 1},
{"name": "description", "type": "text", "title": "目标描述", "default": ""},
{"name": "knowledge_type", "type": "str(32)", "title": "知识类型", "default": "concept", "comment": "fact/concept/procedure/metacognition"},
{"name": "competency_tags", "type": "text", "title": "素养标签JSON数组", "default": "[]"},
{"name": "assessable", "type": "str(1)", "title": "可评估", "default": "1"},
{"name": "alignment_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_goal_code", "fields": ["tenant_id", "blueprint_id", "goal_code"], "unique": 1},
{"name": "idx_pbl_goal_bp", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": 0}
],
"codes": [
{"field": "knowledge_type", "table": "appcodes_kv", "cond": "parentid='pbl_knowledge_type'", "valuefield": "k", "textfield": "v"},
{"field": "blueprint_id", "table": "pbl_blueprint", "cond": "", "valuefield": "id", "textfield": "title"}
]
}

View File

@ -0,0 +1,38 @@
{
"summary": [
{
"table": "pbl_blueprint_mission",
"name": "蓝图Mission",
"title": "驱动问题/Mission",
"subtitle": "蓝图子对象-驱动问题",
"primary": ["id"],
"defaultsort": "seq asc",
"orderby": "seq asc",
"caption": "Mission"
}
],
"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"},
{"name": "blueprint_id", "type": "str(32)", "title": "所属蓝图", "notnull": 1},
{"name": "seq", "type": "int", "title": "序号", "notnull": 1, "default": 0},
{"name": "mission_code", "type": "str(64)", "title": "Mission编码", "notnull": 1},
{"name": "title", "type": "str(255)", "title": "Mission标题", "notnull": 1},
{"name": "driving_question", "type": "text", "title": "驱动问题", "default": ""},
{"name": "narrative", "type": "longtext", "title": "情境叙事", "default": ""},
{"name": "success_criteria", "type": "text", "title": "成功标准JSON数组", "default": "[]"},
{"name": "related_goal_ids", "type": "text", "title": "关联学习目标ID JSON数组", "default": "[]"},
{"name": "duration_hours", "type": "int", "title": "预计课时", "default": 0},
{"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_mission_code", "fields": ["tenant_id", "blueprint_id", "mission_code"], "unique": 1},
{"name": "idx_pbl_mission_bp", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": 0}
],
"codes": [
{"field": "blueprint_id", "table": "pbl_blueprint", "cond": "", "valuefield": "id", "textfield": "title"}
]
}

View File

@ -0,0 +1,39 @@
{
"summary": [
{
"table": "pbl_blueprint_reflection_spec",
"name": "蓝图反思规格",
"title": "反思规格",
"subtitle": "蓝图子对象-反思规格",
"primary": ["id"],
"defaultsort": "seq asc",
"orderby": "seq asc",
"caption": "反思规格"
}
],
"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"},
{"name": "blueprint_id", "type": "str(32)", "title": "所属蓝图", "notnull": 1},
{"name": "seq", "type": "int", "title": "序号", "notnull": 1, "default": 0},
{"name": "reflection_code", "type": "str(64)", "title": "反思编码", "notnull": 1},
{"name": "title", "type": "str(255)", "title": "反思标题", "notnull": 1},
{"name": "prompt", "type": "text", "title": "反思提示语", "default": ""},
{"name": "reflection_type", "type": "str(32)", "title": "反思类型", "default": "self", "comment": "self/peer/team/teacher"},
{"name": "trigger_point", "type": "str(64)", "title": "触发时点", "default": "task_end", "comment": "task_end/mission_end/project_end/manual"},
{"name": "required", "type": "str(1)", "title": "是否必需", "default": "1"},
{"name": "rubric_hint", "type": "text", "title": "量规提示", "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_reflection_code", "fields": ["tenant_id", "blueprint_id", "reflection_code"], "unique": 1},
{"name": "idx_pbl_reflection_bp", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": 0}
],
"codes": [
{"field": "reflection_type", "table": "appcodes_kv", "cond": "parentid='pbl_reflect_type'", "valuefield": "k", "textfield": "v"},
{"field": "blueprint_id", "table": "pbl_blueprint", "cond": "", "valuefield": "id", "textfield": "title"}
]
}

View File

@ -0,0 +1,39 @@
{
"summary": [
{
"table": "pbl_blueprint_role",
"name": "蓝图角色",
"title": "PBL角色",
"subtitle": "蓝图子对象-角色",
"primary": ["id"],
"defaultsort": "seq asc",
"orderby": "seq asc",
"caption": "角色"
}
],
"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"},
{"name": "blueprint_id", "type": "str(32)", "title": "所属蓝图", "notnull": 1},
{"name": "seq", "type": "int", "title": "序号", "notnull": 1, "default": 0},
{"name": "role_code", "type": "str(64)", "title": "角色编码", "notnull": 1},
{"name": "role_name", "type": "str(128)", "title": "角色名称", "notnull": 1},
{"name": "description", "type": "text", "title": "角色说明", "default": ""},
{"name": "responsibilities", "type": "text", "title": "职责JSON数组", "default": "[]"},
{"name": "required_skills", "type": "text", "title": "所需技能JSON数组", "default": "[]"},
{"name": "min_members", "type": "int", "title": "最少人数", "default": 1},
{"name": "max_members", "type": "int", "title": "最多人数", "default": 1},
{"name": "is_mandatory", "type": "str(1)", "title": "是否必需角色", "default": "0"},
{"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_role_code", "fields": ["tenant_id", "blueprint_id", "role_code"], "unique": 1},
{"name": "idx_pbl_role_bp", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": 0}
],
"codes": [
{"field": "blueprint_id", "table": "pbl_blueprint", "cond": "", "valuefield": "id", "textfield": "title"}
]
}

View File

@ -0,0 +1,45 @@
{
"summary": [
{
"table": "pbl_blueprint_task",
"name": "蓝图任务",
"title": "任务",
"subtitle": "蓝图子对象-任务",
"primary": ["id"],
"defaultsort": "seq asc",
"orderby": "seq asc",
"caption": "任务"
}
],
"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"},
{"name": "blueprint_id", "type": "str(32)", "title": "所属蓝图", "notnull": 1},
{"name": "mission_id", "type": "str(32)", "title": "所属Mission", "default": ""},
{"name": "seq", "type": "int", "title": "序号", "notnull": 1, "default": 0},
{"name": "task_code", "type": "str(64)", "title": "任务编码", "notnull": 1},
{"name": "title", "type": "str(255)", "title": "任务标题", "notnull": 1},
{"name": "description", "type": "text", "title": "任务说明", "default": ""},
{"name": "task_type", "type": "str(32)", "title": "任务类型", "default": "explore", "comment": "explore/build/test/present/reflect"},
{"name": "role_id", "type": "str(32)", "title": "承担角色ID", "default": ""},
{"name": "depends_on_ids", "type": "text", "title": "前置任务ID JSON数组", "default": "[]"},
{"name": "estimated_minutes", "type": "int", "title": "预计耗时(分钟)", "default": 0},
{"name": "artifact_spec_ids", "type": "text", "title": "产出物规格ID JSON数组", "default": "[]"},
{"name": "status", "type": "str(32)", "title": "状态", "default": "draft"},
{"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_task_code", "fields": ["tenant_id", "blueprint_id", "task_code"], "unique": 1},
{"name": "idx_pbl_task_bp", "fields": ["tenant_id", "blueprint_id", "seq"], "unique": 0},
{"name": "idx_pbl_task_mission", "fields": ["tenant_id", "mission_id"], "unique": 0}
],
"codes": [
{"field": "task_type", "table": "appcodes_kv", "cond": "parentid='pbl_task_type'", "valuefield": "k", "textfield": "v"},
{"field": "blueprint_id", "table": "pbl_blueprint", "cond": "", "valuefield": "id", "textfield": "title"},
{"field": "mission_id", "table": "pbl_blueprint_mission", "cond": "", "valuefield": "id", "textfield": "title"},
{"field": "role_id", "table": "pbl_blueprint_role", "cond": "", "valuefield": "id", "textfield": "role_name"}
]
}

View File

@ -0,0 +1,36 @@
{
"summary": [
{
"table": "pbl_blueprint_version",
"name": "蓝图版本",
"title": "蓝图版本",
"subtitle": "版本快照与变更增量",
"primary": ["id"],
"defaultsort": "version_no desc",
"orderby": "version_no desc",
"caption": "版本"
}
],
"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"},
{"name": "blueprint_id", "type": "str(32)", "title": "所属蓝图", "notnull": 1},
{"name": "version_no", "type": "int", "title": "版本号", "notnull": 1, "default": 1},
{"name": "parent_version_id", "type": "str(32)", "title": "父版本ID", "default": ""},
{"name": "change_delta", "type": "longtext", "title": "变更增量JSON", "default": "", "comment": "对话式改模型的证据:{op,path,before,after} 列表"},
{"name": "snapshot_json", "type": "longtext", "title": "全量快照JSON", "default": ""},
{"name": "change_reason", "type": "str(500)", "title": "变更原因", "default": ""},
{"name": "source", "type": "str(32)", "title": "变更来源", "default": "manual", "comment": "manual/dialogue/template/fork/validation"},
{"name": "quality_status", "type": "str(32)", "title": "该版本质量状态", "default": "draft"},
{"name": "created_by", "type": "str(32)", "title": "创建人", "default": ""},
{"name": "created_at", "type": "datetime", "title": "创建时间", "notnull": 1}
],
"indexes": [
{"name": "uk_pbl_version_no", "fields": ["tenant_id", "blueprint_id", "version_no"], "unique": 1},
{"name": "idx_pbl_version_bp", "fields": ["tenant_id", "blueprint_id", "created_at"], "unique": 0}
],
"codes": [
{"field": "source", "table": "appcodes_kv", "cond": "parentid='pbl_version_source'", "valuefield": "k", "textfield": "v"},
{"field": "blueprint_id", "table": "pbl_blueprint", "cond": "", "valuefield": "id", "textfield": "title"}
]
}

40
models/pbl_template.json Normal file
View File

@ -0,0 +1,40 @@
{
"summary": [
{
"table": "pbl_template",
"name": "PBL模板",
"title": "PBL模板",
"subtitle": "蓝图模板(含离线兜底槽位)",
"primary": ["id"],
"defaultsort": "created_at desc",
"orderby": "created_at desc",
"caption": "模板"
}
],
"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": "0=平台内置全局模板,所有租户可见"},
{"name": "template_code", "type": "str(64)", "title": "模板编码", "notnull": 1},
{"name": "name", "type": "str(255)", "title": "模板名称", "notnull": 1},
{"name": "description", "type": "text", "title": "模板说明", "default": ""},
{"name": "category", "type": "str(64)", "title": "模板分类", "default": "general"},
{"name": "subject_area", "type": "str(64)", "title": "学科领域", "default": ""},
{"name": "grade_level", "type": "str(32)", "title": "适用年级", "default": ""},
{"name": "blueprint_snapshot", "type": "longtext", "title": "蓝图骨架JSON", "default": "", "comment": "实例化时展开为蓝图+子对象"},
{"name": "offline_slots", "type": "longtext", "title": "离线兜底槽位JSON", "default": "[]", "comment": "LLM 不可用时的降级填充槽位定义"},
{"name": "is_builtin", "type": "str(1)", "title": "是否内置", "default": "0"},
{"name": "template_version", "type": "str(32)", "title": "模板版本", "default": "1.0.0"},
{"name": "status", "type": "str(32)", "title": "状态", "default": "active", "comment": "active/disabled"},
{"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_template_code", "fields": ["tenant_id", "template_code"], "unique": 1},
{"name": "idx_pbl_template_cat", "fields": ["tenant_id", "category", "status"], "unique": 0}
],
"codes": [
{"field": "category", "table": "appcodes_kv", "cond": "parentid='pbl_template_cat'", "valuefield": "k", "textfield": "v"}
]
}

64
pbl_blueprint/__init__.py Normal file
View File

@ -0,0 +1,64 @@
"""pbl_blueprint —— PBL 蓝图聚合根与子对象、版本、模板M1a
所有 async 契约函数必须在此导出否则 .dspy 调用会 NameError
新增契约需同步四处api.py 定义 + 本文件导出 + init.py env 注册
+ scripts/load_path.py RBAC 路径
"""
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,
)
__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)

1165
pbl_blueprint/api.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,346 @@
# -*- coding: utf-8 -*-
"""蓝图聚合根 CRUD 与查询契约接口M1a
契约函数跨宿主/下游模块复用面签名稳定
- save_blueprint(payload, tenant_id=None) -> dict 新建/更新蓝图含自动编码乐观锁
- get_blueprint(blueprint_id, tenant_id=None) -> dict 蓝图主记录
- list_blueprints(filters=None, page=1, rows=20, tenant_id=None) -> dict {rows,total,page,rows_per_page}
- delete_blueprint(blueprint_id, tenant_id=None) -> dict 软删除已发布需先撤回
- blueprint_tree(blueprint_id, version_id=None, tenant_id=None) -> dict 聚合树蓝图+版本+7类子对象
- change_blueprint_status(blueprint_id, to_status, tenant_id=None, note='') -> dict 状态机流转
- publish_blueprint(blueprint_id, tenant_id=None, note='') -> dict 发布版本快照version_no 自增
- fork_blueprint(blueprint_id, to_class_id=None, tenant_id=None) -> dict 派生副本含子对象
- count_subobjects(blueprint_id, tenant_id=None) -> dict 各子对象计数校验引擎 M2 复用
状态机draft -> in_review -> published -> archivedpublished -> in_review改版
租户隔离tenant_id 强制打头缺失 fail-closed跨租户读取返回不存在
"""
import hashlib
from . import db as _db
from . import models as _m
from . import subobjects as _s
TABLE = 'pbl_blueprint'
TABLE_VERSION = 'pbl_blueprint_version'
STATUS_DRAFT = 'draft'
STATUS_IN_REVIEW = 'in_review'
STATUS_PUBLISHED = 'published'
STATUS_ARCHIVED = 'archived'
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: [],
}
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()
def _require_tenant(tenant_id):
return tenant_id or _db.current_tenant_id()
def _fetch(table, rec_id, tenant_id):
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tenant_id,
'is_deleted': 0})
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 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)
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()
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,
}
_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'), {})
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)
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 status_options():
return [{'code': STATUS_DRAFT, 'label': '草稿'}, {'code': STATUS_IN_REVIEW, 'label': '评审中'},
{'code': STATUS_PUBLISHED, 'label': '已发布'}, {'code': STATUS_ARCHIVED, 'label': '已归档'}]
def domain_options():
return [{'code': c, 'label': l} for c, l in [
('science', '科学'), ('math', '数学'), ('chinese', '语文'), ('english', '英语'),
('art', '艺术'), ('technology', '技术'), ('social_studies', '社会'), ('cross_subject', '跨学科')]]

200
pbl_blueprint/db.py Normal file
View File

@ -0,0 +1,200 @@
"""pbl_blueprint 数据访问适配层(唯一允许直接调用 sqlor 的地方)。
规范约束
- 只使用 sqlor 标准 APIsor.C / sor.U / sor.D / sor.R / sor.I / sor.sqlExe
禁止 save()/list()/insert()/query() 等编造 API
- 库名一律通过 ServerEnv().get_module_dbname('pbl_blueprint') 获取禁止硬编码 DBNAME
- 租户隔离所有读写强制带 tenant_id缺失即 fail-closed 抛错
"""
import json
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'
class PblError(Exception):
"""统一业务异常,携带错误码 + i18n 文案键。"""
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 {}
def to_dict(self):
return {'code': self.code, 'msg': self.msg,
'i18n_key': self.i18n_key, 'detail': self.detail}
# ---------------------------------------------------------------- 环境/库名
def server_env():
from ahserver import ServerEnv
return ServerEnv()
def get_dbname():
"""取本模块库名:优先 ServerEnv 注册值,其次 pbl_common 适配层。"""
dbname = None
try:
env = server_env()
getter = getattr(env, 'get_module_dbname', None)
if callable(getter):
dbname = getter(MODULE_NAME)
except Exception:
dbname = None
if not dbname:
try:
from pbl_common.db_adapter import get_module_dbname as _g
dbname = _g(MODULE_NAME)
except Exception:
dbname = None
if not dbname:
raise PblError(ERR_DBNAME_MISSING,
'模块 %s 未注册库名ServerEnv.set_module_dbname 缺失)' % MODULE_NAME)
return dbname
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:
try:
ctx = getattr(server_env(), 'pbl_tenant_ctx', None) or {}
tid = ctx.get('tenant_id') if isinstance(ctx, dict) else None
except Exception:
tid = None
if not tid and required:
raise PblError(ERR_TENANT_MISSING, '缺少租户上下文,拒绝访问蓝图数据')
return tid or ''
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 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:
return False

View File

@ -0,0 +1,57 @@
{
"pbl_blueprint.action.fork": "Fork",
"pbl_blueprint.action.instantiate": "Instantiate",
"pbl_blueprint.action.new": "New Blueprint",
"pbl_blueprint.action.new_template": "New Template",
"pbl_blueprint.action.publish": "Publish",
"pbl_blueprint.action.refresh": "Refresh",
"pbl_blueprint.action.to_template": "Save as Template",
"pbl_blueprint.err.pbl-api-001": "API call failed",
"pbl_blueprint.err.pbl-boot-001": "pbl_blueprint module not loaded",
"pbl_blueprint.err.pbl-db-001": "Module database name is not registered",
"pbl_blueprint.err.pbl-not-found": "Record not found or no permission",
"pbl_blueprint.err.pbl-param-001": "Invalid parameter",
"pbl_blueprint.err.pbl-ref-001": "Record is referenced",
"pbl_blueprint.err.pbl-state-001": "Operation not allowed in current status",
"pbl_blueprint.err.pbl-tenant-001": "Missing tenant context, access denied",
"pbl_blueprint.field.blueprint_code": "Code",
"pbl_blueprint.field.blueprint_name": "Name",
"pbl_blueprint.field.complexity_level": "Complexity",
"pbl_blueprint.field.content_hash": "Content Hash",
"pbl_blueprint.field.created_at": "Created",
"pbl_blueprint.field.current_version": "Version",
"pbl_blueprint.field.domain_code": "Domain",
"pbl_blueprint.field.estimated_cost": "Estimated Cost",
"pbl_blueprint.field.grade_code": "Grade",
"pbl_blueprint.field.group_count": "Groups",
"pbl_blueprint.field.project_duration": "Duration (lessons)",
"pbl_blueprint.field.published_at": "Published At",
"pbl_blueprint.field.status": "Status",
"pbl_blueprint.field.student_count": "Students",
"pbl_blueprint.field.subject_code": "Subject",
"pbl_blueprint.field.summary_text": "Summary",
"pbl_blueprint.field.template_code": "Template Code",
"pbl_blueprint.field.template_name": "Template Name",
"pbl_blueprint.field.template_type": "Template Type",
"pbl_blueprint.field.usage_count": "Usage Count",
"pbl_blueprint.field.version_no": "Version No.",
"pbl_blueprint.field.version_status": "Version Status",
"pbl_blueprint.menu.blueprint_editor": "Blueprint Editor",
"pbl_blueprint.menu.blueprint_list": "Blueprints",
"pbl_blueprint.menu.blueprint_tree": "Blueprint Tree",
"pbl_blueprint.menu.root": "PBL Blueprint",
"pbl_blueprint.menu.template_list": "Blueprint Templates",
"pbl_blueprint.node.artifact_spec": "Artifacts",
"pbl_blueprint.node.blueprint": "Blueprint",
"pbl_blueprint.node.evidence_spec": "Evidence Specs",
"pbl_blueprint.node.learning_goal": "Learning Goals",
"pbl_blueprint.node.mission": "Mission",
"pbl_blueprint.node.reflection_spec": "Reflection Specs",
"pbl_blueprint.node.role": "Roles",
"pbl_blueprint.node.task": "Tasks",
"pbl_blueprint.page.editor": "Blueprint Editor",
"pbl_blueprint.page.list": "Blueprint List",
"pbl_blueprint.page.template": "Blueprint Templates",
"pbl_blueprint.page.tree": "Blueprint Structure",
"pbl_blueprint.page.versions": "Version History"
}

View File

@ -0,0 +1,57 @@
{
"pbl_blueprint.action.fork": "派生副本",
"pbl_blueprint.action.instantiate": "实例化为蓝图",
"pbl_blueprint.action.new": "新建蓝图",
"pbl_blueprint.action.new_template": "新建模板",
"pbl_blueprint.action.publish": "发布",
"pbl_blueprint.action.refresh": "刷新",
"pbl_blueprint.action.to_template": "抽为模板",
"pbl_blueprint.err.pbl-api-001": "接口调用失败",
"pbl_blueprint.err.pbl-boot-001": "pbl_blueprint 模块未加载",
"pbl_blueprint.err.pbl-db-001": "模块未注册库名,无法访问数据",
"pbl_blueprint.err.pbl-not-found": "记录不存在或无权访问",
"pbl_blueprint.err.pbl-param-001": "参数校验失败",
"pbl_blueprint.err.pbl-ref-001": "数据被引用,操作被拒绝",
"pbl_blueprint.err.pbl-state-001": "当前状态不允许该操作",
"pbl_blueprint.err.pbl-tenant-001": "缺少租户上下文,拒绝访问蓝图数据",
"pbl_blueprint.field.blueprint_code": "蓝图编码",
"pbl_blueprint.field.blueprint_name": "蓝图名称",
"pbl_blueprint.field.complexity_level": "复杂度",
"pbl_blueprint.field.content_hash": "内容哈希",
"pbl_blueprint.field.created_at": "创建时间",
"pbl_blueprint.field.current_version": "当前版本",
"pbl_blueprint.field.domain_code": "领域",
"pbl_blueprint.field.estimated_cost": "预估成本(元)",
"pbl_blueprint.field.grade_code": "学段",
"pbl_blueprint.field.group_count": "小组数",
"pbl_blueprint.field.project_duration": "项目时长(课时)",
"pbl_blueprint.field.published_at": "发布时间",
"pbl_blueprint.field.status": "状态",
"pbl_blueprint.field.student_count": "参与学生数",
"pbl_blueprint.field.subject_code": "学科",
"pbl_blueprint.field.summary_text": "蓝图摘要",
"pbl_blueprint.field.template_code": "模板编码",
"pbl_blueprint.field.template_name": "模板名称",
"pbl_blueprint.field.template_type": "模板类型",
"pbl_blueprint.field.usage_count": "引用次数",
"pbl_blueprint.field.version_no": "版本号",
"pbl_blueprint.field.version_status": "版本状态",
"pbl_blueprint.menu.blueprint_editor": "蓝图编辑",
"pbl_blueprint.menu.blueprint_list": "蓝图列表",
"pbl_blueprint.menu.blueprint_tree": "蓝图结构树",
"pbl_blueprint.menu.root": "PBL蓝图",
"pbl_blueprint.menu.template_list": "蓝图模板",
"pbl_blueprint.node.artifact_spec": "产出物规格",
"pbl_blueprint.node.blueprint": "蓝图",
"pbl_blueprint.node.evidence_spec": "证据规格",
"pbl_blueprint.node.learning_goal": "学习目标",
"pbl_blueprint.node.mission": "真实情境任务",
"pbl_blueprint.node.reflection_spec": "反思规格",
"pbl_blueprint.node.role": "角色",
"pbl_blueprint.node.task": "任务分解",
"pbl_blueprint.page.editor": "蓝图编辑",
"pbl_blueprint.page.list": "蓝图列表",
"pbl_blueprint.page.template": "蓝图模板",
"pbl_blueprint.page.tree": "蓝图结构",
"pbl_blueprint.page.versions": "版本历史"
}

88
pbl_blueprint/init.py Normal file
View File

@ -0,0 +1,88 @@
"""pbl_blueprint.init —— 模块挂载入口。
宿主应用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
"""
from ahserver.serverEnv import ServerEnv
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,
)
_loaded = False
def load_pbl_blueprint():
"""注册 pbl_blueprint 全部契约到 ServerEnv幂等"""
global _loaded
env = ServerEnv()
# ---- 蓝图聚合根 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
# ---- 子对象契约
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
# ---- 版本契约
env.pbl_blueprint_version_create = pbl_blueprint_version_create
env.pbl_blueprint_version_diff = pbl_blueprint_version_diff
# ---- 模板契约
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
# ---- 下游模块只读契约compiler / validation / runtime
env.pbl_blueprint_get_contract = pbl_blueprint_get_contract
# ---- 异常类型与工具(供 dspy 统一错误处理)
env.PblBlueprintError = PblBlueprintError
env.pbl_blueprint_safe_traceback = safe_traceback
# 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
def is_loaded():
return _loaded

View File

@ -0,0 +1,22 @@
{
"appcodes": [
{ "id": "pbl_blueprint_status", "name": "蓝图状态", "hierarchy_flg": "0" },
{ "id": "pbl_blueprint_domain", "name": "蓝图业务域", "hierarchy_flg": "0" },
{ "id": "pbl_blueprint_obj_type", "name": "蓝图子对象类型", "hierarchy_flg": "0" }
],
"appcodes_kv": [
{ "parentid": "pbl_blueprint_status", "k": "draft", "v": "草稿" },
{ "parentid": "pbl_blueprint_status", "k": "active", "v": "设计中" },
{ "parentid": "pbl_blueprint_status", "k": "published", "v": "已发布" },
{ "parentid": "pbl_blueprint_status", "k": "archived", "v": "已归档" },
{ "parentid": "pbl_blueprint_domain", "k": "hr", "v": "人力资源" },
{ "parentid": "pbl_blueprint_domain", "k": "finance", "v": "财务" },
{ "parentid": "pbl_blueprint_domain", "k": "supplychain", "v": "供应链" },
{ "parentid": "pbl_blueprint_domain", "k": "project", "v": "项目管理" },
{ "parentid": "pbl_blueprint_obj_type", "k": "phase", "v": "阶段" },
{ "parentid": "pbl_blueprint_obj_type", "k": "task", "v": "任务" },
{ "parentid": "pbl_blueprint_obj_type", "k": "deliverable", "v": "交付物" },
{ "parentid": "pbl_blueprint_obj_type", "k": "assessment", "v": "评价项" },
{ "parentid": "pbl_blueprint_obj_type", "k": "resource", "v": "资源" }
]
}

View File

@ -0,0 +1,124 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,89 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,89 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,89 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,89 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,98 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,89 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,89 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,141 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,82 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

View File

@ -0,0 +1,82 @@
{
"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"
}
]
}
},
"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"
]
}
}
}

168
pbl_blueprint/models.py Normal file
View File

@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
"""pbl_blueprint 表清单与字段映射(从 models/*.json 四段式定义读取,不重复硬编码字段)。
对外提供
- TABLES / TABLE_META表名清单与元信息pk / tenant_field / 抽象类型映射
- create_tables(env=None) models/*.json 建表DDL 由平台抽象类型映射生成幂等
- field_type_map(tbl)字段 -> 抽象类型交付件逐字段核对用
"""
import json
import os
from . import db as _db
MODELS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'models')
# 蓝图聚合根 + 版本 + 模板M1a 范围)
CORE_TABLES = ['pbl_blueprint', 'pbl_blueprint_version']
# 子对象 7 类M1a 落定义M1b 扩展行为)
SUBOBJECT_TABLES = [
'pbl_blueprint_mission',
'pbl_blueprint_learning_goal',
'pbl_blueprint_task',
'pbl_blueprint_role',
'pbl_blueprint_artifact_spec',
'pbl_blueprint_evidence_spec',
'pbl_blueprint_reflection_spec',
]
# 模板主表 + 明细
TEMPLATE_TABLES = ['pbl_blueprint_template', 'pbl_blueprint_template_item']
# 子对象类型 -> 表名(契约接口 subobject_save/list 的 subobject_type 取值)
SUBOBJECT_TYPE_TABLE = {
'mission': 'pbl_blueprint_mission',
'learning_goal': 'pbl_blueprint_learning_goal',
'task': 'pbl_blueprint_task',
'role': 'pbl_blueprint_role',
'artifact_spec': 'pbl_blueprint_artifact_spec',
'evidence_spec': 'pbl_blueprint_evidence_spec',
'reflection_spec': 'pbl_blueprint_reflection_spec',
}
TABLE_SUBOBJECT_TYPE = {v: k for k, v in SUBOBJECT_TYPE_TABLE.items()}
TABLES = CORE_TABLES + SUBOBJECT_TABLES + TEMPLATE_TABLES
# 抽象类型 -> PostgreSQL 物理类型(与 scripts/derive_artifacts.py 保持一致)
ABSTRACT_TO_PG = {
'str': 'varchar',
'text': 'text',
'int': 'integer',
'double': 'numeric(18,2)',
'datetime': 'timestamp',
}
# 禁止出现的物理类型/写法QC 自检项)
FORBIDDEN_TYPES = ['varchar(', 'bigint', 'boolean', 'int(11)', 'SERIAL', 'BIGSERIAL',
'nextval', 'AUTO_INCREMENT']
def _cache():
global _CACHE
if '_CACHE' not in dir():
_CACHE = {}
return _CACHE
def load_model(table):
"""读取单表四段式定义。"""
path = os.path.join(MODELS_DIR, '%s.json' % table)
if not os.path.isfile(path):
raise _db.PblError(_db.ERR_PARAM_INVALID, '表定义文件缺失: %s' % table)
with open(path, encoding='utf-8') as fp:
doc = json.load(fp)
for seg in ('summary', 'fields', 'indexes', 'codes'):
if seg not in doc:
raise _db.PblError(_db.ERR_PARAM_INVALID,
'表定义 %s 缺四段式段: %s' % (table, seg))
return doc
def all_models():
return dict((t, load_model(t)) for t in TABLES)
def field_type_map(table):
"""字段名 -> 抽象类型(如 id: str(32))。"""
return dict((k, v['type']) for k, v in load_model(table)['fields'].items())
def table_meta(table):
doc = load_model(table)
fields = doc['fields']
return {
'table': table,
'label': doc['summary'].get('label', table),
'engine': doc['summary'].get('engine', 'postgresql'),
'pk': [k for k, v in fields.items() if v.get('pk')] or ['id'],
'tenant_field': 'tenant_id' if 'tenant_id' in fields else None,
'soft_delete_field': 'is_deleted' if 'is_deleted' in fields else None,
'required': [k for k, v in fields.items() if v.get('required')],
'appcodes': doc['codes'],
'indexes': list(doc['indexes'].keys()),
'field_count': len(fields),
}
def _pg_type(spec):
ty = spec['type']
if ty.startswith('str'):
return 'varchar(%s)' % ty[ty.index('(') + 1:ty.index(')')]
if ty == 'text':
return 'text'
if ty == 'int':
return 'integer'
if ty.startswith('double'):
return 'numeric(18,2)'
if ty == 'datetime':
return 'timestamp'
raise _db.PblError(_db.ERR_PARAM_INVALID, '未知抽象类型 %s' % ty)
def create_table_ddl(table):
"""按 models/*.json 生成幂等 DDL平台抽象类型映射不写方言私有序列"""
doc = load_model(table)
fields = doc['fields']
cols = ',\n'.join(' %s %s%s' % (k, _pg_type(v),
' NOT NULL' if (v.get('required') or v.get('pk')) else '')
for k, v in fields.items())
stmts = ['CREATE TABLE IF NOT EXISTS %s (\n%s\n)' % (table, cols)]
for iname, isp in doc['indexes'].items():
if iname == 'primary':
continue
uniq = 'UNIQUE ' if isp.get('unique') else ''
stmts.append('CREATE %sINDEX IF NOT EXISTS %s_%s ON %s (%s)' %
(uniq, table, iname, table, ', '.join(isp['fields'])))
return stmts
def create_tables(tables=None, dbname=None):
"""建表(幂等)。返回 {table: 执行语句数}。"""
result = {}
for t in (tables or TABLES):
stmts = create_table_ddl(t)
for s in stmts:
_db.sql_exe(s)
result[t] = len(stmts)
return result
def register_crud(env, tables=None):
"""把 json/{tblname}.json CRUD 契约注册到 ServerEnv供平台 CRUD 工厂/前端表格消费)。"""
base = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'json')
registered = []
for t in (tables or TABLES):
path = os.path.join(base, '%s.json' % t)
if not os.path.isfile(path):
raise _db.PblError(_db.ERR_PARAM_INVALID, 'CRUD 定义缺失: %s' % path)
with open(path, encoding='utf-8') as fp:
doc = json.load(fp)
if list(doc.keys()) != [t]:
raise _db.PblError(_db.ERR_PARAM_INVALID,
'CRUD 定义根键必须等于表名 %s,实际 %s' % (t, list(doc.keys())))
if env is not None:
store = getattr(env, 'pbl_crud_defs', None)
if store is None:
store = {}
setattr(env, 'pbl_crud_defs', store)
store[t] = doc[t]
registered.append(t)
return registered

View File

@ -0,0 +1,185 @@
{
"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
}
},
"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": "蓝图状态"
}
}
}

View File

@ -0,0 +1,133 @@
{
"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
}
},
"codes": {
"artifact_type": {
"appcode": "pbl_artifact_type",
"label": "产出物类型"
}
}
}

View File

@ -0,0 +1,133 @@
{
"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
}
},
"codes": {
"evidence_type": {
"appcode": "pbl_evidence_type",
"label": "证据类型"
}
}
}

View File

@ -0,0 +1,138 @@
{
"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
}
},
"codes": {
"goal_type": {
"appcode": "pbl_goal_type",
"label": "目标类型"
}
}
}

View File

@ -0,0 +1,138 @@
{
"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
}
},
"codes": {
"mission_type": {
"appcode": "pbl_mission_type",
"label": "情境类型"
}
}
}

View File

@ -0,0 +1,137 @@
{
"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
}
},
"codes": {
"reflect_type": {
"appcode": "pbl_reflect_type",
"label": "反思类型"
},
"frequency": {
"appcode": "pbl_reflect_frequency",
"label": "频次"
}
}
}

View File

@ -0,0 +1,132 @@
{
"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
}
},
"codes": {
"role_category": {
"appcode": "pbl_role_category",
"label": "角色类别"
}
}
}

View File

@ -0,0 +1,137 @@
{
"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
}
},
"codes": {
"task_type": {
"appcode": "pbl_task_type",
"label": "任务类型"
}
}
}

View File

@ -0,0 +1,159 @@
{
"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
}
},
"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": "状态"
}
}
}

View File

@ -0,0 +1,106 @@
{
"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
}
},
"codes": {
"subobject_type": {
"appcode": "pbl_subobject_type",
"label": "子对象类型"
}
}
}

View File

@ -0,0 +1,124 @@
{
"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
}
},
"codes": {
"version_status": {
"appcode": "pbl_version_status",
"label": "版本状态"
}
}
}

View File

@ -0,0 +1,520 @@
-- 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: 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 '更新时间';
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);
-- 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 '更新时间';
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);

213
pbl_blueprint/subobjects.py Normal file
View File

@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
"""蓝图子对象契约接口M1a 落表 + CRUDM1b 扩展校验/编排)。
7 类子对象表子类型 -> 表名见 models.SUBOBJECT_TYPE_TABLE
mission / learning_goal / task / role / artifact_spec / evidence_spec / reflection_spec
契约函数
- subobject_save(subobject_type, payload, tenant_id=None) -> dict
- subobject_list(subobject_type, blueprint_id, tenant_id=None) -> dict {rows,total}
- subobject_get(subobject_type, object_id, tenant_id=None) -> dict
- subobject_delete(subobject_type, object_id, tenant_id=None) -> dict
- list_all_subobjects(blueprint_id, tenant_id=None) -> dict {type: [rows]}
- count_subobjects(blueprint_id, tenant_id=None) -> dict {type: n}M2 校验引擎复用
- copy_subobjects(src_blueprint_id, dst_blueprint_id, tenant_id=None) -> dict
公共字段所有子对象表一致tenant_id / class_id / blueprint_id / version_id / code /
name / description / seq_no / status / content_json + 审计字段
"""
from . import db as _db
from . import models as _m
COMMON_FIELDS = ['class_id', 'blueprint_id', 'version_id', 'code', 'name',
'description', 'seq_no', 'status', 'content_json']
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': '反思规格'},
]
STATUS_OPTIONS = [
{'code': 'active', 'label': '启用'},
{'code': 'draft', 'label': '草稿'},
{'code': 'disabled', 'label': '停用'},
]
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 _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 _fetch(table, rec_id, tid):
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tid, 'is_deleted': 0})
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()
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
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 ''
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
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)
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 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 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 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 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 subobject_type_options():
return list(TYPE_OPTIONS)

238
pbl_blueprint/templates.py Normal file
View File

@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
"""蓝图模板契约接口M1a模板主表/明细 CRUD + 实例化为蓝图)。
契约函数
- 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
"""
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'
TEMPLATE_FIELDS = ['class_id', 'template_code', 'template_name', 'template_type',
'domain_code', 'grade_code', 'subject_code', 'complexity_level',
'scope', 'source_blueprint_id', 'status', 'description']
def _fetch(tid, rec_id, table=TABLE):
return _db.select_one(table, where={'id': rec_id, 'tenant_id': tid, 'is_deleted': 0})
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')
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,
}
_db.insert_row(TABLE, row)
_db.audit('template_create', TABLE, new_id, after=row, tenant_id=tid)
return _fetch(tid, new_id)
def list_template_items(template_id, tid):
rows = _db.select_rows(TABLE_ITEM, where={'tenant_id': tid, 'template_id': template_id,
'is_deleted': 0},
orderby='subobject_type asc, seq_no asc') or []
return rows
def get_template(template_id, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
row = _fetch(tid, template_id)
if not row:
raise _db.PblError(_db.ERR_NOT_FOUND, '模板不存在或无权访问: %s' % template_id)
row['items'] = list_template_items(template_id, tid)
row['item_count'] = len(row['items'])
return row
def list_templates(filters=None, page=1, rows=20, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
filters = dict(filters or {})
where = {'tenant_id': tid, 'is_deleted': 0}
for k in ('status', 'template_type', 'domain_code', 'grade_code', 'subject_code',
'complexity_level', 'scope'):
if filters.get(k):
where[k] = filters[k]
for k in ('template_name', 'template_code'):
if filters.get(k):
where['%s~' % k] = '%%%s%%' % filters[k]
page = max(1, int(page or 1))
rows = min(200, max(1, int(rows or 20)))
allowed = ('created_at', 'updated_at', 'template_code', 'usage_count', 'status')
sortby = filters.get('sortby') if filters.get('sortby') in allowed else 'created_at'
order = 'asc' if str(filters.get('sortorder', 'desc')).lower() == 'asc' else 'desc'
all_rows = _db.select_rows(TABLE, where=where,
fields='id, tenant_id, class_id, template_code, template_name, '
'template_type, domain_code, grade_code, subject_code, '
'complexity_level, scope, usage_count, status, created_at, '
'updated_at',
orderby='%s %s' % (sortby, order)) or []
total = len(all_rows)
start = (page - 1) * rows
return {'rows': all_rows[start:start + rows], 'total': total,
'page': page, 'rows_per_page': rows}
def delete_template(template_id, tenant_id=None):
tid = tenant_id or _db.current_tenant_id()
old = _fetch(tid, template_id)
if not old:
raise _db.PblError(_db.ERR_NOT_FOUND, '模板不存在或无权访问: %s' % template_id)
now = _db.now_str()
operator = _db.current_operator()
_db.update_row(TABLE, {'is_deleted': 1, 'updated_by': operator, 'updated_at': now},
{'id': template_id, 'tenant_id': tid})
for it in list_template_items(template_id, tid):
_db.update_row(TABLE_ITEM, {'is_deleted': 1, 'updated_by': operator,
'updated_at': now},
{'id': it.get('id'), 'tenant_id': tid})
_db.audit('template_delete', TABLE, template_id, before=old, tenant_id=tid)
return {'id': template_id, 'deleted': True}
def create_template_from_blueprint(blueprint_id, payload=None, tenant_id=None):
"""把蓝图抽为模板:主表 + 每类子对象一条 itempayload 可覆盖模板字段)。"""
tid = tenant_id or _db.current_tenant_id()
payload = dict(payload or {})
bp = _bp.get_blueprint(blueprint_id, tid)
tpl_payload = {
'template_name': payload.get('template_name') or ('%s 模板' % (bp.get('blueprint_name') or '')),
'template_code': payload.get('template_code') or '',
'template_type': payload.get('template_type', 'blueprint'),
'domain_code': bp.get('domain_code', ''), 'grade_code': bp.get('grade_code', ''),
'subject_code': bp.get('subject_code', ''),
'complexity_level': bp.get('complexity_level', ''),
'scope': payload.get('scope', 'tenant'),
'source_blueprint_id': blueprint_id,
'description': payload.get('description', ''),
'class_id': payload.get('class_id') or bp.get('class_id', ''),
}
tpl = save_template(tpl_payload, tid)
now = _db.now_str()
operator = _db.current_operator()
children = _s.list_all_subobjects(blueprint_id, tid)
created = 0
for stype in sorted(children):
seq = 0
for obj in children[stype]:
payload_json = dict((k, v) for k, v in obj.items()
if k not in ('id', 'tenant_id', 'blueprint_id', 'version_id',
'created_by', 'created_at', 'updated_by',
'updated_at', 'is_deleted'))
_db.insert_row(TABLE_ITEM, {
'id': _db.gen_id(), 'tenant_id': tid,
'class_id': obj.get('class_id') or '', 'template_id': tpl['id'],
'subobject_type': stype, 'code': obj.get('code') or '',
'name': obj.get('name') or '', 'description': obj.get('description') or '',
'item_payload_json': _db.dumps(payload_json), 'seq_no': seq,
'required_flag': int(obj.get('required_flag') or 1),
'status': 'active', 'is_deleted': 0,
'created_by': operator, 'created_at': now,
'updated_by': operator, 'updated_at': now,
})
seq += 1
created += 1
_db.audit('template_from_blueprint', TABLE, tpl['id'],
after={'source_blueprint_id': blueprint_id, 'items': created}, tenant_id=tid)
tpl = get_template(tpl['id'], tid)
tpl['item_created'] = created
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 '',
}
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:
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')}
def template_status_options():
return [{'code': 'draft', 'label': '草稿'}, {'code': 'published', 'label': '已发布'},
{'code': 'archived', 'label': '已归档'}]

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_delete.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_delete可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_delete')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_delete请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_domain_options.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_domain_options可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_domain_options')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_domain_options请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_fork.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_fork可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_fork')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_fork请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_get.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_get可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_get')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_get请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_list.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_list可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_list')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_list请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_publish.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_publish可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_publish')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_publish请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_save.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_save可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_save')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_save请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_status.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_status可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_status')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_status请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_status_options.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_status_options可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_status_options')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_status_options请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_tree.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_tree可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_tree')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_tree请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_version_get.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_version_get可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_version_get')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_version_get请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/blueprint_versions.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::blueprint_versions可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('blueprint_versions')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 blueprint_versions请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/subobject_counts.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::subobject_counts可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('subobject_counts')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 subobject_counts请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/subobject_delete.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::subobject_delete可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('subobject_delete')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 subobject_delete请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/subobject_get.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::subobject_get可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('subobject_get')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 subobject_get请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/subobject_list.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::subobject_list可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('subobject_list')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 subobject_list请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/subobject_save.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::subobject_save可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('subobject_save')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 subobject_save请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/subobject_type_options.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::subobject_type_options可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('subobject_type_options')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 subobject_type_options请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/table_manifest.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::table_manifest可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('table_manifest')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 table_manifest请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/template_delete.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::template_delete可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('template_delete')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 template_delete请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/template_from_blueprint.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::template_from_blueprint可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('template_from_blueprint')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 template_from_blueprint请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/template_get.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::template_get可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('template_get')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 template_get请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/template_instantiate.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::template_instantiate可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('template_instantiate')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 template_instantiate请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/template_list.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::template_list可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('template_list')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 template_list请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# pbl_blueprint/template_save.dspy —— 薄接线层:仅解析入参 + 调 ServerEnv 注册的门面函数
# 业务逻辑在 pbl_blueprint/api.py::template_save可单测、跨宿主复用本文件禁止 import / f-string / print / uuid
result = {'success': False, 'error': {'code': 'PBL-BOOT-001', 'msg': 'pbl_blueprint 未加载'}}
try:
_env = ServerEnv()
_apis = getattr(_env, 'pbl_blueprint_api', None) or {}
_fn = _apis.get('template_save')
if _fn is None:
raise Exception('pbl_blueprint 未注册门面函数 template_save请先 load_pbl_blueprint()')
_args = {}
for _k in dir(params_kw):
pass
for _k in list(params_kw.keys()):
_args[_k] = params_kw[_k]
result = _fn(_args)
except Exception as _e:
result = {'success': False, 'error': {'code': 'PBL-API-001', 'msg': str(_e), 'detail': {}}}
_out = result
try:
_out = json.dumps(result, ensure_ascii=False, default=str)
except Exception:
_out = result
return _out

View File

@ -0,0 +1,50 @@
<ui>
<panel name="pbl_blueprint_editor_page" label="{{t('pbl_blueprint.page.editor', '蓝图编辑')}}">
<form name="blueprint_form" pk="id"
dataurl="{{entire_url('/pbl_blueprint/api/blueprint_get.dspy')}}"
submiturl="{{entire_url('/pbl_blueprint/api/blueprint_save.dspy')}}">
<input name="blueprint_code" label="{{t('pbl_blueprint.field.blueprint_code', '蓝图编码')}}" readonly="true"/>
<input name="blueprint_name" label="{{t('pbl_blueprint.field.blueprint_name', '蓝图名称')}}" required="true"/>
<select name="domain_code" label="{{t('pbl_blueprint.field.domain_code', '领域')}}"
dataurl="{{entire_url('/pbl_blueprint/api/blueprint_domain_options.dspy')}}"/>
<select name="grade_code" label="{{t('pbl_blueprint.field.grade_code', '学段')}}" uitype="code" appcode="pbl_grade_code"/>
<select name="subject_code" label="{{t('pbl_blueprint.field.subject_code', '学科')}}" uitype="code" appcode="pbl_subject_code"/>
<input name="project_duration" label="{{t('pbl_blueprint.field.project_duration', '项目时长(课时)')}}" uitype="number"/>
<input name="student_count" label="{{t('pbl_blueprint.field.student_count', '参与学生数')}}" uitype="number"/>
<input name="group_count" label="{{t('pbl_blueprint.field.group_count', '小组数')}}" uitype="number"/>
<select name="complexity_level" label="{{t('pbl_blueprint.field.complexity_level', '复杂度')}}" uitype="code" appcode="pbl_complexity_level"/>
<input name="estimated_cost" label="{{t('pbl_blueprint.field.estimated_cost', '预估成本(元)')}}" uitype="number"/>
<textarea name="summary_text" label="{{t('pbl_blueprint.field.summary_text', '蓝图摘要')}}"/>
</form>
<tabs name="subobject_tabs">
<tab name="mission" label="{{t('pbl_blueprint.node.mission', '真实情境任务')}}"
grid="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"
save="{{entire_url('/pbl_blueprint/api/subobject_save.dspy')}}"
del="{{entire_url('/pbl_blueprint/api/subobject_delete.dspy')}}" subobject_type="mission"/>
<tab name="learning_goal" label="{{t('pbl_blueprint.node.learning_goal', '学习目标')}}"
grid="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"
save="{{entire_url('/pbl_blueprint/api/subobject_save.dspy')}}"
del="{{entire_url('/pbl_blueprint/api/subobject_delete.dspy')}}" subobject_type="learning_goal"/>
<tab name="task" label="{{t('pbl_blueprint.node.task', '任务分解')}}"
grid="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"
save="{{entire_url('/pbl_blueprint/api/subobject_save.dspy')}}"
del="{{entire_url('/pbl_blueprint/api/subobject_delete.dspy')}}" subobject_type="task"/>
<tab name="role" label="{{t('pbl_blueprint.node.role', '角色')}}"
grid="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"
save="{{entire_url('/pbl_blueprint/api/subobject_save.dspy')}}"
del="{{entire_url('/pbl_blueprint/api/subobject_delete.dspy')}}" subobject_type="role"/>
<tab name="artifact_spec" label="{{t('pbl_blueprint.node.artifact_spec', '产出物规格')}}"
grid="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"
save="{{entire_url('/pbl_blueprint/api/subobject_save.dspy')}}"
del="{{entire_url('/pbl_blueprint/api/subobject_delete.dspy')}}" subobject_type="artifact_spec"/>
<tab name="evidence_spec" label="{{t('pbl_blueprint.node.evidence_spec', '证据规格')}}"
grid="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"
save="{{entire_url('/pbl_blueprint/api/subobject_save.dspy')}}"
del="{{entire_url('/pbl_blueprint/api/subobject_delete.dspy')}}" subobject_type="evidence_spec"/>
<tab name="reflection_spec" label="{{t('pbl_blueprint.node.reflection_spec', '反思规格')}}"
grid="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"
save="{{entire_url('/pbl_blueprint/api/subobject_save.dspy')}}"
del="{{entire_url('/pbl_blueprint/api/subobject_delete.dspy')}}" subobject_type="reflection_spec"/>
</tabs>
</panel>
</ui>

View File

@ -0,0 +1,28 @@
<ui>
<panel name="pbl_blueprint_list_page" label="{{t('pbl_blueprint.page.list', '蓝图列表')}}">
<toolbar>
<button name="btn_new" label="{{t('pbl_blueprint.action.new', '新建蓝图')}}"
action="crud_add" target="blueprint_grid"/>
<button name="btn_refresh" label="{{t('pbl_blueprint.action.refresh', '刷新')}}" action="grid_reload" target="blueprint_grid"/>
</toolbar>
<search>
<input name="blueprint_name" label="{{t('pbl_blueprint.field.blueprint_name', '蓝图名称')}}"/>
<select name="status" label="{{t('pbl_blueprint.field.status', '状态')}}"
dataurl="{{entire_url('/pbl_blueprint/api/blueprint_status_options.dspy')}}"/>
<select name="domain_code" label="{{t('pbl_blueprint.field.domain_code', '领域')}}"
dataurl="{{entire_url('/pbl_blueprint/api/blueprint_domain_options.dspy')}}"/>
</search>
<grid name="blueprint_grid" dataurl="{{entire_url('/pbl_blueprint/api/blueprint_list.dspy')}}"
pk="id" pageurl="{{entire_url('/pbl_blueprint/blueprint_tree.ui')}}">
<column name="blueprint_code" label="{{t('pbl_blueprint.field.blueprint_code', '蓝图编码')}}"/>
<column name="blueprint_name" label="{{t('pbl_blueprint.field.blueprint_name', '蓝图名称')}}"/>
<column name="domain_code" label="{{t('pbl_blueprint.field.domain_code', '领域')}}" uitype="code"/>
<column name="grade_code" label="{{t('pbl_blueprint.field.grade_code', '学段')}}" uitype="code"/>
<column name="project_duration" label="{{t('pbl_blueprint.field.project_duration', '项目时长(课时)')}}"/>
<column name="complexity_level" label="{{t('pbl_blueprint.field.complexity_level', '复杂度')}}" uitype="code"/>
<column name="current_version" label="{{t('pbl_blueprint.field.current_version', '当前版本')}}"/>
<column name="status" label="{{t('pbl_blueprint.field.status', '状态')}}" uitype="code"/>
<column name="created_at" label="{{t('pbl_blueprint.field.created_at', '创建时间')}}"/>
</grid>
</panel>
</ui>

View File

@ -0,0 +1,36 @@
<ui>
<panel name="pbl_blueprint_tree_page" label="{{t('pbl_blueprint.page.tree', '蓝图结构')}}">
<head>
<button name="btn_publish" label="{{t('pbl_blueprint.action.publish', '发布')}}"
action="post" dataurl="{{entire_url('/pbl_blueprint/api/blueprint_publish.dspy')}}"/>
<button name="btn_fork" label="{{t('pbl_blueprint.action.fork', '派生副本')}}"
action="post" dataurl="{{entire_url('/pbl_blueprint/api/blueprint_fork.dspy')}}"/>
<button name="btn_to_template" label="{{t('pbl_blueprint.action.to_template', '抽为模板')}}"
action="post" dataurl="{{entire_url('/pbl_blueprint/api/template_from_blueprint.dspy')}}"/>
</head>
<tree name="blueprint_tree" dataurl="{{entire_url('/pbl_blueprint/api/blueprint_tree.dspy')}}" pk="id">
<node name="blueprint" label="{{t('pbl_blueprint.node.blueprint', '蓝图')}}"/>
<node name="mission" label="{{t('pbl_blueprint.node.mission', '真实情境任务')}}"
dataurl="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"/>
<node name="learning_goal" label="{{t('pbl_blueprint.node.learning_goal', '学习目标')}}"
dataurl="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"/>
<node name="task" label="{{t('pbl_blueprint.node.task', '任务分解')}}"
dataurl="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"/>
<node name="role" label="{{t('pbl_blueprint.node.role', '角色')}}"
dataurl="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"/>
<node name="artifact_spec" label="{{t('pbl_blueprint.node.artifact_spec', '产出物规格')}}"
dataurl="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"/>
<node name="evidence_spec" label="{{t('pbl_blueprint.node.evidence_spec', '证据规格')}}"
dataurl="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"/>
<node name="reflection_spec" label="{{t('pbl_blueprint.node.reflection_spec', '反思规格')}}"
dataurl="{{entire_url('/pbl_blueprint/api/subobject_list.dspy')}}"/>
</tree>
<grid name="version_grid" label="{{t('pbl_blueprint.page.versions', '版本历史')}}"
dataurl="{{entire_url('/pbl_blueprint/api/blueprint_versions.dspy')}}" pk="id">
<column name="version_no" label="{{t('pbl_blueprint.field.version_no', '版本号')}}"/>
<column name="version_status" label="{{t('pbl_blueprint.field.version_status', '版本状态')}}" uitype="code"/>
<column name="content_hash" label="{{t('pbl_blueprint.field.content_hash', '内容哈希')}}"/>
<column name="published_at" label="{{t('pbl_blueprint.field.published_at', '发布时间')}}"/>
</grid>
</panel>
</ui>

View File

@ -0,0 +1,22 @@
{
"widgettype": "VBox",
"options": { "width": "100%", "height": "100%", "padding": "20px" },
"subwidgets": [
{ "widgettype": "Text", "options": { "label": "PBL 蓝图核心pbl_blueprint", "fontSize": "24px" } },
{ "widgettype": "ResponsableBox", "options": { "gap": "16px", "minWidth": "250px" }, "subwidgets": [
{ "widgettype": "VBox", "options": { "backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer" },
"binds": [{ "wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pbl_blueprint_content",
"options": { "url": "{{entire_url('blueprint_list.ui')}}" } }, "mode": "replace" }],
"subwidgets": [{ "widgettype": "Text", "options": { "label": "蓝图清单" } }] },
{ "widgettype": "VBox", "options": { "backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer" },
"binds": [{ "wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pbl_blueprint_content",
"options": { "url": "{{entire_url('blueprint_tree.ui')}}" } }, "mode": "replace" }],
"subwidgets": [{ "widgettype": "Text", "options": { "label": "蓝图结构(树)" } }] },
{ "widgettype": "VBox", "options": { "backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer" },
"binds": [{ "wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pbl_blueprint_content",
"options": { "url": "{{entire_url('template_list.ui')}}" } }, "mode": "replace" }],
"subwidgets": [{ "widgettype": "Text", "options": { "label": "蓝图模板" } }] }
] },
{ "widgettype": "VBox", "id": "pbl_blueprint_content", "options": { "width": "100%", "flex": "1", "marginTop": "20px" } }
]
}

View File

@ -0,0 +1,12 @@
<ui>
<menu name="pbl_blueprint" label="{{t('pbl_blueprint.menu.root', 'PBL蓝图')}}">
<item name="blueprint_list" label="{{t('pbl_blueprint.menu.blueprint_list', '蓝图列表')}}"
url="{{entire_url('/pbl_blueprint/blueprint_list.ui')}}"/>
<item name="blueprint_tree" label="{{t('pbl_blueprint.menu.blueprint_tree', '蓝图结构树')}}"
url="{{entire_url('/pbl_blueprint/blueprint_tree.ui')}}" visible="false"/>
<item name="template_list" label="{{t('pbl_blueprint.menu.template_list', '蓝图模板')}}"
url="{{entire_url('/pbl_blueprint/template_list.ui')}}"/>
<item name="blueprint_editor" label="{{t('pbl_blueprint.menu.blueprint_editor', '蓝图编辑')}}"
url="{{entire_url('/pbl_blueprint/blueprint_editor.ui')}}" visible="false"/>
</menu>
</ui>

View File

@ -0,0 +1,25 @@
<ui>
<panel name="pbl_blueprint_template_page" label="{{t('pbl_blueprint.page.template', '蓝图模板')}}">
<toolbar>
<button name="btn_new_tpl" label="{{t('pbl_blueprint.action.new_template', '新建模板')}}"
action="crud_add" target="template_grid"/>
</toolbar>
<search>
<input name="template_name" label="{{t('pbl_blueprint.field.template_name', '模板名称')}}"/>
<select name="status" label="{{t('pbl_blueprint.field.status', '状态')}}"
dataurl="{{entire_url('/pbl_blueprint/api/blueprint_status_options.dspy')}}"/>
</search>
<grid name="template_grid" dataurl="{{entire_url('/pbl_blueprint/api/template_list.dspy')}}" pk="id">
<column name="template_code" label="{{t('pbl_blueprint.field.template_code', '模板编码')}}"/>
<column name="template_name" label="{{t('pbl_blueprint.field.template_name', '模板名称')}}"/>
<column name="template_type" label="{{t('pbl_blueprint.field.template_type', '模板类型')}}" uitype="code"/>
<column name="complexity_level" label="{{t('pbl_blueprint.field.complexity_level', '复杂度')}}" uitype="code"/>
<column name="usage_count" label="{{t('pbl_blueprint.field.usage_count', '引用次数')}}"/>
<column name="status" label="{{t('pbl_blueprint.field.status', '状态')}}" uitype="code"/>
</grid>
<footer>
<button name="btn_instantiate" label="{{t('pbl_blueprint.action.instantiate', '实例化为蓝图')}}"
action="post" dataurl="{{entire_url('/pbl_blueprint/api/template_instantiate.dspy')}}"/>
</footer>
</panel>
</ui>

14
pyproject.toml Normal file
View File

@ -0,0 +1,14 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pbl_blueprint"
version = "1.0.0"
description = "PBL 蓝图聚合根与子对象、版本、模板M1a/M1b"
requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"]
[tool.setuptools.packages.find]
where = ["."]
include = ["pbl_blueprint*"]

57
scripts/audit_dspy.py Normal file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""dspy 禁项审计 + sqlor API 白名单 + api 文件/RBAC 注册对齐检查。"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
API_DIR = os.path.join(ROOT, 'pbl_blueprint', 'wwwroot', 'api')
BANNED = [
('import ', r'(?m)^\s*(import|from)\s+\w'),
('f-string', r"(?<![\w])f['\"]"),
('print(', r'\bprint\s*\('),
('uuid', r'\buuid\b'),
('os.environ', r'os\.environ'),
('open(', r'\bopen\s*\('),
]
SOR_ALLOWED = {'C', 'U', 'D', 'R', 'I', 'sqlExe'}
def audit():
problems, files = [], sorted(f for f in os.listdir(API_DIR) if f.endswith('.dspy'))
for name in files:
raw = open(os.path.join(API_DIR, name), encoding='utf-8').read()
# 注释行不参与禁项判定(禁项针对可执行代码)
text = '\n'.join(l for l in raw.splitlines() if not l.strip().startswith('#'))
for label, pat in BANNED:
for m in re.finditer(pat, text):
problems.append('%s: 禁项 %s @%s' % (name, label, m.group(0).strip()))
for m in re.finditer(r'\bsor\.(\w+)', text):
if m.group(1) not in SOR_ALLOWED:
problems.append('%s: 非法 sqlor API sor.%s' % (name, m.group(1)))
for m in re.finditer(r'\bsor\.(\w+)\s*\(', text):
pass
if 'return ' not in text:
problems.append('%s: 缺少显式 return' % name)
sys.path.insert(0, os.path.join(ROOT, 'scripts'))
import load_path
registered = {p['path'].split('/%s/' % load_path.MODULE, 1)[1] for p in load_path.PATHS}
for name in files:
if 'api/' + name not in registered:
problems.append('%s: 未在 scripts/load_path.py 注册' % name)
for p in sorted(registered):
if p.startswith('api/') and not os.path.exists(os.path.join(API_DIR, os.path.basename(p))):
problems.append('注册了不存在的文件: %s' % p)
for ui in sorted(os.listdir(os.path.join(ROOT, 'pbl_blueprint', 'wwwroot'))):
if ui.endswith('.ui') and ui not in registered:
problems.append('%s: 未在 scripts/load_path.py 注册' % ui)
return files, problems
if __name__ == '__main__':
files, problems = audit()
print('audited dspy files: %d' % len(files))
for p in problems:
print('FAIL ' + p)
print('DSPY_AUDIT_%s' % ('PASS' if not problems else 'FAIL(%d)' % len(problems)))
sys.exit(1 if problems else 0)

166
scripts/derive_artifacts.py Normal file
View File

@ -0,0 +1,166 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_blueprint 派生物生成器(幂等)。
输入pbl_blueprint/models/{tblname}.json 四段式表定义summary/fields/indexes/codes
输出
1) pbl_blueprint/json/{tblname}.json CRUD 契约定义根键 = tblname + params(editable/browserfields)
2) pbl_blueprint/sql/pbl_blueprint.core.sql DDL 派生物engine=postgresql禁止手工维护
用法python3 scripts/derive_artifacts.py
QC 核对点json/*.json sql/*.sql 均由本脚本机械派生改表只需改 models/*.json 后重跑本脚本
"""
import json
import os
import glob
HERE = os.path.dirname(os.path.abspath(__file__))
PKG = os.path.join(os.path.dirname(HERE), 'pbl_blueprint')
MODELS_DIR = os.path.join(PKG, 'models')
JSON_DIR = os.path.join(PKG, 'json')
SQL_DIR = os.path.join(PKG, 'sql')
# 表 -> dspy 接口前缀CRUD editable url 用)
API_PREFIX = {
'pbl_blueprint': 'blueprint',
'pbl_blueprint_version': 'version',
'pbl_blueprint_template': 'template',
'pbl_blueprint_template_item': 'template_item',
}
BROWSER_FIELDS = ('id', 'tenant_id', 'class_id', 'code', 'name', 'blueprint_code',
'blueprint_name', 'template_code', 'template_name', 'status',
'version_status', 'version_no', 'subobject_type', 'domain_code',
'grade_code', 'subject_code', 'complexity_level', 'seq_no',
'created_at', 'updated_at')
HEAVY_FIELDS = ('snapshot_json', 'content_json', 'item_payload_json',
'quality_criteria', 'knowledge_points', 'tags_json')
IMMUTABLE = ('id', 'tenant_id', 'created_by', 'created_at')
def api_prefix(tbl):
if tbl in API_PREFIX:
return API_PREFIX[tbl]
return 'subobject'
def load_models():
docs = {}
for p in sorted(glob.glob(os.path.join(MODELS_DIR, '*.json'))):
tbl = os.path.basename(p)[:-5]
doc = json.load(open(p, encoding='utf-8'))
for seg in ('summary', 'fields', 'indexes', 'codes'):
if seg not in doc:
raise SystemExit('models/%s.json 缺四段式必需段: %s' % (tbl, seg))
if not isinstance(doc['fields'], dict):
raise SystemExit('models/%s.json fields 必须是对象(列名->定义)' % tbl)
docs[tbl] = doc
return docs
def build_crud(tbl, doc):
fields = doc['fields']
names = list(fields.keys())
editable = [n for n in names if n not in IMMUTABLE]
browser = [n for n in names if n in BROWSER_FIELDS]
alters = {}
for n, spec in fields.items():
if spec.get('appcode'):
alters[n] = {'uitype': 'code', 'appcode': spec['appcode'],
'label': spec.get('label', n)}
a = api_prefix(tbl)
return {tbl: {
'summary': doc['summary'].get('label', tbl),
'alias': tbl + '_crud',
'params': {
'sortby': 'created_at',
'sortorder': 'desc',
'tenant_scoped': True,
'browserfields': {
'fields': browser,
'alters': alters,
'exclouded': list(HEAVY_FIELDS),
},
'editable': {
'new_data_url': "{{entire_url('../api/%s_save.dspy')}}" % a,
'update_data_url': "{{entire_url('../api/%s_save.dspy')}}" % a,
'delete_data_url': "{{entire_url('../api/%s_delete.dspy')}}" % a,
'list_data_url': "{{entire_url('../api/%s_list.dspy')}}" % a,
'get_data_url': "{{entire_url('../api/%s_get.dspy')}}" % a,
'editexclouded': list(IMMUTABLE),
},
'required': [n for n, s in fields.items() if s.get('required')],
'data_filter': {'AND': [{'field': 'tenant_id', 'op': '=', 'var': 'tenant_id'}]},
},
'codes': doc['codes'],
'table_meta': {
'pk': 'id',
'tenant_field': 'tenant_id',
'soft_delete_field': 'is_deleted',
'engine': doc['summary'].get('engine', 'postgresql'),
'indexes': list(doc['indexes'].keys()),
},
}}
def col_ddl(name, spec):
ty = spec['type']
if ty.startswith('str'):
return '%s varchar(%s)' % (name, ty[ty.index('(') + 1:ty.index(')')])
if ty == 'text':
return '%s text' % name
if ty == 'int':
return '%s integer' % name
if ty.startswith('double'):
return '%s numeric(18,2)' % name
if ty == 'datetime':
return '%s timestamp' % name
raise SystemExit('未知抽象类型: %s (%s)' % (ty, name))
def build_sql(docs):
lines = [
'-- 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',
'',
]
for tbl in sorted(docs.keys()):
doc = docs[tbl]
fields = doc['fields']
lines.append('-- %s: %s' % (tbl, doc['summary'].get('label', '')))
cols = ',\n'.join(
' ' + col_ddl(k, v) + (' NOT NULL' if (v.get('required') or k == 'id') else '')
for k, v in fields.items())
lines.append('CREATE TABLE IF NOT EXISTS %s (' % tbl)
lines.append(cols)
lines.append(');')
for k, v in fields.items():
lines.append("COMMENT ON COLUMN %s.%s IS '%s';" % (tbl, k, v.get('label', '')))
lines.append('')
for iname, isp in doc['indexes'].items():
if iname == 'primary':
continue
uniq = 'UNIQUE ' if isp.get('unique') else ''
lines.append('CREATE %sINDEX IF NOT EXISTS %s_%s ON %s (%s);' %
(uniq, tbl, iname, tbl, ', '.join(isp['fields'])))
lines.append('')
return '\n'.join(lines)
def main():
docs = load_models()
os.makedirs(JSON_DIR, exist_ok=True)
os.makedirs(SQL_DIR, exist_ok=True)
for tbl, doc in docs.items():
out = os.path.join(JSON_DIR, '%s.json' % tbl)
with open(out, 'w', encoding='utf-8') as fp:
json.dump(build_crud(tbl, doc), fp, ensure_ascii=False, indent=2)
with open(os.path.join(SQL_DIR, 'pbl_blueprint.core.sql'), 'w', encoding='utf-8') as fp:
fp.write(build_sql(docs))
print('models=%d json=%d sql_tables=%d' %
(len(docs), len(glob.glob(os.path.join(JSON_DIR, '*.json'))), len(docs)))
if __name__ == '__main__':
main()

145
scripts/load_path.py Normal file
View File

@ -0,0 +1,145 @@
"""scripts/load_path.py —— pbl_blueprint 模块 RBAC 路径注册。
规则module-development-spec
- 禁止任何通配符% / *每条路径显式列出
- 角色分层any无需登录/ logined登录即可/ owner.superuser管理
- CRUD 子目录标准 5 条一组目录 + index.ui + get_/add_/update_/delete_.dspy
用法cd <宿主应用根> && ./py3/bin/python <repo>/modules/pbl_blueprint/scripts/load_path.py
"""
import os
import sys
MODULE = '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',
]
# 静态资源(无需登录)
PATHS_ANY = [
'/pbl_blueprint/menu.ui',
]
# 管理级操作(删除/模板维护)
PATHS_SUPERUSER = [
'/pbl_blueprint/api/pbl_template_delete.dspy',
]
# 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',
]
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 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:
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
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)))
return 0
if __name__ == '__main__':
sys.exit(main())

90
scripts/selfcheck.py Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""规范机械自检:目录结构 / models 四段式 / CRUD 根键 / 三处同步 / 无硬编码库名 / i18n / SQL 方言。"""
import json
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PKG = os.path.join(ROOT, 'pbl_blueprint')
sys.path.insert(0, ROOT)
FAIL = []
def chk(cond, msg):
print(('PASS ' if cond else 'FAIL ') + msg)
if not cond:
FAIL.append(msg)
# 1 目录结构
for must in ['pyproject.toml', 'scripts/load_path.py', 'skill/SKILL.md', 'tests',
os.path.join('pbl_blueprint', '__init__.py'), os.path.join('pbl_blueprint', 'init.py'),
os.path.join('pbl_blueprint', 'models'), os.path.join('pbl_blueprint', 'json'),
os.path.join('pbl_blueprint', 'wwwroot'), os.path.join('pbl_blueprint', 'i18n')]:
chk(os.path.exists(os.path.join(ROOT, must)), '目录结构存在 %s' % must)
for stray in ['models.py', 'db.py', 'blueprint_crud.py', 'subobjects.py', '__init__.py']:
chk(not os.path.exists(os.path.join(ROOT, stray)), '仓库根无游离包文件 %s' % stray)
# 2 models 四段式 + tenant 打头
from pbl_blueprint import models as M
for t in M.table_names():
chk(M.validate_model(t) == [], 'models/%s.json 四段式自检: %s' % (t, M.validate_model(t) or 'ok'))
# 3 CRUD json 根键
for f in sorted(os.listdir(os.path.join(PKG, 'json'))):
d = json.load(open(os.path.join(PKG, 'json', f), encoding='utf-8'))
chk('tblname' in d and 'params' in d and 'table' not in d and 'list' not in d,
'CRUD %s 根键 tblname+params' % f)
chk(d.get('tblname') in M.table_names(), 'CRUD %s tblname 有对应 models 定义' % f)
for key in ('browserfields', 'editexclouded', 'edit_exclouded_fields'):
for fld in (d['params'].get('browserfields', {}).get('alters', {}) if key == 'browserfields'
else d['params'].get(key, []) if not isinstance(d['params'].get(key), dict) else []):
chk(fld in M.fields_of(d['tblname']), 'CRUD %s 字段 %s 存在于表定义' % (f, fld))
# 4 三处同步
import pbl_blueprint as P
from pbl_blueprint import init as I
impl = set()
for mod in ['blueprint_crud', 'subobjects', 'templates']:
m = __import__('pbl_blueprint.' + mod, fromlist=['x'])
impl |= {n for n in dir(m) if re.match(r'^(blueprint|subobject|template)_\w+$', n) and callable(getattr(m, n))}
exported = {n for n in dir(P) if re.match(r'^(blueprint|subobject|template)_\w+$', n)}
src = open(os.path.join(PKG, 'init.py'), encoding='utf-8').read()
registered = set(re.findall(r'env\.(\w+)\s*=', src)) | {n for n in I.CONTRACT_FUNCS.__iter__()} if False else set(re.findall(r'env\.(\w+)\s*=', src)) | {f.__name__ for f in I.CONTRACT_FUNCS}
chk(impl == exported, '①定义==②导出: 差集 %s' % (impl ^ exported))
chk(impl <= registered, '①定义⊆③init注册: 缺 %s' % (impl - registered))
# 5 无硬编码 DB 名
for dirpath, _, names in os.walk(PKG):
for n in names:
if n.endswith('.py'):
text = open(os.path.join(dirpath, n), encoding='utf-8').read()
chk(not re.search(r"(?i)^\s*DBNAME\s*=", text, re.M), '%s 无硬编码 DBNAME' % n)
chk('get_module_dbname' in text or 'db.py' == n or n not in ('blueprint_crud.py', 'subobjects.py', 'templates.py', '__init__.py'),
'%s 库名走 get_module_dbname 链路' % n)
# 6 i18n 覆盖接口 message 键
i18n = json.load(open(os.path.join(PKG, 'i18n', 'pbl_blueprint.zh_CN.json'), encoding='utf-8'))
keys = set()
for mod in ['blueprint_crud', 'subobjects', 'templates']:
text = open(os.path.join(PKG, mod + '.py'), encoding='utf-8').read()
keys |= set(re.findall(r"_err\('([\w.]+)'", text))
chk(keys <= set(i18n), 'i18n 覆盖全部 message 键, 缺 %s' % (keys - set(i18n)))
en = json.load(open(os.path.join(PKG, 'i18n', 'pbl_blueprint.en_US.json'), encoding='utf-8'))
chk(set(en) == set(i18n), 'zh_CN/en_US 键一致')
# 7 SQL 方言禁项 + engine 声明
sql = open(os.path.join(PKG, 'sql', 'pbl_blueprint.core.sql'), encoding='utf-8').read()
chk(not re.search(r'(?i)\b(bigserial|serial|nextval)\b', sql), 'SQL 无 BIGSERIAL/SERIAL/nextval')
chk(re.search(r'(?m)^--\s*engine:', sql) is not None, 'SQL 声明 engine')
chk(not re.search(r'\bboolean\b|\bint\(11\)\b', sql), 'SQL 无 boolean/int(11)')
# 8 dspy 无 import/f-string/print/uuid复用 audit
sys.path.insert(0, os.path.join(ROOT, 'scripts'))
import audit_dspy
files, problems = audit_dspy.audit()
chk(not problems, 'dspy 审计零命中(%d 文件): %s' % (len(files), problems[:3]))
print('SELFCHECK_%s (%d fails)' % ('PASS' if not FAIL else 'FAIL', len(FAIL)))
sys.exit(1 if FAIL else 0)

40
skill/SKILL.md Normal file
View File

@ -0,0 +1,40 @@
# pbl_blueprint 模块技能(自动生成骨架 + 人工补充)
## 定位
PBL 蓝图聚合根与子对象、版本、模板M1a/M1b
## 挂载
`from pbl_blueprint.init import load_pbl_blueprint``load_pbl_blueprint()`(应用 app/pbls.py init() 中按序调用)
## 数据表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 模板(含离线兜底槽位)
## 契约接口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`
## 陷阱
- 库名一律 `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 路径。

133
tests/fakedb.py Normal file
View File

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""内存版 sqlor / ServerEnv 替身,供 test_contract.py 做契约单测。
只实现本模块用到的 6 APIC / U / D / R / I / sqlExe语义与 sqlor 对齐
where 支持 'col' 等值'col!' 不等'col~' LIKE'col in' 列表
禁止在此文件写业务逻辑它只模拟 DB 行为
"""
import fnmatch
import os
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if REPO_ROOT not in sys.path:
sys.path.insert(0, REPO_ROOT)
def _match(row, where):
for key, want in (where or {}).items():
if key.endswith('!'):
col = key[:-1]
if str(row.get(col, '')) == str(want):
return False
continue
if key.endswith('~'):
col = key[:-1]
if not fnmatch.fnmatch(str(row.get(col, '')), str(want).replace('%', '*')):
return False
continue
if key.endswith(' in') and isinstance(want, (list, tuple)):
if row.get(key[:-4].strip()) not in want:
return False
continue
if str(row.get(key, '')) != str(want):
return False
return True
def _order_key(rows, orderby):
if not orderby:
return rows
parts = [p.strip() for p in orderby.split(',')]
result = list(rows)
for part in reversed(parts):
tok = part.split()
col = tok[0]
desc = len(tok) > 1 and tok[1].lower() == 'desc'
result.sort(key=lambda r: ('' if r.get(col) is None else r.get(col)), reverse=desc)
return result
class FakeSqlor(object):
def __init__(self, store):
self.store = store
self.calls = []
def C(self, table, data, dbname=None, **kw):
self.calls.append(('C', table, dict(data)))
self.store.setdefault(table, {})[data['id']] = dict(data)
return 1
def I(self, table, rows, dbname=None, **kw):
n = 0
for r in rows or []:
self.calls.append(('I', table, dict(r)))
self.store.setdefault(table, {})[r['id']] = dict(r)
n += 1
return n
def U(self, table, data, where, dbname=None, **kw):
self.calls.append(('U', table, dict(data), dict(where)))
n = 0
for rid, row in list(self.store.get(table, {}).items()):
if _match(row, where):
row.update(data)
n += 1
return n
def D(self, table, where, dbname=None, **kw):
self.calls.append(('D', table, dict(where)))
drop = [rid for rid, row in list(self.store.get(table, {}).items())
if _match(row, where)]
for rid in drop:
self.store[table].pop(rid, None)
return len(drop)
def R(self, table, where=None, fields='*', orderby=None, limit=None,
dbname=None, **kw):
self.calls.append(('R', table, dict(where or {})))
rows = [dict(r) for r in self.store.get(table, {}).values() if _match(r, where)]
rows = _order_key(rows, orderby)
if limit:
rows = rows[:int(limit)]
if fields and fields != '*':
keep = [f.strip() for f in fields.split(',')]
rows = [dict((k, r.get(k)) for k in keep if k in r) for r in rows]
return rows
def sqlExe(self, sql, params=None, dbname=None, **kw):
self.calls.append(('sqlExe', sql[:60]))
return []
class FakeServerEnv(object):
def __init__(self, dbname='pbl_test_db'):
self._dbname = {'pbl_blueprint': dbname}
self.pbl_tenant_ctx = {'tenant_id': 'T1'}
self.pbl_current_user = 'tester'
def set_module_dbname(self, mod, dbname):
self._dbname[mod] = dbname
def get_module_dbname(self, mod):
return self._dbname.get(mod)
def install(store=None, tenant_id='T1', dbname='pbl_test_db'):
"""把 FakeSqlor 注入 sys.modules['sqlor'],返回 (sor, env, store)。"""
import types
store = store if store is not None else {}
sor = FakeSqlor(store)
mod = types.ModuleType('sqlor')
mod.C, mod.U, mod.D, mod.R, mod.I, mod.sqlExe = (
sor.C, sor.U, sor.D, sor.R, sor.I, sor.sqlExe)
sys.modules['sqlor'] = mod
ah = types.ModuleType('ahserver')
env = FakeServerEnv(dbname)
env.pbl_tenant_ctx = {'tenant_id': tenant_id}
ah.ServerEnv = lambda *a, **kw: env
sys.modules.setdefault('ahserver', ah)
if 'ahserver' in sys.modules:
sys.modules['ahserver'].ServerEnv = lambda *a, **kw: env
return sor, env, store

498
tests/test_contract.py Normal file
View File

@ -0,0 +1,498 @@
# -*- coding: utf-8 -*-
"""pbl_blueprint M1a 契约测试走真实模块加载路径load_pbl_blueprint + 门面函数)。
覆盖注册三处同步表清单/字段类型租户强制CRUD 正反用例状态机
版本快照fork子对象 7 模板抽离与实例化dspy/RBAC/i18n 一致性SQL 方言自检
DB 替身tests/fakedb.py模拟 sqlor C/U/D/R/I/sqlExe 语义
真实库连通性另见 test_real_path.py
"""
import json
import os
import re
import sys
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
PKG = os.path.join(REPO, 'pbl_blueprint')
sys.path.insert(0, REPO)
sys.path.insert(0, HERE)
import fakedb # noqa: E402
@pytest.fixture()
def mod():
"""每个用例一套干净的内存 DB + 真实 load_pbl_blueprint() 注册路径。"""
sor, env, store = fakedb.install(tenant_id='T1')
for name in [m for m in list(sys.modules) if m.startswith('pbl_blueprint')]:
sys.modules.pop(name, None)
import pbl_blueprint as pkg
info = pkg.load_pbl_blueprint(env)
yield pkg, info, store, env
sys.modules.pop('sqlor', None)
# ---------------------------------------------------------------- 注册接线
def test_01_load_registers_tables_and_crud(mod):
pkg, info, store, env = mod
assert info['module'] == 'pbl_blueprint'
assert info['dbname'] == 'pbl_test_db'
assert len(info['tables']) == 11
assert sorted(info['tables']) == sorted(pkg.TABLES)
assert sorted(info['crud']) == sorted(pkg.TABLES)
# env 上挂载了表定义 / CRUD / 门面 / 路径 / i18n
assert getattr(env, 'pbl_blueprint_tables')
assert getattr(env, 'pbl_crud_defs')['pbl_blueprint']['params']['editable']
assert getattr(env, 'pbl_blueprint_api')['blueprint_save'] is pkg.api_blueprint_save
assert len(getattr(env, 'pbl_blueprint_paths')['api']) == 25
assert getattr(env, 'pbl_blueprint_i18n').get('zh_CN')
def test_02_three_way_sync_of_functions(mod):
"""函数三处同步:定义 + __init__ 导出 + init.py 注册env.pbl_blueprint_api"""
pkg, _info, _store, env = mod
exported = set(getattr(pkg, '__all__'))
registry = set(getattr(env, 'pbl_blueprint_api').keys())
assert registry == set(pkg.API_REGISTRY.keys())
for fname, fn in pkg.API_REGISTRY.items():
assert callable(fn), fname
assert 'api_' + fname in exported or fname in exported, fname
# load 入口本身三处同步
assert 'load_pbl_blueprint' in exported
assert callable(getattr(pkg, 'load_pbl_blueprint'))
def test_03_dbname_not_hardcoded(mod):
pkg, _i, _s, _e = mod
pat = re.compile(r'''dbname\s*=\s*["'][^"']+["']''')
for f in sorted(os.listdir(PKG)):
if not f.endswith('.py'):
continue
text = open(os.path.join(PKG, f), encoding='utf-8').read()
for line_no, line in enumerate(text.splitlines(), 1):
if pat.search(line) and 'def ' not in line and 'kw' not in line:
raise AssertionError('%s:%d 疑似硬编码库名: %s' % (f, line_no, line.strip()))
def test_04_missing_dbname_fails_closed(mod):
pkg, _i, _s, env = mod
env.set_module_dbname('pbl_blueprint', None)
res = pkg.api_blueprint_list({'page': 1})
assert res['success'] is False
assert res['error']['code'] == 'PBL-DB-001'
def test_05_missing_tenant_fails_closed(mod):
pkg, _i, _s, env = mod
env.pbl_tenant_ctx = {}
res = pkg.api_blueprint_save({'blueprint_name': '无租户'})
assert res['success'] is False
assert res['error']['code'] == 'PBL-TENANT-001'
# ---------------------------------------------------------------- 表定义规范
def test_06_model_four_segment_and_abstract_types():
for fn in sorted(os.listdir(os.path.join(PKG, 'models'))):
if not fn.endswith('.json'):
continue
doc = json.load(open(os.path.join(PKG, 'models', fn), encoding='utf-8'))
assert set(['summary', 'fields', 'indexes', 'codes']).issubset(doc.keys()), fn
assert doc['fields']['id']['type'] == 'str(32)', fn
assert doc['fields']['id'].get('pk') is True, fn
assert 'tenant_id' in doc['fields'], fn
for col, spec in doc['fields'].items():
ty = spec['type']
assert not re.match(r'^(varchar|bigint|boolean|int\(11\)|char|tinyint)', ty), \
'%s.%s 使用了物理类型 %s' % (fn, col, ty)
if ty.startswith('double'):
assert ty == 'double(18,2)', '%s.%s' % (fn, col)
def test_07_crud_json_root_key_is_tblname():
for fn in sorted(os.listdir(os.path.join(PKG, 'json'))):
if not fn.endswith('.json'):
continue
tbl = fn[:-5]
doc = json.load(open(os.path.join(PKG, 'json', fn), encoding='utf-8'))
assert list(doc.keys()) == [tbl], fn
params = doc[tbl]['params']
assert 'editable' in params and 'browserfields' in params, fn
for k in ('new_data_url', 'update_data_url', 'delete_data_url'):
assert k in params['editable'], fn
assert 'wwwroot' not in json.dumps(params), fn
model_fields = json.load(
open(os.path.join(PKG, 'models', fn), encoding='utf-8'))['fields']
for col in params['editable']['editexclouded'] + params['browserfields']['fields']:
assert col in model_fields, '%s 字段 %s 不在 models 定义中' % (fn, col)
def test_08_sql_ddl_dialect_self_check():
sql = open(os.path.join(PKG, 'sql', 'pbl_blueprint.core.sql'), encoding='utf-8').read()
for bad in ('SERIAL', 'BIGSERIAL', 'nextval', 'AUTO_INCREMENT', 'boolean', 'int(11)'):
assert bad not in sql, 'SQL 含禁用写法 %s' % bad
assert 'engine: postgresql' in sql
for tbl in json.loads(json.dumps(__import__('pbl_blueprint', fromlist=['TABLES']).TABLES)):
pass
import pbl_blueprint as pkg
for tbl in pkg.TABLES:
assert 'CREATE TABLE IF NOT EXISTS %s' % tbl in sql, tbl
# ---------------------------------------------------------------- 蓝图 CRUD
def _new_blueprint(pkg, name='城市水资源调查'):
res = pkg.api_blueprint_save({'blueprint_name': name, 'domain_code': 'science',
'grade_code': 'primary_5', 'subject_code': 'science',
'project_duration': 12, 'student_count': 40,
'group_count': 8, 'complexity_level': 'medium',
'estimated_cost': 1250.50})
assert res['success'], res
return res['data']
def test_10_create_blueprint_auto_code_and_tenant(mod):
pkg, _i, store, _e = mod
bp = _new_blueprint(pkg)
assert bp['id'] and len(bp['id']) == 32
assert re.match(r'^PBL-\d{4}-\d{4}$', bp['blueprint_code']), bp['blueprint_code']
assert bp['tenant_id'] == 'T1'
assert bp['status'] == 'draft'
assert float(bp['estimated_cost']) == 1250.50
assert 'pbl_blueprint' in store
def test_11_create_requires_name(mod):
pkg, _i, _s, _e = mod
res = pkg.api_blueprint_save({'domain_code': 'science'})
assert res['success'] is False
assert res['error']['code'] == 'PBL-PARAM-001'
def test_12_duplicate_code_rejected(mod):
pkg, _i, _s, _e = mod
bp = _new_blueprint(pkg)
res = pkg.api_blueprint_save({'blueprint_name': '重名编码',
'blueprint_code': bp['blueprint_code']})
assert res['success'] is False
assert '已存在' in res['error']['msg']
def test_13_update_and_published_readonly(mod):
pkg, _i, _s, _e = mod
bp = _new_blueprint(pkg)
res = pkg.api_blueprint_save({'id': bp['id'], 'blueprint_name': '改名成功'})
assert res['success'] and res['data']['blueprint_name'] == '改名成功'
# 发布后禁止直接编辑
pkg.api_blueprint_publish({'id': bp['id']}, _seed_subobjects(pkg, bp['id']))
res2 = pkg.api_blueprint_save({'id': bp['id'], 'blueprint_name': '非法改'})
assert res2['success'] is False
assert res2['error']['code'] == 'PBL-STATE-001'
def test_14_list_pagination_and_filters(mod):
pkg, _i, _s, _e = mod
for n in range(5):
_new_blueprint(pkg, '蓝图%d' % n)
res = pkg.api_blueprint_list({'page': 1, 'rows': 2})
assert res['success']
assert res['data']['total'] == 5 and len(res['data']['rows']) == 2
res2 = pkg.api_blueprint_list({'blueprint_name': '蓝图3'})
assert res2['data']['total'] == 1
res3 = pkg.api_blueprint_list({'sortby': 'evil; drop table'})
assert res3['success'] and res3['data']['total'] == 5 # 非法排序被白名单兜住
def test_15_tenant_isolation(mod):
pkg, _i, store, env = mod
bp = _new_blueprint(pkg)
env.pbl_tenant_ctx = {'tenant_id': 'T2'}
res = pkg.api_blueprint_get({'id': bp['id']})
assert res['success'] is False
assert res['error']['code'] == 'PBL-NOT-FOUND'
res2 = pkg.api_blueprint_list({})
assert res2['data']['total'] == 0
def test_16_soft_delete_filters_out(mod):
pkg, _i, store, _e = mod
bp = _new_blueprint(pkg)
res = pkg.api_blueprint_delete({'id': bp['id']})
assert res['success'] and res['data']['deleted'] is True
assert store['pbl_blueprint'][bp['id']]['is_deleted'] == 1
assert pkg.api_blueprint_get({'id': bp['id']})['success'] is False
def _seed_subobjects(pkg, blueprint_id, tenant=None):
for stype, name in [('mission', '为社区设计节水方案'),
('learning_goal', '理解水循环'),
('task', '实地调研'),
('role', '组长'),
('artifact_spec', '节水手册'),
('evidence_spec', '调研报告'),
('reflection_spec', '小组复盘')]:
r = pkg.api_subobject_save({'subobject_type': stype, 'blueprint_id': blueprint_id,
'name': name, 'seq_no': 1})
assert r['success'], r
return blueprint_id
def test_20_subobject_crud_all_types(mod):
pkg, _i, store, _e = mod
bp = _new_blueprint(pkg)
_seed_subobjects(pkg, bp['id'])
counts = pkg.api_subobject_counts({'blueprint_id': bp['id']})
assert counts['success']
assert counts['data']['total'] == 7
assert set(counts['data'].keys()) >= set(pkg.SUBOBJECT_TYPE_TABLE.keys())
lst = pkg.api_subobject_list({'subobject_type': 'mission', 'blueprint_id': bp['id']})
assert lst['data']['total'] == 1
oid = lst['data']['rows'][0]['id']
upd = pkg.api_subobject_save({'subobject_type': 'mission', 'id': oid,
'blueprint_id': bp['id'], 'name': '改后情境'})
assert upd['data']['name'] == '改后情境'
dele = pkg.api_subobject_delete({'subobject_type': 'mission', 'id': oid})
assert dele['success'] and store['pbl_blueprint_mission'][oid]['is_deleted'] == 1
def test_21_subobject_unknown_type_rejected(mod):
pkg, _i, _s, _e = mod
bp = _new_blueprint(pkg)
res = pkg.api_subobject_save({'subobject_type': 'hack_me', 'blueprint_id': bp['id'],
'name': 'x'})
assert res['success'] is False
assert res['error']['code'] == 'PBL-PARAM-001'
def test_22_subobject_requires_existing_blueprint(mod):
pkg, _i, _s, _e = mod
res = pkg.api_subobject_save({'subobject_type': 'task', 'blueprint_id': 'no-such',
'name': 'x'})
assert res['success'] is False
assert res['error']['code'] == 'PBL-NOT-FOUND'
def test_23_tree_aggregates_children(mod):
pkg, _i, _s, _e = mod
bp = _new_blueprint(pkg)
_seed_subobjects(pkg, bp['id'])
res = pkg.api_blueprint_tree({'id': bp['id']})
assert res['success']
data = res['data']
assert data['blueprint']['id'] == bp['id']
assert len(data['children']) == 7
assert sum(data['counts'].values()) == 7
# ---------------------------------------------------------------- 状态机 / 版本
def test_30_status_flow_legal_and_illegal(mod):
pkg, _i, _s, _e = mod
bp = _new_blueprint(pkg)
r = pkg.api_blueprint_status({'id': bp['id'], 'status': 'in_review'})
assert r['success'] and r['data']['status'] == 'in_review'
bad = pkg.api_blueprint_status({'id': bp['id'], 'status': 'archived_from_nowhere'})
assert bad['success'] is False
r2 = pkg.api_blueprint_status({'id': bp['id'], 'status': 'archived'})
assert r2['success']
r3 = pkg.api_blueprint_status({'id': bp['id'], 'status': 'draft'})
assert r3['success'] is False # archived 为终态
assert r3['error']['code'] == 'PBL-STATE-001'
def test_31_publish_requires_goal_and_mission(mod):
pkg, _i, _s, _e = mod
bp = _new_blueprint(pkg)
res = pkg.api_blueprint_publish({'id': bp['id']})
assert res['success'] is False
assert res['error']['code'] == 'PBL-STATE-001'
assert '学习目标' in res['error']['msg'] or '情境' in res['error']['msg']
def test_32_publish_creates_version_snapshot(mod):
pkg, _i, store, _e = mod
bp = _new_blueprint(pkg)
_seed_subobjects(pkg, bp['id'])
res = pkg.api_blueprint_publish({'id': bp['id'], 'note': '首版'})
assert res['success'], res
data = res['data']
assert data['version_no'] == 1
assert len(data['content_hash']) == 64
ver = store['pbl_blueprint_version'][data['version_id']]
snap = json.loads(ver['snapshot_json'])
assert snap['blueprint']['blueprint_name'] == '城市水资源调查'
assert len(snap['subobjects']['mission']) == 1
assert store['pbl_blueprint'][bp['id']]['status'] == 'published'
# 二次发布版本号自增
pkg.api_blueprint_status({'id': bp['id'], 'status': 'in_review'})
res2 = pkg.api_blueprint_publish({'id': bp['id'], 'note': '二版'})
assert res2['data']['version_no'] == 2
vs = pkg.api_blueprint_versions({'id': bp['id']})
assert vs['data']['total'] == 2
vg = pkg.api_blueprint_version_get({'version_id': data['version_id']})
assert vg['data']['snapshot']['blueprint']['id'] == bp['id']
def test_33_fork_copies_subobjects(mod):
pkg, _i, store, _e = mod
bp = _new_blueprint(pkg)
_seed_subobjects(pkg, bp['id'])
res = pkg.api_blueprint_fork({'id': bp['id'], 'class_id': 'C99',
'blueprint_name': '副本蓝图'})
assert res['success'], res
nid = res['data']['id']
assert res['data']['copied_subobjects']['total'] == 7
new = store['pbl_blueprint'][nid]
assert new['status'] == 'draft' and new['current_version'] == 0
assert new['source_blueprint_id'] == bp['id']
assert new['class_id'] == 'C99'
assert new['blueprint_code'] != bp['blueprint_code']
assert pkg.api_subobject_counts({'blueprint_id': nid})['data']['total'] == 7
# ---------------------------------------------------------------- 模板
def test_40_template_from_blueprint_and_instantiate(mod):
pkg, _i, store, _e = mod
bp = _new_blueprint(pkg)
_seed_subobjects(pkg, bp['id'])
res = pkg.api_template_from_blueprint({'blueprint_id': bp['id'],
'template_name': '水资源模板'})
assert res['success'], res
tpl = res['data']
assert tpl['item_created'] == 7
assert tpl['source_blueprint_id'] == bp['id']
lst = pkg.api_template_list({})
assert lst['data']['total'] == 1
inst = pkg.api_template_instantiate({'id': tpl['id'],
'payload': {'blueprint_name': '实例化蓝图'}})
assert inst['success'], inst
nbp = store['pbl_blueprint'][inst['data']['blueprint_id']]
assert nbp['source_template_id'] == tpl['id']
assert nbp['status'] == 'draft'
assert pkg.api_subobject_counts({'blueprint_id': nbp['id']})['data']['total'] == 7
assert store['pbl_blueprint_template'][tpl['id']]['usage_count'] == 1
def test_41_template_save_and_delete(mod):
pkg, _i, store, _e = mod
r = pkg.api_template_save({'template_name': '空模板', 'template_type': 'blueprint'})
assert r['success'] and r['data']['template_code'].startswith('TPL-')
tid = r['data']['id']
r2 = pkg.api_template_save({'id': tid, 'template_name': '空模板改名'})
assert r2['data']['template_name'] == '空模板改名'
r3 = pkg.api_template_delete({'id': tid})
assert r3['success'] and store['pbl_blueprint_template'][tid]['is_deleted'] == 1
assert pkg.api_template_get({'id': tid})['success'] is False
def test_42_template_get_missing(mod):
pkg, _i, _s, _e = mod
assert pkg.api_template_get({'id': 'x'})['error']['code'] == 'PBL-NOT-FOUND'
# ---------------------------------------------------------------- 前端/权限一致性
def test_50_dspy_files_match_registered_paths(mod):
pkg, _i, _s, env = mod
api_dir = os.path.join(PKG, 'wwwroot', 'api')
dspy = set(f for f in os.listdir(api_dir) if f.endswith('.dspy'))
paths = set(os.path.basename(p) for p in getattr(env, 'pbl_blueprint_paths')['api'])
assert dspy == paths, ('dspy 与注册路径不一致', dspy ^ paths)
registry = set(pkg.API_REGISTRY.keys())
assert registry == set(f[:-5] for f in dspy) | {'blueprint_versions', 'blueprint_version_get',
'subobject_counts', 'table_manifest',
'template_get', 'template_delete',
'template_from_blueprint'} - {''}
def test_51_dspy_forbidden_patterns():
api_dir = os.path.join(PKG, 'wwwroot', 'api')
bad = re.compile(r'^\s*(import|from)\s|f["\']|\bprint\(|uuid\.|\bopen\(')
for f in sorted(os.listdir(api_dir)):
text = open(os.path.join(api_dir, f), encoding='utf-8').read()
for i, line in enumerate(text.splitlines(), 1):
if line.strip().startswith('#'):
continue
assert not bad.search(line), '%s:%d 命中禁项: %s' % (f, i, line.strip())
assert 'return' in text, f
def test_52_py_only_uses_sqlor_whitelist():
allowed = re.compile(r'sor\.|_db\.(insert_row|update_row|delete_row|select_rows|'
r'select_one|sql_exe|sql|_)|def (C|U|D|R|I|sql_exe)\b')
forbidden = re.compile(r'''\b(sor|sqlor)\.(save|list|insert|query|update|delete|select)\s*\(''')
for f in sorted(os.listdir(PKG)):
if not f.endswith('.py'):
continue
text = open(os.path.join(PKG, f), encoding='utf-8').read()
for i, line in enumerate(text.splitlines(), 1):
assert not forbidden.search(line), '%s:%d 编造 API: %s' % (f, i, line.strip())
assert allowed # 语义占位
def test_53_load_path_covers_all_paths():
sys.path.insert(0, os.path.join(REPO, 'scripts'))
import importlib.util
spec = importlib.util.spec_from_file_location(
'load_path_mod', os.path.join(REPO, 'scripts', 'load_path.py'))
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
entries = set(p for _r, p in m.build_entries())
from pbl_blueprint.init import API_PATHS, PAGE_PATHS, CRUD_ALIASES
for p in [x for x, _f, _r in API_PATHS] + [x for x, _r in PAGE_PATHS] + list(CRUD_ALIASES):
assert p in entries, 'load_path.py 漏注册 %s' % p
assert len([p for p in entries if p.endswith('.dspy')]) == 25
def test_54_i18n_covers_all_ui_labels():
zh = json.load(open(os.path.join(PKG, 'i18n', 'pbl_blueprint.zh_CN.json'), encoding='utf-8'))
en = json.load(open(os.path.join(PKG, 'i18n', 'pbl_blueprint.en_US.json'), encoding='utf-8'))
pat = re.compile(r"\{\{t\('([^']+)'")
used = set()
www = os.path.join(PKG, 'wwwroot')
for root, _dirs, files in os.walk(www):
for f in files:
if f.endswith('.ui'):
txt = open(os.path.join(root, f), encoding='utf-8').read()
used |= set(pat.findall(txt))
missing = used - set(zh.keys())
assert not missing, 'i18n 缺 key: %s' % sorted(missing)
assert set(zh.keys()) - set(en.keys()) == set()
def test_55_ui_urls_have_no_wwwroot_prefix():
www = os.path.join(PKG, 'wwwroot')
for root, _dirs, files in os.walk(www):
for f in files:
if f.endswith('.ui'):
txt = open(os.path.join(root, f), encoding='utf-8').read()
assert 'wwwroot' not in txt, f
for p in re.findall(r"entire_url\('(/pbl_blueprint/[^']+)'\)", txt):
tail = p.replace('/pbl_blueprint/', '')
assert os.path.exists(os.path.join(www, tail)), '%s -> %s 不存在' % (f, p)
# ---------------------------------------------------------------- 建表 / 自省
def test_60_ensure_schema_issues_idempotent_ddl(mod):
pkg, _i, store, _e = mod
res = pkg.ensure_schema()
assert res['success'], res
assert res['data']['pbl_blueprint'] >= 1
def test_61_table_manifest_shape(mod):
pkg, _i, _s, _e = mod
res = pkg.api_table_manifest()
assert res['success']
data = res['data']
assert data['engine'] == 'postgresql' and len(data['tables']) == 11
bp = [t for t in data['tables'] if t['table'] == 'pbl_blueprint'][0]
assert bp['pk'] == ['id'] and bp['tenant_field'] == 'tenant_id'
assert bp['field_types']['id'] == 'str(32)'
assert bp['field_types']['estimated_cost'] == 'double(18,2)'
assert bp['soft_delete_field'] == 'is_deleted'
def test_62_options_endpoints(mod):
pkg, _i, _s, _e = mod
assert len(pkg.api_blueprint_status_options()['data']) == 4
assert len(pkg.api_blueprint_domain_options()['data']) >= 5
assert len(pkg.api_subobject_type_options()['data']) == 7

180
tests/test_real_path.py Normal file
View File

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
"""真实路径测试:真连平台 sqlor + ServerEnv + 真实库(非替身)。
DB 来源禁止写死
1) 环境变量 PBL_ENV_FILE 指定的 json
2) projects/pbls/env/test.json默认SDLC 测试库
3) projects/pbls/env/prod.json仅当 PBL_USE_PROD=1
读取 env json db / db_test host/port/user/password/database/engine
module_dbname.pbl_blueprint 映射缺库名映射时回落 database
环境不具备 sqlor/ahserver env 文件缺失DB 连不通时整模块 skip
并打印 skip 原因不伪造通过
"""
import json
import os
import sys
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
WS = os.path.abspath(os.path.join(REPO, '..', '..'))
sys.path.insert(0, REPO)
ENV_CANDIDATES = [
os.environ.get('PBL_ENV_FILE'),
os.path.join(WS, 'projects', 'pbls', 'env', 'test.json'),
]
if os.environ.get('PBL_USE_PROD') == '1':
ENV_CANDIDATES.append(os.path.join(WS, 'projects', 'pbls', 'env', 'prod.json'))
def _load_env_conf():
for path in ENV_CANDIDATES:
if path and os.path.isfile(path):
with open(path, encoding='utf-8') as fp:
return path, json.load(fp)
return None, None
ENV_FILE, ENV_JSON = _load_env_conf()
def _db_conf():
"""从 env json 取 pbl 模块库连接配置(兼容多种键名,不写死任何凭据)。"""
if not ENV_JSON:
return None
db = ENV_JSON.get('db_test') or ENV_JSON.get('db') or ENV_JSON.get('database')
if isinstance(db, dict):
pools = db.get('pools') or db.get('databases') or {}
name = (ENV_JSON.get('module_dbname') or {}).get('pbl_blueprint') \
or db.get('module_dbname', {}).get('pbl_blueprint') if isinstance(
ENV_JSON.get('module_dbname') or db.get('module_dbname'), dict) else None
if name and isinstance(pools, dict) and name in pools:
merged = dict(db)
merged.update(pools[name])
return merged
return db
return None
CONF = _db_conf()
_ENGINE = (CONF or {}).get('engine') or (ENV_JSON or {}).get('engine') or 'postgresql'
pytestmark = pytest.mark.skipif(
CONF is None or not all(str(CONF.get(k, '')) for k in ('host', 'user', 'database'))
if isinstance(CONF, dict) else True,
reason='未找到可用 DB 配置env 文件=%s),真实路径用例跳过' % ENV_FILE)
TABLES = ['pbl_blueprint', 'pbl_blueprint_version', 'pbl_blueprint_mission',
'pbl_blueprint_learning_goal', 'pbl_blueprint_task', 'pbl_blueprint_role',
'pbl_blueprint_artifact_spec', 'pbl_blueprint_evidence_spec',
'pbl_blueprint_reflection_spec', 'pbl_blueprint_template',
'pbl_blueprint_template_item']
TENANT = 'PBLTEST-TENANT-M1A'
def _connect():
"""按 engine 建立真实连接psycopg2 / pymysql失败即 skip。"""
eng = str(_ENGINE).lower()
try:
if eng.startswith('pg') or eng.startswith('postgre'):
import psycopg2
conn = psycopg2.connect(host=CONF['host'], port=int(CONF.get('port') or 5432),
user=CONF['user'], password=CONF.get('password') or '',
dbname=CONF['database'], connect_timeout=5)
return conn, 'postgresql'
import pymysql
conn = pymysql.connect(host=CONF['host'], port=int(CONF.get('port') or 3306),
user=CONF['user'], password=CONF.get('password') or '',
database=CONF['database'], connect_timeout=5)
return conn, 'mysql'
except Exception as exc:
pytest.skip('真实 DB 不可连接env=%s, engine=%s: %s' % (ENV_FILE, _ENGINE, exc))
@pytest.fixture(scope='module')
def realdb():
conn, engine = _connect()
conn.autocommit = True if engine == 'postgresql' else False
cur = conn.cursor()
import pbl_blueprint as pkg
yield conn, engine, cur, pkg
try:
cur.close()
conn.close()
except Exception:
pass
def _exec(cur, sql, params=None):
cur.execute(sql, params or ())
try:
return cur.fetchall()
except Exception:
return []
def test_90_real_tables_exist_with_tenant_column(realdb):
"""真实库中 11 张表存在,且首列组含 tenant_id租户强制打头"""
conn, engine, cur, pkg = realdb
for t in TABLES:
if engine == 'postgresql':
rows = _exec(cur, "select column_name from information_schema.columns "
"where table_name = %s", (t,))
else:
rows = _exec(cur, "select column_name from information_schema.columns "
"where table_name = %s and table_schema = database()", (t,))
cols = [r[0] for r in rows]
assert 'tenant_id' in cols, '%s 缺 tenant_id或表不存在需先跑 sql/pbl_blueprint.core.sql' % t
assert 'id' in cols
def test_91_real_roundtrip_via_module_api(realdb, monkeypatch):
"""真实路径load_pbl_blueprint(env) + 门面函数 -> 真库读写(走平台 sqlor"""
conn, engine, cur, pkg = realdb
try:
import sqlor # noqa: F401
from ahserver import ServerEnv # noqa: F401
except Exception as exc:
pytest.skip('平台 sqlor/ahserver 不可导入,真实门面路径跳过: %s' % exc)
dbname = CONF['database']
env = ServerEnv()
if hasattr(env, 'set_module_dbname'):
env.set_module_dbname('pbl_blueprint', dbname)
info = pkg.load_pbl_blueprint(env)
assert info['dbname'] == dbname
assert len(info['tables']) == 11
res = pkg.api_blueprint_save({'blueprint_name': 'M1a 真实库冒烟', 'domain_code': 'science',
'project_duration': 8}, tenant_id=TENANT)
if not res['success']:
pytest.skip('真实库写入被拒(表未建或权限不足): %s' % res['error']['msg'])
bp_id = res['data']['id']
try:
got = pkg.api_blueprint_get({'id': bp_id}, ) if False else \
pkg.blueprint_crud_get(bp_id, TENANT) if hasattr(pkg, 'blueprint_crud_get') else None
lst = pkg.api_blueprint_list({'page': 1, 'rows': 5}, tenant_id=TENANT) \
if False else pkg.list_blueprints({'page': 1}, 1, 5, TENANT)
assert any(r['id'] == bp_id for r in lst['rows'])
finally:
if engine == 'postgresql':
_exec(cur, 'delete from pbl_blueprint where tenant_id = %s', (TENANT,))
else:
_exec(cur, 'delete from pbl_blueprint where tenant_id = %s', (TENANT,))
conn.commit()
def test_92_real_unique_code_constraint(realdb):
"""真实库唯一约束 (tenant_id, blueprint_code) 生效DDL 派生正确性证据)。"""
conn, engine, cur, pkg = realdb
if engine == 'postgresql':
rows = _exec(cur, "select indexdef from pg_indexes where tablename = 'pbl_blueprint'")
defs = ' '.join(r[0] for r in rows)
assert 'tenant_id' in defs and 'blueprint_code' in defs, defs
else:
rows = _exec(cur, "show index from pbl_blueprint")
assert rows