869 lines
36 KiB
Python
869 lines
36 KiB
Python
"""租户主数据服务实现。"""
|
||
|
||
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)
|