213 lines
7.4 KiB
Python
Raw 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.

# -*- coding: utf-8 -*-
"""pbl_domain_ext 公共内核:常量、参数校验、租户上下文、审计字段。
设计依据projects/pbls/docs/01-design/data-model.md §J1基础域薄扩展
铁律:
1. 只维护 1 张关联表 ``pbl_domain_ref``**不改** world/scene/entity 三张基表结构;
2. 所有读写 ``tenant_id`` 强制打头,缺失即抛 ``PBL_DE_TENANT_MISSING``
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
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", "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",
]
#: 本模块唯一自有表
TABLE = "pbl_domain_ref"
#: 基础域基表(只读引用,禁止写入/改结构)
BASE_TABLES = {
"world": "world",
"scene": "scene",
"entity": "entity",
}
#: ref_type 合法枚举
REF_TYPES = ("world", "scene", "entity")
#: bind_state 合法枚举
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, "is_deleted",
"creator_id", "created_at", "updater_id", "updated_at",
]
#: 表全部列(= LIST_FIELDS本表无隐藏列供 DDL/契约一致性校验)
ALL_COLUMNS = list(LIST_FIELDS)
_MAX_PAGE_SIZE = 500
def now_str():
"""统一时间串格式 ``YYYY-MM-DD HH:MM:SS``。"""
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
def gen_id():
"""生成 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=""):
"""安全转字符串None -> default"""
if value is None:
return default
if isinstance(value, bytes):
try:
return value.decode("utf-8")
except UnicodeDecodeError:
return value.decode("utf-8", "replace")
if isinstance(value, str):
return value
return str(value)
def as_list(value):
"""把 None/单值/列表统一成 list。"""
if value is None or value == "":
return []
if isinstance(value, (list, tuple, set)):
return list(value)
return [value]
def require_tenant(params):
"""取并校验 tenant_id所有契约第一道门禁
:return: 去空白后的 tenant_id
:raises PblDomainExtError: PBL_DE_TENANT_MISSING
"""
tenant_id = ""
if isinstance(params, dict):
tenant_id = as_text(params.get("tenant_id")).strip()
if not tenant_id or len(tenant_id) > 32:
raise PblDomainExtError("PBL_DE_TENANT_MISSING",
detail="tenant_id=%r" % (tenant_id,))
return tenant_id
def check_ref_type(ref_type, allow_empty=False):
"""校验 ref_type ∈ REF_TYPES。"""
ref_type = as_text(ref_type).strip().lower()
if not ref_type:
if allow_empty:
return ""
raise PblDomainExtError("PBL_DE_REF_TYPE_INVALID", detail="ref_type 为空")
if ref_type not in REF_TYPES:
raise PblDomainExtError("PBL_DE_REF_TYPE_INVALID",
detail="ref_type=%r 允许=%s" % (ref_type, list(REF_TYPES)))
return ref_type
def check_bind_state(bind_state, allow_empty=True):
"""校验 bind_state ∈ BIND_STATES。"""
bind_state = as_text(bind_state).strip().lower()
if not bind_state:
if allow_empty:
return ""
return BIND_STATES[0]
if bind_state not in BIND_STATES:
raise PblDomainExtError("PBL_DE_STATE_INVALID",
detail="bind_state=%r" % (bind_state,))
return bind_state
def normalize_ext_json(raw):
"""把入参 ext_json 归一为 **合法 JSON 文本**(列类型 LONGTEXT存字符串
接受None/""-> ""、dict/list-> json.dumps、已是 JSON 文本的 str。
非法 JSON 文本抛 ``PBL_DE_EXT_JSON_INVALID``。
"""
if raw is None:
return ""
if isinstance(raw, (dict, list)):
return json.dumps(raw, ensure_ascii=False, sort_keys=True)
text = as_text(raw).strip()
if not text:
return ""
try:
parsed = json.loads(text)
except (ValueError, TypeError):
raise PblDomainExtError("PBL_DE_EXT_JSON_INVALID", detail=text[:120])
if not isinstance(parsed, (dict, list)):
raise PblDomainExtError("PBL_DE_EXT_JSON_INVALID",
detail="ext_json 顶层必须是 object/array")
return json.dumps(parsed, ensure_ascii=False, sort_keys=True)
def audit_on_create(row, operator_id):
"""补创建审计四件套(就地修改并返回 row。列名 = creator_id/updater_idQC#1"""
ts = now_str()
row["creator_id"] = as_text(operator_id)
row["created_at"] = ts
row["updater_id"] = as_text(operator_id)
row["updated_at"] = ts
row.setdefault("is_deleted", 0)
return row
def audit_on_update(row, operator_id):
"""补更新审计(就地修改并返回 row"""
row["updater_id"] = as_text(operator_id)
row["updated_at"] = now_str()
return row
def page_args(params):
"""解析分页参数,返回 (page, page_size, offset)。越界自动收敛。"""
try:
page = int(params.get("page") or 1)
except (TypeError, ValueError):
page = 1
try:
page_size = int(params.get("page_size") or params.get("limit") or 20)
except (TypeError, ValueError):
page_size = 20
if page < 1:
page = 1
if page_size < 1:
page_size = 1
if page_size > _MAX_PAGE_SIZE:
page_size = _MAX_PAGE_SIZE
return page, page_size, (page - 1) * page_size