feat: add tenant-scoped rule and permission management

This commit is contained in:
wren
2026-05-21 22:03:08 +08:00
parent a2c2bf1969
commit 1f1bccf3b3
193 changed files with 64463 additions and 1771 deletions
@@ -0,0 +1,868 @@
"""租户主数据服务实现。"""
from __future__ import annotations
from typing import Any
from sqlalchemy import text
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
from fastapi_modules.fastapi_leaudit.domian.Dto.tenantDto import (
TenantCreateDTO,
TenantStatusUpdateDTO,
TenantUpdateDTO,
)
from fastapi_modules.fastapi_leaudit.services.impl.ruleTenantMaterializer import (
GetRuleTenantMaterializerSingleton,
RuleTenantMaterializer,
)
from fastapi_modules.fastapi_leaudit.services.tenantService import ITenantService
class TenantServiceImpl(ITenantService):
"""租户主数据服务实现。"""
_BUILTIN_TENANT_CODES: tuple[str, ...] = ("PUBLIC", "PROVINCIAL")
_SUPPORTED_FEATURE_KEYS: tuple[str, ...] = (
"home.entry_module",
"documents.upload",
"rag.dataset",
)
def __init__(self, RuleTenantMaterializer: RuleTenantMaterializer | None = None) -> None:
self._table_exists_cache: dict[str, bool] = {}
self.RuleTenantMaterializer = RuleTenantMaterializer or GetRuleTenantMaterializerSingleton()
async def ListTenants(self, IncludeDisabled: bool = False) -> list[dict[str, Any]]:
if not await self._table_exists("sys_tenants"):
return await self._list_legacy_tenants()
filters = ["t.deleted_at IS NULL"]
params: dict[str, Any] = {}
if not IncludeDisabled:
filters.append("t.is_enabled = TRUE")
where_sql = " AND ".join(filters)
async with GetAsyncSession() as session:
rows = (
await session.execute(
text(
f"""
SELECT
t.tenant_code,
t.tenant_name,
t.tenant_short_name,
t.tenant_type,
t.parent_tenant_code,
t.display_order,
t.is_enabled,
t.is_builtin,
t.is_public,
t.can_host_entry_module,
t.can_host_documents,
t.can_host_rag,
t.can_host_templates,
t.ext,
COALESCE(
ARRAY(
SELECT f.feature_key
FROM sys_tenant_feature_flags f
WHERE f.tenant_code = t.tenant_code
AND f.deleted_at IS NULL
AND f.is_enabled = TRUE
ORDER BY f.feature_key ASC
),
ARRAY[]::VARCHAR[]
) AS feature_keys,
COALESCE(
ARRAY(
SELECT a.alias_value
FROM sys_tenant_aliases a
WHERE a.tenant_code = t.tenant_code
AND a.deleted_at IS NULL
AND a.is_enabled = TRUE
ORDER BY
CASE a.alias_type
WHEN 'DISPLAY' THEN 1
WHEN 'SHORT_NAME' THEN 2
WHEN 'LEGACY_AREA' THEN 3
ELSE 9
END ASC,
a.id ASC
),
ARRAY[]::VARCHAR[]
) AS alias_values
FROM sys_tenants t
WHERE {where_sql}
ORDER BY t.display_order ASC, t.id ASC
"""
),
params,
)
).mappings().all()
return [dict(row) for row in rows]
async def ListTenantOptions(self, FeatureKey: str | None = None) -> list[dict[str, Any]]:
if not await self._table_exists("sys_tenants"):
items = await self._list_legacy_tenant_options()
return items
filters = ["t.deleted_at IS NULL", "t.is_enabled = TRUE"]
params: dict[str, Any] = {}
join_sql = ""
if FeatureKey and FeatureKey.strip() and await self._table_exists("sys_tenant_feature_flags"):
join_sql = """
JOIN sys_tenant_feature_flags f
ON f.tenant_code = t.tenant_code
AND f.deleted_at IS NULL
AND f.is_enabled = TRUE
AND f.feature_key = :feature_key
"""
params["feature_key"] = FeatureKey.strip()
where_sql = " AND ".join(filters)
async with GetAsyncSession() as session:
rows = (
await session.execute(
text(
f"""
SELECT
t.tenant_code,
t.tenant_name,
t.tenant_short_name,
t.tenant_type,
t.is_public,
t.display_order
FROM sys_tenants t
{join_sql}
WHERE {where_sql}
ORDER BY t.display_order ASC, t.id ASC
"""
),
params,
)
).mappings().all()
return [dict(row) for row in rows]
async def GetTenant(self, TenantCode: str) -> dict[str, Any] | None:
tenant_code = str(TenantCode or "").strip()
if not tenant_code:
return None
if not await self._table_exists("sys_tenants"):
for item in await self._list_legacy_tenants():
if str(item.get("tenant_code") or "").strip() == tenant_code:
return item
return None
async with GetAsyncSession() as session:
row = (
await session.execute(
text(
"""
SELECT
t.tenant_code,
t.tenant_name,
t.tenant_short_name,
t.tenant_type,
t.parent_tenant_code,
t.display_order,
t.is_enabled,
t.is_builtin,
t.is_public,
t.can_host_entry_module,
t.can_host_documents,
t.can_host_rag,
t.can_host_templates,
t.ext
FROM sys_tenants t
WHERE t.tenant_code = :tenant_code
AND t.deleted_at IS NULL
LIMIT 1
"""
),
{"tenant_code": tenant_code},
)
).mappings().first()
return dict(row) if row else None
async def GetTenantFeatures(self, TenantCode: str) -> list[str]:
tenant_code = str(TenantCode or "").strip()
if not tenant_code:
return []
if not await self._table_exists("sys_tenant_feature_flags"):
return []
async with GetAsyncSession() as session:
rows = (
await session.execute(
text(
"""
SELECT feature_key
FROM sys_tenant_feature_flags
WHERE tenant_code = :tenant_code
AND deleted_at IS NULL
AND is_enabled = TRUE
ORDER BY feature_key ASC
"""
),
{"tenant_code": tenant_code},
)
).all()
return [str(row[0]) for row in rows]
async def GetTenantAliases(self, TenantCode: str) -> list[str]:
tenant_code = str(TenantCode or "").strip()
if not tenant_code:
return []
return await self._getTenantAliases(tenant_code)
async def CreateTenant(self, CurrentUserId: int, Body: TenantCreateDTO) -> dict[str, Any]:
del CurrentUserId
await self._ensureWritableTenantFoundation()
tenant_code = self._normalizeTenantCode(Body.tenant_code)
tenant_name = self._normalizeRequiredText(Body.tenant_name, "租户名称")
tenant_short_name = self._normalizeOptionalText(Body.tenant_short_name) or tenant_name
tenant_type = self._normalizeTenantType(Body.tenant_type)
parent_tenant_code = self._normalizeOptionalCode(Body.parent_tenant_code)
feature_keys = self._normalizeFeatureKeys(Body.feature_keys)
alias_values = self._normalizeAliasValues(Body.alias_values, tenant_name=tenant_name, tenant_short_name=tenant_short_name)
ext = Body.ext or {}
async with GetAsyncSession() as session:
exists = (
await session.execute(
text(
"""
SELECT 1
FROM sys_tenants
WHERE tenant_code = :tenant_code
AND deleted_at IS NULL
LIMIT 1
"""
),
{"tenant_code": tenant_code},
)
).scalar_one_or_none()
if exists:
raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, f"租户编码已存在: {tenant_code}")
if parent_tenant_code:
await self._assertTenantExists(session, parent_tenant_code, "父级租户不存在")
await session.execute(
text(
"""
INSERT INTO sys_tenants (
tenant_code,
tenant_name,
tenant_short_name,
tenant_type,
parent_tenant_code,
display_order,
is_enabled,
is_builtin,
is_public,
can_host_entry_module,
can_host_documents,
can_host_rag,
can_host_templates,
ext,
created_at,
updated_at,
deleted_at
) VALUES (
:tenant_code,
:tenant_name,
:tenant_short_name,
:tenant_type,
:parent_tenant_code,
:display_order,
:is_enabled,
FALSE,
:is_public,
:can_host_entry_module,
:can_host_documents,
:can_host_rag,
:can_host_templates,
CAST(:ext AS jsonb),
NOW(),
NOW(),
NULL
)
"""
),
{
"tenant_code": tenant_code,
"tenant_name": tenant_name,
"tenant_short_name": tenant_short_name,
"tenant_type": tenant_type,
"parent_tenant_code": parent_tenant_code,
"display_order": int(Body.display_order or 0),
"is_enabled": bool(Body.is_enabled),
"is_public": bool(Body.is_public),
"can_host_entry_module": bool(Body.can_host_entry_module),
"can_host_documents": bool(Body.can_host_documents),
"can_host_rag": bool(Body.can_host_rag),
"can_host_templates": bool(Body.can_host_templates),
"ext": self._dumpJson(ext),
},
)
await self._replaceTenantAliases(session, tenant_code, alias_values)
await self._replaceTenantFeatures(session, tenant_code, feature_keys)
await session.commit()
await self.RuleTenantMaterializer.MaterializeTenant(tenant_code)
return await self._getTenantDetailOrFail(tenant_code)
async def UpdateTenant(self, CurrentUserId: int, TenantCode: str, Body: TenantUpdateDTO) -> dict[str, Any]:
del CurrentUserId
await self._ensureWritableTenantFoundation()
tenant_code = self._normalizeTenantCode(TenantCode)
async with GetAsyncSession() as session:
current = await self._loadTenantRow(session, tenant_code)
if not current:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "租户不存在")
is_builtin = bool(current.get("is_builtin"))
tenant_name = self._normalizeOptionalText(Body.tenant_name) if Body.tenant_name is not None else str(current.get("tenant_name") or "")
if not tenant_name:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "租户名称不能为空")
tenant_short_name = (
self._normalizeOptionalText(Body.tenant_short_name)
if Body.tenant_short_name is not None
else self._normalizeOptionalText(current.get("tenant_short_name"))
) or tenant_name
tenant_type = self._normalizeTenantType(Body.tenant_type) if Body.tenant_type is not None else str(current.get("tenant_type") or "CUSTOM")
parent_tenant_code = self._normalizeOptionalCode(Body.parent_tenant_code) if Body.parent_tenant_code is not None else current.get("parent_tenant_code")
if parent_tenant_code == tenant_code:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "父级租户不能指向自身")
if parent_tenant_code:
await self._assertTenantExists(session, str(parent_tenant_code), "父级租户不存在", exclude_tenant_code=tenant_code)
if is_builtin and tenant_code in self._BUILTIN_TENANT_CODES:
tenant_type = str(current.get("tenant_type") or tenant_type)
parent_tenant_code = current.get("parent_tenant_code")
await session.execute(
text(
"""
UPDATE sys_tenants
SET tenant_name = :tenant_name,
tenant_short_name = :tenant_short_name,
tenant_type = :tenant_type,
parent_tenant_code = :parent_tenant_code,
display_order = :display_order,
is_public = :is_public,
can_host_entry_module = :can_host_entry_module,
can_host_documents = :can_host_documents,
can_host_rag = :can_host_rag,
can_host_templates = :can_host_templates,
ext = CAST(:ext AS jsonb),
updated_at = NOW()
WHERE tenant_code = :tenant_code
AND deleted_at IS NULL
"""
),
{
"tenant_code": tenant_code,
"tenant_name": tenant_name,
"tenant_short_name": tenant_short_name,
"tenant_type": tenant_type,
"parent_tenant_code": parent_tenant_code,
"display_order": int(Body.display_order if Body.display_order is not None else current.get("display_order") or 0),
"is_public": bool(Body.is_public if Body.is_public is not None else current.get("is_public")),
"can_host_entry_module": bool(
Body.can_host_entry_module if Body.can_host_entry_module is not None else current.get("can_host_entry_module")
),
"can_host_documents": bool(
Body.can_host_documents if Body.can_host_documents is not None else current.get("can_host_documents")
),
"can_host_rag": bool(Body.can_host_rag if Body.can_host_rag is not None else current.get("can_host_rag")),
"can_host_templates": bool(
Body.can_host_templates if Body.can_host_templates is not None else current.get("can_host_templates")
),
"ext": self._dumpJson(Body.ext if Body.ext is not None else (current.get("ext") or {})),
},
)
if Body.alias_values is not None:
alias_values = self._normalizeAliasValues(Body.alias_values, tenant_name=tenant_name, tenant_short_name=tenant_short_name)
await self._replaceTenantAliases(session, tenant_code, alias_values)
if Body.feature_keys is not None:
feature_keys = self._normalizeFeatureKeys(Body.feature_keys)
await self._replaceTenantFeatures(session, tenant_code, feature_keys)
await session.commit()
return await self._getTenantDetailOrFail(tenant_code)
async def UpdateTenantStatus(self, CurrentUserId: int, TenantCode: str, Body: TenantStatusUpdateDTO) -> dict[str, Any]:
del CurrentUserId
await self._ensureWritableTenantFoundation()
tenant_code = self._normalizeTenantCode(TenantCode)
is_enabled = bool(Body.is_enabled)
async with GetAsyncSession() as session:
current = await self._loadTenantRow(session, tenant_code)
if not current:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "租户不存在")
if bool(current.get("is_builtin")) and tenant_code in self._BUILTIN_TENANT_CODES and not is_enabled:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "内建核心租户不允许禁用")
if not is_enabled:
await self._assertTenantCanBeDisabled(session, tenant_code)
await session.execute(
text(
"""
UPDATE sys_tenants
SET is_enabled = :is_enabled,
updated_at = NOW()
WHERE tenant_code = :tenant_code
AND deleted_at IS NULL
"""
),
{"tenant_code": tenant_code, "is_enabled": is_enabled},
)
await session.commit()
return await self._getTenantDetailOrFail(tenant_code)
async def _table_exists(self, table_name: str) -> bool:
cached = self._table_exists_cache.get(table_name)
if cached is not None:
return cached
async with GetAsyncSession() as session:
exists = bool(
(
await session.execute(
text(
"""
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name = :table_name
)
"""
),
{"table_name": table_name},
)
).scalar_one()
)
self._table_exists_cache[table_name] = exists
return exists
async def _list_legacy_tenants(self) -> list[dict[str, Any]]:
async with GetAsyncSession() as session:
user_rows = (
await session.execute(
text(
"""
SELECT DISTINCT COALESCE(NULLIF(area, ''), '') AS area
FROM sso_users
WHERE deleted_at IS NULL
AND COALESCE(NULLIF(area, ''), '') <> ''
ORDER BY COALESCE(NULLIF(area, ''), '') ASC
"""
)
)
).all()
items: list[dict[str, Any]] = [
{
"tenant_code": "PUBLIC",
"tenant_name": "公共",
"tenant_short_name": "公共",
"tenant_type": "PUBLIC",
"parent_tenant_code": None,
"display_order": 0,
"is_enabled": True,
"is_builtin": True,
"is_public": True,
"can_host_entry_module": True,
"can_host_documents": True,
"can_host_rag": True,
"can_host_templates": True,
"ext": {},
"feature_keys": [],
}
]
for index, row in enumerate(user_rows, start=1):
area = str(row[0] or "").strip()
if not area or area == "公共":
continue
items.append(
{
"tenant_code": area,
"tenant_name": area,
"tenant_short_name": area,
"tenant_type": "LEGACY_AREA",
"parent_tenant_code": None,
"display_order": index * 10,
"is_enabled": True,
"is_builtin": False,
"is_public": False,
"can_host_entry_module": True,
"can_host_documents": True,
"can_host_rag": True,
"can_host_templates": True,
"ext": {},
"feature_keys": [],
}
)
return items
async def _list_legacy_tenant_options(self) -> list[dict[str, Any]]:
return [
{
"tenant_code": str(item.get("tenant_code") or ""),
"tenant_name": str(item.get("tenant_name") or ""),
"tenant_short_name": item.get("tenant_short_name"),
"tenant_type": item.get("tenant_type"),
"is_public": bool(item.get("is_public")),
"display_order": item.get("display_order"),
}
for item in await self._list_legacy_tenants()
]
async def _ensureWritableTenantFoundation(self) -> None:
required_tables = ("sys_tenants", "sys_tenant_aliases", "sys_tenant_feature_flags")
missing = [table_name for table_name in required_tables if not await self._table_exists(table_name)]
if missing:
joined = ", ".join(missing)
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, f"租户主数据底座未初始化,缺少表: {joined}")
async def _getTenantDetailOrFail(self, tenant_code: str) -> dict[str, Any]:
item = await self.GetTenant(tenant_code)
if not item:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "租户不存在")
item["feature_keys"] = await self.GetTenantFeatures(tenant_code)
item["alias_values"] = await self.GetTenantAliases(tenant_code)
return item
async def _getTenantAliases(self, tenant_code: str) -> list[str]:
if not await self._table_exists("sys_tenant_aliases"):
return []
async with GetAsyncSession() as session:
rows = (
await session.execute(
text(
"""
SELECT alias_value
FROM sys_tenant_aliases
WHERE tenant_code = :tenant_code
AND deleted_at IS NULL
AND is_enabled = TRUE
ORDER BY
CASE alias_type
WHEN 'DISPLAY' THEN 1
WHEN 'SHORT_NAME' THEN 2
WHEN 'LEGACY_AREA' THEN 3
ELSE 9
END ASC,
id ASC
"""
),
{"tenant_code": tenant_code},
)
).all()
return [str(row[0]).strip() for row in rows if str(row[0] or "").strip()]
async def _loadTenantRow(self, session: Any, tenant_code: str) -> dict[str, Any] | None:
row = (
await session.execute(
text(
"""
SELECT
tenant_code,
tenant_name,
tenant_short_name,
tenant_type,
parent_tenant_code,
display_order,
is_enabled,
is_builtin,
is_public,
can_host_entry_module,
can_host_documents,
can_host_rag,
can_host_templates,
ext
FROM sys_tenants
WHERE tenant_code = :tenant_code
AND deleted_at IS NULL
LIMIT 1
"""
),
{"tenant_code": tenant_code},
)
).mappings().first()
return dict(row) if row else None
async def _assertTenantExists(
self,
session: Any,
tenant_code: str,
error_message: str,
exclude_tenant_code: str | None = None,
) -> None:
params: dict[str, Any] = {"tenant_code": tenant_code}
sql = """
SELECT 1
FROM sys_tenants
WHERE tenant_code = :tenant_code
AND deleted_at IS NULL
"""
if exclude_tenant_code:
sql += " AND tenant_code <> :exclude_tenant_code"
params["exclude_tenant_code"] = exclude_tenant_code
sql += " LIMIT 1"
exists = (await session.execute(text(sql), params)).scalar_one_or_none()
if not exists:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, error_message)
async def _assertTenantCanBeDisabled(self, session: Any, tenant_code: str) -> None:
references = await self._collectDisableReferences(session, tenant_code)
if not references:
return
joined = "".join(references)
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"当前租户仍被引用,不能禁用:{joined}")
async def _collectDisableReferences(self, session: Any, tenant_code: str) -> list[str]:
references: list[str] = []
if await self._table_exists("sys_tenants"):
child_count = int(
(
await session.execute(
text(
"""
SELECT COUNT(*)
FROM sys_tenants
WHERE parent_tenant_code = :tenant_code
AND deleted_at IS NULL
AND is_enabled = TRUE
"""
),
{"tenant_code": tenant_code},
)
).scalar_one()
)
if child_count > 0:
references.append(f"存在 {child_count} 个启用中的子租户")
if await self._table_exists("leaudit_entry_module_tenants"):
entry_module_count = int(
(
await session.execute(
text(
"""
SELECT COUNT(DISTINCT emt.entry_module_id)
FROM leaudit_entry_module_tenants emt
WHERE emt.tenant_code = :tenant_code
AND emt.deleted_at IS NULL
AND COALESCE(emt.is_enabled, TRUE) = TRUE
"""
),
{"tenant_code": tenant_code},
)
).scalar_one()
)
if entry_module_count > 0:
references.append(f"仍绑定 {entry_module_count} 个入口模块")
if await self._table_exists("sso_users") and await self._column_exists("sso_users", "tenant_code"):
user_count = int(
(
await session.execute(
text(
"""
SELECT COUNT(*)
FROM sso_users
WHERE tenant_code = :tenant_code
AND deleted_at IS NULL
AND status = 0
"""
),
{"tenant_code": tenant_code},
)
).scalar_one()
)
if user_count > 0:
references.append(f"仍有 {user_count} 个启用用户归属该租户")
return references
async def _replaceTenantAliases(self, session: Any, tenant_code: str, alias_values: list[str]) -> None:
await session.execute(
text(
"""
DELETE FROM sys_tenant_aliases
WHERE tenant_code = :tenant_code
"""
),
{"tenant_code": tenant_code},
)
for index, alias in enumerate(alias_values):
alias_type = "DISPLAY" if index == 0 else "SHORT_NAME"
await session.execute(
text(
"""
INSERT INTO sys_tenant_aliases (
tenant_code,
alias_type,
alias_value,
is_enabled,
created_at,
updated_at,
deleted_at
) VALUES (
:tenant_code,
:alias_type,
:alias_value,
TRUE,
NOW(),
NOW(),
NULL
)
"""
),
{
"tenant_code": tenant_code,
"alias_type": alias_type,
"alias_value": alias,
},
)
async def _replaceTenantFeatures(self, session: Any, tenant_code: str, feature_keys: list[str]) -> None:
await session.execute(
text(
"""
DELETE FROM sys_tenant_feature_flags
WHERE tenant_code = :tenant_code
"""
),
{"tenant_code": tenant_code},
)
for feature_key in feature_keys:
await session.execute(
text(
"""
INSERT INTO sys_tenant_feature_flags (
tenant_code,
feature_key,
is_enabled,
created_at,
updated_at,
deleted_at
) VALUES (
:tenant_code,
:feature_key,
TRUE,
NOW(),
NOW(),
NULL
)
"""
),
{
"tenant_code": tenant_code,
"feature_key": feature_key,
},
)
def _normalizeTenantCode(self, value: str | None) -> str:
tenant_code = str(value or "").strip()
if not tenant_code:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "租户编码不能为空")
if len(tenant_code) > 64:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "租户编码长度不能超过 64")
return tenant_code
def _normalizeOptionalCode(self, value: str | None) -> str | None:
normalized = self._normalizeOptionalText(value)
return normalized or None
def _normalizeRequiredText(self, value: str | None, field_name: str) -> str:
normalized = self._normalizeOptionalText(value)
if not normalized:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"{field_name}不能为空")
return normalized
@staticmethod
def _normalizeOptionalText(value: Any) -> str | None:
if value is None:
return None
text_value = str(value).strip()
return text_value or None
def _normalizeTenantType(self, value: str | None) -> str:
tenant_type = self._normalizeOptionalText(value) or "CUSTOM"
if len(tenant_type) > 32:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "租户类型长度不能超过 32")
return tenant_type.upper()
def _normalizeFeatureKeys(self, values: list[str] | None) -> list[str]:
normalized: list[str] = []
seen: set[str] = set()
for item in values or []:
feature_key = self._normalizeOptionalText(item)
if not feature_key:
continue
if feature_key not in self._SUPPORTED_FEATURE_KEYS:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"不支持的功能标识: {feature_key}")
if feature_key in seen:
continue
seen.add(feature_key)
normalized.append(feature_key)
return normalized
def _normalizeAliasValues(self, values: list[str] | None, *, tenant_name: str, tenant_short_name: str) -> list[str]:
normalized: list[str] = []
seen: set[str] = set()
seeds = [tenant_name, tenant_short_name, *(values or [])]
for item in seeds:
alias = self._normalizeOptionalText(item)
if not alias or alias in seen:
continue
if len(alias) > 100:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"租户别名过长: {alias}")
seen.add(alias)
normalized.append(alias)
return normalized
async def _column_exists(self, table_name: str, column_name: str) -> bool:
cache_key = f"{table_name}.{column_name}"
cached = self._table_exists_cache.get(cache_key)
if cached is not None:
return cached
async with GetAsyncSession() as session:
exists = bool(
(
await session.execute(
text(
"""
SELECT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = :table_name
AND column_name = :column_name
)
"""
),
{"table_name": table_name, "column_name": column_name},
)
).scalar_one()
)
self._table_exists_cache[cache_key] = exists
return exists
@staticmethod
def _dumpJson(value: dict[str, Any]) -> str:
import json
return json.dumps(value, ensure_ascii=False)