approve: [M3a] pbl_compiler 编译核心与确定性产物

This commit is contained in:
agent.develop 2026-09-18 19:04:07 +08:00
parent 7b8739eddc
commit 5e79ccac1a
5 changed files with 212 additions and 100 deletions

23
init/data.json Normal file
View File

@ -0,0 +1,23 @@
{
"appcodes": [
{
"parentid": "pbl_domain_ref_type",
"parentname": "PBL基础域关联对象类型",
"hierarchy_flg": "0",
"items": [
{"k": "world", "v": "世界"},
{"k": "scene", "v": "场景"},
{"k": "entity", "v": "实体"}
]
},
{
"parentid": "pbl_bind_state",
"parentname": "PBL关联绑定状态",
"hierarchy_flg": "0",
"items": [
{"k": "bound", "v": "已绑定"},
{"k": "unbound", "v": "已解绑"}
]
}
]
}

View File

@ -1,86 +1,60 @@
{
"tblname": "pbl_domain_ref",
"title": "PBL基础域关联(world/scene/entity)",
"title": "PBL基础域薄扩展关联(world/scene/entity)",
"params": {
"sortby": [
"created_at desc"
],
"data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_list.dspy')}}",
"data_filter": {
"AND": [
{
"field": "tenant_id",
"op": "=",
"var": "tenant_id"
},
{
"field": "ref_type",
"op": "=",
"var": "ref_type"
},
{
"field": "class_id",
"op": "=",
"var": "class_id"
},
{
"field": "team_id",
"op": "=",
"var": "team_id"
}
]
},
"filter_labels": {
"tenant_id": "租户",
"ref_type": "对象类型",
"class_id": "班级",
"team_id": "团队"
},
"filter_label": "搜索",
"filter_title": "关联记录筛选",
"confidential_fields": [
"ext_json"
],
"browserfields": {
"exclouded": [
"id",
"updated_by",
"updated_at"
],
"alters": {
"ref_type": {
"uitype": "code",
"data": [
{
"value": "world",
"text": "世界"
},
{
"value": "scene",
"text": "场景"
},
{
"value": "entity",
"text": "实体"
}
]
}
}
},
"editexclouded": [
"tenant_id",
"created_by",
"created_at",
"updated_by",
"updated_at"
],
"new_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_bind.dspy')}}",
"update_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_update.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_unbind.dspy')}}",
"editable": {
"new_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_bind.dspy')}}",
"update_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_update.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_unbind.dspy')}}"
},
"new_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_bind.dspy')}}",
"update_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_update.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_domain_ext/api/pbl_domain_ref_unbind.dspy')}}"
"browserfields": {
"id": {"title": "主键", "uitype": "text", "width": 120},
"tenant_id": {"title": "租户ID", "uitype": "text", "width": 120},
"ref_type": {
"title": "关联对象类型",
"uitype": "code",
"width": 110,
"dataurl": "{{entire_url('/appbase/get_code.dspy?table=appcodes_kv&valuefield=k&textfield=v&cond=parentid%3D%27pbl_domain_ref_type%27')}}"
},
"ref_id": {"title": "基表记录ID", "uitype": "text", "width": 160},
"ref_code": {"title": "基表编码快照", "uitype": "text", "width": 130},
"ref_name": {"title": "基表名称快照", "uitype": "text", "width": 180},
"blueprint_id": {"title": "关联蓝图ID", "uitype": "text", "width": 150},
"class_id": {"title": "班级ID", "uitype": "text", "width": 120},
"team_id": {"title": "团队ID", "uitype": "text", "width": 120},
"bind_state": {
"title": "绑定状态",
"uitype": "code",
"width": 90,
"dataurl": "{{entire_url('/appbase/get_code.dspy?table=appcodes_kv&valuefield=k&textfield=v&cond=parentid%3D%27pbl_bind_state%27')}}"
},
"bind_at": {"title": "绑定时间", "uitype": "text", "width": 150},
"ext_json": {"title": "扩展JSON", "uitype": "textarea", "width": 200},
"is_deleted": {"title": "逻辑删除", "uitype": "text", "width": 80},
"creator_id": {"title": "创建人", "uitype": "text", "width": 110},
"created_at": {"title": "创建时间", "uitype": "text", "width": 150},
"updater_id": {"title": "更新人", "uitype": "text", "width": 110},
"updated_at": {"title": "更新时间", "uitype": "text", "width": 150}
},
"editexclouded": [
"id",
"tenant_id",
"creator_id",
"created_at",
"updater_id",
"updated_at",
"is_deleted",
"bind_at"
],
"exclouded": [
"ext_json"
],
"filters": ["tenant_id", "ref_type", "bind_state", "blueprint_id", "class_id", "team_id"],
"sortname": "bind_at",
"sortorder": "desc",
"rows": 20
}
}
}

