296 lines
11 KiB
Python
296 lines
11 KiB
Python
# -*- 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"
|