feat: update audit platform workspace
This commit is contained in:
@@ -21,6 +21,7 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.entryModuleDto import (
|
||||
EntryModuleUpdateDTO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.entryModuleAdminVo import (
|
||||
EntryModuleBusinessScopeVO,
|
||||
EntryModuleImageUploadVO,
|
||||
EntryModuleListVO,
|
||||
EntryModuleTenantVO,
|
||||
@@ -31,6 +32,31 @@ from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServ
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolution, TenantResolver
|
||||
|
||||
|
||||
_ALLOWED_MENU_PROFILES = {"document_review", "contract", "govdoc", "cross_checking", "custom"}
|
||||
_ALLOWED_FEATURES = {
|
||||
"home",
|
||||
"documents",
|
||||
"upload",
|
||||
"rules",
|
||||
"rule_groups",
|
||||
"contract_template_search",
|
||||
"contract_template_list",
|
||||
"govdoc_audits",
|
||||
"govdoc_upload",
|
||||
"cross_checking",
|
||||
"cross_checking_upload",
|
||||
"cross_checking_list",
|
||||
"usage_stats",
|
||||
}
|
||||
_DEFAULT_FEATURES_BY_PROFILE = {
|
||||
"document_review": ["home", "documents", "upload", "rules", "rule_groups"],
|
||||
"contract": ["home", "documents", "upload", "rules", "contract_template_search", "contract_template_list"],
|
||||
"govdoc": ["home", "govdoc_audits", "govdoc_upload", "rule_groups"],
|
||||
"cross_checking": ["cross_checking", "cross_checking_upload", "cross_checking_list"],
|
||||
"custom": ["home", "documents"],
|
||||
}
|
||||
|
||||
|
||||
class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
"""入口模块管理服务实现。"""
|
||||
|
||||
@@ -39,6 +65,7 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
self.TenantResolver = TenantResolver()
|
||||
self._tenant_table_exists_cache: bool | None = None
|
||||
self._entry_module_tenant_table_exists_cache: bool | None = None
|
||||
self._entry_module_menu_columns_exist_cache: bool | None = None
|
||||
|
||||
async def ListModules(
|
||||
self,
|
||||
@@ -128,6 +155,8 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
if has_tenant_mapping_table
|
||||
else "'[]'::jsonb AS tenants"
|
||||
)
|
||||
menu_select_sql = await self._entry_module_menu_select_sql()
|
||||
business_scope_select_sql = self._entry_module_business_scope_select_sql()
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
total = int(
|
||||
@@ -154,6 +183,8 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
em.is_enabled,
|
||||
em.created_at,
|
||||
em.updated_at,
|
||||
{menu_select_sql},
|
||||
{business_scope_select_sql},
|
||||
{tenant_select_sql}
|
||||
FROM leaudit_entry_modules em
|
||||
WHERE {where_clause}
|
||||
@@ -182,29 +213,37 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
)
|
||||
self._ensureTenantAssignments(normalized_tenants)
|
||||
legacy_areas_json = self._legacyAreasJson(normalized_tenants)
|
||||
menu_profile = self._normalizeMenuProfile(Body.menu_profile)
|
||||
features = self._normalizeFeatures(Body.features, menu_profile)
|
||||
has_menu_columns = await self._entry_module_menu_columns_exist()
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
try:
|
||||
menu_insert_columns = ", menu_profile, features" if has_menu_columns else ""
|
||||
menu_insert_values = ", :menu_profile, CAST(:features AS jsonb)" if has_menu_columns else ""
|
||||
params = {
|
||||
"name": Body.name.strip(),
|
||||
"description": (Body.description or "").strip() or None,
|
||||
"route_path": route_path,
|
||||
"icon_path": None,
|
||||
"areas": legacy_areas_json,
|
||||
"sort_order": await self._nextSortOrder(session),
|
||||
"menu_profile": menu_profile,
|
||||
"features": json.dumps(features, ensure_ascii=False),
|
||||
}
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
f"""
|
||||
INSERT INTO leaudit_entry_modules (
|
||||
name, description, path, icon_path, areas, sort_order, is_enabled, created_at, updated_at, deleted_at
|
||||
name, description, path, icon_path, areas, sort_order, is_enabled, created_at, updated_at, deleted_at{menu_insert_columns}
|
||||
) VALUES (
|
||||
:name, :description, :route_path, :icon_path, CAST(:areas AS jsonb), :sort_order, TRUE, NOW(), NOW(), NULL
|
||||
:name, :description, :route_path, :icon_path, CAST(:areas AS jsonb), :sort_order, TRUE, NOW(), NOW(), NULL{menu_insert_values}
|
||||
)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"name": Body.name.strip(),
|
||||
"description": (Body.description or "").strip() or None,
|
||||
"route_path": route_path,
|
||||
"icon_path": None,
|
||||
"areas": legacy_areas_json,
|
||||
"sort_order": await self._nextSortOrder(session),
|
||||
},
|
||||
params,
|
||||
)
|
||||
).mappings().one()
|
||||
module_id = int(row["id"])
|
||||
@@ -226,12 +265,30 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
else:
|
||||
normalized_tenants = await self._extractTenantsFromRow(current)
|
||||
self._ensureTenantAssignments(normalized_tenants)
|
||||
current_menu_profile = self._safeMenuProfile(current.get("menu_profile"))
|
||||
menu_profile = self._normalizeMenuProfile(Body.menu_profile) if Body.menu_profile is not None else current_menu_profile
|
||||
features = (
|
||||
self._normalizeFeatures(Body.features, menu_profile)
|
||||
if Body.features is not None
|
||||
else self._parseFeatures(current.get("features"), menu_profile)
|
||||
)
|
||||
has_menu_columns = await self._entry_module_menu_columns_exist()
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
menu_update_sql = ", menu_profile = :menu_profile, features = CAST(:features AS jsonb)" if has_menu_columns else ""
|
||||
params = {
|
||||
"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.get("description"),
|
||||
"route_path": incoming_route_path.strip() if incoming_route_path is not None else current.get("path"),
|
||||
"areas": self._legacyAreasJson(normalized_tenants),
|
||||
"menu_profile": menu_profile,
|
||||
"features": json.dumps(features, ensure_ascii=False),
|
||||
}
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
f"""
|
||||
UPDATE leaudit_entry_modules
|
||||
SET
|
||||
name = :name,
|
||||
@@ -239,18 +296,13 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
path = :route_path,
|
||||
areas = CAST(:areas AS jsonb),
|
||||
updated_at = NOW()
|
||||
{menu_update_sql}
|
||||
WHERE id = :module_id
|
||||
AND deleted_at IS NULL
|
||||
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.get("description"),
|
||||
"route_path": incoming_route_path.strip() if incoming_route_path is not None else current.get("path"),
|
||||
"areas": self._legacyAreasJson(normalized_tenants),
|
||||
},
|
||||
params,
|
||||
)
|
||||
).mappings().first()
|
||||
if not row:
|
||||
@@ -341,6 +393,8 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
if has_tenant_mapping_table
|
||||
else "'[]'::jsonb AS tenants"
|
||||
)
|
||||
menu_select_sql = await self._entry_module_menu_select_sql()
|
||||
business_scope_select_sql = self._entry_module_business_scope_select_sql()
|
||||
async with GetAsyncSession() as session:
|
||||
row = (
|
||||
await session.execute(
|
||||
@@ -357,6 +411,8 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
em.is_enabled,
|
||||
em.created_at,
|
||||
em.updated_at,
|
||||
{menu_select_sql},
|
||||
{business_scope_select_sql},
|
||||
{tenant_select_sql}
|
||||
FROM leaudit_entry_modules em
|
||||
WHERE em.id = :module_id
|
||||
@@ -386,9 +442,12 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
description=Row.get("description"),
|
||||
path=Row.get("icon_path"),
|
||||
route_path=Row.get("path"),
|
||||
menu_profile=self._safeMenuProfile(Row.get("menu_profile")),
|
||||
features=self._parseFeatures(Row.get("features"), Row.get("menu_profile")),
|
||||
sort_order=int(Row.get("sort_order") or 0),
|
||||
is_enabled=bool(Row.get("is_enabled", True)),
|
||||
tenants=tenants,
|
||||
business_scope=self._parseBusinessScope(Row.get("business_scope")),
|
||||
created_at=self._toIso(Row.get("created_at")),
|
||||
updated_at=self._toIso(Row.get("updated_at")),
|
||||
)
|
||||
@@ -568,6 +627,102 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
self._entry_module_tenant_table_exists_cache = exists
|
||||
return exists
|
||||
|
||||
async def _entry_module_menu_columns_exist(self) -> bool:
|
||||
if self._entry_module_menu_columns_exist_cache is not None:
|
||||
return self._entry_module_menu_columns_exist_cache
|
||||
async with GetAsyncSession() as session:
|
||||
count = int(
|
||||
(
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'leaudit_entry_modules'
|
||||
AND column_name IN ('menu_profile', 'features')
|
||||
"""
|
||||
)
|
||||
)
|
||||
).scalar_one()
|
||||
)
|
||||
self._entry_module_menu_columns_exist_cache = count == 2
|
||||
return self._entry_module_menu_columns_exist_cache
|
||||
|
||||
async def _entry_module_menu_select_sql(self) -> str:
|
||||
if await self._entry_module_menu_columns_exist():
|
||||
return "em.menu_profile, em.features"
|
||||
return "'document_review'::varchar AS menu_profile, '[]'::jsonb AS features"
|
||||
|
||||
def _entry_module_business_scope_select_sql(self) -> str:
|
||||
return """
|
||||
COALESCE(
|
||||
(
|
||||
SELECT jsonb_build_object(
|
||||
'category_count', COUNT(DISTINCT dt.id),
|
||||
'business_type_count', GREATEST(
|
||||
COUNT(DISTINCT child_by_type.id),
|
||||
COALESCE(
|
||||
(
|
||||
SELECT COUNT(DISTINCT child_by_entry.id)
|
||||
FROM leaudit_evaluation_point_groups root_by_entry
|
||||
JOIN leaudit_evaluation_point_groups child_by_entry
|
||||
ON child_by_entry.pid = root_by_entry.id
|
||||
AND child_by_entry.deleted_at IS NULL
|
||||
WHERE root_by_entry.entry_module_id = em.id
|
||||
AND root_by_entry.pid = 0
|
||||
AND root_by_entry.deleted_at IS NULL
|
||||
),
|
||||
0
|
||||
)
|
||||
),
|
||||
'categories', COALESCE(
|
||||
(
|
||||
SELECT jsonb_agg(category_name ORDER BY category_name)
|
||||
FROM (
|
||||
SELECT DISTINCT dt2.name AS category_name
|
||||
FROM leaudit_document_types dt2
|
||||
WHERE dt2.entry_module_id = em.id
|
||||
AND dt2.deleted_at IS NULL
|
||||
AND dt2.is_enabled = TRUE
|
||||
) category_rows
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
)
|
||||
FROM leaudit_document_types dt
|
||||
LEFT JOIN leaudit_evaluation_point_groups root
|
||||
ON root.document_type_id = dt.id
|
||||
AND root.pid = 0
|
||||
AND root.deleted_at IS NULL
|
||||
LEFT JOIN leaudit_evaluation_point_groups child_by_type
|
||||
ON child_by_type.pid = root.id
|
||||
AND child_by_type.deleted_at IS NULL
|
||||
WHERE dt.entry_module_id = em.id
|
||||
AND dt.deleted_at IS NULL
|
||||
AND dt.is_enabled = TRUE
|
||||
),
|
||||
jsonb_build_object('category_count', 0, 'business_type_count', 0, 'categories', '[]'::jsonb)
|
||||
) AS business_scope
|
||||
"""
|
||||
|
||||
def _parseBusinessScope(self, RawValue: object) -> EntryModuleBusinessScopeVO:
|
||||
if isinstance(RawValue, str):
|
||||
try:
|
||||
RawValue = json.loads(RawValue)
|
||||
except json.JSONDecodeError:
|
||||
RawValue = {}
|
||||
if not isinstance(RawValue, dict):
|
||||
RawValue = {}
|
||||
|
||||
categories_raw = RawValue.get("categories") or []
|
||||
categories = [str(item).strip() for item in categories_raw if str(item or "").strip()] if isinstance(categories_raw, list) else []
|
||||
return EntryModuleBusinessScopeVO(
|
||||
category_count=int(RawValue.get("category_count") or len(categories)),
|
||||
business_type_count=int(RawValue.get("business_type_count") or 0),
|
||||
categories=categories,
|
||||
)
|
||||
|
||||
async def _resolveLegacyTenantValue(self, *, RawValue: str, Source: str) -> TenantResolution:
|
||||
resolution = await self.TenantResolver.Resolve(
|
||||
RawValue=RawValue,
|
||||
@@ -705,6 +860,65 @@ class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
|
||||
"入口模块至少需要配置一个适用租户",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalizeMenuProfile(MenuProfile: str | None) -> str:
|
||||
value = str(MenuProfile or "document_review").strip() or "document_review"
|
||||
if value not in _ALLOWED_MENU_PROFILES:
|
||||
raise LeauditException(
|
||||
StatusCodeEnum.HTTP_400_BAD_REQUEST,
|
||||
f"不支持的菜单模板: {value}",
|
||||
)
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _safeMenuProfile(MenuProfile: str | None) -> str:
|
||||
value = str(MenuProfile or "document_review").strip() or "document_review"
|
||||
return value if value in _ALLOWED_MENU_PROFILES else "document_review"
|
||||
|
||||
@staticmethod
|
||||
def _normalizeFeatures(Features: list[str] | None, MenuProfile: str) -> list[str]:
|
||||
raw_features = Features if Features else _DEFAULT_FEATURES_BY_PROFILE.get(MenuProfile, _DEFAULT_FEATURES_BY_PROFILE["document_review"])
|
||||
normalized: list[str] = []
|
||||
invalid: list[str] = []
|
||||
for item in raw_features:
|
||||
feature = str(item or "").strip()
|
||||
if not feature:
|
||||
continue
|
||||
if feature not in _ALLOWED_FEATURES:
|
||||
invalid.append(feature)
|
||||
continue
|
||||
if feature not in normalized:
|
||||
normalized.append(feature)
|
||||
if invalid:
|
||||
raise LeauditException(
|
||||
StatusCodeEnum.HTTP_400_BAD_REQUEST,
|
||||
f"不支持的功能编码: {', '.join(invalid)}",
|
||||
)
|
||||
return normalized or list(_DEFAULT_FEATURES_BY_PROFILE.get(MenuProfile, _DEFAULT_FEATURES_BY_PROFILE["document_review"]))
|
||||
|
||||
@classmethod
|
||||
def _parseFeatures(cls, RawFeatures: Any, MenuProfile: str | None) -> list[str]:
|
||||
menu_profile = cls._safeMenuProfile(MenuProfile)
|
||||
if isinstance(RawFeatures, list):
|
||||
return cls._filterFeatures([str(item) for item in RawFeatures], menu_profile)
|
||||
if isinstance(RawFeatures, str) and RawFeatures.strip():
|
||||
try:
|
||||
parsed = json.loads(RawFeatures)
|
||||
except json.JSONDecodeError:
|
||||
parsed = []
|
||||
if isinstance(parsed, list):
|
||||
return cls._filterFeatures([str(item) for item in parsed], menu_profile)
|
||||
return list(_DEFAULT_FEATURES_BY_PROFILE.get(menu_profile, _DEFAULT_FEATURES_BY_PROFILE["document_review"]))
|
||||
|
||||
@staticmethod
|
||||
def _filterFeatures(Features: list[str], MenuProfile: str) -> list[str]:
|
||||
normalized: list[str] = []
|
||||
for item in Features:
|
||||
feature = str(item or "").strip()
|
||||
if feature in _ALLOWED_FEATURES and feature not in normalized:
|
||||
normalized.append(feature)
|
||||
return normalized or list(_DEFAULT_FEATURES_BY_PROFILE.get(MenuProfile, _DEFAULT_FEATURES_BY_PROFILE["document_review"]))
|
||||
|
||||
@staticmethod
|
||||
def _toIso(Value) -> str | None:
|
||||
"""时间转 ISO 字符串。"""
|
||||
|
||||
Reference in New Issue
Block a user