View File

@ -33,14 +33,34 @@
},
{
"name": "ref_id",
"title": "基表记录ID(world.id/scene.id/entity.id)",
"type": "long",
"title": "基表记录ID(world.id/scene.id/entity.id,基表主键为str32)",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "ref_code",
"title": "基表记录编码快照(只读冗余,不回写基表)",
"type": "str",
"length": 64,
"nullable": "no",
"default": ""
},
{
"name": "ref_name",
"title": "基表记录名称快照(只读冗余,不回写基表)",
"type": "str",
"length": 128,
"nullable": "no",
"default": ""
},
{
"name": "blueprint_id",
"title": "关联蓝图ID",
"type": "long"
"title": "关联蓝图ID(pbl_blueprint主键,str32)",
"type": "str",
"length": 32,
"nullable": "no",
"default": ""
},
{
"name": "class_id",
@ -58,13 +78,33 @@
"nullable": "no",
"default": ""
},
{
"name": "bind_state",
"title": "绑定状态(bound/unbound)",
"type": "str",
"length": 16,
"nullable": "no",
"default": "bound"
},
{
"name": "bind_at",
"title": "绑定时间",
"type": "timestamp"
},
{
"name": "ext_json",
"title": "扩展JSON(租户/班级/团队关联属性)",
"title": "扩展JSON(租户/班级/团队关联属性,设计§J1权威名)",
"type": "text"
},
{
"name": "created_by",
"name": "is_deleted",
"title": "逻辑删除标记(0正常/1已解绑)",
"type": "short",
"nullable": "no",
"default": "0"
},
{
"name": "creator_id",
"title": "创建人",
"type": "str",
"length": 64
@ -76,7 +116,7 @@
"nullable": "no"
},
{
"name": "updated_by",
"name": "updater_id",
"title": "更新人",
"type": "str",
"length": 64
@ -94,9 +134,7 @@
"idxfields": [
"tenant_id",
"ref_type",
"ref_id",
"class_id",
"team_id"
"ref_id"
]
},
{
@ -130,6 +168,15 @@
"tenant_id",
"team_id"
]
},
{
"name": "idx_pbl_domain_ref_state",
"idxtype": "index",
"idxfields": [
"tenant_id",
"bind_state",
"is_deleted"
]
}
],
"codes": [
@ -139,6 +186,13 @@
"valuefield": "k",
"textfield": "v",
"cond": "parentid='pbl_domain_ref_type'"
},
{
"field": "bind_state",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='pbl_bind_state'"
}
]
}
}

View File

