132 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: pbl_blueprint
description: PBL 蓝图聚合根与子对象、版本、模板(M1a)。蓝图 CRUD/树/fork、7 类子对象泛化契约、版本 change_delta、模板实例化与离线兜底。所有读写 tenant_id 强制打头,缺失即 fail-closed 拒绝。
---
# pbl_blueprint 模块技能
## 1. 模块定位
`pbl_blueprint` 是 PBL(Project-Based Learning)平台的**蓝图域聚合根模块**,负责:
- 蓝图主表 `pbl_blueprint` 的 CRUD / 列表 / 树 / fork
- 7 类子对象的**泛化契约**(一套 CRUD 覆盖 7 张表)
- 版本快照 `pbl_blueprint_version`(含 `change_delta` 增量、diff、rollback)
- 模板 `pbl_blueprint_template` / `pbl_blueprint_template_item`(实例化 + 离线兜底)
模块是 **Python 包**(无 app.py / 无端口 / 无 Dockerfile),通过 `load_pbl_blueprint(env)` 挂到应用 `apps/pbls`。
## 2. 目录结构
```
modules/pbl_blueprint/
├── pyproject.toml
├── scripts/load_path.py # RBAC 路径注册(API/页面/表权限)
└── pbl_blueprint/ # 包目录 = 模块名(禁 src/)
├── __init__.py # 导出 load_pbl_blueprint
├── init.py # 装配入口 + env 注册 + 契约字典
├── db.py # 库名解析 / sqlor 获取 / 异常体系 / 建表
├── blueprint_crud.py # 蓝图 CRUD + 树 + fork + 版本契约
├── subobjects.py # 7 类子对象泛化 CRUD
├── templates.py # 模板 CRUD + 实例化 + 离线兜底
├── api.py # HTTP API 层 + API_PATHS + HANDLERS
├── models/*.json # 表定义(四段式 summary/fields/indexes/codes)
├── json/*.json # CRUD 定义(根键 tblname + params.editable/browserfields)
├── sql/pbl_blueprint.core.sql # 核心 4 表 DDL
├── sql/pbl_blueprint.subobjects.sql # 子对象 7 表 DDL
├── init/data.json # 初始化数据(内置模板)
└── wwwroot/index.ui # 前端页面
```
## 3. 数据表(11 张)
核心 4 张:`pbl_blueprint`、`pbl_blueprint_version`、`pbl_blueprint_template`、`pbl_blueprint_template_item`
子对象 7 张:`pbl_blueprint_task`、`pbl_blueprint_mission`、`pbl_blueprint_role`、
`pbl_blueprint_learning_goal`、`pbl_blueprint_evidence_spec`、`pbl_blueprint_artifact_spec`、
`pbl_blueprint_reflection_spec`
统一约定:
- 主键 `id` = varchar(32),32 位无横线 UUID(`db.new_id()`)
- `tenant_id` varchar(32) NOT NULL,**每个索引首列**
- 软删除 `deleted` tinyint(0 正常 / 1 已删除)
- 结构化字段用 `json` 类型(`spec` / `config` / `snapshot` / `change_delta` / `rule` / `permissions`)
- 审计五件套 `created_by/created_at/updated_by/updated_at/deleted`
## 4. 子对象泛化契约(重点)
7 类子对象共用一套函数,`obj_type` 分派到表:
| obj_type | 表 | 编码前缀 | 特有字段 |
|---|---|---|---|
| task | pbl_blueprint_task | TASK | parent_id |
| mission | pbl_blueprint_mission | MISSION | task_id(必填), unlock_condition |
| role | pbl_blueprint_role | ROLE | task_id, permissions |
| learning_goal | pbl_blueprint_learning_goal | GOAL | task_id, dimension, weight |
| evidence_spec | pbl_blueprint_evidence_spec | EVI | task_id, mission_id, learning_goal_id, evidence_type, collect_mode, rule, weight |
| artifact_spec | pbl_blueprint_artifact_spec | ART | task_id, mission_id, artifact_type, accept_criteria, required, weight |
| reflection_spec | pbl_blueprint_reflection_spec | REF | task_id, mission_id, trigger_point, reflection_type, rubric_ref, weight |
```python
from pbl_blueprint import load_pbl_blueprint
contract = load_pbl_blueprint(env)
contract['create_subobject'](tenant_id, 'mission', {'blueprint_id': bp_id, 'task_id': t_id, 'name': '关卡1'}, env=env)
contract['list_subobjects'](tenant_id, 'task', bp_id, env=env)
contract['batch_upsert_subobjects'](tenant_id, 'task', bp_id, rows, env=env) # Designer Agent 整树写入
```
## 5. 蓝图契约
```python
create_blueprint(tenant_id, data, env, operator) # 自动生成 code PBL-{yyyyMMdd}-{seq4}
update_blueprint(tenant_id, blueprint_id, data, ...) # 状态流转校验 STATUS_FLOW
delete_blueprint(tenant_id, blueprint_id, ...) # 软删(连带子对象+版本)
get_blueprint(tenant_id, blueprint_id, with_children) # with_children=True 附带 7 类子树
list_blueprints(tenant_id, filters, page, page_size) # 分页 + keyword/status/subject 过滤
get_blueprint_tree(tenant_id, blueprint_id) # {obj_type: [rows]}
fork_blueprint(tenant_id, blueprint_id, new_name, target_tenant_id) # 跨租户复制,ID 重映射
```
状态机:`draft → validating → validated → compiling → compiled → published → archived`(非法流转抛 `PblValidationError`)。
## 6. 版本契约
```python
save_version(tenant_id, blueprint_id, remark) # 存全量 snapshot + 计算 change_delta,主表 version_no 自增
list_versions(tenant_id, blueprint_id, page, page_size)
diff_versions(tenant_id, blueprint_id, version_a, version_b) # {added, updated, removed}
rollback_version(tenant_id, blueprint_id, version_no) # 先自动存当前版本再回滚
```
`change_delta` 结构:`{"added":[{"obj_type","id","name"}], "updated":[{"obj_type","id","fields"}], "removed":[...]}`
## 7. 模板与离线兜底
```python
create_template(tenant_id, {'name':..., 'items':[{'obj_type':'task','ref_key':'T1','name':...}, ...]})
instantiate_template(tenant_id, template_id, name, overrides) # ref_key/parent_ref 解析为真实 ID,use_count 自增
build_offline_fallback_blueprint(tenant_id, name) # Agent/网络不可用时的最小可用蓝图(9 条子对象)
```
模板条目用 `ref_key` / `parent_ref` 做**模板内相对引用**,实例化时按 `SUBOBJECT_TYPES` 顺序解析为真实 ID(父先于子)。
## 8. tenant_id 强制打头(铁律)
- 每个契约函数第一入参 `tenant_id`,内部 `db.require_tenant()` 校验
- 缺失/空/非字符串 → 抛 `PblTenantRequired`(code=`PBL_TENANT_REQUIRED`),**fail-closed 不放行**
- 所有 where 条件由 `_where(tenant_id, extra)` 构造,`tenant_id=` 永远在最前
- API 层 `_tid(params)` 从入参或 `context.tenant_id` 取,取不到直接失败
- 平台内置模板 `tenant_id='platform'`,`list_templates(include_platform=True)` 才并入
## 9. 异常码
| code | 含义 |
|---|---|
| PBL_TENANT_REQUIRED | 租户上下文缺失 |
| PBL_VALIDATION_ERROR | 入参校验失败 |
| PBL_NOT_FOUND / PBL_BLUEPRINT_NOT_FOUND / PBL_SUBOBJECT_NOT_FOUND / PBL_VERSION_NOT_FOUND / PBL_TEMPLATE_NOT_FOUND | 记录不存在 |
| PBL_UNKNOWN_OBJ_TYPE | 未知子对象类型 |
| PBL_DB_UNAVAILABLE | sqlor 不可用 |
## 10. 数据访问约束
- 库名:`ServerEnv().get_module_dbname('pbl_blueprint')`,**禁止硬编码 DBNAME**
- SQL 只用 sqlor 标准 API:`sor.C / sor.U / sor.D / sor.R / sor.sqlExe`,禁编造 save/list/insert
- 建表:`db.ensure_tables()` 幂等执行 `sql/*.sql`(`CREATE TABLE IF NOT EXISTS`)
## 11. API 路径(25 个,见 api.py API_PATHS)
前缀 `/pbl_blueprint/`,分四组:`blueprint/*`(7)、`version/*`(4)、`subobject/*`(7)、`template/*`(7)。
RBAC 注册:`python -m pbl_blueprint.scripts.load_path`(幂等,注册 API + 页面 + 表权限共 39 条路径)。
## 12. 下游模块依赖
- `pbl_validation`(M2)读 `get_blueprint_tree` 做 14 维校验,回写 `quality_level`
- `pbl_compiler`(M3)读蓝图树生成 Game Definition
- `pbl_evidence`(M5)按 `evidence_spec` / `artifact_spec` 采集
- `pbl_assessment`(M6)按 `learning_goal.weight` / `reflection_spec.rubric_ref` 评分
- `pbl_agent_runtime`(M4)用 `batch_upsert_subobjects` 写 Designer 产出,fail-closed 裁决