approve: [M3a] pbl_compiler 编译核心与确定性产物
This commit is contained in:
parent
7b8739eddc
commit
5e79ccac1a
23
init/data.json
Normal file
23
init/data.json
Normal 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": "已解绑"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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_id(QC#1)。"""
|
||||
ts = now_str()
|
||||
row["creator_id"] = as_text(operator_id)
|
||||
row["created_at"] = ts
|
||||
|
||||
@ -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 条件并入 ns(crud-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))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user