691 lines
35 KiB
Python
691 lines
35 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""M1a 表定义对齐设计定稿生成器(权威:projects/pbls/docs/01-design/modules/pbl_blueprint.md §2)。
|
||
|
||
产出
|
||
----
|
||
1. pbl_blueprint/models/*.json —— 设计定稿 B1~B11 共 11 张表,四段式
|
||
(summary/fields/indexes/codes),primary=["id"],id str32,tenant_id 首业务字段,
|
||
金额字段 double(18,2)
|
||
2. pbl_blueprint/json/*.json —— 11 个 CRUD 契约,根键 tblname+params,
|
||
editable 3 个 .dspy URL,browserfields 非空
|
||
3. pbl_blueprint/sql/*.sql —— mariadb 方言 DDL(无 FK / 无 ENUM / 无 TIMESTAMP)
|
||
core.sql = B1~B3 + 模板;subobjects.sql = B4~B11;support.sql = 运维支撑表
|
||
4. 在途旧表定义(node/edge/lock/fork/audit/offline/publish/version_delta/template)
|
||
归档到 models/support/ 与 json/support/,不计入 11 表主清单,功能不丢失
|
||
|
||
幂等:可重复执行。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import shutil
|
||
|
||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
REPO = os.path.dirname(SCRIPT_DIR)
|
||
PKG = os.path.join(REPO, "pbl_blueprint")
|
||
MODELS = os.path.join(PKG, "models")
|
||
JSOND = os.path.join(PKG, "json")
|
||
SQLD = os.path.join(PKG, "sql")
|
||
|
||
# 设计定稿 B1~B11 主清单(权威表名,顺序即编号)
|
||
MAIN_TABLES = [
|
||
"pbl_blueprint",
|
||
"pbl_blueprint_version",
|
||
"pbl_blueprint_approval",
|
||
"pbl_learner",
|
||
"pbl_learning_goal",
|
||
"pbl_problem",
|
||
"pbl_driving_question",
|
||
"pbl_project",
|
||
"pbl_role",
|
||
"pbl_mission",
|
||
"pbl_artifact_def",
|
||
]
|
||
|
||
# 在途旧表 -> 归档为支撑表(不计入 11 表主清单)
|
||
SUPPORT_TABLES = [
|
||
"pbl_blueprint_audit",
|
||
"pbl_blueprint_edge",
|
||
"pbl_blueprint_fork",
|
||
"pbl_blueprint_lock",
|
||
"pbl_blueprint_node",
|
||
"pbl_blueprint_offline",
|
||
"pbl_blueprint_publish",
|
||
"pbl_blueprint_template",
|
||
"pbl_blueprint_version_delta",
|
||
]
|
||
|
||
|
||
# ------------------------------------------------------------------ 字段构造助手
|
||
def f_str(size, summary, notnull=True, default="", query=False, editable=True, code=None):
|
||
d = {"type": "str", "size": size, "notnull": notnull, "default": default,
|
||
"editable": editable, "query": query, "summary": summary}
|
||
if code:
|
||
d["code"] = code
|
||
return d
|
||
|
||
|
||
def f_id():
|
||
return {"type": "str", "size": 32, "notnull": True, "default": "",
|
||
"editable": True, "query": False, "summary": "主键ID(str32,UUID去横线)"}
|
||
|
||
|
||
def f_tenant():
|
||
return {"type": "str", "size": 32, "notnull": True, "default": "",
|
||
"editable": True, "query": True,
|
||
"summary": "租户ID(首业务字段,所有读写强制打头,缺失即 fail-closed 拒绝)"}
|
||
|
||
|
||
def f_int(summary, default=0, query=False, editable=True, notnull=True):
|
||
return {"type": "int", "notnull": notnull, "default": default,
|
||
"editable": editable, "query": query, "summary": summary}
|
||
|
||
|
||
def f_money(summary, query=False):
|
||
"""金额字段:double(18,2)(规范硬约束)"""
|
||
return {"type": "double", "size": [18, 2], "notnull": True, "default": 0,
|
||
"editable": True, "query": query, "summary": summary}
|
||
|
||
|
||
def f_dt(summary, editable=False, query=False):
|
||
return {"type": "datetime", "notnull": False, "default": None,
|
||
"editable": editable, "query": query, "summary": summary}
|
||
|
||
|
||
def f_text(summary, editable=True, query=False, ttype="text"):
|
||
return {"type": ttype, "notnull": False, "editable": editable,
|
||
"query": query, "summary": summary}
|
||
|
||
|
||
def audit_tail():
|
||
"""通用审计尾字段(所有表统一)"""
|
||
return [
|
||
("create_user", f_str(32, "创建人", editable=False)),
|
||
("create_time", f_dt("创建时间")),
|
||
("update_user", f_str(32, "更新人", editable=False)),
|
||
("update_time", f_dt("更新时间")),
|
||
("deleted", f_int("逻辑删除标记 0正常 1删除", default=0, editable=False)),
|
||
]
|
||
|
||
|
||
def sub_head(extra=None):
|
||
"""子对象公共头:id / tenant_id / blueprint_id / version_no / sort_no"""
|
||
out = [
|
||
("id", f_id()),
|
||
("tenant_id", f_tenant()),
|
||
("blueprint_id", f_str(32, "所属蓝图ID(聚合根外键,逻辑关联无物理FK)", query=True)),
|
||
("version_no", f_int("所属蓝图版本号(子对象随版本快照)", default=1, query=True)),
|
||
("sort_no", f_int("排序号", default=0)),
|
||
]
|
||
if extra:
|
||
out += extra
|
||
return out
|
||
|
||
|
||
# ------------------------------------------------------------------ 11 表定义
|
||
TABLES = {}
|
||
|
||
TABLES["pbl_blueprint"] = {
|
||
"summary": "PBL 蓝图聚合根(B1)。一条记录=一个可编译的项目式学习蓝图,"
|
||
"承载标题/学科/年级/班级/负责教师/状态/质量等级/当前版本号,"
|
||
"并记录生成来源(人工/模板/AI生成)与 fork 溯源。"
|
||
"所有读写 tenant_id 强制打头,跨租户一律 404。",
|
||
"fields": [
|
||
("id", f_id()),
|
||
("tenant_id", f_tenant()),
|
||
("code", f_str(64, "蓝图编码(租户内唯一,创建自动生成)", query=True)),
|
||
("title", f_str(200, "蓝图标题", query=True)),
|
||
("summary", f_str(1000, "蓝图摘要")),
|
||
("subject", f_str(64, "学科", query=True, code="pbl_subject")),
|
||
("grade_level", f_str(32, "适用年级", query=True, code="pbl_grade_level")),
|
||
("class_id", f_str(32, "关联班级ID(可空=未绑定班级)", query=True, notnull=False)),
|
||
("owner_teacher_id", f_str(32, "负责教师ID", query=True)),
|
||
("status", f_str(32, "蓝图状态", default="draft", query=True, code="pbl_blueprint_status")),
|
||
("quality_level", f_str(32, "质量等级(5级,禁跳级上升)", default="draft",
|
||
query=True, code="pbl_quality_level")),
|
||
("current_version", f_int("当前生效版本号", default=1, query=True)),
|
||
("generation_source", f_str(32, "生成来源", default="manual", query=True,
|
||
code="pbl_generation_source")),
|
||
("source_template_id", f_str(32, "来源模板ID(generation_source=template 时必填)",
|
||
notnull=False, query=True)),
|
||
("fork_from_id", f_str(32, "fork 源蓝图ID(溯源,可空)", notnull=False, query=True)),
|
||
("cover_url", f_str(512, "封面图URL", notnull=False)),
|
||
("tags", f_str(512, "标签(逗号分隔)", query=True)),
|
||
("duration_hours", f_int("预计总课时(小时)", default=0, query=True)),
|
||
("budget_amount", f_money("蓝图预算金额(元,double(18,2))")),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_code", "type": "unique", "fields": ["tenant_id", "code", "deleted"]},
|
||
{"name": "idx_tenant_status", "type": "normal", "fields": ["tenant_id", "status"]},
|
||
{"name": "idx_tenant_owner", "type": "normal", "fields": ["tenant_id", "owner_teacher_id"]},
|
||
{"name": "idx_tenant_class", "type": "normal", "fields": ["tenant_id", "class_id"]},
|
||
],
|
||
"codes": ["pbl_blueprint_status", "pbl_quality_level", "pbl_generation_source",
|
||
"pbl_subject", "pbl_grade_level"],
|
||
}
|
||
|
||
TABLES["pbl_blueprint_version"] = {
|
||
"summary": "蓝图版本快照(B2,append-only)。每次 update/commit 生成一条新版本,"
|
||
"存全量 snapshot_json + sha256 快照哈希 + change_delta 三分类变更"
|
||
"(added/modified/removed),is_current 标记当前生效版本。"
|
||
"历史版本永不修改,rollback 采用软删重建后提交新版本。",
|
||
"fields": [
|
||
("id", f_id()),
|
||
("tenant_id", f_tenant()),
|
||
("blueprint_id", f_str(32, "所属蓝图ID", query=True)),
|
||
("version_no", f_int("版本号(蓝图内递增)", default=1, query=True)),
|
||
("version_type", f_str(16, "版本类型", default="draft", query=True,
|
||
code="pbl_version_type")),
|
||
("change_summary", f_str(512, "变更说明")),
|
||
("change_reason", f_str(512, "变更原因(审计留痕)")),
|
||
("snapshot_json", f_text("全量快照JSON(聚合根+7类子对象+关系)", ttype="longtext")),
|
||
("snapshot_hash", f_str(64, "快照sha256哈希(确定性序列化 sort_keys+紧凑分隔符)", query=True)),
|
||
("change_delta_json", f_text("变更增量JSON(added/modified/removed 三分类 + stat_*)")),
|
||
("stat_added", f_int("新增对象数", default=0)),
|
||
("stat_modified", f_int("修改对象数", default=0)),
|
||
("stat_removed", f_int("删除对象数", default=0)),
|
||
("is_current", f_int("是否当前生效版本 0否 1是", default=0, query=True)),
|
||
("commit_user", f_str(32, "提交人")),
|
||
("commit_time", f_dt("提交时间", editable=True, query=True)),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_ver", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "deleted"]},
|
||
{"name": "idx_tenant_current", "type": "normal",
|
||
"fields": ["tenant_id", "blueprint_id", "is_current"]},
|
||
],
|
||
"codes": ["pbl_version_type"],
|
||
}
|
||
|
||
TABLES["pbl_blueprint_approval"] = {
|
||
"summary": "蓝图审批记录(B3)。记录提交人/审批人/审批状态/审批意见与时间,"
|
||
"支撑 draft→submitted→approved/rejected→published 状态流转的审批留痕。"
|
||
"一次提交一条记录,驳回后可再次提交形成多条。",
|
||
"fields": [
|
||
("id", f_id()),
|
||
("tenant_id", f_tenant()),
|
||
("blueprint_id", f_str(32, "所属蓝图ID", query=True)),
|
||
("version_no", f_int("被审批的版本号", default=1, query=True)),
|
||
("submit_user", f_str(32, "提交人ID", query=True)),
|
||
("submit_time", f_dt("提交时间", editable=True, query=True)),
|
||
("approver_id", f_str(32, "审批人ID", query=True, notnull=False)),
|
||
("approval_status", f_str(32, "审批状态", default="pending", query=True,
|
||
code="pbl_approval_status")),
|
||
("approval_opinion", f_str(1000, "审批意见")),
|
||
("approval_time", f_dt("审批时间", editable=True, query=True)),
|
||
("approval_round", f_int("审批轮次(驳回重提递增)", default=1, query=True)),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "idx_tenant_bp", "type": "normal", "fields": ["tenant_id", "blueprint_id"]},
|
||
{"name": "idx_tenant_approver", "type": "normal", "fields": ["tenant_id", "approver_id"]},
|
||
{"name": "idx_tenant_status", "type": "normal", "fields": ["tenant_id", "approval_status"]},
|
||
],
|
||
"codes": ["pbl_approval_status"],
|
||
}
|
||
|
||
TABLES["pbl_learner"] = {
|
||
"summary": "学习者画像子对象(B4)。描述本蓝图面向的学习者群体特征:"
|
||
"先备知识、技能基线、兴趣标签、人数区间与互动方式,"
|
||
"供 Compiler 生成差异化任务与 Assessment 设定评估基线。",
|
||
"fields": sub_head() + [
|
||
("name", f_str(200, "画像名称", query=True)),
|
||
("description", f_str(2000, "画像描述")),
|
||
("prior_knowledge", f_str(2000, "先备知识")),
|
||
("skill_baseline", f_str(2000, "技能基线")),
|
||
("interest_tags", f_str(512, "兴趣标签(逗号分隔)", query=True)),
|
||
("learner_count", f_int("预计学习者人数", default=0, query=True)),
|
||
("min_count", f_int("最小人数", default=1)),
|
||
("max_count", f_int("最大人数", default=40)),
|
||
("interaction_type", f_str(32, "互动方式", default="individual", query=True,
|
||
code="pbl_interaction_type")),
|
||
("group_mode", f_str(32, "分组模式", default="free", query=True, code="pbl_group_mode")),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_sort", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "sort_no", "deleted"]},
|
||
{"name": "idx_tenant_bp", "type": "normal", "fields": ["tenant_id", "blueprint_id"]},
|
||
],
|
||
"codes": ["pbl_interaction_type", "pbl_group_mode"],
|
||
}
|
||
|
||
TABLES["pbl_learning_goal"] = {
|
||
"summary": "学习目标子对象(B5)。可测量的学习目标定义:目标类型、布卢姆认知层级、"
|
||
"素养维度、是否可测量、测量方式与目标值、解锁条件。"
|
||
"是 Assessment Rubric 权重分配的锚点。",
|
||
"fields": sub_head() + [
|
||
("goal_code", f_str(64, "目标编码(蓝图内唯一)", query=True)),
|
||
("title", f_str(200, "目标标题", query=True)),
|
||
("description", f_str(2000, "目标描述")),
|
||
("goal_type", f_str(32, "目标类型", default="knowledge", query=True,
|
||
code="pbl_goal_type")),
|
||
("measurable", f_int("是否可测量 0否 1是", default=1, query=True)),
|
||
("measure_method", f_str(512, "测量方式")),
|
||
("target_value", f_str(128, "目标值(如 ≥80分 / 完成3次)")),
|
||
("bloom_level", f_str(32, "布卢姆认知层级", default="understand", query=True,
|
||
code="pbl_bloom_level")),
|
||
("competency_dim", f_str(64, "素养维度", query=True, code="pbl_competency_dim")),
|
||
("weight", f_int("评估权重(百分比 0-100)", default=0, query=True)),
|
||
("unlock_condition_json", f_text("解锁条件JSON(前置目标/任务达成后解锁)")),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_code", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "goal_code", "deleted"]},
|
||
{"name": "idx_tenant_bp", "type": "normal", "fields": ["tenant_id", "blueprint_id"]},
|
||
],
|
||
"codes": ["pbl_goal_type", "pbl_bloom_level", "pbl_competency_dim"],
|
||
}
|
||
|
||
TABLES["pbl_problem"] = {
|
||
"summary": "真实世界问题子对象(B6)。PBL 的核心锚点——定义待解决的真实问题:"
|
||
"真实性等级、现实情境、利益相关方、数据来源与复杂度,"
|
||
"驱动问题(B7)由本表派生。",
|
||
"fields": sub_head() + [
|
||
("title", f_str(200, "问题标题", query=True)),
|
||
("description", f_str(4000, "问题描述")),
|
||
("authenticity_level", f_str(32, "真实性等级", default="simulated", query=True,
|
||
code="pbl_authenticity_level")),
|
||
("real_world_context", f_str(2000, "现实情境描述")),
|
||
("stakeholder", f_str(512, "利益相关方")),
|
||
("data_source", f_str(1000, "数据来源(真实数据集/调研/文献)")),
|
||
("complexity_level", f_str(32, "复杂度等级", default="medium", query=True,
|
||
code="pbl_complexity_level")),
|
||
("domain", f_str(64, "所属领域", query=True, code="pbl_domain")),
|
||
("sdg_goals", f_str(256, "关联联合国可持续发展目标(逗号分隔)")),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_sort", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "sort_no", "deleted"]},
|
||
{"name": "idx_tenant_bp", "type": "normal", "fields": ["tenant_id", "blueprint_id"]},
|
||
],
|
||
"codes": ["pbl_authenticity_level", "pbl_complexity_level", "pbl_domain"],
|
||
}
|
||
|
||
TABLES["pbl_driving_question"] = {
|
||
"summary": "驱动问题子对象(B7)。由真实问题(B6)提炼的可探究开放性问题:"
|
||
"问题文本、类型、是否开放、关联问题与关联目标、难度。"
|
||
"是 Compiler 生成任务链的起点。",
|
||
"fields": sub_head() + [
|
||
("question_text", f_str(1000, "驱动问题文本", query=True)),
|
||
("question_type", f_str(32, "问题类型", default="open", query=True,
|
||
code="pbl_question_type")),
|
||
("is_open_ended", f_int("是否开放式问题 0否 1是", default=1, query=True)),
|
||
("linked_problem_id", f_str(32, "关联真实问题ID(B6)", notnull=False, query=True)),
|
||
("linked_goal_ids", f_str(512, "关联学习目标ID列表(逗号分隔,B5)")),
|
||
("difficulty", f_str(32, "难度", default="medium", query=True, code="pbl_difficulty")),
|
||
("expected_outcome", f_str(2000, "预期探究成果")),
|
||
("sub_questions", f_text("子问题列表JSON(分解探究路径)")),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_sort", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "sort_no", "deleted"]},
|
||
{"name": "idx_tenant_problem", "type": "normal",
|
||
"fields": ["tenant_id", "linked_problem_id"]},
|
||
],
|
||
"codes": ["pbl_question_type", "pbl_difficulty"],
|
||
}
|
||
|
||
TABLES["pbl_project"] = {
|
||
"summary": "项目子对象(B8)。蓝图的执行约束定义:周期、起止日期、团队规模区间、"
|
||
"约束条件JSON、资源清单JSON、项目预算金额(double(18,2))与交付物摘要。"
|
||
"Compiler 据此生成 Game Definition 的时间与资源边界。",
|
||
"fields": sub_head() + [
|
||
("name", f_str(200, "项目名称", query=True)),
|
||
("description", f_str(2000, "项目描述")),
|
||
("duration_hours", f_int("项目总课时(小时)", default=0, query=True)),
|
||
("start_date", {"type": "date", "notnull": False, "default": None,
|
||
"editable": True, "query": True, "summary": "计划开始日期"}),
|
||
("end_date", {"type": "date", "notnull": False, "default": None,
|
||
"editable": True, "query": True, "summary": "计划结束日期"}),
|
||
("team_size_min", f_int("团队最小人数", default=1)),
|
||
("team_size_max", f_int("团队最大人数", default=6)),
|
||
("team_count", f_int("团队数量", default=1, query=True)),
|
||
("constraint_json", f_text("约束条件JSON(时间/资源/规则边界)")),
|
||
("resource_json", f_text("资源清单JSON(场地/设备/材料/经费)")),
|
||
("budget_amount", f_money("项目预算金额(元,double(18,2))", query=True)),
|
||
("deliverable_summary", f_str(2000, "交付物摘要")),
|
||
("milestone_json", f_text("里程碑JSON(阶段节点+验收标准)")),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_sort", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "sort_no", "deleted"]},
|
||
{"name": "idx_tenant_bp", "type": "normal", "fields": ["tenant_id", "blueprint_id"]},
|
||
],
|
||
"codes": [],
|
||
}
|
||
|
||
TABLES["pbl_role"] = {
|
||
"summary": "角色子对象(B9)。项目内学习者角色定义:角色编码/名称/职责/所需技能/"
|
||
"人数区间/归属权(ownership:个人/团队/全班)。"
|
||
"scense_runtime 据此分配玩家角色与权限。",
|
||
"fields": sub_head() + [
|
||
("role_code", f_str(64, "角色编码(蓝图内唯一)", query=True)),
|
||
("role_name", f_str(128, "角色名称", query=True)),
|
||
("description", f_str(2000, "角色描述")),
|
||
("responsibility", f_str(2000, "角色职责")),
|
||
("required_skills", f_str(1000, "所需技能(逗号分隔)")),
|
||
("min_count", f_int("最小人数", default=1)),
|
||
("max_count", f_int("最大人数", default=1)),
|
||
("ownership", f_str(32, "归属权", default="team", query=True, code="pbl_ownership")),
|
||
("permission_json", f_text("角色权限JSON(可操作对象与动作白名单)")),
|
||
("avatar_hint", f_str(512, "形象提示(供文生图/角色卡渲染)")),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_code", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "role_code", "deleted"]},
|
||
{"name": "idx_tenant_bp", "type": "normal", "fields": ["tenant_id", "blueprint_id"]},
|
||
],
|
||
"codes": ["pbl_ownership"],
|
||
}
|
||
|
||
TABLES["pbl_mission"] = {
|
||
"summary": "任务子对象(B10)。可执行的任务/关卡定义:任务编码/标题/类型/解锁条件/"
|
||
"前置任务/预计耗时/是否可评估/关联目标。"
|
||
"Compiler 按 prerequisite 拓扑排序生成任务链,runtime 按 unlock_condition 放行。",
|
||
"fields": sub_head() + [
|
||
("mission_code", f_str(64, "任务编码(蓝图内唯一)", query=True)),
|
||
("title", f_str(200, "任务标题", query=True)),
|
||
("description", f_str(4000, "任务描述")),
|
||
("mission_type", f_str(32, "任务类型", default="explore", query=True,
|
||
code="pbl_mission_type")),
|
||
("unlock_condition_json", f_text("解锁条件JSON(前置任务/时间/评分门槛)")),
|
||
("prerequisite_ids", f_str(512, "前置任务ID列表(逗号分隔,拓扑排序依据)")),
|
||
("duration_hours", f_int("预计耗时(小时)", default=0, query=True)),
|
||
("assessable", f_int("是否可评估 0否 1是", default=1, query=True)),
|
||
("linked_goal_ids", f_str(512, "关联学习目标ID列表(逗号分隔,B5)")),
|
||
("linked_role_ids", f_str(512, "关联角色ID列表(逗号分隔,B9)")),
|
||
("reward_json", f_text("奖励JSON(积分/徽章/解锁物)")),
|
||
("score_weight", f_int("评分权重(百分比 0-100)", default=0, query=True)),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_code", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "mission_code", "deleted"]},
|
||
{"name": "idx_tenant_bp", "type": "normal", "fields": ["tenant_id", "blueprint_id"]},
|
||
{"name": "idx_tenant_type", "type": "normal", "fields": ["tenant_id", "mission_type"]},
|
||
],
|
||
"codes": ["pbl_mission_type"],
|
||
}
|
||
|
||
TABLES["pbl_artifact_def"] = {
|
||
"summary": "产出物定义子对象(B11)。学习者需提交的产出物规格:编码/名称/类型/"
|
||
"文件格式/是否可评估/关联Rubric/归属权/数量区间/是否必须留证。"
|
||
"pbl_evidence(M5)按本表采集与校验证据。",
|
||
"fields": sub_head() + [
|
||
("artifact_code", f_str(64, "产出物编码(蓝图内唯一)", query=True)),
|
||
("name", f_str(200, "产出物名称", query=True)),
|
||
("description", f_str(2000, "产出物描述")),
|
||
("artifact_type", f_str(32, "产出物类型", default="document", query=True,
|
||
code="pbl_artifact_type")),
|
||
("file_format", f_str(64, "允许文件格式(逗号分隔,如 mp4,pdf,png)", query=True)),
|
||
("max_size_mb", f_int("单文件最大体积(MB)", default=100)),
|
||
("assessable", f_int("是否可评估 0否 1是", default=1, query=True)),
|
||
("rubric_id", f_str(32, "关联评分量规ID(pbl_assessment M6)", notnull=False, query=True)),
|
||
("ownership", f_str(32, "归属权", default="team", query=True, code="pbl_ownership")),
|
||
("min_count", f_int("最少提交数", default=1)),
|
||
("max_count", f_int("最多提交数", default=1)),
|
||
("evidence_required", f_int("是否必须留证 0否 1是", default=1, query=True)),
|
||
("linked_mission_ids", f_str(512, "关联任务ID列表(逗号分隔,B10)")),
|
||
("acceptance_criteria", f_str(2000, "验收标准")),
|
||
] + audit_tail(),
|
||
"indexes": [
|
||
{"name": "PRIMARY", "type": "primary", "fields": ["id"]},
|
||
{"name": "uk_tenant_bp_code", "type": "unique",
|
||
"fields": ["tenant_id", "blueprint_id", "version_no", "artifact_code", "deleted"]},
|
||
{"name": "idx_tenant_bp", "type": "normal", "fields": ["tenant_id", "blueprint_id"]},
|
||
{"name": "idx_tenant_type", "type": "normal", "fields": ["tenant_id", "artifact_type"]},
|
||
],
|
||
"codes": ["pbl_artifact_type", "pbl_ownership"],
|
||
}
|
||
|
||
# 支撑表(在途旧定义,归档保留;不计入 11 表主清单)
|
||
SUPPORT_SUMMARY = {
|
||
"pbl_blueprint_audit": "蓝图操作审计日志(append-only,支撑表)",
|
||
"pbl_blueprint_edge": "子对象有向关系边(支撑表,weight+关系类型)",
|
||
"pbl_blueprint_fork": "蓝图 fork 溯源链(支撑表,双向血缘)",
|
||
"pbl_blueprint_lock": "蓝图编辑锁(支撑表,lock_token+expire 过期判定)",
|
||
"pbl_blueprint_node": "子对象泛化节点(支撑表,obj_type 判别+payload JSON)",
|
||
"pbl_blueprint_offline": "离线包导出/导入记录(支撑表,三级回落兜底)",
|
||
"pbl_blueprint_publish": "发布记录(支撑表,published 后禁删)",
|
||
"pbl_blueprint_template": "蓝图模板(支撑表,body+body_hash+builtin 平台内置)",
|
||
"pbl_blueprint_version_delta": "版本变更增量明细(支撑表,added/modified/removed)",
|
||
}
|
||
|
||
|
||
# ------------------------------------------------------------------ 写 models
|
||
def write_models():
|
||
os.makedirs(MODELS, exist_ok=True)
|
||
sup_dir = os.path.join(MODELS, "support")
|
||
os.makedirs(sup_dir, exist_ok=True)
|
||
|
||
# 归档在途旧表定义(若存在于主目录)
|
||
archived = []
|
||
for t in SUPPORT_TABLES:
|
||
src = os.path.join(MODELS, t + ".json")
|
||
if os.path.isfile(src):
|
||
shutil.move(src, os.path.join(sup_dir, t + ".json"))
|
||
archived.append(t)
|
||
# 主目录只保留设计定稿 11 表
|
||
for fn in os.listdir(MODELS):
|
||
if fn.endswith(".json") and fn[:-5] not in MAIN_TABLES:
|
||
shutil.move(os.path.join(MODELS, fn), os.path.join(sup_dir, fn))
|
||
archived.append(fn[:-5])
|
||
|
||
for t in MAIN_TABLES:
|
||
spec = TABLES[t]
|
||
doc = {
|
||
"summary": spec["summary"],
|
||
"primary": ["id"],
|
||
"fields": {k: v for k, v in spec["fields"]},
|
||
"indexes": spec["indexes"],
|
||
"codes": spec["codes"],
|
||
}
|
||
with open(os.path.join(MODELS, t + ".json"), "w", encoding="utf-8") as f:
|
||
json.dump(doc, f, ensure_ascii=False, indent=2)
|
||
f.write("\n")
|
||
print("[models] 主清单 11 表已写入;归档支撑表 %d 个: %s" % (len(archived), archived))
|
||
|
||
|
||
# ------------------------------------------------------------------ 写 json CRUD
|
||
BROWSER_PICK = {
|
||
"pbl_blueprint": ["code", "title", "subject", "grade_level", "status",
|
||
"quality_level", "owner_teacher_id", "current_version"],
|
||
"pbl_blueprint_version": ["blueprint_id", "version_no", "version_type",
|
||
"is_current", "snapshot_hash", "commit_time"],
|
||
"pbl_blueprint_approval": ["blueprint_id", "version_no", "approval_status",
|
||
"submit_user", "approver_id", "approval_time"],
|
||
"pbl_learner": ["blueprint_id", "name", "interaction_type", "group_mode",
|
||
"learner_count", "sort_no"],
|
||
"pbl_learning_goal": ["blueprint_id", "goal_code", "title", "goal_type",
|
||
"bloom_level", "measurable", "weight"],
|
||
"pbl_problem": ["blueprint_id", "title", "authenticity_level",
|
||
"complexity_level", "domain"],
|
||
"pbl_driving_question": ["blueprint_id", "question_text", "question_type",
|
||
"is_open_ended", "difficulty"],
|
||
"pbl_project": ["blueprint_id", "name", "duration_hours", "start_date",
|
||
"end_date", "team_count", "budget_amount"],
|
||
"pbl_role": ["blueprint_id", "role_code", "role_name", "ownership",
|
||
"min_count", "max_count"],
|
||
"pbl_mission": ["blueprint_id", "mission_code", "title", "mission_type",
|
||
"assessable", "duration_hours", "score_weight"],
|
||
"pbl_artifact_def": ["blueprint_id", "artifact_code", "name", "artifact_type",
|
||
"file_format", "assessable", "ownership"],
|
||
}
|
||
|
||
|
||
def write_json():
|
||
os.makedirs(JSOND, exist_ok=True)
|
||
sup_dir = os.path.join(JSOND, "support")
|
||
os.makedirs(sup_dir, exist_ok=True)
|
||
archived = []
|
||
for fn in os.listdir(JSOND):
|
||
if fn.endswith(".json") and fn[:-5] not in MAIN_TABLES:
|
||
shutil.move(os.path.join(JSOND, fn), os.path.join(sup_dir, fn))
|
||
archived.append(fn[:-5])
|
||
|
||
for t in MAIN_TABLES:
|
||
spec = TABLES[t]
|
||
params = {k: v for k, v in spec["fields"]}
|
||
doc = {
|
||
"tblname": t,
|
||
"params": params,
|
||
"editable": [
|
||
"api/%s/list.dspy" % t,
|
||
"api/%s/edit.dspy" % t,
|
||
"api/%s/view.dspy" % t,
|
||
],
|
||
"browserfields": BROWSER_PICK.get(t, ["id", "tenant_id"]),
|
||
}
|
||
with open(os.path.join(JSOND, t + ".json"), "w", encoding="utf-8") as f:
|
||
json.dump(doc, f, ensure_ascii=False, indent=2)
|
||
f.write("\n")
|
||
print("[json] 主清单 11 个 CRUD 契约已写入;归档支撑契约 %d 个: %s"
|
||
% (len(archived), archived))
|
||
|
||
|
||
# ------------------------------------------------------------------ 写 DDL
|
||
TYPE_MAP = {
|
||
"str": lambda v: "varchar(%d)" % (v.get("size") or 255),
|
||
"int": lambda v: "int",
|
||
"double": lambda v: "double(%d,%d)" % (
|
||
(v.get("size")[0] if isinstance(v.get("size"), list) else (v.get("size") or 18)),
|
||
(v.get("size")[1] if isinstance(v.get("size"), list) else (v.get("scale") or 2))),
|
||
"datetime": lambda v: "datetime",
|
||
"date": lambda v: "date",
|
||
"text": lambda v: "text",
|
||
"longtext": lambda v: "longtext",
|
||
}
|
||
|
||
|
||
def col_ddl(name, spec):
|
||
t = spec.get("type", "str")
|
||
sqltype = TYPE_MAP.get(t, lambda v: "varchar(255)")(spec)
|
||
parts = [" `%s` %s" % (name, sqltype)]
|
||
if spec.get("notnull"):
|
||
parts.append("NOT NULL")
|
||
d = spec.get("default")
|
||
if d is not None:
|
||
if isinstance(d, str):
|
||
parts.append("DEFAULT '%s'" % d.replace("'", "''"))
|
||
else:
|
||
parts.append("DEFAULT %s" % d)
|
||
elif not spec.get("notnull"):
|
||
parts.append("DEFAULT NULL")
|
||
c = spec.get("summary", "")
|
||
parts.append("COMMENT '%s'" % c.replace("'", "''"))
|
||
return " ".join(parts)
|
||
|
||
|
||
def table_ddl(t, spec):
|
||
lines = ["-- %s" % t, "CREATE TABLE IF NOT EXISTS `%s` (" % t]
|
||
cols = [col_ddl(k, v) for k, v in spec["fields"]]
|
||
for idx in spec["indexes"]:
|
||
kind = idx.get("type", "normal")
|
||
flds = ", ".join("`%s`" % x for x in idx["fields"])
|
||
if kind == "primary":
|
||
cols.append(" PRIMARY KEY (%s)" % flds)
|
||
elif kind == "unique":
|
||
cols.append(" UNIQUE KEY `%s` (%s)" % (idx["name"], flds))
|
||
else:
|
||
cols.append(" KEY `%s` (%s)" % (idx["name"], flds))
|
||
lines.append(",\n".join(cols))
|
||
lines.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
|
||
" COMMENT='%s';" % spec["summary"].split("。")[0].replace("'", "''"))
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
|
||
HEADER = """-- =============================================================================
|
||
-- pbl_blueprint %s
|
||
-- 方言:mariadb(无 FOREIGN KEY / 无 ENUM / 无 TIMESTAMP,时间统一 datetime)
|
||
-- 权威来源:projects/pbls/docs/01-design/modules/pbl_blueprint.md §2 表清单 B1~B11
|
||
-- 铁律:每表首业务字段 tenant_id varchar(32) NOT NULL,所有读写强制打头;
|
||
-- 金额字段 double(18,2);主键 id varchar(32)(UUID 去横线)。
|
||
-- =============================================================================
|
||
|
||
"""
|
||
|
||
|
||
def write_sql():
|
||
os.makedirs(SQLD, exist_ok=True)
|
||
core = ["pbl_blueprint", "pbl_blueprint_version", "pbl_blueprint_approval"]
|
||
subs = [t for t in MAIN_TABLES if t not in core]
|
||
|
||
with open(os.path.join(SQLD, "pbl_blueprint.core.sql"), "w", encoding="utf-8") as f:
|
||
f.write(HEADER % "核心表 DDL(B1 聚合根 / B2 版本 / B3 审批)")
|
||
for t in core:
|
||
f.write(table_ddl(t, TABLES[t]))
|
||
|
||
with open(os.path.join(SQLD, "pbl_blueprint.subobjects.sql"), "w", encoding="utf-8") as f:
|
||
f.write(HEADER % "7 类子对象表 DDL(B4~B11,独立建表,blueprint_id+version_no 归属聚合根)")
|
||
for t in subs:
|
||
f.write(table_ddl(t, TABLES[t]))
|
||
|
||
# 支撑表 DDL:从归档的 models/support 读取(若在途有定义)
|
||
sup_dir = os.path.join(MODELS, "support")
|
||
sup_lines = []
|
||
if os.path.isdir(sup_dir):
|
||
for fn in sorted(os.listdir(sup_dir)):
|
||
if not fn.endswith(".json"):
|
||
continue
|
||
try:
|
||
with open(os.path.join(sup_dir, fn), "r", encoding="utf-8") as f:
|
||
d = json.load(f)
|
||
flds = d.get("fields") or {}
|
||
idxs = d.get("indexes") or []
|
||
if not flds:
|
||
continue
|
||
cols = []
|
||
for k, v in flds.items():
|
||
if isinstance(v, dict):
|
||
cols.append(col_ddl(k, v))
|
||
pk = d.get("primary") or ["id"]
|
||
cols.append(" PRIMARY KEY (%s)" % ", ".join("`%s`" % x for x in pk))
|
||
for idx in idxs:
|
||
if not isinstance(idx, dict):
|
||
continue
|
||
fldl = ", ".join("`%s`" % x for x in (idx.get("fields") or []))
|
||
if not fldl:
|
||
continue
|
||
kind = idx.get("type", "normal")
|
||
nm = idx.get("name", "idx")
|
||
if kind == "unique":
|
||
cols.append(" UNIQUE KEY `%s` (%s)" % (nm, fldl))
|
||
elif kind != "primary":
|
||
cols.append(" KEY `%s` (%s)" % (nm, fldl))
|
||
sup_lines.append("-- %s:%s" % (fn[:-5], SUPPORT_SUMMARY.get(fn[:-5], "支撑表")))
|
||
sup_lines.append("CREATE TABLE IF NOT EXISTS `%s` (" % fn[:-5])
|
||
sup_lines.append(",\n".join(cols))
|
||
sup_lines.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 "
|
||
"COLLATE=utf8mb4_general_ci COMMENT='%s';\n"
|
||
% SUPPORT_SUMMARY.get(fn[:-5], "支撑表"))
|
||
except Exception as e: # noqa: BLE001
|
||
sup_lines.append("-- [WARN] %s 生成失败: %s\n" % (fn, e))
|
||
with open(os.path.join(SQLD, "pbl_blueprint.support.sql"), "w", encoding="utf-8") as f:
|
||
f.write(HEADER % "运维支撑表 DDL(锁/fork溯源/审计/离线包/发布/模板/关系边/泛化节点/版本增量)")
|
||
f.write("\n".join(sup_lines) if sup_lines else "-- (无支撑表定义)\n")
|
||
print("[sql] core(%d表) + subobjects(%d表) + support(%d表) 已写入"
|
||
% (len(core), len(subs), len(sup_lines) // 4 if sup_lines else 0))
|
||
|
||
|
||
def main():
|
||
write_models()
|
||
write_json()
|
||
write_sql()
|
||
print("=" * 70)
|
||
print("M1a 表定义已对齐设计定稿 B1~B11:models 11 + json 11 + sql 3 文件")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|