651 lines
28 KiB
Python
651 lines
28 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""pbl_domain_ext 契约接口层([M8] 基础域薄扩展 world/scene/entity)。
|
||
|
||
对外 13 个契约函数,与 ``wwwroot/api/*.dspy`` 一一对应:
|
||
|
||
关联表 CRUD/查询(6)
|
||
pbl_domain_ref_bind / pbl_domain_ref_unbind / pbl_domain_ref_update
|
||
pbl_domain_ref_get / pbl_domain_ref_list / pbl_domain_ref_check_access
|
||
基础域只读视图(4)
|
||
pbl_world_list_by_tenant / pbl_world_get_context
|
||
pbl_scene_list_by_world / pbl_entity_list_by_scene
|
||
团队-世界绑定(3)
|
||
pbl_team_bind_world / pbl_team_world_list / pbl_team_list_by_class
|
||
|
||
编译期物化(1,供 pbl_compiler import 闭包)
|
||
pbl_domain_materialize_game_definition
|
||
|
||
铁律:
|
||
* 所有函数第一个业务参数是 ``tenant_id``,缺失/非法即 ``PBL_DE_TENANT_MISSING``;
|
||
* 只写 ``pbl_domain_ref`` 一张表,world/scene/entity 基表**只读**;
|
||
* 扩展字段名统一 ``ext_json``(设计 data-model.md §J1),禁止旧名 ``ext``;
|
||
* 返回统一 ``{"success": True, "code": "PBL_DE_OK", "data": ...}``,异常转错误体不裸抛。
|
||
"""
|
||
|
||
from . import db
|
||
from .base import (TABLE, BASE_TABLES, REF_TYPES, BIND_STATES, EXT_FIELD,
|
||
LIST_FIELDS, gen_id, now_str, require_tenant, check_ref_type,
|
||
check_bind_state, normalize_ext_json, audit_on_create,
|
||
audit_on_update, page_args, as_text, as_list)
|
||
from .errors import PblDomainExtError, err_payload
|
||
|
||
__all__ = [
|
||
"TABLE", "EXT_FIELD", "REF_TYPES", "BIND_STATES",
|
||
"pbl_domain_ref_bind", "pbl_domain_ref_unbind", "pbl_domain_ref_update",
|
||
"pbl_domain_ref_get", "pbl_domain_ref_list", "pbl_domain_ref_check_access",
|
||
"pbl_world_list_by_tenant", "pbl_world_get_context",
|
||
"pbl_scene_list_by_world", "pbl_entity_list_by_scene",
|
||
"pbl_team_bind_world", "pbl_team_world_list", "pbl_team_list_by_class",
|
||
"pbl_domain_materialize_game_definition",
|
||
"ok", "fail", "dispatch",
|
||
]
|
||
|
||
#: 基表只读视图对外暴露的列(基表列名差异大,取交集常见列,缺失自动跳过)
|
||
_BASE_VIEW_FIELDS = ("id", "tenant_id", "code", "name", "title", "status",
|
||
"state", "world_id", "scene_id", "parent_id",
|
||
"created_at", "updated_at")
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 统一响应
|
||
# --------------------------------------------------------------------------
|
||
def ok(data=None, **extra):
|
||
"""成功响应体。"""
|
||
payload = {"success": True, "code": "PBL_DE_OK", "message": "成功"}
|
||
if data is not None:
|
||
payload["data"] = data
|
||
payload.update(extra)
|
||
return payload
|
||
|
||
|
||
def fail(code, message=None, detail=None):
|
||
"""失败响应体(不抛异常)。"""
|
||
return err_payload(code, message=message, detail=detail)
|
||
|
||
|
||
def _base_view(row):
|
||
"""把基表原始行裁剪成只读视图 dict(只保留白名单列,不改基表)。"""
|
||
out = {}
|
||
if not isinstance(row, dict):
|
||
return out
|
||
for key in _BASE_VIEW_FIELDS:
|
||
if key in row and row[key] is not None:
|
||
out[key] = row[key]
|
||
if "id" not in out:
|
||
for alt in ("world_id", "scene_id", "entity_id"):
|
||
if row.get(alt):
|
||
out["id"] = row[alt]
|
||
break
|
||
return out
|
||
|
||
|
||
def _ref_out(row):
|
||
"""关联表行 -> 对外 dict(保证 ext_json 键名存在且为文本)。"""
|
||
out = {}
|
||
if not isinstance(row, dict):
|
||
return out
|
||
for key in LIST_FIELDS:
|
||
if key in row:
|
||
out[key] = row[key]
|
||
if EXT_FIELD in out and out[EXT_FIELD] is None:
|
||
out[EXT_FIELD] = ""
|
||
out["is_deleted"] = int(row.get("is_deleted") or 0)
|
||
return out
|
||
|
||
|
||
def _operator(params):
|
||
"""取操作人(creator_id/updater_id 审计用)。"""
|
||
if not isinstance(params, dict):
|
||
return ""
|
||
for key in ("operator_id", "user_id", "creator_id", "updater_id", "uid"):
|
||
val = as_text(params.get(key)).strip()
|
||
if val:
|
||
return val
|
||
return ""
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 1. 关联表:绑定 / 解绑 / 更新 / 查询
|
||
# --------------------------------------------------------------------------
|
||
def pbl_domain_ref_bind(params):
|
||
"""绑定基础域对象到 PBL 蓝图/团队/班级(幂等 upsert)。
|
||
|
||
入参:tenant_id*, ref_type*(world|scene|entity), ref_id*,
|
||
ref_code, ref_name, blueprint_id, team_id, class_id, ext_json, operator_id
|
||
出参:data = 关联记录 dict(含 id / bind_state=bound)
|
||
"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
ref_type = check_ref_type(params.get("ref_type"))
|
||
ref_id = as_text(params.get("ref_id")).strip()
|
||
if not ref_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="ref_id 为空")
|
||
if len(ref_id) > 64:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="ref_id 超长(>64)")
|
||
|
||
ref_code = as_text(params.get("ref_code")).strip()[:64]
|
||
ref_name = as_text(params.get("ref_name")).strip()[:128]
|
||
# 未显式给编码/名称时,从基表只读快照(不改基表)
|
||
if not ref_code or not ref_name:
|
||
base_row = db.read_base_row(ref_type, ref_id, tenant_id)
|
||
if base_row is None:
|
||
raise PblDomainExtError("PBL_DE_BASE_MISSING",
|
||
detail="%s.%s 未命中" % (BASE_TABLES[ref_type], ref_id))
|
||
view = _base_view(base_row)
|
||
ref_code = ref_code or as_text(view.get("code") or view.get("id"))[:64]
|
||
ref_name = ref_name or as_text(view.get("name") or view.get("title"))[:128]
|
||
|
||
ext_json = normalize_ext_json(params.get(EXT_FIELD) or params.get("ext_json"))
|
||
bind_state = check_bind_state(params.get("bind_state")) or BIND_STATES[0]
|
||
operator_id = _operator(params)
|
||
|
||
exist = db.select_ref_by_key(tenant_id, ref_type, ref_id)
|
||
if exist:
|
||
row = {
|
||
"ref_code": ref_code, "ref_name": ref_name,
|
||
"blueprint_id": as_text(params.get("blueprint_id")).strip()[:32],
|
||
"team_id": as_text(params.get("team_id")).strip()[:32],
|
||
"class_id": as_text(params.get("class_id")).strip()[:32],
|
||
"bind_state": bind_state, "bind_at": now_str(),
|
||
EXT_FIELD: ext_json, "is_deleted": 0,
|
||
}
|
||
audit_on_update(row, operator_id)
|
||
db.update_ref({"tenant_id": tenant_id, "id": exist.get("id")}, row)
|
||
fresh = db.select_ref_by_id(tenant_id, exist.get("id"))
|
||
return ok(_ref_out(fresh or dict(exist, **row)), rebound=True)
|
||
|
||
row = {
|
||
"id": gen_id(), "tenant_id": tenant_id,
|
||
"ref_type": ref_type, "ref_id": ref_id,
|
||
"ref_code": ref_code, "ref_name": ref_name,
|
||
"blueprint_id": as_text(params.get("blueprint_id")).strip()[:32],
|
||
"team_id": as_text(params.get("team_id")).strip()[:32],
|
||
"class_id": as_text(params.get("class_id")).strip()[:32],
|
||
"bind_state": bind_state, "bind_at": now_str(),
|
||
EXT_FIELD: ext_json,
|
||
}
|
||
audit_on_create(row, operator_id)
|
||
db.insert_ref(row)
|
||
return ok(_ref_out(row), rebound=False)
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_domain_ref_unbind(params):
|
||
"""解绑(逻辑删除 is_deleted=1 + bind_state=unbound),绝不物理删。
|
||
|
||
入参:tenant_id*, id 或 (ref_type*+ref_id*),operator_id
|
||
"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
ref_pk = as_text(params.get("id")).strip()
|
||
if ref_pk:
|
||
exist = db.select_ref_by_id(tenant_id, ref_pk)
|
||
else:
|
||
ref_type = check_ref_type(params.get("ref_type"))
|
||
ref_id = as_text(params.get("ref_id")).strip()
|
||
if not ref_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID",
|
||
detail="需 id 或 ref_type+ref_id")
|
||
exist = db.select_ref_by_key(tenant_id, ref_type, ref_id)
|
||
if not exist:
|
||
raise PblDomainExtError("PBL_DE_NOT_FOUND", detail="关联记录不存在")
|
||
|
||
row = {"bind_state": "unbound", "is_deleted": 1}
|
||
audit_on_update(row, _operator(params))
|
||
affected = db.update_ref({"tenant_id": tenant_id, "id": exist.get("id")}, row)
|
||
return ok({"id": exist.get("id"), "bind_state": "unbound",
|
||
"is_deleted": 1, "affected": int(affected or 0)})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_domain_ref_update(params):
|
||
"""更新关联记录的可变字段(ref_type/ref_id/tenant_id 不可改)。
|
||
|
||
入参:tenant_id*, id*, 以及 ref_code/ref_name/blueprint_id/team_id/class_id/
|
||
bind_state/ext_json 中任意子集,operator_id
|
||
"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
ref_pk = as_text(params.get("id")).strip()
|
||
if not ref_pk:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="id 为空")
|
||
exist = db.select_ref_by_id(tenant_id, ref_pk)
|
||
if not exist:
|
||
raise PblDomainExtError("PBL_DE_NOT_FOUND", detail="id=%s" % ref_pk)
|
||
|
||
row = {}
|
||
for key in ("ref_code", "ref_name", "blueprint_id", "team_id", "class_id"):
|
||
if key in params:
|
||
row[key] = as_text(params.get(key)).strip()[:128]
|
||
if "bind_state" in params:
|
||
state = check_bind_state(params.get("bind_state"), allow_empty=False)
|
||
row["bind_state"] = state
|
||
if state == "bound" and not exist.get("bind_at"):
|
||
row["bind_at"] = now_str()
|
||
if EXT_FIELD in params or "ext_json" in params:
|
||
raw = params.get(EXT_FIELD) if EXT_FIELD in params else params.get("ext_json")
|
||
row[EXT_FIELD] = normalize_ext_json(raw)
|
||
if not row:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="无可更新字段")
|
||
|
||
audit_on_update(row, _operator(params))
|
||
affected = db.update_ref({"tenant_id": tenant_id, "id": ref_pk}, row)
|
||
fresh = db.select_ref_by_id(tenant_id, ref_pk)
|
||
return ok(_ref_out(fresh or dict(exist, **row)), affected=int(affected or 0))
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_domain_ref_get(params):
|
||
"""取单条关联记录。入参:tenant_id*, id 或 ref_type+ref_id。"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
ref_pk = as_text(params.get("id")).strip()
|
||
if ref_pk:
|
||
row = db.select_ref_by_id(tenant_id, ref_pk)
|
||
else:
|
||
ref_type = check_ref_type(params.get("ref_type"), allow_empty=True)
|
||
ref_id = as_text(params.get("ref_id")).strip()
|
||
if not ref_type or not ref_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID",
|
||
detail="需 id 或 ref_type+ref_id")
|
||
row = db.select_ref_by_key(tenant_id, ref_type, ref_id)
|
||
if not row:
|
||
raise PblDomainExtError("PBL_DE_NOT_FOUND")
|
||
data = _ref_out(row)
|
||
if params.get("with_base"):
|
||
data["base"] = _base_view(db.read_base_row(row.get("ref_type"),
|
||
row.get("ref_id"), tenant_id) or {})
|
||
return ok(data)
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_domain_ref_list(params):
|
||
"""分页查询关联记录。
|
||
|
||
入参:tenant_id*, ref_type, bind_state, blueprint_id, team_id, class_id,
|
||
ref_ids(list), keyword, page, page_size
|
||
出参:data = {"items": [...], "total": n, "page": p, "page_size": s}
|
||
"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
conds = {}
|
||
ref_type = check_ref_type(params.get("ref_type"), allow_empty=True)
|
||
if ref_type:
|
||
conds["ref_type"] = ref_type
|
||
state = check_bind_state(params.get("bind_state"))
|
||
if state:
|
||
conds["bind_state"] = state
|
||
for key in ("blueprint_id", "team_id", "class_id", "ref_id"):
|
||
val = as_text(params.get(key)).strip()
|
||
if val:
|
||
conds[key] = val
|
||
ref_ids = [as_text(x).strip() for x in as_list(params.get("ref_ids"))]
|
||
ref_ids = [x for x in ref_ids if x]
|
||
if ref_ids:
|
||
conds["ref_id"] = ref_ids
|
||
|
||
page, page_size, offset = page_args(params)
|
||
total = db.count_refs(tenant_id, conds)
|
||
items = db.select_refs(tenant_id, conds=conds, offset=offset, limit=page_size)
|
||
rows = [_ref_out(r) for r in items]
|
||
|
||
keyword = as_text(params.get("keyword")).strip().lower()
|
||
if keyword:
|
||
rows = [r for r in rows
|
||
if keyword in as_text(r.get("ref_name")).lower()
|
||
or keyword in as_text(r.get("ref_code")).lower()]
|
||
return ok({"items": rows, "total": int(total),
|
||
"page": page, "page_size": page_size})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_domain_ref_check_access(params):
|
||
"""访问判定:某主体(蓝图/团队/班级)能否访问某基础域对象。
|
||
|
||
规则(fail-closed):
|
||
1. tenant_id 必须一致;
|
||
2. 必须存在 bind_state=bound 且 is_deleted=0 的关联记录;
|
||
3. 若传了 blueprint_id/team_id/class_id,关联记录对应列必须匹配。
|
||
出参:data = {"allowed": bool, "reason": str, "ref": dict|None}
|
||
"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
ref_type = check_ref_type(params.get("ref_type"))
|
||
ref_id = as_text(params.get("ref_id")).strip()
|
||
if not ref_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="ref_id 为空")
|
||
|
||
row = db.select_ref_by_key(tenant_id, ref_type, ref_id)
|
||
if not row:
|
||
return ok({"allowed": False, "reason": "no_binding", "ref": None})
|
||
if as_text(row.get("bind_state")) != "bound" or int(row.get("is_deleted") or 0):
|
||
return ok({"allowed": False, "reason": "not_bound", "ref": _ref_out(row)})
|
||
for key in ("blueprint_id", "team_id", "class_id"):
|
||
want = as_text(params.get(key)).strip()
|
||
if not want:
|
||
continue
|
||
got = as_text(row.get(key)).strip()
|
||
if got and got != want:
|
||
return ok({"allowed": False, "reason": "%s_mismatch" % key,
|
||
"ref": _ref_out(row)})
|
||
return ok({"allowed": True, "reason": "ok", "ref": _ref_out(row)})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 2. 基础域只读视图(world / scene / entity)
|
||
# --------------------------------------------------------------------------
|
||
def pbl_world_list_by_tenant(params):
|
||
"""只读列出租户下 world 基表记录,并标注 PBL 绑定状态。"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
limit = page_args(params)[1]
|
||
rows = db.read_base_rows("world", tenant_id, limit=limit)
|
||
bound = {}
|
||
for r in db.select_refs(tenant_id, conds={"ref_type": "world"}, limit=limit * 2 or 200):
|
||
bound[as_text(r.get("ref_id"))] = _ref_out(r)
|
||
items = []
|
||
for raw in rows:
|
||
view = _base_view(raw)
|
||
wid = as_text(view.get("id"))
|
||
ref = bound.get(wid)
|
||
view["pbl_bound"] = bool(ref and ref.get("bind_state") == "bound")
|
||
view["pbl_ref_id"] = (ref or {}).get("id", "")
|
||
view["pbl_blueprint_id"] = (ref or {}).get("blueprint_id", "")
|
||
items.append(view)
|
||
keyword = as_text(params.get("keyword")).strip().lower()
|
||
if keyword:
|
||
items = [i for i in items
|
||
if keyword in as_text(i.get("name")).lower()
|
||
or keyword in as_text(i.get("code")).lower()
|
||
or keyword in as_text(i.get("title")).lower()]
|
||
if params.get("only_bound"):
|
||
items = [i for i in items if i.get("pbl_bound")]
|
||
return ok({"items": items, "total": len(items), "ref_type": "world",
|
||
"readonly": True, "table": TABLE})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_world_get_context(params):
|
||
"""取世界上下文:world 只读视图 + 其下 scene/entity 关联 + PBL 绑定信息。"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
world_id = as_text(params.get("world_id") or params.get("ref_id")
|
||
or params.get("id")).strip()
|
||
if not world_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="world_id 为空")
|
||
|
||
base = db.read_base_row("world", world_id, tenant_id)
|
||
world_view = _base_view(base or {})
|
||
ref = db.select_ref_by_key(tenant_id, "world", world_id)
|
||
|
||
scenes = [_base_view(r) for r in db.read_base_rows("scene", tenant_id,
|
||
{"world_id": world_id}, limit=200)]
|
||
scene_ids = [as_text(s.get("id")) for s in scenes if s.get("id")]
|
||
entities = []
|
||
if params.get("with_entities") and scene_ids:
|
||
for raw in db.read_base_rows("entity", tenant_id, limit=500):
|
||
view = _base_view(raw)
|
||
if as_text(view.get("scene_id")) in scene_ids:
|
||
entities.append(view)
|
||
|
||
return ok({
|
||
"world": world_view,
|
||
"pbl_ref": _ref_out(ref) if ref else None,
|
||
"scenes": scenes,
|
||
"entities": entities,
|
||
"counts": {"scene": len(scenes), "entity": len(entities)},
|
||
"readonly": True,
|
||
})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_scene_list_by_world(params):
|
||
"""只读列出某 world 下的 scene(基表按 world_id 过滤)+ 绑定标注。"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
world_id = as_text(params.get("world_id")).strip()
|
||
if not world_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="world_id 为空")
|
||
rows = db.read_base_rows("scene", tenant_id, {"world_id": world_id}, limit=200)
|
||
bound = {}
|
||
for r in db.select_refs(tenant_id, conds={"ref_type": "scene"}, limit=500):
|
||
bound[as_text(r.get("ref_id"))] = _ref_out(r)
|
||
items = []
|
||
for raw in rows:
|
||
view = _base_view(raw)
|
||
ref = bound.get(as_text(view.get("id")))
|
||
view["world_id"] = world_id
|
||
view["pbl_bound"] = bool(ref and ref.get("bind_state") == "bound")
|
||
view["pbl_ref_id"] = (ref or {}).get("id", "")
|
||
items.append(view)
|
||
return ok({"items": items, "total": len(items), "world_id": world_id,
|
||
"ref_type": "scene", "readonly": True})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_entity_list_by_scene(params):
|
||
"""只读列出某 scene 下的 entity(基表按 scene_id 过滤)+ 绑定标注。"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
scene_id = as_text(params.get("scene_id")).strip()
|
||
if not scene_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="scene_id 为空")
|
||
rows = db.read_base_rows("entity", tenant_id, {"scene_id": scene_id}, limit=500)
|
||
bound = {}
|
||
for r in db.select_refs(tenant_id, conds={"ref_type": "entity"}, limit=500):
|
||
bound[as_text(r.get("ref_id"))] = _ref_out(r)
|
||
items = []
|
||
for raw in rows:
|
||
view = _base_view(raw)
|
||
ref = bound.get(as_text(view.get("id")))
|
||
view["scene_id"] = scene_id
|
||
view["pbl_bound"] = bool(ref and ref.get("bind_state") == "bound")
|
||
view["pbl_ref_id"] = (ref or {}).get("id", "")
|
||
items.append(view)
|
||
return ok({"items": items, "total": len(items), "scene_id": scene_id,
|
||
"ref_type": "entity", "readonly": True})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 3. 团队-世界绑定
|
||
# --------------------------------------------------------------------------
|
||
def pbl_team_bind_world(params):
|
||
"""把 world 绑定到团队/班级(写 pbl_domain_ref,ref_type=world)。
|
||
|
||
入参:tenant_id*, world_id*, team_id 或 class_id(至少一个), blueprint_id, ext_json
|
||
"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
world_id = as_text(params.get("world_id") or params.get("ref_id")).strip()
|
||
if not world_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="world_id 为空")
|
||
team_id = as_text(params.get("team_id")).strip()
|
||
class_id = as_text(params.get("class_id")).strip()
|
||
if not team_id and not class_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID",
|
||
detail="team_id / class_id 至少给一个")
|
||
inner = dict(params)
|
||
inner.update({"tenant_id": tenant_id, "ref_type": "world",
|
||
"ref_id": world_id, "team_id": team_id,
|
||
"class_id": class_id, "bind_state": "bound"})
|
||
result = pbl_domain_ref_bind(inner)
|
||
if not result.get("success"):
|
||
return result
|
||
data = result.get("data") or {}
|
||
data["world_id"] = world_id
|
||
data["team_id"] = team_id
|
||
data["class_id"] = class_id
|
||
return ok(data, action="team_bind_world")
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_team_world_list(params):
|
||
"""列出团队(或班级)已绑定的 world。入参:tenant_id*, team_id 或 class_id。"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
team_id = as_text(params.get("team_id")).strip()
|
||
class_id = as_text(params.get("class_id")).strip()
|
||
if not team_id and not class_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID",
|
||
detail="team_id / class_id 至少给一个")
|
||
conds = {"ref_type": "world", "bind_state": "bound"}
|
||
if team_id:
|
||
conds["team_id"] = team_id
|
||
if class_id:
|
||
conds["class_id"] = class_id
|
||
rows = db.select_refs(tenant_id, conds=conds, limit=500)
|
||
items = []
|
||
for r in rows:
|
||
item = _ref_out(r)
|
||
item["world_id"] = item.get("ref_id")
|
||
item["world_code"] = item.get("ref_code")
|
||
item["world_name"] = item.get("ref_name")
|
||
items.append(item)
|
||
return ok({"items": items, "total": len(items),
|
||
"team_id": team_id, "class_id": class_id})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
def pbl_team_list_by_class(params):
|
||
"""按班级列出团队及其绑定世界数(聚合自 pbl_domain_ref,不新建表)。
|
||
|
||
入参:tenant_id*, class_id*
|
||
出参:data = {"items": [{"team_id", "class_id", "world_count", "worlds": [...]}]}
|
||
"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
class_id = as_text(params.get("class_id")).strip()
|
||
if not class_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="class_id 为空")
|
||
rows = db.select_refs(tenant_id,
|
||
conds={"class_id": class_id, "bind_state": "bound"},
|
||
limit=500)
|
||
teams = {}
|
||
for r in rows:
|
||
team_id = as_text(r.get("team_id")).strip() or "(unassigned)"
|
||
bucket = teams.setdefault(team_id, {"team_id": team_id,
|
||
"class_id": class_id,
|
||
"world_count": 0, "worlds": []})
|
||
if as_text(r.get("ref_type")) == "world":
|
||
bucket["world_count"] += 1
|
||
bucket["worlds"].append({"world_id": r.get("ref_id"),
|
||
"code": r.get("ref_code"),
|
||
"name": r.get("ref_name")})
|
||
items = sorted(teams.values(), key=lambda x: x["team_id"])
|
||
return ok({"items": items, "total": len(items), "class_id": class_id})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 4. 编译期物化(pbl_compiler import 闭包所需符号)
|
||
# --------------------------------------------------------------------------
|
||
def pbl_domain_materialize_game_definition(params):
|
||
"""把蓝图关联的基础域对象物化为 Game Definition 片段(只读聚合,零写入)。
|
||
|
||
供 pbl_compiler 在编译期调用:按 blueprint_id 取全部 bound 关联,
|
||
按 ref_type 分组,输出 ``{"worlds": [...], "scenes": [...], "entities": [...]}``,
|
||
每项含 ref_id/ref_code/ref_name/ext_json(解析为对象)与基表只读视图。
|
||
|
||
入参:tenant_id*, blueprint_id*(或 team_id/class_id 二选一), with_base(bool)
|
||
"""
|
||
try:
|
||
tenant_id = require_tenant(params)
|
||
blueprint_id = as_text(params.get("blueprint_id")).strip()
|
||
team_id = as_text(params.get("team_id")).strip()
|
||
class_id = as_text(params.get("class_id")).strip()
|
||
if not blueprint_id and not team_id and not class_id:
|
||
raise PblDomainExtError("PBL_DE_PARAM_INVALID",
|
||
detail="blueprint_id / team_id / class_id 至少给一个")
|
||
conds = {"bind_state": "bound"}
|
||
if blueprint_id:
|
||
conds["blueprint_id"] = blueprint_id
|
||
if team_id:
|
||
conds["team_id"] = team_id
|
||
if class_id:
|
||
conds["class_id"] = class_id
|
||
rows = db.select_refs(tenant_id, conds=conds, limit=500)
|
||
|
||
grouped = {"worlds": [], "scenes": [], "entities": []}
|
||
bucket_of = {"world": "worlds", "scene": "scenes", "entity": "entities"}
|
||
with_base = bool(params.get("with_base"))
|
||
for r in rows:
|
||
ref_type = as_text(r.get("ref_type"))
|
||
bucket = bucket_of.get(ref_type)
|
||
if not bucket:
|
||
continue
|
||
ext_raw = as_text(r.get(EXT_FIELD)).strip()
|
||
try:
|
||
import json as _json
|
||
ext_obj = _json.loads(ext_raw) if ext_raw else {}
|
||
except (ValueError, TypeError):
|
||
ext_obj = {}
|
||
item = {
|
||
"ref_id": r.get("ref_id"),
|
||
"ref_type": ref_type,
|
||
"code": r.get("ref_code"),
|
||
"name": r.get("ref_name"),
|
||
"ext_json": ext_obj,
|
||
"pbl_domain_ref_id": r.get("id"),
|
||
}
|
||
if with_base:
|
||
item["base"] = _base_view(db.read_base_row(ref_type, r.get("ref_id"),
|
||
tenant_id) or {})
|
||
grouped[bucket].append(item)
|
||
|
||
return ok({
|
||
"tenant_id": tenant_id,
|
||
"blueprint_id": blueprint_id,
|
||
"team_id": team_id,
|
||
"class_id": class_id,
|
||
"game_definition_fragment": grouped,
|
||
"counts": {k: len(v) for k, v in grouped.items()},
|
||
"readonly": True,
|
||
"source_table": TABLE,
|
||
})
|
||
except PblDomainExtError as exc:
|
||
return exc.to_payload()
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# dspy 分发入口
|
||
# --------------------------------------------------------------------------
|
||
_DISPATCH = {
|
||
"pbl_domain_ref_bind": pbl_domain_ref_bind,
|
||
"pbl_domain_ref_unbind": pbl_domain_ref_unbind,
|
||
"pbl_domain_ref_update": pbl_domain_ref_update,
|
||
"pbl_domain_ref_get": pbl_domain_ref_get,
|
||
"pbl_domain_ref_list": pbl_domain_ref_list,
|
||
"pbl_domain_ref_check_access": pbl_domain_ref_check_access,
|
||
"pbl_world_list_by_tenant": pbl_world_list_by_tenant,
|
||
"pbl_world_get_context": pbl_world_get_context,
|
||
"pbl_scene_list_by_world": pbl_scene_list_by_world,
|
||
"pbl_entity_list_by_scene": pbl_entity_list_by_scene,
|
||
"pbl_team_bind_world": pbl_team_bind_world,
|
||
"pbl_team_world_list": pbl_team_world_list,
|
||
"pbl_team_list_by_class": pbl_team_list_by_class,
|
||
"pbl_domain_materialize_game_definition": pbl_domain_materialize_game_definition,
|
||
}
|
||
|
||
|
||
def dispatch(action, params):
|
||
"""按 action 名分发到契约函数;未知 action 返回 PBL_DE_PARAM_INVALID。"""
|
||
func = _DISPATCH.get(as_text(action).strip())
|
||
if func is None:
|
||
return fail("PBL_DE_PARAM_INVALID", detail="unknown action=%r" % (action,))
|
||
return func(params if isinstance(params, dict) else {})
|