From a51815075f06b37aedacdaeb9c6eef6c592ced9d Mon Sep 17 00:00:00 2001 From: pipeline Date: Tue, 15 Sep 2026 17:18:54 +0800 Subject: [PATCH] =?UTF-8?q?M1a:=20pbl=5Fblueprint=20=E8=93=9D=E5=9B=BE?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E8=A1=A8=E4=B8=8ECRUD=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=EF=BC=88=E6=9C=AC=E5=9C=B0=E5=BC=80=E5=8F=91=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=EF=BC=8C=E6=8E=A5=E5=85=A5=E8=BF=9C=E7=A8=8B=E4=BB=93=E5=BA=93?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + json/pbl_blueprint.json | 31 + json/pbl_blueprint_artifact_spec.json | 21 + json/pbl_blueprint_evidence_spec.json | 21 + json/pbl_blueprint_learning_goal.json | 21 + json/pbl_blueprint_mission.json | 21 + json/pbl_blueprint_reflection_spec.json | 21 + json/pbl_blueprint_role.json | 21 + json/pbl_blueprint_task.json | 21 + json/pbl_blueprint_version.json | 21 + json/pbl_template.json | 21 + models/pbl_blueprint.json | 50 + models/pbl_blueprint_artifact_spec.json | 44 + models/pbl_blueprint_evidence_spec.json | 45 + models/pbl_blueprint_learning_goal.json | 39 + models/pbl_blueprint_mission.json | 38 + models/pbl_blueprint_reflection_spec.json | 39 + models/pbl_blueprint_role.json | 39 + models/pbl_blueprint_task.json | 45 + models/pbl_blueprint_version.json | 36 + models/pbl_template.json | 40 + pbl_blueprint/__init__.py | 64 + pbl_blueprint/api.py | 1165 +++++++++++++++++ pbl_blueprint/blueprint_crud.py | 346 +++++ pbl_blueprint/db.py | 200 +++ pbl_blueprint/i18n/pbl_blueprint.en_US.json | 57 + pbl_blueprint/i18n/pbl_blueprint.zh_CN.json | 57 + pbl_blueprint/init.py | 88 ++ pbl_blueprint/init/data.json | 22 + pbl_blueprint/json/pbl_blueprint.json | 124 ++ .../json/pbl_blueprint_artifact_spec.json | 89 ++ .../json/pbl_blueprint_evidence_spec.json | 89 ++ .../json/pbl_blueprint_learning_goal.json | 89 ++ pbl_blueprint/json/pbl_blueprint_mission.json | 89 ++ .../json/pbl_blueprint_reflection_spec.json | 98 ++ pbl_blueprint/json/pbl_blueprint_role.json | 89 ++ pbl_blueprint/json/pbl_blueprint_task.json | 89 ++ .../json/pbl_blueprint_template.json | 141 ++ .../json/pbl_blueprint_template_item.json | 82 ++ pbl_blueprint/json/pbl_blueprint_version.json | 82 ++ pbl_blueprint/models.py | 168 +++ pbl_blueprint/models/pbl_blueprint.json | 185 +++ .../models/pbl_blueprint_artifact_spec.json | 133 ++ .../models/pbl_blueprint_evidence_spec.json | 133 ++ .../models/pbl_blueprint_learning_goal.json | 138 ++ .../models/pbl_blueprint_mission.json | 138 ++ .../models/pbl_blueprint_reflection_spec.json | 137 ++ pbl_blueprint/models/pbl_blueprint_role.json | 132 ++ pbl_blueprint/models/pbl_blueprint_task.json | 137 ++ .../models/pbl_blueprint_template.json | 159 +++ .../models/pbl_blueprint_template_item.json | 106 ++ .../models/pbl_blueprint_version.json | 124 ++ pbl_blueprint/sql/pbl_blueprint.core.sql | 520 ++++++++ pbl_blueprint/subobjects.py | 213 +++ pbl_blueprint/templates.py | 238 ++++ .../wwwroot/api/blueprint_delete.dspy | 24 + .../wwwroot/api/blueprint_domain_options.dspy | 24 + pbl_blueprint/wwwroot/api/blueprint_fork.dspy | 24 + pbl_blueprint/wwwroot/api/blueprint_get.dspy | 24 + pbl_blueprint/wwwroot/api/blueprint_list.dspy | 24 + .../wwwroot/api/blueprint_publish.dspy | 24 + pbl_blueprint/wwwroot/api/blueprint_save.dspy | 24 + .../wwwroot/api/blueprint_status.dspy | 24 + .../wwwroot/api/blueprint_status_options.dspy | 24 + pbl_blueprint/wwwroot/api/blueprint_tree.dspy | 24 + .../wwwroot/api/blueprint_version_get.dspy | 24 + .../wwwroot/api/blueprint_versions.dspy | 24 + .../wwwroot/api/subobject_counts.dspy | 24 + .../wwwroot/api/subobject_delete.dspy | 24 + pbl_blueprint/wwwroot/api/subobject_get.dspy | 24 + pbl_blueprint/wwwroot/api/subobject_list.dspy | 24 + pbl_blueprint/wwwroot/api/subobject_save.dspy | 24 + .../wwwroot/api/subobject_type_options.dspy | 24 + pbl_blueprint/wwwroot/api/table_manifest.dspy | 24 + .../wwwroot/api/template_delete.dspy | 24 + .../wwwroot/api/template_from_blueprint.dspy | 24 + pbl_blueprint/wwwroot/api/template_get.dspy | 24 + .../wwwroot/api/template_instantiate.dspy | 24 + pbl_blueprint/wwwroot/api/template_list.dspy | 24 + pbl_blueprint/wwwroot/api/template_save.dspy | 24 + pbl_blueprint/wwwroot/blueprint_editor.ui | 50 + pbl_blueprint/wwwroot/blueprint_list.ui | 28 + pbl_blueprint/wwwroot/blueprint_tree.ui | 36 + pbl_blueprint/wwwroot/index.ui | 22 + pbl_blueprint/wwwroot/menu.ui | 12 + pbl_blueprint/wwwroot/template_list.ui | 25 + pyproject.toml | 14 + scripts/audit_dspy.py | 57 + scripts/derive_artifacts.py | 166 +++ scripts/load_path.py | 145 ++ scripts/selfcheck.py | 90 ++ skill/SKILL.md | 40 + tests/fakedb.py | 133 ++ tests/test_contract.py | 498 +++++++ tests/test_real_path.py | 180 +++ 95 files changed, 8458 insertions(+) create mode 100644 .gitignore create mode 100644 json/pbl_blueprint.json create mode 100644 json/pbl_blueprint_artifact_spec.json create mode 100644 json/pbl_blueprint_evidence_spec.json create mode 100644 json/pbl_blueprint_learning_goal.json create mode 100644 json/pbl_blueprint_mission.json create mode 100644 json/pbl_blueprint_reflection_spec.json create mode 100644 json/pbl_blueprint_role.json create mode 100644 json/pbl_blueprint_task.json create mode 100644 json/pbl_blueprint_version.json create mode 100644 json/pbl_template.json create mode 100644 models/pbl_blueprint.json create mode 100644 models/pbl_blueprint_artifact_spec.json create mode 100644 models/pbl_blueprint_evidence_spec.json create mode 100644 models/pbl_blueprint_learning_goal.json create mode 100644 models/pbl_blueprint_mission.json create mode 100644 models/pbl_blueprint_reflection_spec.json create mode 100644 models/pbl_blueprint_role.json create mode 100644 models/pbl_blueprint_task.json create mode 100644 models/pbl_blueprint_version.json create mode 100644 models/pbl_template.json create mode 100644 pbl_blueprint/__init__.py create mode 100644 pbl_blueprint/api.py create mode 100644 pbl_blueprint/blueprint_crud.py create mode 100644 pbl_blueprint/db.py create mode 100644 pbl_blueprint/i18n/pbl_blueprint.en_US.json create mode 100644 pbl_blueprint/i18n/pbl_blueprint.zh_CN.json create mode 100644 pbl_blueprint/init.py create mode 100644 pbl_blueprint/init/data.json create mode 100644 pbl_blueprint/json/pbl_blueprint.json create mode 100644 pbl_blueprint/json/pbl_blueprint_artifact_spec.json create mode 100644 pbl_blueprint/json/pbl_blueprint_evidence_spec.json create mode 100644 pbl_blueprint/json/pbl_blueprint_learning_goal.json create mode 100644 pbl_blueprint/json/pbl_blueprint_mission.json create mode 100644 pbl_blueprint/json/pbl_blueprint_reflection_spec.json create mode 100644 pbl_blueprint/json/pbl_blueprint_role.json create mode 100644 pbl_blueprint/json/pbl_blueprint_task.json create mode 100644 pbl_blueprint/json/pbl_blueprint_template.json create mode 100644 pbl_blueprint/json/pbl_blueprint_template_item.json create mode 100644 pbl_blueprint/json/pbl_blueprint_version.json create mode 100644 pbl_blueprint/models.py create mode 100644 pbl_blueprint/models/pbl_blueprint.json create mode 100644 pbl_blueprint/models/pbl_blueprint_artifact_spec.json create mode 100644 pbl_blueprint/models/pbl_blueprint_evidence_spec.json create mode 100644 pbl_blueprint/models/pbl_blueprint_learning_goal.json create mode 100644 pbl_blueprint/models/pbl_blueprint_mission.json create mode 100644 pbl_blueprint/models/pbl_blueprint_reflection_spec.json create mode 100644 pbl_blueprint/models/pbl_blueprint_role.json create mode 100644 pbl_blueprint/models/pbl_blueprint_task.json create mode 100644 pbl_blueprint/models/pbl_blueprint_template.json create mode 100644 pbl_blueprint/models/pbl_blueprint_template_item.json create mode 100644 pbl_blueprint/models/pbl_blueprint_version.json create mode 100644 pbl_blueprint/sql/pbl_blueprint.core.sql create mode 100644 pbl_blueprint/subobjects.py create mode 100644 pbl_blueprint/templates.py create mode 100644 pbl_blueprint/wwwroot/api/blueprint_delete.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_domain_options.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_fork.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_get.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_list.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_publish.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_save.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_status.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_status_options.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_tree.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_version_get.dspy create mode 100644 pbl_blueprint/wwwroot/api/blueprint_versions.dspy create mode 100644 pbl_blueprint/wwwroot/api/subobject_counts.dspy create mode 100644 pbl_blueprint/wwwroot/api/subobject_delete.dspy create mode 100644 pbl_blueprint/wwwroot/api/subobject_get.dspy create mode 100644 pbl_blueprint/wwwroot/api/subobject_list.dspy create mode 100644 pbl_blueprint/wwwroot/api/subobject_save.dspy create mode 100644 pbl_blueprint/wwwroot/api/subobject_type_options.dspy create mode 100644 pbl_blueprint/wwwroot/api/table_manifest.dspy create mode 100644 pbl_blueprint/wwwroot/api/template_delete.dspy create mode 100644 pbl_blueprint/wwwroot/api/template_from_blueprint.dspy create mode 100644 pbl_blueprint/wwwroot/api/template_get.dspy create mode 100644 pbl_blueprint/wwwroot/api/template_instantiate.dspy create mode 100644 pbl_blueprint/wwwroot/api/template_list.dspy create mode 100644 pbl_blueprint/wwwroot/api/template_save.dspy create mode 100644 pbl_blueprint/wwwroot/blueprint_editor.ui create mode 100644 pbl_blueprint/wwwroot/blueprint_list.ui create mode 100644 pbl_blueprint/wwwroot/blueprint_tree.ui create mode 100644 pbl_blueprint/wwwroot/index.ui create mode 100644 pbl_blueprint/wwwroot/menu.ui create mode 100644 pbl_blueprint/wwwroot/template_list.ui create mode 100644 pyproject.toml create mode 100644 scripts/audit_dspy.py create mode 100644 scripts/derive_artifacts.py create mode 100644 scripts/load_path.py create mode 100644 scripts/selfcheck.py create mode 100644 skill/SKILL.md create mode 100644 tests/fakedb.py create mode 100644 tests/test_contract.py create mode 100644 tests/test_real_path.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..047cdbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +*.egg-info/ +.venv/ diff --git a/json/pbl_blueprint.json b/json/pbl_blueprint.json new file mode 100644 index 0000000..5d05208 --- /dev/null +++ b/json/pbl_blueprint.json @@ -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')}}"} + ] + } +} diff --git a/json/pbl_blueprint_artifact_spec.json b/json/pbl_blueprint_artifact_spec.json new file mode 100644 index 0000000..7427e51 --- /dev/null +++ b/json/pbl_blueprint_artifact_spec.json @@ -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"] + } +} diff --git a/json/pbl_blueprint_evidence_spec.json b/json/pbl_blueprint_evidence_spec.json new file mode 100644 index 0000000..0d4115a --- /dev/null +++ b/json/pbl_blueprint_evidence_spec.json @@ -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"] + } +} diff --git a/json/pbl_blueprint_learning_goal.json b/json/pbl_blueprint_learning_goal.json new file mode 100644 index 0000000..68eb6d5 --- /dev/null +++ b/json/pbl_blueprint_learning_goal.json @@ -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"] + } +} diff --git a/json/pbl_blueprint_mission.json b/json/pbl_blueprint_mission.json new file mode 100644 index 0000000..f566e8e --- /dev/null +++ b/json/pbl_blueprint_mission.json @@ -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"] + } +} diff --git a/json/pbl_blueprint_reflection_spec.json b/json/pbl_blueprint_reflection_spec.json new file mode 100644 index 0000000..7181ce9 --- /dev/null +++ b/json/pbl_blueprint_reflection_spec.json @@ -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"] + } +} diff --git a/json/pbl_blueprint_role.json b/json/pbl_blueprint_role.json new file mode 100644 index 0000000..c74e833 --- /dev/null +++ b/json/pbl_blueprint_role.json @@ -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"] + } +} diff --git a/json/pbl_blueprint_task.json b/json/pbl_blueprint_task.json new file mode 100644 index 0000000..8035411 --- /dev/null +++ b/json/pbl_blueprint_task.json @@ -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"] + } +} diff --git a/json/pbl_blueprint_version.json b/json/pbl_blueprint_version.json new file mode 100644 index 0000000..bed96ca --- /dev/null +++ b/json/pbl_blueprint_version.json @@ -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"] + } +} diff --git a/json/pbl_template.json b/json/pbl_template.json new file mode 100644 index 0000000..e2955e0 --- /dev/null +++ b/json/pbl_template.json @@ -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"] + } +} diff --git a/models/pbl_blueprint.json b/models/pbl_blueprint.json new file mode 100644 index 0000000..1b02c1c --- /dev/null +++ b/models/pbl_blueprint.json @@ -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"} + ] +} diff --git a/models/pbl_blueprint_artifact_spec.json b/models/pbl_blueprint_artifact_spec.json new file mode 100644 index 0000000..917b6da --- /dev/null +++ b/models/pbl_blueprint_artifact_spec.json @@ -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"} + ] +} diff --git a/models/pbl_blueprint_evidence_spec.json b/models/pbl_blueprint_evidence_spec.json new file mode 100644 index 0000000..09b14d7 --- /dev/null +++ b/models/pbl_blueprint_evidence_spec.json @@ -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"} + ] +} diff --git a/models/pbl_blueprint_learning_goal.json b/models/pbl_blueprint_learning_goal.json new file mode 100644 index 0000000..7ab59c7 --- /dev/null +++ b/models/pbl_blueprint_learning_goal.json @@ -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"} + ] +} diff --git a/models/pbl_blueprint_mission.json b/models/pbl_blueprint_mission.json new file mode 100644 index 0000000..8c44401 --- /dev/null +++ b/models/pbl_blueprint_mission.json @@ -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"} + ] +} diff --git a/models/pbl_blueprint_reflection_spec.json b/models/pbl_blueprint_reflection_spec.json new file mode 100644 index 0000000..ec24309 --- /dev/null +++ b/models/pbl_blueprint_reflection_spec.json @@ -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"} + ] +} diff --git a/models/pbl_blueprint_role.json b/models/pbl_blueprint_role.json new file mode 100644 index 0000000..edc43c4 --- /dev/null +++ b/models/pbl_blueprint_role.json @@ -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"} + ] +} diff --git a/models/pbl_blueprint_task.json b/models/pbl_blueprint_task.json new file mode 100644 index 0000000..1d975d8 --- /dev/null +++ b/models/pbl_blueprint_task.json @@ -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"} + ] +} diff --git a/models/pbl_blueprint_version.json b/models/pbl_blueprint_version.json new file mode 100644 index 0000000..a9c326e --- /dev/null +++ b/models/pbl_blueprint_version.json @@ -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"} + ] +} diff --git a/models/pbl_template.json b/models/pbl_template.json new file mode 100644 index 0000000..bbc2c55 --- /dev/null +++ b/models/pbl_template.json @@ -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"} + ] +} diff --git a/pbl_blueprint/__init__.py b/pbl_blueprint/__init__.py new file mode 100644 index 0000000..13a471e --- /dev/null +++ b/pbl_blueprint/__init__.py @@ -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) diff --git a/pbl_blueprint/api.py b/pbl_blueprint/api.py new file mode 100644 index 0000000..baa0f63 --- /dev/null +++ b/pbl_blueprint/api.py @@ -0,0 +1,1165 @@ +"""pbl_blueprint.api —— PBL 蓝图聚合根 CRUD 与查询契约实现(M1a)。 + +设计约束(见 projects/pbls/docs/01-design/modules/pbl_blueprint.md): +1. tenant_id 强制打头:所有读/写/删 SQL 的 WHERE 第一条件必须是 tenant_id, + 缺失即 fail-closed 抛 PblTenantMissing,绝不降级为「全租户可见」。 +2. 库名不硬编码:统一 ServerEnv().get_module_dbname('pbl_blueprint')。 +3. sqlor 只用 C/U/D/R/I/sqlExe(+ sqlPaging 分页),不编造 save/list/insert。 +4. 聚合根写入与子对象写入均落审计(pbl_common.audit,缺失时本地降级)。 +""" + +import json +import traceback + +from ahserver.serverEnv import ServerEnv + +MODULE_NAME = 'pbl_blueprint' + +# ---------------------------------------------------------------- 错误码 +ERR_TENANT_MISSING = 'PBL_TENANT_MISSING' +ERR_NOT_FOUND = 'PBL_NOT_FOUND' +ERR_DUPLICATE = 'PBL_DUPLICATE' +ERR_INVALID_PARAM = 'PBL_INVALID_PARAM' +ERR_SUBOBJECT_UNKNOWN = 'PBL_SUBOBJECT_UNKNOWN' +ERR_STATE_CONFLICT = 'PBL_STATE_CONFLICT' + + +class PblBlueprintError(Exception): + """蓝图模块业务异常基类(fail-closed)。""" + + code = 'PBL_BLUEPRINT_ERROR' + + def __init__(self, message, code=None, **extra): + super().__init__(message) + self.message = message + if code: + self.code = code + self.extra = extra + + def to_dict(self): + d = {'status': 'error', 'code': self.code, 'message': self.message} + d.update(self.extra) + return d + + +class PblTenantMissing(PblBlueprintError): + code = ERR_TENANT_MISSING + + +class PblNotFound(PblBlueprintError): + code = ERR_NOT_FOUND + + +class PblDuplicate(PblBlueprintError): + code = ERR_DUPLICATE + + +class PblInvalidParam(PblBlueprintError): + code = ERR_INVALID_PARAM + + +class PblSubobjectUnknown(PblBlueprintError): + code = ERR_SUBOBJECT_UNKNOWN + + +class PblStateConflict(PblBlueprintError): + code = ERR_STATE_CONFLICT + + +# ---------------------------------------------------------------- 子对象注册表 +# 子对象名 -> (表名, 业务编码字段, 标题字段) +SUBOBJECTS = { + 'learning_goal': ('pbl_blueprint_learning_goal', 'goal_code', 'title'), + 'role': ('pbl_blueprint_role', 'role_code', 'role_name'), + 'mission': ('pbl_blueprint_mission', 'mission_code', 'title'), + 'task': ('pbl_blueprint_task', 'task_code', 'title'), + 'artifact_spec': ('pbl_blueprint_artifact_spec', 'artifact_code', 'name'), + 'evidence_spec': ('pbl_blueprint_evidence_spec', 'evidence_code', 'name'), + 'reflection_spec': ('pbl_blueprint_reflection_spec', 'reflection_code', 'title'), +} + +BLUEPRINT_TABLE = 'pbl_blueprint' +VERSION_TABLE = 'pbl_blueprint_version' +TEMPLATE_TABLE = 'pbl_template' + +# 蓝图主表可写字段(白名单,防止越权写 tenant_id/created_at 等) +BLUEPRINT_WRITABLE = [ + 'blueprint_code', 'title', 'subject_area', 'grade_level', 'duration_hours', + 'driving_question', 'summary', 'extra_json', 'template_id', 'status', +] +# 仅允许状态机内部/校验模块写的字段 +BLUEPRINT_PROTECTED = [ + 'id', 'tenant_id', 'org_id', 'quality_status', 'validation_score', + 'version_no', 'current_version_id', 'forked_from_id', + 'created_by', 'created_at', 'updated_by', 'updated_at', +] + +QUALITY_STATUS_ORDER = ['draft', 'partial', 'valid', 'complete', 'published'] + + +# ---------------------------------------------------------------- 基础设施 +def _env(): + return ServerEnv() + + +def _dbname(): + """取库名——禁止硬编码。""" + env = _env() + fn = getattr(env, 'get_module_dbname', None) + if fn is None: + raise PblInvalidParam( + 'ServerEnv.get_module_dbname 未挂载,宿主应用 app/pbls.py 必须先注册', + code=ERR_INVALID_PARAM) + return fn(MODULE_NAME) + + +def _resolve_tenant_id(explicit=None, request=None): + """tenant_id 强制打头:显式参数 > pbl_common 上下文 > request 上下文。 + + 任一来源都拿不到 -> fail-closed 抛 PblTenantMissing。 + """ + tid = None + if explicit: + tid = str(explicit).strip() + if not tid: + try: + from pbl_common.api import tenant_id as _pc_tenant_id + tid = _pc_tenant_id() + except Exception: + tid = None + if not tid and request is not None: + ns = getattr(request, '_run_ns', None) + if ns is not None: + pk = getattr(ns, 'params_kw', None) or {} + tid = pk.get('tenant_id') or None + if not tid: + tid = getattr(ns, 'tenant_id', None) or None + if not tid: + raise PblTenantMissing( + 'tenant_id 缺失:蓝图模块所有读写必须显式携带租户上下文(fail-closed)') + return str(tid).strip() + + +def _now(): + try: + from appPublic.utils import curDateString + return curDateString() + except Exception: + import datetime + return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + +def _new_id(): + try: + from appPublic.utils import getID + return getID() + except Exception: + import uuid + return uuid.uuid4().hex + + +async def _user_id(request=None): + if request is None: + return '' + ns = getattr(request, '_run_ns', None) + if ns is None: + return '' + fn = getattr(ns, 'get_user', None) + if fn is None: + return '' + try: + return (await fn()) or '' + except Exception: + return '' + + +async def _org_id(request=None): + if request is None: + return '0' + ns = getattr(request, '_run_ns', None) + if ns is None: + return '0' + fn = getattr(ns, 'get_userorgid', None) + if fn is None: + return '0' + try: + return (await fn()) or '0' + except Exception: + return '0' + + +async def _audit(request, action, table, record_id, tenant_id, detail=None): + """审计写入:优先 pbl_common.audit,缺失时静默降级(不阻断主流程)。""" + try: + from pbl_common.audit import write_audit + await write_audit( + request=request, module=MODULE_NAME, action=action, table=table, + record_id=record_id, tenant_id=tenant_id, detail=detail or {}) + return True + except Exception: + return False + + +def _clean(ns, writable, allow_extra=None): + """按白名单过滤写入字段;剔除 *_text 展示字段与空 id。""" + allow = set(writable) | set(allow_extra or []) + out = {} + for k, v in dict(ns).items(): + if k.endswith('_text'): + continue + if k in ('page', 'rows', 'sort', 'order', '_webbricks_'): + continue + if k not in allow: + continue + if isinstance(v, (dict, list)): + v = json.dumps(v, ensure_ascii=False) + out[k] = v + return out + + +def _row_to_dict(r): + """sqlExe/R 返回 DictObject,不能 dict(r);逐属性取值。""" + if r is None: + return None + if isinstance(r, dict): + return dict(r) + try: + return {k: v for k, v in r.items()} + except Exception: + pass + out = {} + for k in getattr(r, '__dict__', {}) or {}: + out[k] = getattr(r, k) + return out + + +async def _q_all(sor, sql, ns): + recs = await sor.sqlExe(sql, ns) + return [_row_to_dict(r) for r in (recs or [])] + + +async def _q_one(sor, sql, ns): + rows = await _q_all(sor, sql, ns) + return rows[0] if rows else None + + +# ================================================================ 蓝图 CRUD +async def pbl_blueprint_create(ns, request=None, tenant_id=None): + """创建蓝图聚合根。tenant_id 强制打头,blueprint_code 租户内唯一。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + data = _clean(ns, BLUEPRINT_WRITABLE) + title = (data.get('title') or '').strip() + if not title: + raise PblInvalidParam('title 不能为空') + code = (data.get('blueprint_code') or '').strip() + if not code: + code = 'BP' + _new_id()[-10:].upper() + data['blueprint_code'] = code + data['title'] = title + data['status'] = data.get('status') or 'draft' + + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + dup = await _q_one( + sor, + "SELECT id FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND blueprint_code=${blueprint_code}$", + {'tenant_id': tid, 'blueprint_code': code}) + if dup: + raise PblDuplicate('蓝图编码已存在: %s' % code, blueprint_code=code) + rid = _new_id() + data['id'] = rid + data['tenant_id'] = tid + data['org_id'] = ns.get('org_id') or await _org_id(request) + data['quality_status'] = 'draft' + data['validation_score'] = 0 + data['version_no'] = 1 + data['created_by'] = await _user_id(request) + data['created_at'] = _now() + data['updated_by'] = data['created_by'] + data['updated_at'] = data['created_at'] + await sor.C(BLUEPRINT_TABLE, data) + + # 聚合根创建即产生 v1 版本快照(change_delta 证明对话式改模型) + ver_id = _new_id() + await sor.C(VERSION_TABLE, { + 'id': ver_id, 'tenant_id': tid, 'blueprint_id': rid, + 'version_no': 1, 'parent_version_id': '', + 'change_delta': json.dumps( + [{'op': 'create', 'path': '/pbl_blueprint', 'before': None, + 'after': {k: v for k, v in data.items() + if k not in ('extra_json',)}}], ensure_ascii=False), + 'snapshot_json': json.dumps(data, ensure_ascii=False, default=str), + 'change_reason': ns.get('change_reason') or '蓝图创建', + 'source': 'manual', 'quality_status': 'draft', + 'created_by': data['created_by'], 'created_at': data['created_at'], + }) + await sor.U(BLUEPRINT_TABLE, { + 'id': rid, 'tenant_id': tid, 'current_version_id': ver_id}) + + await _audit(request, 'create', BLUEPRINT_TABLE, rid, tid, + {'blueprint_code': code, 'title': title}) + return {'status': 'success', 'data': {'id': rid, 'tenant_id': tid, + 'blueprint_code': code, + 'version_no': 1, + 'current_version_id': ver_id}} + + +async def pbl_blueprint_read(ns, request=None, tenant_id=None): + """读取单个蓝图(含 with_children 时返回全部子对象)。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + rid = ns.get('id') or ns.get('blueprint_id') + if not rid: + raise PblInvalidParam('id 不能为空') + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + row = await _q_one( + sor, + "SELECT * FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': rid}) + if not row: + raise PblNotFound('蓝图不存在或不属于当前租户', id=rid) + children = {} + if str(ns.get('with_children') or '1') in ('1', 'true', 'True', 'yes'): + for name, (tbl, _c, _t) in SUBOBJECTS.items(): + children[name] = await _q_all( + sor, + "SELECT * FROM %s WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$ ORDER BY seq ASC" % tbl, + {'tenant_id': tid, 'blueprint_id': rid}) + children['version'] = await _q_all( + sor, + "SELECT id,version_no,parent_version_id,change_reason,source," + "quality_status,created_by,created_at FROM pbl_blueprint_version " + "WHERE tenant_id=${tenant_id}$ AND blueprint_id=${blueprint_id}$ " + "ORDER BY version_no DESC", + {'tenant_id': tid, 'blueprint_id': rid}) + data = dict(row) + if children: + data['children'] = children + return {'status': 'success', 'data': data} + + +async def pbl_blueprint_update(ns, request=None, tenant_id=None): + """更新蓝图主表字段(白名单),自动升版本并写 change_delta。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + rid = ns.get('id') or ns.get('blueprint_id') + if not rid: + raise PblInvalidParam('id 不能为空') + data = _clean(ns, BLUEPRINT_WRITABLE) + if not data: + raise PblInvalidParam('无可更新字段(受保护字段不可写: %s)' + % ','.join(BLUEPRINT_PROTECTED)) + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + old = await _q_one( + sor, "SELECT * FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': rid}) + if not old: + raise PblNotFound('蓝图不存在或不属于当前租户', id=rid) + if old.get('status') == 'published' and 'status' not in data: + raise PblStateConflict('已发布蓝图不可直接修改,请先 fork 或回退状态', + id=rid) + delta = [] + for k, v in data.items(): + before = old.get(k) + if str(before) != str(v): + delta.append({'op': 'update', 'path': '/pbl_blueprint/%s' % k, + 'before': before, 'after': v}) + uid = await _user_id(request) + data['id'] = rid + data['tenant_id'] = tid + data['updated_by'] = uid + data['updated_at'] = _now() + await sor.U(BLUEPRINT_TABLE, data) + + new_no = int(old.get('version_no') or 1) + 1 + ver_id = _new_id() + merged = dict(old) + merged.update(data) + await sor.C(VERSION_TABLE, { + 'id': ver_id, 'tenant_id': tid, 'blueprint_id': rid, + 'version_no': new_no, + 'parent_version_id': old.get('current_version_id') or '', + 'change_delta': json.dumps(delta, ensure_ascii=False, default=str), + 'snapshot_json': json.dumps(merged, ensure_ascii=False, default=str), + 'change_reason': ns.get('change_reason') or '蓝图更新', + 'source': ns.get('source') or 'dialogue', + 'quality_status': old.get('quality_status') or 'draft', + 'created_by': uid, 'created_at': data['updated_at'], + }) + await sor.U(BLUEPRINT_TABLE, { + 'id': rid, 'tenant_id': tid, 'version_no': new_no, + 'current_version_id': ver_id, 'updated_by': uid, + 'updated_at': data['updated_at']}) + + await _audit(request, 'update', BLUEPRINT_TABLE, rid, tid, + {'version_no': new_no, 'fields': list(data.keys())}) + return {'status': 'success', + 'data': {'id': rid, 'version_no': new_no, + 'current_version_id': ver_id, 'change_delta': delta}} + + +async def pbl_blueprint_delete(ns, request=None, tenant_id=None): + """软删除蓝图(status=deleted),级联软删子对象不做物理删除。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + rid = ns.get('id') or ns.get('blueprint_id') + if not rid: + raise PblInvalidParam('id 不能为空') + hard = str(ns.get('hard') or '0') in ('1', 'true', 'True') + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + old = await _q_one( + sor, "SELECT id,status FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': rid}) + if not old: + raise PblNotFound('蓝图不存在或不属于当前租户', id=rid) + if hard: + for _name, (tbl, _c, _t) in SUBOBJECTS.items(): + await sor.sqlExe( + "DELETE FROM %s WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$" % tbl, + {'tenant_id': tid, 'blueprint_id': rid}) + await sor.sqlExe( + "DELETE FROM pbl_blueprint_version WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$", + {'tenant_id': tid, 'blueprint_id': rid}) + await sor.D(BLUEPRINT_TABLE, {'tenant_id': tid, 'id': rid}) + else: + if old.get('status') == 'published': + raise PblStateConflict('已发布蓝图不可删除,请先归档', id=rid) + await sor.U(BLUEPRINT_TABLE, { + 'id': rid, 'tenant_id': tid, 'status': 'deleted', + 'updated_by': await _user_id(request), 'updated_at': _now()}) + await _audit(request, 'delete' if hard else 'soft_delete', + BLUEPRINT_TABLE, rid, tid, {'hard': hard}) + return {'status': 'success', 'data': {'id': rid, 'deleted': True, + 'hard': hard}} + + +async def pbl_blueprint_list(ns, request=None, tenant_id=None): + """蓝图列表查询(分页 + 租户隔离 + 状态/学科过滤)。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + where = ["tenant_id=${tenant_id}$"] + params = {'tenant_id': tid} + status = ns.get('status') + if status: + where.append("status=${status}$") + params['status'] = status + else: + where.append("status<>'deleted'") + kw = ns.get('keyword') or ns.get('q') + if kw: + where.append("(title LIKE ${kw}$ OR blueprint_code LIKE ${kw}$)") + params['kw'] = '%' + str(kw) + '%' + for f in ('subject_area', 'grade_level', 'quality_status', 'org_id', + 'template_id'): + if ns.get(f): + where.append("%s=${%s}$" % (f, f)) + params[f] = ns.get(f) + sql = ("SELECT id,tenant_id,org_id,blueprint_code,title,subject_area," + "grade_level,duration_hours,quality_status,validation_score," + "version_no,status,template_id,created_by,created_at,updated_at " + "FROM pbl_blueprint WHERE " + ' AND '.join(where)) + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + page_ns = dict(params) + page_ns['page'] = int(ns.get('page') or 1) + page_ns['rows'] = int(ns.get('rows') or 20) + page_ns['sort'] = ns.get('sort') or 'created_at' + page_ns['order'] = ns.get('order') or 'desc' + try: + res = await sor.sqlPaging(sql, page_ns) + rows = [_row_to_dict(r) for r in (res.get('rows') or [])] + total = res.get('total') or len(rows) + except Exception: + rows = await _q_all(sor, sql + ' ORDER BY created_at DESC', params) + total = len(rows) + return {'status': 'success', 'data': rows, 'total': total, + 'tenant_id': tid} + + +async def pbl_blueprint_tree(ns, request=None, tenant_id=None): + """蓝图聚合树:蓝图 -> mission -> task -> artifact/evidence/reflection。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + rid = ns.get('id') or ns.get('blueprint_id') + if not rid: + raise PblInvalidParam('id 不能为空') + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + root = await _q_one( + sor, "SELECT * FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': rid}) + if not root: + raise PblNotFound('蓝图不存在或不属于当前租户', id=rid) + kids = {} + for name, (tbl, _c, _t) in SUBOBJECTS.items(): + kids[name] = await _q_all( + sor, "SELECT * FROM %s WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$ ORDER BY seq ASC" % tbl, + {'tenant_id': tid, 'blueprint_id': rid}) + + def _by_id(rows): + return {r.get('id'): dict(r) for r in rows} + + missions = _by_id(kids['mission']) + tasks = kids['task'] + artifacts = kids['artifact_spec'] + evidences = kids['evidence_spec'] + loose_tasks = [] + for t in tasks: + node = dict(t) + node['children'] = [a for a in artifacts if a.get('task_id') == t.get('id')] + node['evidence_specs'] = [e for e in evidences + if e.get('task_id') == t.get('id')] + mid = t.get('mission_id') + if mid and mid in missions: + missions[mid].setdefault('children', []).append(node) + else: + loose_tasks.append(node) + tree = dict(root) + tree['children'] = [m for m in missions.values()] + loose_tasks + tree['learning_goals'] = kids['learning_goal'] + tree['roles'] = kids['role'] + tree['reflection_specs'] = kids['reflection_spec'] + tree['unassigned_evidence_specs'] = [ + e for e in evidences if not e.get('task_id')] + return {'status': 'success', 'data': tree, 'tenant_id': tid} + + +async def pbl_blueprint_fork(ns, request=None, tenant_id=None): + """派生蓝图:深拷贝聚合根 + 全部子对象到新蓝图(新 id、新 code)。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + src = ns.get('id') or ns.get('blueprint_id') + if not src: + raise PblInvalidParam('id 不能为空') + from sqlor import DBPools + db = DBPools() + uid = await _user_id(request) + now = _now() + async with db.sqlorContext(_dbname()) as sor: + old = await _q_one( + sor, "SELECT * FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': src}) + if not old: + raise PblNotFound('源蓝图不存在或不属于当前租户', id=src) + new_id = _new_id() + new_code = (ns.get('blueprint_code') + or (str(old.get('blueprint_code') or 'BP') + '_copy')) + dup = await _q_one( + sor, "SELECT id FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND blueprint_code=${blueprint_code}$", + {'tenant_id': tid, 'blueprint_code': new_code}) + if dup: + raise PblDuplicate('派生蓝图编码已存在: %s' % new_code) + row = dict(old) + row.pop('id', None) + keep = {k: v for k, v in row.items() if k in BLUEPRINT_WRITABLE} + keep.update({ + 'id': new_id, 'tenant_id': tid, + 'org_id': old.get('org_id') or await _org_id(request), + 'blueprint_code': new_code, + 'title': ns.get('title') or (str(old.get('title') or '') + '(副本)'), + 'status': 'draft', 'quality_status': 'draft', 'validation_score': 0, + 'version_no': 1, 'forked_from_id': src, + 'template_id': old.get('template_id') or '', + 'created_by': uid, 'created_at': now, 'updated_by': uid, + 'updated_at': now, + }) + await sor.C(BLUEPRINT_TABLE, keep) + + id_map = {} + counts = {} + for name, (tbl, code_f, _t) in SUBOBJECTS.items(): + rows = await _q_all( + sor, "SELECT * FROM %s WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$ ORDER BY seq ASC" % tbl, + {'tenant_id': tid, 'blueprint_id': src}) + for r in rows: + d = dict(r) + oid = d.pop('id', None) + nid = _new_id() + id_map[oid] = nid + d['id'] = nid + d['tenant_id'] = tid + d['blueprint_id'] = new_id + d['created_by'] = uid + d['created_at'] = now + d['updated_by'] = uid + d['updated_at'] = now + await sor.C(tbl, d) + counts[name] = counts.get(name, 0) + 1 + # 修正子对象内部引用(mission_id / task_id / artifact_spec_id / role_id) + ref_fixes = [ + ('pbl_blueprint_task', 'mission_id'), + ('pbl_blueprint_artifact_spec', 'task_id'), + ('pbl_blueprint_evidence_spec', 'task_id'), + ('pbl_blueprint_evidence_spec', 'artifact_spec_id'), + ('pbl_blueprint_task', 'role_id'), + ] + for tbl, col in ref_fixes: + for oid, nid in id_map.items(): + await sor.sqlExe( + "UPDATE %s SET %s=${new}$ WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$ AND %s=${old}$" + % (tbl, col, col), + {'new': nid, 'old': oid, 'tenant_id': tid, + 'blueprint_id': new_id}) + + ver_id = _new_id() + await sor.C(VERSION_TABLE, { + 'id': ver_id, 'tenant_id': tid, 'blueprint_id': new_id, + 'version_no': 1, 'parent_version_id': '', + 'change_delta': json.dumps( + [{'op': 'fork', 'path': '/pbl_blueprint', + 'before': {'id': src}, 'after': {'id': new_id}}], + ensure_ascii=False), + 'snapshot_json': json.dumps(keep, ensure_ascii=False, default=str), + 'change_reason': ns.get('change_reason') or '从蓝图 %s 派生' % src, + 'source': 'fork', 'quality_status': 'draft', + 'created_by': uid, 'created_at': now}) + await sor.U(BLUEPRINT_TABLE, { + 'id': new_id, 'tenant_id': tid, 'current_version_id': ver_id}) + + await _audit(request, 'fork', BLUEPRINT_TABLE, new_id, tid, + {'from': src, 'copied': counts}) + return {'status': 'success', + 'data': {'id': new_id, 'blueprint_code': new_code, + 'forked_from_id': src, 'copied': counts, + 'current_version_id': ver_id}} + + +# ================================================================ 子对象契约 +def _subobject_meta(name): + if name not in SUBOBJECTS: + raise PblSubobjectUnknown( + '未知子对象类型: %s(合法: %s)' % (name, ','.join(sorted(SUBOBJECTS)))) + return SUBOBJECTS[name] + + +async def pbl_blueprint_subobject_save(ns, request=None, tenant_id=None): + """子对象新增/更新(有 id 走更新,无 id 走新增)。tenant_id 强制打头。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + name = ns.get('subobject') or ns.get('subobject_type') + tbl, code_f, title_f = _subobject_meta(name) + bp = ns.get('blueprint_id') + if not bp: + raise PblInvalidParam('blueprint_id 不能为空') + rid = ns.get('id') + uid = await _user_id(request) + now = _now() + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + bprow = await _q_one( + sor, "SELECT id,status FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': bp}) + if not bprow: + raise PblNotFound('所属蓝图不存在或不属于当前租户', blueprint_id=bp) + if bprow.get('status') == 'published': + raise PblStateConflict('已发布蓝图不可修改子对象', blueprint_id=bp) + + # 字段白名单 = 该表 models 定义中除受保护列外的全部列 + cols = _subobject_columns(name) + writable = [c for c in cols if c not in ( + 'id', 'tenant_id', 'blueprint_id', 'created_by', 'created_at', + 'updated_by', 'updated_at')] + data = _clean(ns, writable) + code_val = (data.get(code_f) or '').strip() + if not code_val and not rid: + raise PblInvalidParam('%s 不能为空' % code_f) + + if rid: + old = await _q_one( + sor, "SELECT * FROM %s WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$ AND blueprint_id=${blueprint_id}$" % tbl, + {'tenant_id': tid, 'id': rid, 'blueprint_id': bp}) + if not old: + raise PblNotFound('子对象不存在或不属于当前租户/蓝图', id=rid) + if not code_val: + data[code_f] = old.get(code_f) + data['id'] = rid + data['tenant_id'] = tid + data['blueprint_id'] = bp + data['updated_by'] = uid + data['updated_at'] = now + await sor.U(tbl, data) + action = 'update' + else: + dup = await _q_one( + sor, "SELECT id FROM %s WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$ AND %s=${code}$" + % (tbl, code_f), + {'tenant_id': tid, 'blueprint_id': bp, 'code': data[code_f]}) + if dup: + raise PblDuplicate('子对象编码已存在: %s' % data[code_f]) + if 'seq' not in data or not data.get('seq'): + mx = await _q_one( + sor, "SELECT MAX(seq) AS m FROM %s " + "WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$" % tbl, + {'tenant_id': tid, 'blueprint_id': bp}) + data['seq'] = int((mx or {}).get('m') or 0) + 1 + rid = _new_id() + data['id'] = rid + data['tenant_id'] = tid + data['blueprint_id'] = bp + data['created_by'] = uid + data['created_at'] = now + data['updated_by'] = uid + data['updated_at'] = now + await sor.C(tbl, data) + action = 'create' + await _bump_blueprint_version(sor, tid, bp, uid, now, [{ + 'op': action, 'path': '/%s/%s' % (name, rid), + 'before': None, 'after': {k: v for k, v in data.items()}}], + '%s %s' % (action, name)) + + await _audit(request, 'subobject_' + action, tbl, rid, tid, + {'blueprint_id': bp, 'subobject': name}) + return {'status': 'success', + 'data': {'id': rid, 'subobject': name, 'table': tbl, + 'action': action, 'blueprint_id': bp}} + + +async def pbl_blueprint_subobject_list(ns, request=None, tenant_id=None): + """子对象列表查询(按 blueprint_id + subobject 类型)。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + name = ns.get('subobject') or ns.get('subobject_type') + tbl, _c, _t = _subobject_meta(name) + bp = ns.get('blueprint_id') + if not bp: + raise PblInvalidParam('blueprint_id 不能为空') + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + rows = await _q_all( + sor, "SELECT * FROM %s WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$ ORDER BY seq ASC" % tbl, + {'tenant_id': tid, 'blueprint_id': bp}) + return {'status': 'success', 'data': rows, 'total': len(rows), + 'subobject': name, 'tenant_id': tid} + + +async def pbl_blueprint_subobject_delete(ns, request=None, tenant_id=None): + """子对象删除(物理删除,蓝图未发布时)。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + name = ns.get('subobject') or ns.get('subobject_type') + tbl, _c, _t = _subobject_meta(name) + rid = ns.get('id') + bp = ns.get('blueprint_id') + if not rid: + raise PblInvalidParam('id 不能为空') + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + where = {'tenant_id': tid, 'id': rid} + sql = ("SELECT * FROM %s WHERE tenant_id=${tenant_id}$ AND id=${id}$" % tbl) + if bp: + where['blueprint_id'] = bp + sql += " AND blueprint_id=${blueprint_id}$" + old = await _q_one(sor, sql, where) + if not old: + raise PblNotFound('子对象不存在或不属于当前租户', id=rid) + bp_id = old.get('blueprint_id') + bprow = await _q_one( + sor, "SELECT status FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': bp_id}) + if bprow and bprow.get('status') == 'published': + raise PblStateConflict('已发布蓝图不可删除子对象', blueprint_id=bp_id) + await sor.D(tbl, {'tenant_id': tid, 'id': rid}) + await _bump_blueprint_version( + sor, tid, bp_id, await _user_id(request), _now(), + [{'op': 'delete', 'path': '/%s/%s' % (name, rid), + 'before': old, 'after': None}], 'delete %s' % name) + await _audit(request, 'subobject_delete', tbl, rid, tid, + {'blueprint_id': bp_id, 'subobject': name}) + return {'status': 'success', 'data': {'id': rid, 'deleted': True}} + + +_SUBOBJECT_COLUMNS_CACHE = {} + + +def _subobject_columns(name): + """从 models/{table}.json 读取列名(不猜列名)。""" + tbl = SUBOBJECTS[name][0] + if tbl in _SUBOBJECT_COLUMNS_CACHE: + return _SUBOBJECT_COLUMNS_CACHE[tbl] + import os + cols = [] + for base in (os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + os.path.dirname(os.path.abspath(__file__))): + p = os.path.join(base, 'models', tbl + '.json') + if os.path.exists(p): + try: + with open(p, encoding='utf-8') as f: + mj = json.load(f) + cols = [fl.get('name') for fl in mj.get('fields', []) + if fl.get('name')] + except Exception: + cols = [] + break + if not cols: + cols = ['id', 'tenant_id', 'blueprint_id', 'seq', 'title', + 'created_at', 'updated_at'] + _SUBOBJECT_COLUMNS_CACHE[tbl] = cols + return cols + + +async def _bump_blueprint_version(sor, tid, bp_id, uid, now, delta, reason): + """子对象变更后升蓝图版本号并追加 change_delta 版本记录。""" + old = await _q_one( + sor, "SELECT version_no,current_version_id FROM pbl_blueprint " + "WHERE tenant_id=${tenant_id}$ AND id=${id}$", + {'tenant_id': tid, 'id': bp_id}) + if not old: + return None + new_no = int(old.get('version_no') or 1) + 1 + ver_id = _new_id() + await sor.C(VERSION_TABLE, { + 'id': ver_id, 'tenant_id': tid, 'blueprint_id': bp_id, + 'version_no': new_no, + 'parent_version_id': old.get('current_version_id') or '', + 'change_delta': json.dumps(delta, ensure_ascii=False, default=str), + 'snapshot_json': '', 'change_reason': reason, 'source': 'dialogue', + 'quality_status': 'draft', 'created_by': uid, 'created_at': now}) + await sor.U(BLUEPRINT_TABLE, { + 'id': bp_id, 'tenant_id': tid, 'version_no': new_no, + 'current_version_id': ver_id, 'updated_by': uid, 'updated_at': now}) + return ver_id + + +# ================================================================ 版本契约 +async def pbl_blueprint_version_create(ns, request=None, tenant_id=None): + """显式创建版本快照(对话式改模型/校验后打点)。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + bp = ns.get('blueprint_id') or ns.get('id') + if not bp: + raise PblInvalidParam('blueprint_id 不能为空') + from sqlor import DBPools + db = DBPools() + uid = await _user_id(request) + now = _now() + async with db.sqlorContext(_dbname()) as sor: + root = await _q_one( + sor, "SELECT * FROM pbl_blueprint WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': bp}) + if not root: + raise PblNotFound('蓝图不存在或不属于当前租户', id=bp) + delta = ns.get('change_delta') + if isinstance(delta, (dict, list)): + delta = json.dumps(delta, ensure_ascii=False) + snapshot = ns.get('snapshot_json') + if not snapshot: + snap = dict(root) + for name, (tbl, _c, _t) in SUBOBJECTS.items(): + snap[name] = await _q_all( + sor, "SELECT * FROM %s WHERE tenant_id=${tenant_id}$ " + "AND blueprint_id=${blueprint_id}$" % tbl, + {'tenant_id': tid, 'blueprint_id': bp}) + snapshot = json.dumps(snap, ensure_ascii=False, default=str) + ver_id = _new_id() + new_no = int(root.get('version_no') or 1) + 1 + await sor.C(VERSION_TABLE, { + 'id': ver_id, 'tenant_id': tid, 'blueprint_id': bp, + 'version_no': new_no, + 'parent_version_id': root.get('current_version_id') or '', + 'change_delta': delta or '[]', 'snapshot_json': snapshot, + 'change_reason': ns.get('change_reason') or '手动打版本', + 'source': ns.get('source') or 'manual', + 'quality_status': ns.get('quality_status') + or root.get('quality_status') or 'draft', + 'created_by': uid, 'created_at': now}) + await sor.U(BLUEPRINT_TABLE, { + 'id': bp, 'tenant_id': tid, 'version_no': new_no, + 'current_version_id': ver_id, 'updated_by': uid, 'updated_at': now}) + await _audit(request, 'version_create', VERSION_TABLE, ver_id, tid, + {'blueprint_id': bp, 'version_no': new_no}) + return {'status': 'success', + 'data': {'id': ver_id, 'blueprint_id': bp, 'version_no': new_no}} + + +async def pbl_blueprint_version_diff(ns, request=None, tenant_id=None): + """版本对比:返回两版本 change_delta 与快照差异摘要。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + bp = ns.get('blueprint_id') + va = ns.get('from_version') or ns.get('version_a') + vb = ns.get('to_version') or ns.get('version_b') + if not bp: + raise PblInvalidParam('blueprint_id 不能为空') + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + def _pick(v): + return v + rows = [] + if va and vb: + rows = await _q_all( + sor, "SELECT * FROM pbl_blueprint_version " + "WHERE tenant_id=${tenant_id}$ AND blueprint_id=${bp}$ " + "AND version_no IN (${va}$,${vb}$)", + {'tenant_id': tid, 'bp': bp, 'va': int(va), 'vb': int(vb)}) + else: + rows = await _q_all( + sor, "SELECT * FROM pbl_blueprint_version " + "WHERE tenant_id=${tenant_id}$ AND blueprint_id=${bp}$ " + "ORDER BY version_no DESC LIMIT 2", + {'tenant_id': tid, 'bp': bp}) + if not rows: + raise PblNotFound('未找到版本记录', blueprint_id=bp) + rows = sorted(rows, key=lambda r: int(r.get('version_no') or 0)) + older, newer = rows[0], rows[-1] + + def _load(r): + try: + return json.loads(r.get('change_delta') or '[]') + except Exception: + return [] + + diff = {'from_version': older.get('version_no'), + 'to_version': newer.get('version_no'), + 'from_id': older.get('id'), 'to_id': newer.get('id'), + 'changes': _load(newer), 'history': _load(older), + 'reasons': [older.get('change_reason'), newer.get('change_reason')]} + try: + sa = json.loads(older.get('snapshot_json') or '{}') + sb = json.loads(newer.get('snapshot_json') or '{}') + diff['snapshot_field_diff'] = [ + {'field': k, 'before': sa.get(k), 'after': sb.get(k)} + for k in set(list(sa.keys()) + list(sb.keys())) + if str(sa.get(k)) != str(sb.get(k)) + and k not in ('updated_at', 'updated_by', 'version_no', + 'current_version_id')] + except Exception: + diff['snapshot_field_diff'] = [] + _pick(None) + return {'status': 'success', 'data': diff, 'tenant_id': tid} + + +# ================================================================ 模板契约 +async def pbl_template_list(ns, request=None, tenant_id=None): + """模板列表:本租户 + 平台内置(tenant_id='0')兜底可见。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + from sqlor import DBPools + db = DBPools() + params = {'tenant_id': tid, 'global_tid': '0'} + where = ["(tenant_id=${tenant_id}$ OR tenant_id=${global_tid}$)", + "status='active'"] + if ns.get('category'): + where.append("category=${category}$") + params['category'] = ns.get('category') + if ns.get('subject_area'): + where.append("subject_area=${subject_area}$") + params['subject_area'] = ns.get('subject_area') + kw = ns.get('keyword') + if kw: + where.append("(name LIKE ${kw}$ OR template_code LIKE ${kw}$)") + params['kw'] = '%' + str(kw) + '%' + sql = ("SELECT id,tenant_id,template_code,name,description,category," + "subject_area,grade_level,is_builtin,template_version,status," + "created_at FROM pbl_template WHERE " + ' AND '.join(where) + + " ORDER BY is_builtin DESC, created_at DESC") + async with db.sqlorContext(_dbname()) as sor: + rows = await _q_all(sor, sql, params) + return {'status': 'success', 'data': rows, 'total': len(rows), + 'tenant_id': tid} + + +async def pbl_template_instantiate(ns, request=None, tenant_id=None): + """模板实例化为新蓝图:展开 blueprint_snapshot + offline_slots 兜底。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + tpl_id = ns.get('template_id') or ns.get('id') + if not tpl_id: + raise PblInvalidParam('template_id 不能为空') + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + tpl = await _q_one( + sor, "SELECT * FROM pbl_template WHERE id=${id}$ " + "AND (tenant_id=${tenant_id}$ OR tenant_id=${global_tid}$)", + {'id': tpl_id, 'tenant_id': tid, 'global_tid': '0'}) + if not tpl: + raise PblNotFound('模板不存在或当前租户不可见', template_id=tpl_id) + try: + snap = json.loads(tpl.get('blueprint_snapshot') or '{}') + except Exception: + raise PblInvalidParam('模板 blueprint_snapshot 非法 JSON', + template_id=tpl_id) + try: + slots = json.loads(tpl.get('offline_slots') or '[]') + except Exception: + slots = [] + + overrides = ns.get('overrides') or {} + if isinstance(overrides, str): + try: + overrides = json.loads(overrides) + except Exception: + overrides = {} + # 离线兜底:LLM 不可用时用 slot.default 填充缺失字段 + filled = [] + for s in slots: + path = (s or {}).get('path') or '' + key = path.rsplit('/', 1)[-1] if path else (s or {}).get('field') + if not key: + continue + if not snap.get(key) and s.get('default') is not None: + snap[key] = s.get('default') + filled.append(key) + snap.update({k: v for k, v in overrides.items() if k in BLUEPRINT_WRITABLE}) + snap['title'] = ns.get('title') or snap.get('title') \ + or (str(tpl.get('name') or 'PBL蓝图')) + snap['template_id'] = tpl_id + snap['blueprint_code'] = ns.get('blueprint_code') or snap.get( + 'blueprint_code') or '' + sub = snap.pop('subobjects', None) or {} + + res = await pbl_blueprint_create(snap, request=request, tenant_id=tid) + bp_id = res['data']['id'] + created = {} + if isinstance(sub, dict): + for name, items in sub.items(): + if name not in SUBOBJECTS or not isinstance(items, list): + continue + n = 0 + for it in items: + if not isinstance(it, dict): + continue + it = dict(it) + it.pop('id', None) + it['blueprint_id'] = bp_id + await pbl_blueprint_subobject_save( + dict(it, subobject=name), request=request, tenant_id=tid) + n += 1 + created[name] = n + await _audit(request, 'template_instantiate', BLUEPRINT_TABLE, bp_id, tid, + {'template_id': tpl_id, 'offline_filled': filled, + 'subobjects': created}) + return {'status': 'success', + 'data': {'id': bp_id, 'template_id': tpl_id, + 'blueprint_code': res['data']['blueprint_code'], + 'subobjects_created': created, + 'offline_slots_filled': filled}} + + +async def pbl_template_save(ns, request=None, tenant_id=None): + """模板新增/更新(本租户私有模板;内置模板只读)。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + writable = ['template_code', 'name', 'description', 'category', + 'subject_area', 'grade_level', 'blueprint_snapshot', + 'offline_slots', 'template_version', 'status'] + data = _clean(ns, writable) + if not data.get('name'): + raise PblInvalidParam('name 不能为空') + rid = ns.get('id') + uid = await _user_id(request) + now = _now() + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + if rid: + old = await _q_one( + sor, "SELECT * FROM pbl_template WHERE tenant_id=${tenant_id}$ " + "AND id=${id}$", {'tenant_id': tid, 'id': rid}) + if not old: + raise PblNotFound('模板不存在或不属于当前租户', id=rid) + if str(old.get('is_builtin')) == '1': + raise PblStateConflict('内置模板只读,请另存为租户模板', id=rid) + data['id'] = rid + data['tenant_id'] = tid + data['updated_by'] = uid + data['updated_at'] = now + await sor.U(TEMPLATE_TABLE, data) + action = 'update' + else: + code = data.get('template_code') or ('TPL' + _new_id()[-10:].upper()) + data['template_code'] = code + dup = await _q_one( + sor, "SELECT id FROM pbl_template WHERE tenant_id=${tenant_id}$ " + "AND template_code=${code}$", + {'tenant_id': tid, 'code': code}) + if dup: + raise PblDuplicate('模板编码已存在: %s' % code) + rid = _new_id() + data['id'] = rid + data['tenant_id'] = tid + data['is_builtin'] = '0' + data['status'] = data.get('status') or 'active' + data['template_version'] = data.get('template_version') or '1.0.0' + data['created_by'] = uid + data['created_at'] = now + data['updated_by'] = uid + data['updated_at'] = now + await sor.C(TEMPLATE_TABLE, data) + action = 'create' + await _audit(request, 'template_' + action, TEMPLATE_TABLE, rid, tid, + {'template_code': data.get('template_code')}) + return {'status': 'success', + 'data': {'id': rid, 'action': action, + 'template_code': data.get('template_code')}} + + +async def pbl_template_delete(ns, request=None, tenant_id=None): + """删除租户私有模板(内置模板禁删)。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + rid = ns.get('id') or ns.get('template_id') + if not rid: + raise PblInvalidParam('id 不能为空') + from sqlor import DBPools + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + old = await _q_one( + sor, "SELECT id,is_builtin FROM pbl_template " + "WHERE tenant_id=${tenant_id}$ AND id=${id}$", + {'tenant_id': tid, 'id': rid}) + if not old: + raise PblNotFound('模板不存在或不属于当前租户', id=rid) + if str(old.get('is_builtin')) == '1': + raise PblStateConflict('内置模板不可删除', id=rid) + await sor.D(TEMPLATE_TABLE, {'tenant_id': tid, 'id': rid}) + await _audit(request, 'template_delete', TEMPLATE_TABLE, rid, tid, {}) + return {'status': 'success', 'data': {'id': rid, 'deleted': True}} + + +# ================================================================ 只读契约(供下游模块) +async def pbl_blueprint_get_contract(ns, request=None, tenant_id=None): + """下游模块(compiler/validation/runtime)读取蓝图契约的只读入口。""" + tid = _resolve_tenant_id(tenant_id or ns.get('tenant_id'), request) + rid = ns.get('id') or ns.get('blueprint_id') + if not rid: + raise PblInvalidParam('id 不能为空') + res = await pbl_blueprint_read({'id': rid, 'with_children': '1'}, + request=request, tenant_id=tid) + data = res['data'] + kids = data.pop('children', {}) + return {'status': 'success', 'tenant_id': tid, + 'data': {'blueprint': data, + 'learning_goals': kids.get('learning_goal', []), + 'roles': kids.get('role', []), + 'missions': kids.get('mission', []), + 'tasks': kids.get('task', []), + 'artifact_specs': kids.get('artifact_spec', []), + 'evidence_specs': kids.get('evidence_spec', []), + 'reflection_specs': kids.get('reflection_spec', []), + 'versions': kids.get('version', [])}} + + +CONTRACT_FUNCTIONS = [ + 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, +] + + +def safe_traceback(): + return traceback.format_exc() diff --git a/pbl_blueprint/blueprint_crud.py b/pbl_blueprint/blueprint_crud.py new file mode 100644 index 0000000..605f436 --- /dev/null +++ b/pbl_blueprint/blueprint_crud.py @@ -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', '跨学科')]] diff --git a/pbl_blueprint/db.py b/pbl_blueprint/db.py new file mode 100644 index 0000000..996890a --- /dev/null +++ b/pbl_blueprint/db.py @@ -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 diff --git a/pbl_blueprint/i18n/pbl_blueprint.en_US.json b/pbl_blueprint/i18n/pbl_blueprint.en_US.json new file mode 100644 index 0000000..2a0ae7c --- /dev/null +++ b/pbl_blueprint/i18n/pbl_blueprint.en_US.json @@ -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" +} \ No newline at end of file diff --git a/pbl_blueprint/i18n/pbl_blueprint.zh_CN.json b/pbl_blueprint/i18n/pbl_blueprint.zh_CN.json new file mode 100644 index 0000000..1da3c31 --- /dev/null +++ b/pbl_blueprint/i18n/pbl_blueprint.zh_CN.json @@ -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": "版本历史" +} \ No newline at end of file diff --git a/pbl_blueprint/init.py b/pbl_blueprint/init.py new file mode 100644 index 0000000..52da618 --- /dev/null +++ b/pbl_blueprint/init.py @@ -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 diff --git a/pbl_blueprint/init/data.json b/pbl_blueprint/init/data.json new file mode 100644 index 0000000..19195e2 --- /dev/null +++ b/pbl_blueprint/init/data.json @@ -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": "资源" } + ] +} diff --git a/pbl_blueprint/json/pbl_blueprint.json b/pbl_blueprint/json/pbl_blueprint.json new file mode 100644 index 0000000..1e7e1b0 --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_artifact_spec.json b/pbl_blueprint/json/pbl_blueprint_artifact_spec.json new file mode 100644 index 0000000..d823f2a --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_artifact_spec.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_evidence_spec.json b/pbl_blueprint/json/pbl_blueprint_evidence_spec.json new file mode 100644 index 0000000..e220561 --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_evidence_spec.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_learning_goal.json b/pbl_blueprint/json/pbl_blueprint_learning_goal.json new file mode 100644 index 0000000..a68ddfd --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_learning_goal.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_mission.json b/pbl_blueprint/json/pbl_blueprint_mission.json new file mode 100644 index 0000000..b7632da --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_mission.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_reflection_spec.json b/pbl_blueprint/json/pbl_blueprint_reflection_spec.json new file mode 100644 index 0000000..18919ae --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_reflection_spec.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_role.json b/pbl_blueprint/json/pbl_blueprint_role.json new file mode 100644 index 0000000..0457a25 --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_role.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_task.json b/pbl_blueprint/json/pbl_blueprint_task.json new file mode 100644 index 0000000..15a14c3 --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_task.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_template.json b/pbl_blueprint/json/pbl_blueprint_template.json new file mode 100644 index 0000000..ca8590e --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_template.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_template_item.json b/pbl_blueprint/json/pbl_blueprint_template_item.json new file mode 100644 index 0000000..1869bd3 --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_template_item.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/json/pbl_blueprint_version.json b/pbl_blueprint/json/pbl_blueprint_version.json new file mode 100644 index 0000000..52286d7 --- /dev/null +++ b/pbl_blueprint/json/pbl_blueprint_version.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models.py b/pbl_blueprint/models.py new file mode 100644 index 0000000..38611d8 --- /dev/null +++ b/pbl_blueprint/models.py @@ -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 diff --git a/pbl_blueprint/models/pbl_blueprint.json b/pbl_blueprint/models/pbl_blueprint.json new file mode 100644 index 0000000..f1946fa --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint.json @@ -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": "蓝图状态" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_artifact_spec.json b/pbl_blueprint/models/pbl_blueprint_artifact_spec.json new file mode 100644 index 0000000..463993a --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_artifact_spec.json @@ -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": "产出物类型" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_evidence_spec.json b/pbl_blueprint/models/pbl_blueprint_evidence_spec.json new file mode 100644 index 0000000..aec65a4 --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_evidence_spec.json @@ -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": "证据类型" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_learning_goal.json b/pbl_blueprint/models/pbl_blueprint_learning_goal.json new file mode 100644 index 0000000..090c9c2 --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_learning_goal.json @@ -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": "目标类型" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_mission.json b/pbl_blueprint/models/pbl_blueprint_mission.json new file mode 100644 index 0000000..836f252 --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_mission.json @@ -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": "情境类型" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_reflection_spec.json b/pbl_blueprint/models/pbl_blueprint_reflection_spec.json new file mode 100644 index 0000000..cd4e917 --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_reflection_spec.json @@ -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": "频次" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_role.json b/pbl_blueprint/models/pbl_blueprint_role.json new file mode 100644 index 0000000..b83302a --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_role.json @@ -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": "角色类别" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_task.json b/pbl_blueprint/models/pbl_blueprint_task.json new file mode 100644 index 0000000..fe56d08 --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_task.json @@ -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": "任务类型" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_template.json b/pbl_blueprint/models/pbl_blueprint_template.json new file mode 100644 index 0000000..97185fb --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_template.json @@ -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": "状态" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_template_item.json b/pbl_blueprint/models/pbl_blueprint_template_item.json new file mode 100644 index 0000000..2b5abac --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_template_item.json @@ -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": "子对象类型" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/models/pbl_blueprint_version.json b/pbl_blueprint/models/pbl_blueprint_version.json new file mode 100644 index 0000000..8257570 --- /dev/null +++ b/pbl_blueprint/models/pbl_blueprint_version.json @@ -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": "版本状态" + } + } +} \ No newline at end of file diff --git a/pbl_blueprint/sql/pbl_blueprint.core.sql b/pbl_blueprint/sql/pbl_blueprint.core.sql new file mode 100644 index 0000000..4f2edec --- /dev/null +++ b/pbl_blueprint/sql/pbl_blueprint.core.sql @@ -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); diff --git a/pbl_blueprint/subobjects.py b/pbl_blueprint/subobjects.py new file mode 100644 index 0000000..a550cca --- /dev/null +++ b/pbl_blueprint/subobjects.py @@ -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) diff --git a/pbl_blueprint/templates.py b/pbl_blueprint/templates.py new file mode 100644 index 0000000..e588c28 --- /dev/null +++ b/pbl_blueprint/templates.py @@ -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': '已归档'}] diff --git a/pbl_blueprint/wwwroot/api/blueprint_delete.dspy b/pbl_blueprint/wwwroot/api/blueprint_delete.dspy new file mode 100644 index 0000000..d4e102e --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_delete.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_domain_options.dspy b/pbl_blueprint/wwwroot/api/blueprint_domain_options.dspy new file mode 100644 index 0000000..3eb016d --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_domain_options.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_fork.dspy b/pbl_blueprint/wwwroot/api/blueprint_fork.dspy new file mode 100644 index 0000000..ffca6a9 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_fork.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_get.dspy b/pbl_blueprint/wwwroot/api/blueprint_get.dspy new file mode 100644 index 0000000..18f776c --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_get.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_list.dspy b/pbl_blueprint/wwwroot/api/blueprint_list.dspy new file mode 100644 index 0000000..bcc7973 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_list.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_publish.dspy b/pbl_blueprint/wwwroot/api/blueprint_publish.dspy new file mode 100644 index 0000000..35324bd --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_publish.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_save.dspy b/pbl_blueprint/wwwroot/api/blueprint_save.dspy new file mode 100644 index 0000000..65f8261 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_save.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_status.dspy b/pbl_blueprint/wwwroot/api/blueprint_status.dspy new file mode 100644 index 0000000..1ecd22e --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_status.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_status_options.dspy b/pbl_blueprint/wwwroot/api/blueprint_status_options.dspy new file mode 100644 index 0000000..042030b --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_status_options.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_tree.dspy b/pbl_blueprint/wwwroot/api/blueprint_tree.dspy new file mode 100644 index 0000000..cf95422 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_tree.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_version_get.dspy b/pbl_blueprint/wwwroot/api/blueprint_version_get.dspy new file mode 100644 index 0000000..e2eeae3 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_version_get.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/blueprint_versions.dspy b/pbl_blueprint/wwwroot/api/blueprint_versions.dspy new file mode 100644 index 0000000..991838a --- /dev/null +++ b/pbl_blueprint/wwwroot/api/blueprint_versions.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/subobject_counts.dspy b/pbl_blueprint/wwwroot/api/subobject_counts.dspy new file mode 100644 index 0000000..ffbd6f6 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/subobject_counts.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/subobject_delete.dspy b/pbl_blueprint/wwwroot/api/subobject_delete.dspy new file mode 100644 index 0000000..87e152b --- /dev/null +++ b/pbl_blueprint/wwwroot/api/subobject_delete.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/subobject_get.dspy b/pbl_blueprint/wwwroot/api/subobject_get.dspy new file mode 100644 index 0000000..e06dfc0 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/subobject_get.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/subobject_list.dspy b/pbl_blueprint/wwwroot/api/subobject_list.dspy new file mode 100644 index 0000000..a165414 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/subobject_list.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/subobject_save.dspy b/pbl_blueprint/wwwroot/api/subobject_save.dspy new file mode 100644 index 0000000..0c3e35f --- /dev/null +++ b/pbl_blueprint/wwwroot/api/subobject_save.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/subobject_type_options.dspy b/pbl_blueprint/wwwroot/api/subobject_type_options.dspy new file mode 100644 index 0000000..67bb331 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/subobject_type_options.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/table_manifest.dspy b/pbl_blueprint/wwwroot/api/table_manifest.dspy new file mode 100644 index 0000000..c2ae624 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/table_manifest.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/template_delete.dspy b/pbl_blueprint/wwwroot/api/template_delete.dspy new file mode 100644 index 0000000..2bd671d --- /dev/null +++ b/pbl_blueprint/wwwroot/api/template_delete.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/template_from_blueprint.dspy b/pbl_blueprint/wwwroot/api/template_from_blueprint.dspy new file mode 100644 index 0000000..17d692b --- /dev/null +++ b/pbl_blueprint/wwwroot/api/template_from_blueprint.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/template_get.dspy b/pbl_blueprint/wwwroot/api/template_get.dspy new file mode 100644 index 0000000..85ef78d --- /dev/null +++ b/pbl_blueprint/wwwroot/api/template_get.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/template_instantiate.dspy b/pbl_blueprint/wwwroot/api/template_instantiate.dspy new file mode 100644 index 0000000..208fbb3 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/template_instantiate.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/template_list.dspy b/pbl_blueprint/wwwroot/api/template_list.dspy new file mode 100644 index 0000000..7c5c3a4 --- /dev/null +++ b/pbl_blueprint/wwwroot/api/template_list.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/api/template_save.dspy b/pbl_blueprint/wwwroot/api/template_save.dspy new file mode 100644 index 0000000..164036f --- /dev/null +++ b/pbl_blueprint/wwwroot/api/template_save.dspy @@ -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 diff --git a/pbl_blueprint/wwwroot/blueprint_editor.ui b/pbl_blueprint/wwwroot/blueprint_editor.ui new file mode 100644 index 0000000..099605e --- /dev/null +++ b/pbl_blueprint/wwwroot/blueprint_editor.ui @@ -0,0 +1,50 @@ + + +
+ + + + + + + +