9.7 KiB
Raw Permalink Blame History

pbl_domain_ext — world/scene/entity 薄扩展M8

Agent 必读技能文档。改本模块前先读完「铁律」与「陷阱」两节。

1. 模块定位

PBL 侧对复用平台 world / scene / entity 三张基表的薄扩展:只加 1 张关联表 + 查询封装, 不 ALTER 基表、不侵入 world/scene/entity 模块代码

  • 里程碑M8Wave 4
  • 自有表:1 张 pbl_domain_ref(权威 DDLprojects/pbls/docs/01-design/data-model.md §J1
  • 表总账:projects/pbls/pbls_spec.jsontables_by_module.pbl_domain_ext = 1tables_total = 36
  • depends_onworld、scene、entity复用只读、pbl_governance班级/团队/成员只读、pbl_common租户上下文/错误码)
  • 被依赖pbl_scense_ext前端拉本租户世界列表、pbl_runtime_ext共享会话访问权校验、pbl_compilerapply 后 bind_ref

2. 数据模型(唯一自有表)

pbl_domain_ref — world/scene/entity 关联叠加:

类型 说明
id BIGINT PK AI 主键
tenant_id VARCHAR(64) NOT NULL 租户ID基表无此列PBL 多租户隔离全靠它)
ref_type VARCHAR(32) NOT NULL appcodes:pbl_domain_ref_type = world / scene / entity
ref_id BIGINT NOT NULL 复用基表主键world.id / scene.id / entity.id
blueprint_id VARCHAR(64) NULL 关联蓝图compiler apply 后回写)
class_id VARCHAR(64) NULL 教学班pbl_governance.pbl_class
team_id VARCHAR(64) NULL 团队pbl_governance.pbl_team共享世界分组
ext_json JSON NULL 扩展属性(薄扩展附加维度,禁止改基表
is_deleted TINYINT NOT NULL 0 软删unbind_ref 置 1不物理删
created_at / updated_at TIMESTAMP 时间戳

唯一约束UNIQUE(tenant_id, ref_type, ref_id)(设计 §2 硬性要求)。 索引:(tenant_id,blueprint_id) / (tenant_id,class_id) / (tenant_id,team_id) / (ref_type,ref_id)

模型文件:models/pbl_domain_ref.json(四段式 summary(array)/fields/indexes/codes。 DDLsql/pbl_domain_ext.sql。CRUD 浏览定义:json/pbl_domain_ref.json(运维只读浏览用)。

3. 对外契约13 个,与设计 §3 一一对应)

# 设计接口 实现位置 dspy 端点 env 注册名
1 bind_ref pbl_domain_ext/api.py:bind_ref api/pbl_domain_ref_bind.dspy pbl_bind_ref / bind_ref
2 unbind_ref api.py:unbind_ref api/pbl_domain_ref_unbind.dspy pbl_unbind_ref / unbind_ref
3 get_ref api.py:get_ref api/pbl_domain_ref_get.dspy pbl_get_ref / get_ref
4 list_refs api.py:list_refs api/pbl_domain_ref_list.dspy pbl_list_refs / list_refs
5 update_ref api.py:update_ref api/pbl_domain_ref_update.dspy pbl_update_ref / update_ref
6 list_worlds_by_tenant api.py:list_worlds_by_tenant api/pbl_world_list_by_tenant.dspy pbl_list_worlds_by_tenant
7 list_scenes_by_world api.py:list_scenes_by_world api/pbl_scene_list_by_world.dspy pbl_list_scenes_by_world
8 list_entities_by_scene api.py:list_entities_by_scene api/pbl_entity_list_by_scene.dspy pbl_list_entities_by_scene
9 get_world_with_pbl_context api.py:get_world_with_pbl_context api/pbl_world_get_context.dspy pbl_get_world_with_pbl_context
10 check_ref_access api.py:check_ref_access api/pbl_domain_ref_check_access.dspy pbl_check_ref_access
11 list_teams_by_class api.py:list_teams_by_class api/pbl_team_list_by_class.dspy pbl_list_teams_by_class
12 bind_team_to_world api.py:bind_team_to_world api/pbl_team_bind_world.dspy pbl_bind_team_to_world
13 get_team_worlds api.py:get_team_worlds api/pbl_team_world_list.dspy pbl_get_team_worlds

统一响应包体:{success, error_code, message, data, detail?, http_status?}。 错误码:PBL_E_VALIDATION(400) / PBL_E_NOT_FOUND(404) / PBL_E_DUPLICATE(409) / PBL_E_FORBIDDEN(403) / PBL_E_INTERNAL(500)。

关键语义

  • tenant_id 强制打头:所有接口缺 tenant_id 且 pbl_common 上下文取不到 → PBL_E_VALIDATION
  • bind_ref 前置存在性校验:基表无此记录 → PBL_E_NOT_FOUND(防悬挂引用);已绑定 → PBL_E_DUPLICATE;软删记录重绑 = 复活(幂等)。
  • unbind_ref 软删is_deleted=1,不物理删除(保留审计痕迹)。
  • update_ref 白名单:只允许改 blueprint_id/class_id/team_id/ext_json;试图改 ref_type/ref_id/tenant_idPBL_E_VALIDATION
  • 租户隔离:基表无租户列 → 先查 pbl_domain_ref 得白名单,再按白名单只读回查基表;未绑定即不可见。跨租户 → PBL_E_FORBIDDENUS-21 要求 403/404
  • 悬挂引用list_refs 结果标 dangling=True 并给 valid_total联合查询6/7/8/9直接过滤掉基表已删的行check_ref_access 对悬挂 ref 返回 False。
  • check_ref_access 不抛异常:返回 bool运行时热路径用入参非法/无关联/班级团队不匹配/悬挂 → False。
  • list_teams_by_class:按 team_id 分组,members 只读取自 pbl_governance.pbl_team_member(表不存在时降级为空列表,绝不报错、绝不写入他模块表)。
  • bind_team_to_world:已有 world 关联 → 幂等更新 team_id不产生重复行同团队重复绑定 → PBL_E_DUPLICATE

4. 铁律(违反即退回)

  1. 不改基表world/scene/entity 零 ALTER、零写入只 SELECT。禁止给基表加 tenant_id 列——租户维度只落 pbl_domain_ref
  2. 不新增表:自有表恒为 1 张。pbl_tenant/pbl_class/pbl_teampbl_governance,本模块只读引用,不得出现在 OWN_TABLES/models/
  3. OWN_TABLES ↔ models/ 一一对应:加/减表必须同步 init.py:OWN_TABLESmodels/*.jsonsql/*.sqlpbls_spec.json 表总账。
  4. 三处同步注册:新增契约函数必须同时改 ① api.py 实现 ② __init__.py 导入 ③ init.py:load_pbl_domain_extenv.xxx = xxx。漏 ② → ImportError漏 ③ → dspy NameError
  5. dspy 端点齐备:每个契约必须有 wwwroot/api/<name>.dspy 薄封装(禁 import、显式 returndebug() 带文件名前缀、转发全部客户端参数不硬编码)。
  6. RBAC 显式注册:新增 .dspy/.ui 必须在 scripts/load_path.pyAPI_PATHS/UI_PATHS/ROLE_GRANTS 三处登记,禁通配符,否则上线 403。
  7. 只用 sqlor 标准 APIsor.C/U/D/R/I/sqlExe。禁编造 save/list/insert/query/delete
  8. 库名不硬编码ServerEnv().get_module_dbname('pbl_domain_ext')(见 db.py:resolve_dbname)。
  9. SQL 占位符:模块内一律 sqlor 风格 ${name}$sqlite 测试适配器自动转 :namedb.to_named_params)。

5. 陷阱(踩过的坑)

  • return 不能写在 async with 块内 → 静默返回 None。db.SqlorAdapter.query/execute 先在块内收集结果,退出块后再 return。
  • 基表列名不要猜base.resolve_column 运行时探测MySQL information_schema → 退化 sqlite PRAGMA table_infoscene 父列候选 world_id/worldid/parent_identity 候选 scene_id/sceneid/parent_id。改列名只改候选表,别写死。
  • sqlite row_factory 污染测试SqliteAdapter 把 conn 的 row_factory 设成 dict 工厂,测试里取标量必须用 fake_db.scalar()/raw_sql()(临时摘掉工厂),否则 row[0]KeyError: 0
  • 悬挂判定别写反filter_existing_ids 返回空集 = 基表已删 = 悬挂,必须拒绝;写成 if alive and rid not in alive 会漏判(空集时短路成"允许")。
  • 基表不可探测 ≠ 拒绝访问check_ref_access 里探测异常(基础设施问题)应放行,只有明确查到"基表无此行"才判悬挂。
  • py_compile 不能验 .dspy(顶层 return/await 是合法的),.dspy 用 grep 审计:无 import、有显式 returndebug() 带文件名。
  • dspy 里 json 是预注入全局,可直接 json.loads,不要 import json

6. 挂载与验证

# 应用入口 apps/pbls/pbls.py 的 init() 中load_order 见 pbls_spec.json
from pbl_domain_ext import load_pbl_domain_ext
load_pbl_domain_ext(env)          # 注册 13 个契约到 ServerEnv
# RBAC 路径注册
import sys; sys.path.append('modules/pbl_domain_ext/scripts')
import load_path
load_path.register(env)           # 或宿主 rbac 批量读 load_path.get_paths()

离线验证(不依赖 MySQL / 宿主):

cd modules/pbl_domain_ext
python3 -m py_compile pbl_domain_ext/*.py scripts/load_path.py tests/*.py
python3 tests/test_domain_ref.py            # 62 tests覆盖 13 契约 + 薄扩展铁律
python3 scripts/load_path.py                # 路径注册自检(无通配符/授权齐备)

7. 需求追溯

需求锚点 落点
M8 薄扩展不改基表 pbl_domain_ref 单表 + base.py 只读投影(测试 test_base_tables_untouched 断言基表行数/列不变)
第 25 章多租户 list_worlds_by_tenant + check_ref_access + tenant_id 强制打头
US-21 跨租户 403/404 PBL_E_FORBIDDEN(403) / PBL_E_NOT_FOUND(404),测试 test_*_cross_tenant_*
US-13 共享世界团队分组 list_teams_by_class + bind_team_to_world + get_team_worlds
F-CP-02 落库映射关联 bind_refcompiler apply_game_definition 后调用)
F-RT-02 加入共享会话 check_ref_access(团队/班级访问权,返回 bool 不抛异常)
班级维度F-AS-03 class_id 关联 + list_refs 按 class_id 过滤