M1a: pbl_blueprint 蓝图核心表与CRUD契约(本地开发快照,接入远程仓库)
This commit is contained in:
parent
4d5dc65538
commit
a51815075f
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
*.egg-info/
|
||||
.venv/
|
||||
31
json/pbl_blueprint.json
Normal file
31
json/pbl_blueprint.json
Normal 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')}}"}
|
||||
]
|
||||
}
|
||||
}
|
||||
21
json/pbl_blueprint_artifact_spec.json
Normal file
21
json/pbl_blueprint_artifact_spec.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
json/pbl_blueprint_evidence_spec.json
Normal file
21
json/pbl_blueprint_evidence_spec.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
json/pbl_blueprint_learning_goal.json
Normal file
21
json/pbl_blueprint_learning_goal.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
json/pbl_blueprint_mission.json
Normal file
21
json/pbl_blueprint_mission.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
json/pbl_blueprint_reflection_spec.json
Normal file
21
json/pbl_blueprint_reflection_spec.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
json/pbl_blueprint_role.json
Normal file
21
json/pbl_blueprint_role.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
json/pbl_blueprint_task.json
Normal file
21
json/pbl_blueprint_task.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
json/pbl_blueprint_version.json
Normal file
21
json/pbl_blueprint_version.json
Normal 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
21
json/pbl_template.json
Normal 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
50
models/pbl_blueprint.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
44
models/pbl_blueprint_artifact_spec.json
Normal file
44
models/pbl_blueprint_artifact_spec.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
45
models/pbl_blueprint_evidence_spec.json
Normal file
45
models/pbl_blueprint_evidence_spec.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
39
models/pbl_blueprint_learning_goal.json
Normal file
39
models/pbl_blueprint_learning_goal.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
38
models/pbl_blueprint_mission.json
Normal file
38
models/pbl_blueprint_mission.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
39
models/pbl_blueprint_reflection_spec.json
Normal file
39
models/pbl_blueprint_reflection_spec.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
39
models/pbl_blueprint_role.json
Normal file
39
models/pbl_blueprint_role.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
45
models/pbl_blueprint_task.json
Normal file
45
models/pbl_blueprint_task.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
36
models/pbl_blueprint_version.json
Normal file
36
models/pbl_blueprint_version.json
Normal 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
40
models/pbl_template.json
Normal 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
64
pbl_blueprint/__init__.py
Normal 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
1165
pbl_blueprint/api.py
Normal file
File diff suppressed because it is too large
Load Diff
346
pbl_blueprint/blueprint_crud.py
Normal file
346
pbl_blueprint/blueprint_crud.py
Normal 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 -> archived;published -> 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
200
pbl_blueprint/db.py
Normal file
@ -0,0 +1,200 @@
|
||||
"""pbl_blueprint 数据访问适配层(唯一允许直接调用 sqlor 的地方)。
|
||||
|
||||
规范约束:
|
||||
- 只使用 sqlor 标准 API:sor.C / sor.U / sor.D / sor.R / sor.I / sor.sqlExe
|
||||
(禁止 save()/list()/insert()/query() 等编造 API)。
|
||||
- 库名一律通过 ServerEnv().get_module_dbname('pbl_blueprint') 获取,禁止硬编码 DBNAME。
|
||||
- 租户隔离:所有读写强制带 tenant_id,缺失即 fail-closed 抛错。
|
||||
"""
|
||||
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
|
||||
57
pbl_blueprint/i18n/pbl_blueprint.en_US.json
Normal file
57
pbl_blueprint/i18n/pbl_blueprint.en_US.json
Normal 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"
|
||||
}
|
||||
57
pbl_blueprint/i18n/pbl_blueprint.zh_CN.json
Normal file
57
pbl_blueprint/i18n/pbl_blueprint.zh_CN.json
Normal 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
88
pbl_blueprint/init.py
Normal 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
|
||||
22
pbl_blueprint/init/data.json
Normal file
22
pbl_blueprint/init/data.json
Normal 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": "资源" }
|
||||
]
|
||||
}
|
||||
124
pbl_blueprint/json/pbl_blueprint.json
Normal file
124
pbl_blueprint/json/pbl_blueprint.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
pbl_blueprint/json/pbl_blueprint_artifact_spec.json
Normal file
89
pbl_blueprint/json/pbl_blueprint_artifact_spec.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
pbl_blueprint/json/pbl_blueprint_evidence_spec.json
Normal file
89
pbl_blueprint/json/pbl_blueprint_evidence_spec.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
pbl_blueprint/json/pbl_blueprint_learning_goal.json
Normal file
89
pbl_blueprint/json/pbl_blueprint_learning_goal.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
pbl_blueprint/json/pbl_blueprint_mission.json
Normal file
89
pbl_blueprint/json/pbl_blueprint_mission.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
98
pbl_blueprint/json/pbl_blueprint_reflection_spec.json
Normal file
98
pbl_blueprint/json/pbl_blueprint_reflection_spec.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
pbl_blueprint/json/pbl_blueprint_role.json
Normal file
89
pbl_blueprint/json/pbl_blueprint_role.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
pbl_blueprint/json/pbl_blueprint_task.json
Normal file
89
pbl_blueprint/json/pbl_blueprint_task.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
141
pbl_blueprint/json/pbl_blueprint_template.json
Normal file
141
pbl_blueprint/json/pbl_blueprint_template.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
82
pbl_blueprint/json/pbl_blueprint_template_item.json
Normal file
82
pbl_blueprint/json/pbl_blueprint_template_item.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
82
pbl_blueprint/json/pbl_blueprint_version.json
Normal file
82
pbl_blueprint/json/pbl_blueprint_version.json
Normal 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
168
pbl_blueprint/models.py
Normal 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
|
||||
185
pbl_blueprint/models/pbl_blueprint.json
Normal file
185
pbl_blueprint/models/pbl_blueprint.json
Normal 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": "蓝图状态"
|
||||
}
|
||||
}
|
||||
}
|
||||
133
pbl_blueprint/models/pbl_blueprint_artifact_spec.json
Normal file
133
pbl_blueprint/models/pbl_blueprint_artifact_spec.json
Normal 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": "产出物类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
133
pbl_blueprint/models/pbl_blueprint_evidence_spec.json
Normal file
133
pbl_blueprint/models/pbl_blueprint_evidence_spec.json
Normal 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": "证据类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
138
pbl_blueprint/models/pbl_blueprint_learning_goal.json
Normal file
138
pbl_blueprint/models/pbl_blueprint_learning_goal.json
Normal 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": "目标类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
138
pbl_blueprint/models/pbl_blueprint_mission.json
Normal file
138
pbl_blueprint/models/pbl_blueprint_mission.json
Normal 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": "情境类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
137
pbl_blueprint/models/pbl_blueprint_reflection_spec.json
Normal file
137
pbl_blueprint/models/pbl_blueprint_reflection_spec.json
Normal 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": "频次"
|
||||
}
|
||||
}
|
||||
}
|
||||
132
pbl_blueprint/models/pbl_blueprint_role.json
Normal file
132
pbl_blueprint/models/pbl_blueprint_role.json
Normal 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": "角色类别"
|
||||
}
|
||||
}
|
||||
}
|
||||
137
pbl_blueprint/models/pbl_blueprint_task.json
Normal file
137
pbl_blueprint/models/pbl_blueprint_task.json
Normal 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": "任务类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
159
pbl_blueprint/models/pbl_blueprint_template.json
Normal file
159
pbl_blueprint/models/pbl_blueprint_template.json
Normal 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": "状态"
|
||||
}
|
||||
}
|
||||
}
|
||||
106
pbl_blueprint/models/pbl_blueprint_template_item.json
Normal file
106
pbl_blueprint/models/pbl_blueprint_template_item.json
Normal 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": "子对象类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
124
pbl_blueprint/models/pbl_blueprint_version.json
Normal file
124
pbl_blueprint/models/pbl_blueprint_version.json
Normal 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": "版本状态"
|
||||
}
|
||||
}
|
||||
}
|
||||
520
pbl_blueprint/sql/pbl_blueprint.core.sql
Normal file
520
pbl_blueprint/sql/pbl_blueprint.core.sql
Normal 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
213
pbl_blueprint/subobjects.py
Normal file
@ -0,0 +1,213 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""蓝图子对象契约接口(M1a 落表 + CRUD;M1b 扩展校验/编排)。
|
||||
|
||||
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
238
pbl_blueprint/templates.py
Normal 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):
|
||||
"""把蓝图抽为模板:主表 + 每类子对象一条 item(payload 可覆盖模板字段)。"""
|
||||
tid = tenant_id or _db.current_tenant_id()
|
||||
payload = dict(payload or {})
|
||||
bp = _bp.get_blueprint(blueprint_id, tid)
|
||||
tpl_payload = {
|
||||
'template_name': payload.get('template_name') or ('%s 模板' % (bp.get('blueprint_name') or '')),
|
||||
'template_code': payload.get('template_code') or '',
|
||||
'template_type': payload.get('template_type', 'blueprint'),
|
||||
'domain_code': bp.get('domain_code', ''), 'grade_code': bp.get('grade_code', ''),
|
||||
'subject_code': bp.get('subject_code', ''),
|
||||
'complexity_level': bp.get('complexity_level', ''),
|
||||
'scope': payload.get('scope', 'tenant'),
|
||||
'source_blueprint_id': blueprint_id,
|
||||
'description': payload.get('description', ''),
|
||||
'class_id': payload.get('class_id') or bp.get('class_id', ''),
|
||||
}
|
||||
tpl = save_template(tpl_payload, tid)
|
||||
now = _db.now_str()
|
||||
operator = _db.current_operator()
|
||||
children = _s.list_all_subobjects(blueprint_id, tid)
|
||||
created = 0
|
||||
for stype in sorted(children):
|
||||
seq = 0
|
||||
for obj in children[stype]:
|
||||
payload_json = dict((k, v) for k, v in obj.items()
|
||||
if k not in ('id', 'tenant_id', 'blueprint_id', 'version_id',
|
||||
'created_by', 'created_at', 'updated_by',
|
||||
'updated_at', 'is_deleted'))
|
||||
_db.insert_row(TABLE_ITEM, {
|
||||
'id': _db.gen_id(), 'tenant_id': tid,
|
||||
'class_id': obj.get('class_id') or '', 'template_id': tpl['id'],
|
||||
'subobject_type': stype, 'code': obj.get('code') or '',
|
||||
'name': obj.get('name') or '', 'description': obj.get('description') or '',
|
||||
'item_payload_json': _db.dumps(payload_json), 'seq_no': seq,
|
||||
'required_flag': int(obj.get('required_flag') or 1),
|
||||
'status': 'active', 'is_deleted': 0,
|
||||
'created_by': operator, 'created_at': now,
|
||||
'updated_by': operator, 'updated_at': now,
|
||||
})
|
||||
seq += 1
|
||||
created += 1
|
||||
_db.audit('template_from_blueprint', TABLE, tpl['id'],
|
||||
after={'source_blueprint_id': blueprint_id, 'items': created}, tenant_id=tid)
|
||||
tpl = get_template(tpl['id'], tid)
|
||||
tpl['item_created'] = created
|
||||
return tpl
|
||||
|
||||
|
||||
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': '已归档'}]
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_delete.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_delete.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_domain_options.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_domain_options.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_fork.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_fork.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_get.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_get.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_list.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_list.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_publish.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_publish.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_save.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_save.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_status.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_status.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_status_options.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_status_options.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_tree.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_tree.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_version_get.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_version_get.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/blueprint_versions.dspy
Normal file
24
pbl_blueprint/wwwroot/api/blueprint_versions.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/subobject_counts.dspy
Normal file
24
pbl_blueprint/wwwroot/api/subobject_counts.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/subobject_delete.dspy
Normal file
24
pbl_blueprint/wwwroot/api/subobject_delete.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/subobject_get.dspy
Normal file
24
pbl_blueprint/wwwroot/api/subobject_get.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/subobject_list.dspy
Normal file
24
pbl_blueprint/wwwroot/api/subobject_list.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/subobject_save.dspy
Normal file
24
pbl_blueprint/wwwroot/api/subobject_save.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/subobject_type_options.dspy
Normal file
24
pbl_blueprint/wwwroot/api/subobject_type_options.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/table_manifest.dspy
Normal file
24
pbl_blueprint/wwwroot/api/table_manifest.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/template_delete.dspy
Normal file
24
pbl_blueprint/wwwroot/api/template_delete.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/template_from_blueprint.dspy
Normal file
24
pbl_blueprint/wwwroot/api/template_from_blueprint.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/template_get.dspy
Normal file
24
pbl_blueprint/wwwroot/api/template_get.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/template_instantiate.dspy
Normal file
24
pbl_blueprint/wwwroot/api/template_instantiate.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/template_list.dspy
Normal file
24
pbl_blueprint/wwwroot/api/template_list.dspy
Normal 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
|
||||
24
pbl_blueprint/wwwroot/api/template_save.dspy
Normal file
24
pbl_blueprint/wwwroot/api/template_save.dspy
Normal 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
|
||||
50
pbl_blueprint/wwwroot/blueprint_editor.ui
Normal file
50
pbl_blueprint/wwwroot/blueprint_editor.ui
Normal 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>
|
||||
28
pbl_blueprint/wwwroot/blueprint_list.ui
Normal file
28
pbl_blueprint/wwwroot/blueprint_list.ui
Normal 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>
|
||||
36
pbl_blueprint/wwwroot/blueprint_tree.ui
Normal file
36
pbl_blueprint/wwwroot/blueprint_tree.ui
Normal 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>
|
||||
22
pbl_blueprint/wwwroot/index.ui
Normal file
22
pbl_blueprint/wwwroot/index.ui
Normal 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" } }
|
||||
]
|
||||
}
|
||||
12
pbl_blueprint/wwwroot/menu.ui
Normal file
12
pbl_blueprint/wwwroot/menu.ui
Normal 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>
|
||||
25
pbl_blueprint/wwwroot/template_list.ui
Normal file
25
pbl_blueprint/wwwroot/template_list.ui
Normal 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
14
pyproject.toml
Normal 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
57
scripts/audit_dspy.py
Normal 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
166
scripts/derive_artifacts.py
Normal 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
145
scripts/load_path.py
Normal 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
90
scripts/selfcheck.py
Normal 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
40
skill/SKILL.md
Normal 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
133
tests/fakedb.py
Normal file
@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""内存版 sqlor / ServerEnv 替身,供 test_contract.py 做契约单测。
|
||||
|
||||
只实现本模块用到的 6 个 API:C / 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
498
tests/test_contract.py
Normal 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
180
tests/test_real_path.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user