feat: add tenant-scoped rule and permission management
This commit is contained in:
@@ -2,25 +2,33 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import bindparam, text
|
||||
|
||||
from fastapi_admin.config import OSS_BASE_URL, OSS_BUCKET
|
||||
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.entryModuleDto import EntryModuleCreateDTO, EntryModuleUpdateDTO
|
||||
from fastapi_modules.fastapi_leaudit.domian.Dto.entryModuleDto import (
|
||||
EntryModuleAreaDTO,
|
||||
EntryModuleCreateDTO,
|
||||
EntryModuleTenantDTO,
|
||||
EntryModuleUpdateDTO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.entryModuleAdminVo import (
|
||||
EntryModuleAreaVO,
|
||||
EntryModuleImageUploadVO,
|
||||
EntryModuleListVO,
|
||||
EntryModuleTenantVO,
|
||||
EntryModuleVO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.services.entryModuleAdminService import IEntryModuleAdminService
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolution, TenantResolver
|
||||
|
||||
|
||||
class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
@@ -28,43 +36,128 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.OssService = OssServiceImpl()
|
||||
self.TenantResolver = TenantResolver()
|
||||
self._tenant_table_exists_cache: bool | None = None
|
||||
self._entry_module_tenant_table_exists_cache: bool | None = None
|
||||
|
||||
async def ListModules(self, Name: str | None, Area: str | None, Page: int, PageSize: int) -> EntryModuleListVO:
|
||||
async def ListModules(
|
||||
self,
|
||||
Name: str | None,
|
||||
Area: str | None,
|
||||
TenantCode: str | None,
|
||||
Page: int,
|
||||
PageSize: int,
|
||||
) -> EntryModuleListVO:
|
||||
"""分页查询入口模块。"""
|
||||
offset = max(Page - 1, 0) * PageSize
|
||||
filters = ["deleted_at IS NULL"]
|
||||
filters = ["em.deleted_at IS NULL"]
|
||||
params: dict[str, object] = {"limit": PageSize, "offset": offset}
|
||||
|
||||
if Name:
|
||||
filters.append("name ILIKE :name")
|
||||
filters.append("em.name ILIKE :name")
|
||||
params["name"] = f"%{Name.strip()}%"
|
||||
|
||||
if Area:
|
||||
filters.append(
|
||||
"EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(areas, '[]'::jsonb)) AS area_item WHERE area_item->>'area' = :area)"
|
||||
)
|
||||
params["area"] = Area.strip()
|
||||
resolved_filter_tenant_code = await self._resolveFilterTenantCode(Area=Area, TenantCode=TenantCode)
|
||||
has_tenant_mapping_table = await self._entry_module_tenant_table_exists()
|
||||
if resolved_filter_tenant_code:
|
||||
legacy_area = (Area or "").strip()
|
||||
if not legacy_area:
|
||||
tenant_name_map = await self._loadTenantNameMap([resolved_filter_tenant_code])
|
||||
legacy_area = tenant_name_map.get(resolved_filter_tenant_code, "")
|
||||
if has_tenant_mapping_table:
|
||||
filters.append(
|
||||
"""
|
||||
(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM leaudit_entry_module_tenants emt
|
||||
WHERE emt.entry_module_id = em.id
|
||||
AND emt.deleted_at IS NULL
|
||||
AND emt.tenant_code = :tenant_code
|
||||
)
|
||||
OR (
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM leaudit_entry_module_tenants emt0
|
||||
WHERE emt0.entry_module_id = em.id
|
||||
AND emt0.deleted_at IS NULL
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(COALESCE(em.areas, '[]'::jsonb)) AS area_item
|
||||
WHERE area_item->>'area' = :legacy_area
|
||||
)
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
params["tenant_code"] = resolved_filter_tenant_code
|
||||
else:
|
||||
filters.append(
|
||||
"""
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(COALESCE(em.areas, '[]'::jsonb)) AS area_item
|
||||
WHERE area_item->>'area' = :legacy_area
|
||||
)
|
||||
"""
|
||||
)
|
||||
params["legacy_area"] = legacy_area
|
||||
|
||||
whereClause = " AND ".join(filters)
|
||||
where_clause = " AND ".join(filters)
|
||||
tenant_select_sql = (
|
||||
"""
|
||||
COALESCE(
|
||||
(
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'tenant_code', emt.tenant_code,
|
||||
'tenant_name', emt.tenant_name,
|
||||
'enabled', emt.is_enabled,
|
||||
'sort_order', emt.sort_order
|
||||
)
|
||||
ORDER BY emt.sort_order ASC, emt.id ASC
|
||||
)
|
||||
FROM leaudit_entry_module_tenants emt
|
||||
WHERE emt.entry_module_id = em.id
|
||||
AND emt.deleted_at IS NULL
|
||||
),
|
||||
'[]'::jsonb
|
||||
) AS tenants
|
||||
"""
|
||||
if has_tenant_mapping_table
|
||||
else "'[]'::jsonb AS tenants"
|
||||
)
|
||||
|
||||
async with GetAsyncSession() as Session:
|
||||
async with GetAsyncSession() as session:
|
||||
total = int(
|
||||
(
|
||||
await Session.execute(
|
||||
text(f"SELECT COUNT(*) FROM leaudit_entry_modules WHERE {whereClause}"),
|
||||
await session.execute(
|
||||
text(f"SELECT COUNT(*) FROM leaudit_entry_modules em WHERE {where_clause}"),
|
||||
params,
|
||||
)
|
||||
).scalar_one()
|
||||
)
|
||||
|
||||
rows = (
|
||||
await Session.execute(
|
||||
await session.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT id, name, description, path, icon_path, areas, sort_order, is_enabled, created_at, updated_at
|
||||
FROM leaudit_entry_modules
|
||||
WHERE {whereClause}
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
SELECT
|
||||
em.id,
|
||||
em.name,
|
||||
em.description,
|
||||
em.path,
|
||||
em.icon_path,
|
||||
em.areas,
|
||||
em.sort_order,
|
||||
em.is_enabled,
|
||||
em.created_at,
|
||||
em.updated_at,
|
||||
{tenant_select_sql}
|
||||
FROM leaudit_entry_modules em
|
||||
WHERE {where_clause}
|
||||
ORDER BY em.sort_order ASC, em.id ASC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
),
|
||||
@@ -72,25 +165,28 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
return EntryModuleListVO(
|
||||
total=total,
|
||||
page=Page,
|
||||
page_size=PageSize,
|
||||
items=[self._toModuleVo(row) for row in rows],
|
||||
)
|
||||
items = [await self._toModuleVo(row) for row in rows]
|
||||
return EntryModuleListVO(total=total, page=Page, page_size=PageSize, items=items)
|
||||
|
||||
async def GetModule(self, ModuleId: int) -> EntryModuleVO:
|
||||
"""获取入口模块详情。"""
|
||||
row = await self._getModuleRow(ModuleId)
|
||||
return self._toModuleVo(row)
|
||||
return await self._toModuleVo(row)
|
||||
|
||||
async def CreateModule(self, Body: EntryModuleCreateDTO) -> EntryModuleVO:
|
||||
"""创建入口模块。"""
|
||||
route_path = (Body.route_path if Body.route_path is not None else Body.path or "").strip() or None
|
||||
async with GetAsyncSession() as Session:
|
||||
normalized_tenants = await self._normalizeTenants(
|
||||
Tenants=Body.tenants,
|
||||
Areas=Body.areas,
|
||||
)
|
||||
self._ensureTenantAssignments(normalized_tenants)
|
||||
legacy_areas_json = self._legacyAreasJson(normalized_tenants)
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
try:
|
||||
row = (
|
||||
await Session.execute(
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_entry_modules (
|
||||
@@ -98,7 +194,7 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
) VALUES (
|
||||
:name, :description, :route_path, :icon_path, CAST(:areas AS jsonb), :sort_order, TRUE, NOW(), NOW(), NULL
|
||||
)
|
||||
RETURNING id, name, description, path, icon_path, areas, sort_order, is_enabled, created_at, updated_at
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
@@ -106,24 +202,34 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
"description": (Body.description or "").strip() or None,
|
||||
"route_path": route_path,
|
||||
"icon_path": None,
|
||||
"areas": self._areasJson(Body.areas),
|
||||
"sort_order": await self._nextSortOrder(Session),
|
||||
"areas": legacy_areas_json,
|
||||
"sort_order": await self._nextSortOrder(session),
|
||||
},
|
||||
)
|
||||
).mappings().one()
|
||||
await Session.commit()
|
||||
module_id = int(row["id"])
|
||||
await self._syncModuleTenants(session, module_id, normalized_tenants)
|
||||
await session.commit()
|
||||
except Exception as exc:
|
||||
await Session.rollback()
|
||||
await session.rollback()
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"创建入口模块失败: {exc}") from exc
|
||||
return self._toModuleVo(row)
|
||||
|
||||
return await self.GetModule(module_id)
|
||||
|
||||
async def UpdateModule(self, ModuleId: int, Body: EntryModuleUpdateDTO) -> EntryModuleVO:
|
||||
"""更新入口模块。"""
|
||||
current = await self._getModuleRow(ModuleId)
|
||||
incoming_route_path = Body.route_path if Body.route_path is not None else Body.path
|
||||
async with GetAsyncSession() as Session:
|
||||
|
||||
if Body.tenants is not None or Body.areas is not None:
|
||||
normalized_tenants = await self._normalizeTenants(Tenants=Body.tenants, Areas=Body.areas)
|
||||
else:
|
||||
normalized_tenants = await self._extractTenantsFromRow(current)
|
||||
self._ensureTenantAssignments(normalized_tenants)
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
row = (
|
||||
await Session.execute(
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_entry_modules
|
||||
@@ -135,28 +241,31 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
updated_at = NOW()
|
||||
WHERE id = :module_id
|
||||
AND deleted_at IS NULL
|
||||
RETURNING id, name, description, path, icon_path, areas, sort_order, is_enabled, created_at, updated_at
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"module_id": ModuleId,
|
||||
"name": Body.name.strip() if Body.name is not None else current["name"],
|
||||
"description": (Body.description.strip() if Body.description is not None else current["description"]),
|
||||
"route_path": (incoming_route_path.strip() if incoming_route_path is not None else current["path"]),
|
||||
"areas": self._areasJson(Body.areas) if Body.areas is not None else self._areasJson(current["areas"]),
|
||||
"description": Body.description.strip() if Body.description is not None else current.get("description"),
|
||||
"route_path": incoming_route_path.strip() if incoming_route_path is not None else current.get("path"),
|
||||
"areas": self._legacyAreasJson(normalized_tenants),
|
||||
},
|
||||
)
|
||||
).mappings().first()
|
||||
if not row:
|
||||
await Session.rollback()
|
||||
await session.rollback()
|
||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "入口模块不存在")
|
||||
await Session.commit()
|
||||
return self._toModuleVo(row)
|
||||
|
||||
await self._syncModuleTenants(session, ModuleId, normalized_tenants)
|
||||
await session.commit()
|
||||
|
||||
return await self.GetModule(ModuleId)
|
||||
|
||||
async def DeleteModule(self, ModuleId: int) -> None:
|
||||
"""删除入口模块。"""
|
||||
async with GetAsyncSession() as Session:
|
||||
await Session.execute(
|
||||
async with GetAsyncSession() as session:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_entry_modules
|
||||
@@ -166,17 +275,28 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
),
|
||||
{"module_id": ModuleId},
|
||||
)
|
||||
await Session.commit()
|
||||
if await self._entry_module_tenant_table_exists():
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_entry_module_tenants
|
||||
SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE entry_module_id = :module_id AND deleted_at IS NULL
|
||||
"""
|
||||
),
|
||||
{"module_id": ModuleId},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
async def UploadModuleImage(self, ModuleId: int, FileName: str, ContentType: str, Content: bytes) -> EntryModuleImageUploadVO:
|
||||
"""上传入口模块图标。"""
|
||||
module = await self._getModuleRow(ModuleId)
|
||||
suffix = Path(FileName).suffix.lower() or ".png"
|
||||
objectKey = f"documents/mz/static/img/entry_module_{ModuleId}{suffix}"
|
||||
await self.OssService.UploadBytes(ObjectKey=objectKey, Content=Content, ContentType=ContentType or "application/octet-stream")
|
||||
object_key = f"documents/mz/static/img/entry_module_{ModuleId}{suffix}"
|
||||
await self.OssService.UploadBytes(ObjectKey=object_key, Content=Content, ContentType=ContentType or "application/octet-stream")
|
||||
|
||||
async with GetAsyncSession() as Session:
|
||||
await Session.execute(
|
||||
async with GetAsyncSession() as session:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_entry_modules
|
||||
@@ -184,27 +304,63 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
WHERE id = :module_id AND deleted_at IS NULL
|
||||
"""
|
||||
),
|
||||
{"module_id": ModuleId, "icon_path": objectKey},
|
||||
{"module_id": ModuleId, "icon_path": object_key},
|
||||
)
|
||||
await Session.commit()
|
||||
await session.commit()
|
||||
|
||||
return EntryModuleImageUploadVO(
|
||||
module_id=ModuleId,
|
||||
path=objectKey,
|
||||
url=f"{OSS_BASE_URL.rstrip('/')}/{OSS_BUCKET}/{objectKey}",
|
||||
path=object_key,
|
||||
url=f"{OSS_BASE_URL.rstrip('/')}/{OSS_BUCKET}/{object_key}",
|
||||
message=f"入口模块 {module['name']} 图标上传成功",
|
||||
)
|
||||
|
||||
async def _getModuleRow(self, ModuleId: int):
|
||||
"""查询入口模块原始记录。"""
|
||||
async with GetAsyncSession() as Session:
|
||||
has_tenant_mapping_table = await self._entry_module_tenant_table_exists()
|
||||
tenant_select_sql = (
|
||||
"""
|
||||
COALESCE(
|
||||
(
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'tenant_code', emt.tenant_code,
|
||||
'tenant_name', emt.tenant_name,
|
||||
'enabled', emt.is_enabled,
|
||||
'sort_order', emt.sort_order
|
||||
)
|
||||
ORDER BY emt.sort_order ASC, emt.id ASC
|
||||
)
|
||||
FROM leaudit_entry_module_tenants emt
|
||||
WHERE emt.entry_module_id = em.id
|
||||
AND emt.deleted_at IS NULL
|
||||
),
|
||||
'[]'::jsonb
|
||||
) AS tenants
|
||||
"""
|
||||
if has_tenant_mapping_table
|
||||
else "'[]'::jsonb AS tenants"
|
||||
)
|
||||
async with GetAsyncSession() as session:
|
||||
row = (
|
||||
await Session.execute(
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id, name, description, path, icon_path, areas, sort_order, is_enabled, created_at, updated_at
|
||||
FROM leaudit_entry_modules
|
||||
WHERE id = :module_id AND deleted_at IS NULL
|
||||
f"""
|
||||
SELECT
|
||||
em.id,
|
||||
em.name,
|
||||
em.description,
|
||||
em.path,
|
||||
em.icon_path,
|
||||
em.areas,
|
||||
em.sort_order,
|
||||
em.is_enabled,
|
||||
em.created_at,
|
||||
em.updated_at,
|
||||
{tenant_select_sql}
|
||||
FROM leaudit_entry_modules em
|
||||
WHERE em.id = :module_id
|
||||
AND em.deleted_at IS NULL
|
||||
"""
|
||||
),
|
||||
{"module_id": ModuleId},
|
||||
@@ -216,24 +372,14 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
|
||||
async def _nextSortOrder(self, Session) -> int:
|
||||
"""获取下一个排序号。"""
|
||||
maxSort = (
|
||||
max_sort = (
|
||||
await Session.execute(text("SELECT COALESCE(MAX(sort_order), 0) FROM leaudit_entry_modules WHERE deleted_at IS NULL"))
|
||||
).scalar_one()
|
||||
return int(maxSort or 0) + 10
|
||||
return int(max_sort or 0) + 10
|
||||
|
||||
def _toModuleVo(self, Row) -> EntryModuleVO:
|
||||
async def _toModuleVo(self, Row) -> EntryModuleVO:
|
||||
"""把数据库记录转换为 VO。"""
|
||||
rawAreas = Row.get("areas") or []
|
||||
areas = [
|
||||
EntryModuleAreaVO(
|
||||
area=str(item.get("area") or ""),
|
||||
enabled=bool(item.get("enabled", False)),
|
||||
sort_order=int(item.get("sort_order", 0)),
|
||||
)
|
||||
for item in rawAreas
|
||||
if isinstance(item, dict) and item.get("area")
|
||||
]
|
||||
areas.sort(key=lambda item: (item.sort_order, item.area))
|
||||
tenants = await self._extractTenantsFromRow(Row)
|
||||
return EntryModuleVO(
|
||||
id=int(Row["id"]),
|
||||
name=str(Row["name"] or ""),
|
||||
@@ -242,38 +388,325 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
route_path=Row.get("path"),
|
||||
sort_order=int(Row.get("sort_order") or 0),
|
||||
is_enabled=bool(Row.get("is_enabled", True)),
|
||||
areas=areas,
|
||||
tenants=tenants,
|
||||
created_at=self._toIso(Row.get("created_at")),
|
||||
updated_at=self._toIso(Row.get("updated_at")),
|
||||
)
|
||||
|
||||
def _areasJson(self, Areas) -> str:
|
||||
"""序列化地区配置。"""
|
||||
import json
|
||||
async def _extractTenantsFromRow(self, Row) -> list[EntryModuleTenantVO]:
|
||||
raw_tenants = Row.get("tenants") or []
|
||||
normalized: list[EntryModuleTenantVO] = []
|
||||
if isinstance(raw_tenants, list) and raw_tenants:
|
||||
for item in raw_tenants:
|
||||
if not isinstance(item, dict) or not item.get("tenant_code"):
|
||||
continue
|
||||
normalized.append(
|
||||
EntryModuleTenantVO(
|
||||
tenant_code=str(item["tenant_code"]),
|
||||
tenant_name=item.get("tenant_name"),
|
||||
enabled=bool(item.get("enabled", True)),
|
||||
sort_order=int(item.get("sort_order", 0)),
|
||||
)
|
||||
)
|
||||
normalized.sort(key=lambda item: (item.sort_order, item.tenant_code))
|
||||
if normalized:
|
||||
return normalized
|
||||
|
||||
if not Areas:
|
||||
return "[]"
|
||||
raw_areas = Row.get("areas") or []
|
||||
if not isinstance(raw_areas, list):
|
||||
return []
|
||||
|
||||
normalized: list[dict[str, object]] = []
|
||||
for index, item in enumerate(Areas, start=1):
|
||||
if hasattr(item, "model_dump"):
|
||||
payload = item.model_dump()
|
||||
elif isinstance(item, dict):
|
||||
payload = item
|
||||
else:
|
||||
fallback_tenants: list[EntryModuleTenantVO] = []
|
||||
for index, item in enumerate(raw_areas, start=1):
|
||||
if not isinstance(item, dict) or not item.get("area"):
|
||||
continue
|
||||
if not payload.get("area"):
|
||||
continue
|
||||
normalized.append(
|
||||
{
|
||||
"area": str(payload["area"]),
|
||||
"enabled": bool(payload.get("enabled", True)),
|
||||
"sort_order": int(payload.get("sort_order", index)),
|
||||
}
|
||||
resolution = await self._resolveLegacyTenantValue(
|
||||
RawValue=str(item.get("area") or ""),
|
||||
Source="entry_module_legacy_area",
|
||||
)
|
||||
fallback_tenants.append(
|
||||
EntryModuleTenantVO(
|
||||
tenant_code=resolution.tenant_code,
|
||||
tenant_name=resolution.tenant_name,
|
||||
enabled=bool(item.get("enabled", True)),
|
||||
sort_order=int(item.get("sort_order", index)),
|
||||
)
|
||||
)
|
||||
return json.dumps(normalized, ensure_ascii=False)
|
||||
|
||||
def _toIso(self, Value) -> str | None:
|
||||
unique: dict[str, EntryModuleTenantVO] = {}
|
||||
for item in fallback_tenants:
|
||||
unique[item.tenant_code] = item
|
||||
return sorted(unique.values(), key=lambda item: (item.sort_order, item.tenant_code))
|
||||
|
||||
async def _normalizeTenants(
|
||||
self,
|
||||
*,
|
||||
Tenants: list[EntryModuleTenantDTO] | None,
|
||||
Areas: list[EntryModuleAreaDTO] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if Tenants is not None:
|
||||
return await self._normalizeTenantDtos(Tenants)
|
||||
if Areas is not None:
|
||||
# 仅兼容旧请求体,优先要求前端使用 tenants。
|
||||
return await self._normalizeAreas(Areas)
|
||||
return []
|
||||
|
||||
async def _normalizeTenantDtos(self, Tenants: list[EntryModuleTenantDTO]) -> list[dict[str, Any]]:
|
||||
if not Tenants:
|
||||
return []
|
||||
|
||||
tenant_codes = [str(item.tenant_code).strip() for item in Tenants if str(item.tenant_code).strip()]
|
||||
tenant_name_map = await self._loadTenantNameMap(tenant_codes)
|
||||
|
||||
normalized: dict[str, dict[str, Any]] = {}
|
||||
for index, item in enumerate(Tenants, start=1):
|
||||
tenant_code = str(item.tenant_code).strip()
|
||||
if not tenant_code:
|
||||
continue
|
||||
if tenant_code not in tenant_name_map:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"未知租户编码: {tenant_code}")
|
||||
normalized[tenant_code] = {
|
||||
"tenant_code": tenant_code,
|
||||
"tenant_name": str(item.tenant_name or tenant_name_map[tenant_code] or "").strip() or tenant_name_map[tenant_code],
|
||||
"enabled": bool(item.enabled),
|
||||
"sort_order": int(item.sort_order or index),
|
||||
}
|
||||
return sorted(normalized.values(), key=lambda item: (int(item["sort_order"]), str(item["tenant_code"])))
|
||||
|
||||
async def _normalizeAreas(self, Areas: list[EntryModuleAreaDTO]) -> list[dict[str, Any]]:
|
||||
if not Areas:
|
||||
return []
|
||||
|
||||
normalized: dict[str, dict[str, Any]] = {}
|
||||
for index, item in enumerate(Areas, start=1):
|
||||
area_name = str(item.area or "").strip()
|
||||
if not area_name:
|
||||
continue
|
||||
resolution = await self._resolveLegacyTenantValue(
|
||||
RawValue=area_name,
|
||||
Source="entry_module_area_payload",
|
||||
)
|
||||
normalized[resolution.tenant_code] = {
|
||||
"tenant_code": resolution.tenant_code,
|
||||
"tenant_name": resolution.tenant_name or area_name,
|
||||
"enabled": bool(item.enabled),
|
||||
"sort_order": int(item.sort_order or index),
|
||||
}
|
||||
return sorted(normalized.values(), key=lambda item: (int(item["sort_order"]), str(item["tenant_code"])))
|
||||
|
||||
async def _loadTenantNameMap(self, TenantCodes: list[str]) -> dict[str, str]:
|
||||
tenant_codes = [code for code in {str(item).strip() for item in TenantCodes} if code]
|
||||
if not tenant_codes:
|
||||
return {}
|
||||
if not await self._tenant_table_exists():
|
||||
return {
|
||||
code: ("公共" if code == "PUBLIC" else "省局" if code == "PROVINCIAL" else code)
|
||||
for code in tenant_codes
|
||||
}
|
||||
async with GetAsyncSession() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT tenant_code, tenant_name
|
||||
FROM sys_tenants
|
||||
WHERE tenant_code IN :tenant_codes
|
||||
AND deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
"""
|
||||
).bindparams(bindparam("tenant_codes", expanding=True)),
|
||||
{"tenant_codes": tenant_codes},
|
||||
)
|
||||
).mappings().all()
|
||||
return {str(row["tenant_code"]): str(row["tenant_name"] or "") for row in rows}
|
||||
|
||||
async def _tenant_table_exists(self) -> bool:
|
||||
"""兼容旧环境未落 sys_tenants 时的入口模块读写。"""
|
||||
if self._tenant_table_exists_cache is not None:
|
||||
return self._tenant_table_exists_cache
|
||||
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 = 'sys_tenants'
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
).scalar_one()
|
||||
)
|
||||
self._tenant_table_exists_cache = exists
|
||||
return exists
|
||||
|
||||
async def _entry_module_tenant_table_exists(self) -> bool:
|
||||
"""兼容旧环境未落入口模块租户映射表时的读写。"""
|
||||
if self._entry_module_tenant_table_exists_cache is not None:
|
||||
return self._entry_module_tenant_table_exists_cache
|
||||
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 = 'leaudit_entry_module_tenants'
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
).scalar_one()
|
||||
)
|
||||
self._entry_module_tenant_table_exists_cache = exists
|
||||
return exists
|
||||
|
||||
async def _resolveLegacyTenantValue(self, *, RawValue: str, Source: str) -> TenantResolution:
|
||||
resolution = await self.TenantResolver.Resolve(
|
||||
RawValue=RawValue,
|
||||
Source=Source,
|
||||
)
|
||||
if resolution.tenant_code:
|
||||
return resolution
|
||||
|
||||
normalized = str(RawValue or "").strip()
|
||||
if normalized in {"", "default", "省级", "公共"}:
|
||||
public_resolution = await self.TenantResolver.Resolve(
|
||||
RawValue="",
|
||||
Source=Source,
|
||||
)
|
||||
if public_resolution.tenant_code:
|
||||
return public_resolution
|
||||
return TenantResolution(
|
||||
tenant_code="PUBLIC",
|
||||
tenant_name="公共",
|
||||
tenant_type="PUBLIC",
|
||||
raw_value=RawValue,
|
||||
normalized_value=normalized,
|
||||
source=Source,
|
||||
is_public=True,
|
||||
)
|
||||
if normalized == "省局":
|
||||
return TenantResolution(
|
||||
tenant_code="PROVINCIAL",
|
||||
tenant_name="省局",
|
||||
tenant_type="PROVINCIAL",
|
||||
raw_value=RawValue,
|
||||
normalized_value=normalized,
|
||||
source=Source,
|
||||
is_public=False,
|
||||
)
|
||||
return TenantResolution(
|
||||
tenant_code=normalized or None,
|
||||
tenant_name=normalized or None,
|
||||
tenant_type="LEGACY_AREA" if normalized else None,
|
||||
raw_value=RawValue,
|
||||
normalized_value=normalized,
|
||||
source=Source,
|
||||
is_public=False,
|
||||
)
|
||||
|
||||
async def _syncModuleTenants(self, Session, ModuleId: int, Tenants: list[dict[str, Any]]) -> None:
|
||||
if not await self._entry_module_tenant_table_exists():
|
||||
return
|
||||
tenant_codes = [str(item["tenant_code"]) for item in Tenants if item.get("tenant_code")]
|
||||
|
||||
if tenant_codes:
|
||||
await Session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_entry_module_tenants
|
||||
SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE entry_module_id = :module_id
|
||||
AND deleted_at IS NULL
|
||||
AND tenant_code NOT IN :tenant_codes
|
||||
"""
|
||||
).bindparams(bindparam("tenant_codes", expanding=True)),
|
||||
{"module_id": ModuleId, "tenant_codes": tenant_codes},
|
||||
)
|
||||
else:
|
||||
await Session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_entry_module_tenants
|
||||
SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE entry_module_id = :module_id
|
||||
AND deleted_at IS NULL
|
||||
"""
|
||||
),
|
||||
{"module_id": ModuleId},
|
||||
)
|
||||
|
||||
for item in Tenants:
|
||||
await Session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_entry_module_tenants (
|
||||
entry_module_id, tenant_code, tenant_name, is_enabled, sort_order, created_at, updated_at, deleted_at
|
||||
) VALUES (
|
||||
:module_id, :tenant_code, :tenant_name, :is_enabled, :sort_order, NOW(), NOW(), NULL
|
||||
)
|
||||
ON CONFLICT (entry_module_id, tenant_code)
|
||||
DO UPDATE SET
|
||||
tenant_name = EXCLUDED.tenant_name,
|
||||
is_enabled = EXCLUDED.is_enabled,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
updated_at = NOW(),
|
||||
deleted_at = NULL
|
||||
"""
|
||||
),
|
||||
{
|
||||
"module_id": ModuleId,
|
||||
"tenant_code": item["tenant_code"],
|
||||
"tenant_name": item.get("tenant_name"),
|
||||
"is_enabled": bool(item.get("enabled", True)),
|
||||
"sort_order": int(item.get("sort_order", 0)),
|
||||
},
|
||||
)
|
||||
|
||||
async def _resolveFilterTenantCode(self, *, Area: str | None, TenantCode: str | None) -> str | None:
|
||||
if TenantCode and TenantCode.strip():
|
||||
return TenantCode.strip()
|
||||
if Area and Area.strip():
|
||||
resolution = await self._resolveLegacyTenantValue(
|
||||
RawValue=Area.strip(),
|
||||
Source="entry_module_filter_area",
|
||||
)
|
||||
return resolution.tenant_code
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _legacyAreasJson(Tenants: list[dict[str, Any]]) -> str:
|
||||
# 继续回写 areas 仅用于兼容旧读链路,主配置来源是 leaudit_entry_module_tenants。
|
||||
areas = [
|
||||
{
|
||||
"area": str(item.get("tenant_name") or item.get("tenant_code") or ""),
|
||||
"enabled": bool(item.get("enabled", True)),
|
||||
"sort_order": int(item.get("sort_order", 0)),
|
||||
}
|
||||
for item in Tenants
|
||||
if item.get("tenant_code")
|
||||
]
|
||||
return json.dumps(areas, ensure_ascii=False)
|
||||
|
||||
@staticmethod
|
||||
def _ensureTenantAssignments(Tenants: list[dict[str, Any]]) -> None:
|
||||
if Tenants:
|
||||
return
|
||||
raise LeauditException(
|
||||
StatusCodeEnum.HTTP_400_BAD_REQUEST,
|
||||
"入口模块至少需要配置一个适用租户",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _toIso(Value) -> str | None:
|
||||
"""时间转 ISO 字符串。"""
|
||||
if Value is None:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user