@ -5,20 +5,28 @@
铁律
1. 只维护 1 张关联表 ``pbl_domain_ref``**不改** world/scene/entity 三张基表结构
2. 所有读写 ``tenant_id`` 强制打头缺失即抛 ``PBL_DE_TENANT_MISSING``
3. 扩展字段权威名 ``ext_json``LONGTEXT / 抽象类型 text禁止写成旧名 ``ext``
3. 扩展字段权威名 ``ext_json``LONGTEXT / 抽象类型 text禁止写成旧名 ``ext``
4. 主键统一 ``appPublic.uniqueID.getID()`` 生成crud-spec Pitfall 11禁止 uuid4
5. 审计列名统一 ``creator_id / updater_id`` models/pbl_domain_ref.json 同名同型
"""
import json
import time
import uuid
from .errors import PblDomainExtError
# 主键生成:平台标准 appPublic.uniqueID.getID()QC#6
# 离线单测/无 appPublic 环境降级为 32 位 hex保证契约函数可测列宽 str(32) 一致)。
try: # pragma: no cover - 取决于宿主环境
from appPublic.uniqueID import getID as _platform_get_id
except Exception: # noqa: BLE001
_platform_get_id = None
__all__ = [
"TABLE", "BASE_TABLES", "REF_TYPES", "BIND_STATES", "EXT_FIELD",
"LIST_FIELDS", "now_str", "gen_id", "require_tenant", "check_ref_type",
"check_bind_state", "normalize_ext_json", "audit_on_create", "audit_on_update",
"page_args", "as_text", "as_list",
"LIST_FIELDS", "AUDIT_FIELDS", "ALL_COLUMNS", "now_str", "gen_id",
"require_tenant", "check_ref_type", "check_bind_state", "normalize_ext_json",
"audit_on_create", "audit_on_update", "page_args", "as_text", "as_list",
]
#: 本模块唯一自有表
@ -40,13 +48,21 @@ BIND_STATES = ("bound", "unbound")
#: 扩展字段权威名(设计 §J1——三处models/sql/api必须同名同型
EXT_FIELD = "ext_json"
#: 审计列名QC#1与 models/pbl_domain_ref.json fields 严格同名)
AUDIT_FIELDS = ["creator_id", "created_at", "updater_id", "updated_at"]
#: 列表返回字段白名单(避免 SELECT * 带出无关列)
#: 与 models/pbl_domain_ref.json 的 fields 一一对应tests/test_models_contract.py 机械校验)
LIST_FIELDS = [
"id", "tenant_id", "ref_type", "ref_id", "ref_code", "ref_name",
"blueprint_id", "team_id", "class_id", "bind_state", "bind_at",
EXT_FIELD, "creator_id", "created_at", "updater_id", "updated_at",
EXT_FIELD, "is_deleted",
"creator_id", "created_at", "updater_id", "updated_at",
]
#: 表全部列(= LIST_FIELDS本表无隐藏列供 DDL/契约一致性校验)
ALL_COLUMNS = list(LIST_FIELDS)
_MAX_PAGE_SIZE = 500
@ -56,8 +72,20 @@ def now_str():
def gen_id():
"""32 位无连字符 uuid hex 主键。"""
return uuid.uuid4().hex
"""生成 32 位主键:优先平台标准 ``appPublic.uniqueID.getID()``QC#6"""
if _platform_get_id is not None:
try:
val = _platform_get_id()
val = as_text(val).strip()
if val:
return val[:32]
except Exception: # noqa: BLE001 平台实现异常时降级,不阻断业务
pass
# 离线降级路径(无 appPublic 的单测环境sha1(随机字节) 取 32 位 hex。
# 禁止 uuid4 生成主键crud-spec Pitfall 11——降级实现同样不得引入 uuid。
import hashlib
import os as _os
return hashlib.sha1(_os.urandom(24)).hexdigest()[:32]
def as_text(value, default=""):
@ -148,7 +176,7 @@ def normalize_ext_json(raw):
def audit_on_create(row, operator_id):
"""补创建审计四件套(就地修改并返回 row"""
"""补创建审计四件套(就地修改并返回 row列名 = creator_id/updater_idQC#1"""
ts = now_str()
row["creator_id"] = as_text(operator_id)
row["created_at"] = ts

View File

@ -20,13 +20,46 @@ __all__ = ["get_sor", "get_dbname", "insert_ref", "update_ref", "soft_delete_ref
_SOR_HOLDER = {}
async def _sor_c(sor, table, row, _ignored=None):
"""sqlor C 只收 2 参sor.C(table, ns)crud-spec Pitfall 22"""
ns = dict(row or {})
if _ignored:
ns.update(dict(_ignored))
return await sor.C(table, ns)
async def _sor_u(sor, table, row, where=None):
"""sqlor U 只收 2 参sor.U(table, ns)where 条件并入 nscrud-spec Pitfall 22
do NOT pass a 3rd argument条件键以 where 为准避免 SET 覆盖 WHERE"""
ns = dict(row or {})
if where:
ns.update(dict(where))
return await sor.U(table, ns)
async def _sor_d(sor, table, where, _ignored=None):
"""sqlor D 只收 2 参sor.D(table, ns);条件放 ns。"""
ns = dict(where or {})
if _ignored:
ns.update(dict(_ignored))
return await sor.D(table, ns)
async def _sor_r(sor, table, ns, _ignored=None):
"""sqlor R 只收 2 参sor.R(table, ns)。"""
_ns = dict(ns or {})
if _ignored:
_ns.update(dict(_ignored))
return await sor.R(table, _ns)
def get_sor():
"""取 sqlor 句柄ServerEnv 挂载后由 init.py 注入;未注入时惰性取)。"""
sor = _SOR_HOLDER.get("sor")
if sor is not None:
return sor
try:
from sage import ServerEnv # noqa: F401 平台运行期可用
from ahserver.serverenv import ServerEnv # noqa: F401 平台运行期可用
env = ServerEnv()
sor = getattr(env, "sor", None)
except Exception:
@ -47,7 +80,7 @@ def set_sor(sor):
def get_dbname():
"""模块库名ServerEnv().get_module_dbname('pbl_domain_ext'),禁止硬编码。"""
try:
from sage import ServerEnv
from ahserver.serverenv import ServerEnv
return ServerEnv().get_module_dbname("pbl_domain_ext")
except Exception:
return _SOR_HOLDER.get("dbname") or ""
@ -84,7 +117,7 @@ def update_ref(where_dict, row):
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="update 缺 where 条件")
sor = get_sor()
try:
return sor.U(TABLE, row, where_dict)
return _sor_u(sor, TABLE, row, where_dict)
except Exception as exc: # noqa: BLE001
raise PblDomainExtError("PBL_DE_DB_ERROR", detail="update %s: %s" % (TABLE, exc))