296 lines
11 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 数据访问层。
只走 sqlor 标准 API(``sor.C / sor.U / sor.D / sor.R / sor.I / sor.sqlExe``),
禁止编造 save/list/insert 等不存在的方法。
库名取自 ``ServerEnv().get_module_dbname('pbl_domain_ext')``,禁止硬编码 DBNAME。
基础域基表(world/scene/entity)在本层**只读**:仅暴露 ``read_base_rows``,
不提供任何写基表的函数(Q-OPEN-3:薄扩展不改基表)。
"""
from .base import (TABLE, BASE_TABLES, LIST_FIELDS, EXT_FIELD, as_text)
from .errors import PblDomainExtError
__all__ = ["get_sor", "get_dbname", "insert_ref", "update_ref", "soft_delete_ref",
"select_ref_by_id", "select_ref_by_key", "select_refs", "count_refs",
"read_base_rows", "read_base_row", "base_table_exists", "ensure_ext_column",
"execute_sql"]
_SOR_HOLDER = {}
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 平台运行期可用
env = ServerEnv()
sor = getattr(env, "sor", None)
except Exception:
sor = None
if sor is None:
raise PblDomainExtError("PBL_DE_DB_ERROR",
detail="sqlor 句柄未就绪(模块未挂载到应用?)")
_SOR_HOLDER["sor"] = sor
return sor
def set_sor(sor):
"""init.py 挂载时注入 sqlor 句柄(测试可注入 fake_db)。"""
_SOR_HOLDER["sor"] = sor
return sor
def get_dbname():
"""模块库名:ServerEnv().get_module_dbname('pbl_domain_ext'),禁止硬编码。"""
try:
from sage import ServerEnv
return ServerEnv().get_module_dbname("pbl_domain_ext")
except Exception:
return _SOR_HOLDER.get("dbname") or ""
def execute_sql(sql, args=None):
"""只读/DDL 统一走 sqlExe;写数据请用 insert_ref/update_ref。"""
sor = get_sor()
try:
if args:
return sor.sqlExe(sql, args)
return sor.sqlExe(sql)
except PblDomainExtError:
raise
except Exception as exc: # noqa: BLE001
raise PblDomainExtError("PBL_DE_DB_ERROR", detail="%s | %s" % (type(exc).__name__, exc))
# --------------------------------------------------------------------------
# 关联表 pbl_domain_ref 读写
# --------------------------------------------------------------------------
def insert_ref(row):
"""新增关联记录,返回受影响行数。"""
sor = get_sor()
try:
return sor.C(TABLE, row)
except Exception as exc: # noqa: BLE001
raise PblDomainExtError("PBL_DE_DB_ERROR", detail="insert %s: %s" % (TABLE, exc))
def update_ref(where_dict, row):
"""按 where 更新关联记录,返回受影响行数。"""
if not where_dict:
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="update 缺 where 条件")
sor = get_sor()
try:
return sor.U(TABLE, row, where_dict)
except Exception as exc: # noqa: BLE001
raise PblDomainExtError("PBL_DE_DB_ERROR", detail="update %s: %s" % (TABLE, exc))
def soft_delete_ref(where_dict, operator_id=""):
"""逻辑删除(is_deleted=1),绝不物理 DELETE。"""
from .base import audit_on_update
row = audit_on_update({"is_deleted": 1}, operator_id)
return update_ref(where_dict, row)
def _fields_clause(fields=None):
cols = fields or LIST_FIELDS
return ", ".join("`%s`" % c for c in cols)
def select_ref_by_id(tenant_id, ref_pk, fields=None):
"""按主键取单条(强制带 tenant_id)。"""
sql = "SELECT %s FROM `%s` WHERE `tenant_id`=%%s AND `id`=%%s AND `is_deleted`=0 LIMIT 1" % (
_fields_clause(fields), TABLE)
rows = execute_sql(sql, (tenant_id, ref_pk))
return rows[0] if rows else None
def select_ref_by_key(tenant_id, ref_type, ref_id, fields=None):
"""按业务唯一键 (tenant_id, ref_type, ref_id) 取单条。"""
sql = ("SELECT %s FROM `%s` WHERE `tenant_id`=%%s AND `ref_type`=%%s "
"AND `ref_id`=%%s AND `is_deleted`=0 LIMIT 1") % (_fields_clause(fields), TABLE)
rows = execute_sql(sql, (tenant_id, ref_type, ref_id))
return rows[0] if rows else None
def select_refs(tenant_id, conds=None, order_by=None, offset=0, limit=20, fields=None):
"""条件查询关联记录列表。
:param conds: ``{列名: 值}`` 或 ``{列名: [值,...]}``(IN),值一律参数化绑定
"""
where = ["`tenant_id`=%s", "`is_deleted`=0"]
args = [tenant_id]
for key, val in (conds or {}).items():
col = as_text(key).strip("`")
if col not in LIST_FIELDS:
# 非白名单列直接忽略,防注入
continue
if isinstance(val, (list, tuple, set)):
vals = [v for v in val if v not in (None, "")]
if not vals:
continue
where.append("`%s` IN (%s)" % (col, ", ".join(["%s"] * len(vals))))
args.extend(vals)
elif val not in (None, ""):
where.append("`%s`=%%s" % col)
args.append(val)
order = as_text(order_by).strip() or "`bind_at` DESC, `id` DESC"
sql = "SELECT %s FROM `%s` WHERE %s ORDER BY %s LIMIT %%s OFFSET %%s" % (
_fields_clause(fields), TABLE, " AND ".join(where), order)
args.extend([int(limit), int(offset)])
return execute_sql(sql, tuple(args)) or []
def count_refs(tenant_id, conds=None):
"""与 select_refs 同条件的总数(分页用)。"""
where = ["`tenant_id`=%s", "`is_deleted`=0"]
args = [tenant_id]
for key, val in (conds or {}).items():
col = as_text(key).strip("`")
if col not in LIST_FIELDS:
continue
if isinstance(val, (list, tuple, set)):
vals = [v for v in val if v not in (None, "")]
if not vals:
continue
where.append("`%s` IN (%s)" % (col, ", ".join(["%s"] * len(vals))))
args.extend(vals)
elif val not in (None, ""):
where.append("`%s`=%%s" % col)
args.append(val)
sql = "SELECT COUNT(1) AS `cnt` FROM `%s` WHERE %s" % (TABLE, " AND ".join(where))
rows = execute_sql(sql, tuple(args))
if not rows:
return 0
row = rows[0]
if isinstance(row, dict):
return int(row.get("cnt") or 0)
return int(row[0] or 0)
# --------------------------------------------------------------------------
# 基础域基表:只读
# --------------------------------------------------------------------------
def read_base_rows(ref_type, tenant_id, extra_conds=None, limit=200):
"""只读拉取基础域基表记录(world/scene/entity)。
基表列名因模块而异,这里用 ``SELECT *`` 只读取、绝不写入;
返回原始 dict 列表,由 api 层做字段映射与裁剪。
"""
check = as_text(ref_type).strip().lower()
if check not in BASE_TABLES:
raise PblDomainExtError("PBL_DE_REF_TYPE_INVALID", detail="ref_type=%r" % (ref_type,))
tbl = BASE_TABLES[check]
where = ["`tenant_id`=%s"]
args = [tenant_id]
for key, val in (extra_conds or {}).items():
col = as_text(key).strip("`")
if not col.isidentifier():
continue
if val in (None, ""):
continue
where.append("`%s`=%%s" % col)
args.append(val)
sql = "SELECT * FROM `%s` WHERE %s LIMIT %%s" % (tbl, " AND ".join(where))
args.append(int(limit))
try:
return execute_sql(sql, tuple(args)) or []
except PblDomainExtError as exc:
# 基表可能不带 tenant_id 列(历史库),降级为无租户过滤的只读查询
if exc.code == "PBL_DE_DB_ERROR":
sql2 = "SELECT * FROM `%s` LIMIT %%s" % tbl
try:
return execute_sql(sql2, (int(limit),)) or []
except PblDomainExtError:
return []
raise
def read_base_row(ref_type, ref_id, tenant_id=None):
"""按主键只读单条基表记录。"""
check = as_text(ref_type).strip().lower()
if check not in BASE_TABLES:
raise PblDomainExtError("PBL_DE_REF_TYPE_INVALID", detail="ref_type=%r" % (ref_type,))
tbl = BASE_TABLES[check]
rid = as_text(ref_id).strip()
if not rid:
raise PblDomainExtError("PBL_DE_PARAM_INVALID", detail="ref_id 为空")
if tenant_id:
sql = "SELECT * FROM `%s` WHERE `id`=%%s AND `tenant_id`=%%s LIMIT 1" % tbl
rows = _safe_select(sql, (rid, tenant_id))
else:
sql = "SELECT * FROM `%s` WHERE `id`=%%s LIMIT 1" % tbl
rows = _safe_select(sql, (rid,))
if rows:
return rows[0]
# 兼容基表主键列名为 {tbl}_id 的历史实现
alt = "%s_id" % tbl
sql_alt = "SELECT * FROM `%s` WHERE `%s`=%%s LIMIT 1" % (tbl, alt)
rows = _safe_select(sql_alt, (rid,))
return rows[0] if rows else None
def _safe_select(sql, args):
try:
return execute_sql(sql, args) or []
except PblDomainExtError:
return []
def base_table_exists(ref_type):
"""探测基础域基表是否存在(部署校验用,只读 information_schema)。"""
tbl = BASE_TABLES.get(as_text(ref_type).strip().lower())
if not tbl:
return False
dbname = get_dbname()
if not dbname:
return True # 无法判定时不阻断
sql = ("SELECT COUNT(1) AS `cnt` FROM information_schema.tables "
"WHERE table_schema=%s AND table_name=%s")
try:
rows = execute_sql(sql, (dbname, tbl))
except PblDomainExtError:
return True
if not rows:
return False
row = rows[0]
return int((row.get("cnt") if isinstance(row, dict) else row[0]) or 0) > 0
def ensure_ext_column():
"""幂等保障:扩展列名必须是设计 §J1 权威名 ``ext_json``(LONGTEXT)。
历史库若为旧名 ``ext`` 则 RENAME 为 ``ext_json``;若两列都无则 ADD。返回执行的动作描述。
"""
dbname = get_dbname()
if not dbname:
return "skip(no dbname)"
probe = ("SELECT column_name FROM information_schema.columns "
"WHERE table_schema=%s AND table_name=%s")
try:
rows = execute_sql(probe, (dbname, TABLE)) or []
except PblDomainExtError:
return "skip(probe failed)"
cols = set()
for r in rows:
name = r.get("column_name") if isinstance(r, dict) else r[0]
cols.add(as_text(name).lower())
if not cols:
return "skip(table absent)"
if EXT_FIELD in cols and "ext" in cols:
execute_sql("ALTER TABLE `%s` DROP COLUMN `ext`" % TABLE)
return "dropped legacy ext"
if EXT_FIELD in cols:
return "ok(ext_json present)"
if "ext" in cols:
execute_sql("ALTER TABLE `%s` CHANGE COLUMN `ext` `%s` longtext "
"COMMENT '扩展JSON(设计§J1)'" % (TABLE, EXT_FIELD))
return "renamed ext -> ext_json"
execute_sql("ALTER TABLE `%s` ADD COLUMN `%s` longtext "
"COMMENT '扩展JSON(设计§J1)'" % (TABLE, EXT_FIELD))
return "added ext_json"