feat: update audit platform workspace
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
@@ -9,12 +13,16 @@ 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.vo.homeVo import (
|
||||
HomeDashboardGrowthVO,
|
||||
HomeDashboardStatisticsVO,
|
||||
HomeEntryAreaVO,
|
||||
HomeEntryDocumentTypeVO,
|
||||
HomeEntryModuleVO,
|
||||
HomeEntryTenantVO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.services import IDocumentService
|
||||
from fastapi_modules.fastapi_leaudit.services.homeService import IHomeService
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.documentServiceImpl import DocumentServiceImpl
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.rbacServiceImpl import RbacServiceImpl
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ssoUserCompat import SsoUserCompat
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolution, TenantResolver
|
||||
@@ -29,17 +37,34 @@ class HomeServiceImpl(IHomeService):
|
||||
"/documents",
|
||||
"/chat-with-llm/chat",
|
||||
"/cross-checking",
|
||||
"/govdoc",
|
||||
"/govdoc/home",
|
||||
"/govdoc/audits",
|
||||
"/govdoc/upload",
|
||||
"/govdoc-audit",
|
||||
"/govdoc-audit/home",
|
||||
"/govdoc-audit/audits",
|
||||
"/govdoc-audit/upload",
|
||||
)
|
||||
_DOCUMENT_ENTRY_TARGETS: tuple[str, ...] = (
|
||||
"/files/upload",
|
||||
"/documents",
|
||||
"/documents/list",
|
||||
)
|
||||
_DEFAULT_FEATURES_BY_PROFILE: dict[str, list[str]] = {
|
||||
"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"],
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, DocumentService: IDocumentService | None = None) -> None:
|
||||
self.RbacService = RbacServiceImpl()
|
||||
self.TenantResolver = TenantResolver()
|
||||
self.DocumentService = DocumentService or DocumentServiceImpl()
|
||||
self._entry_module_tenant_table_exists_cache: bool | None = None
|
||||
self._entry_module_menu_columns_exist_cache: bool | None = None
|
||||
|
||||
async def GetEntryModules(self, UserId: int) -> list[HomeEntryModuleVO]:
|
||||
"""获取当前用户可见的首页入口模块。"""
|
||||
@@ -125,6 +150,9 @@ class HomeServiceImpl(IHomeService):
|
||||
if has_tenant_mapping_table
|
||||
else "'[]'::jsonb AS tenants"
|
||||
)
|
||||
has_menu_columns = await self._entry_module_menu_columns_exist()
|
||||
menu_select_sql = self._entry_module_menu_select_sql(has_menu_columns)
|
||||
menu_group_by_sql = ",\n em.menu_profile,\n em.features" if has_menu_columns else ""
|
||||
tenant_scope_filter_sql = (
|
||||
"""
|
||||
(
|
||||
@@ -195,6 +223,7 @@ class HomeServiceImpl(IHomeService):
|
||||
em.icon_path,
|
||||
em.areas,
|
||||
em.sort_order,
|
||||
{menu_select_sql},
|
||||
{tenant_select_sql},
|
||||
COALESCE(
|
||||
json_agg(
|
||||
@@ -225,6 +254,7 @@ class HomeServiceImpl(IHomeService):
|
||||
em.icon_path,
|
||||
em.areas,
|
||||
em.sort_order
|
||||
{menu_group_by_sql}
|
||||
ORDER BY em.sort_order ASC, em.id ASC
|
||||
"""
|
||||
),
|
||||
@@ -311,6 +341,9 @@ class HomeServiceImpl(IHomeService):
|
||||
description=row["description"],
|
||||
targetPath=target_path,
|
||||
routePath=target_path,
|
||||
menuProfile=self._normalizeMenuProfile(row.get("menu_profile")),
|
||||
features=self._parseFeatures(row.get("features"), row.get("menu_profile")),
|
||||
tenantCode=effective_tenant_code or None,
|
||||
iconPath=row["icon_path"],
|
||||
sortOrder=int(row["sort_order"] or 0),
|
||||
requiresDocumentTypes=requires_document_types,
|
||||
@@ -322,6 +355,95 @@ class HomeServiceImpl(IHomeService):
|
||||
|
||||
return modules
|
||||
|
||||
async def GetDashboardStatistics(
|
||||
self,
|
||||
UserId: int,
|
||||
Today: str | None = None,
|
||||
TypeIds: list[int] | None = None,
|
||||
EntryModuleId: int | None = None,
|
||||
) -> HomeDashboardStatisticsVO:
|
||||
"""获取当前业务入口的首页统计卡片数据。"""
|
||||
today = date.fromisoformat(Today) if Today else date.today()
|
||||
current_month_start = today.replace(day=1)
|
||||
previous_month_end = current_month_start.fromordinal(current_month_start.toordinal() - 1)
|
||||
previous_month_start = previous_month_end.replace(day=1)
|
||||
normalized_type_ids = [int(typeId) for typeId in (TypeIds or []) if int(typeId) > 0]
|
||||
normalized_entry_module_id = int(EntryModuleId) if EntryModuleId and int(EntryModuleId) > 0 else None
|
||||
|
||||
async def load_documents() -> list[Any]:
|
||||
page = 1
|
||||
documents: list[Any] = []
|
||||
while True:
|
||||
result = await self.DocumentService.ListDocuments(
|
||||
CurrentUserId=UserId,
|
||||
Page=page,
|
||||
PageSize=100,
|
||||
TypeIds=normalized_type_ids or None,
|
||||
EntryModuleId=normalized_entry_module_id,
|
||||
)
|
||||
documents.extend(result.documents)
|
||||
total_pages = max(1, int(result.totalPages or 1))
|
||||
if page >= total_pages:
|
||||
return documents
|
||||
page += 1
|
||||
|
||||
def issue_count(Documents: list[Any]) -> int:
|
||||
return sum(max(0, int(document.failedCount or 0)) for document in Documents)
|
||||
|
||||
def pass_rate(Documents: list[Any]) -> int:
|
||||
if not Documents:
|
||||
return 0
|
||||
passed_count = len([document for document in Documents if int(document.failedCount or 0) == 0])
|
||||
return round((passed_count / len(Documents)) * 100)
|
||||
|
||||
def growth(Current: int, Previous: int) -> HomeDashboardGrowthVO:
|
||||
if Previous <= 0:
|
||||
return HomeDashboardGrowthVO(value=100 if Current > 0 else 0, isUp=Current >= Previous)
|
||||
return HomeDashboardGrowthVO(
|
||||
value=round(abs(((Current - Previous) / Previous) * 100)),
|
||||
isUp=Current >= Previous,
|
||||
)
|
||||
|
||||
def document_date(Document: Any) -> date | None:
|
||||
raw_value = str(getattr(Document, "updatedAt", "") or "").strip()
|
||||
if not raw_value:
|
||||
return None
|
||||
normalized = raw_value.replace("Z", "+00:00")
|
||||
try:
|
||||
return datetime.fromisoformat(normalized).date()
|
||||
except ValueError:
|
||||
try:
|
||||
return datetime.strptime(raw_value, "%Y-%m-%d %H:%M:%S").date()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def in_range(Document: Any, DateFrom: date, DateTo: date) -> bool:
|
||||
current_date = document_date(Document)
|
||||
return current_date is not None and DateFrom <= current_date <= DateTo
|
||||
|
||||
documents = await load_documents()
|
||||
today_documents = [document for document in documents if in_range(document, today, today)]
|
||||
current_month_documents = [document for document in documents if in_range(document, current_month_start, today)]
|
||||
previous_month_documents = [
|
||||
document for document in documents if in_range(document, previous_month_start, previous_month_end)
|
||||
]
|
||||
current_reviewed_documents = [document for document in current_month_documents if int(document.auditStatus or 0) == 1]
|
||||
previous_reviewed_documents = [document for document in previous_month_documents if int(document.auditStatus or 0) == 1]
|
||||
current_pass_rate = pass_rate(current_reviewed_documents)
|
||||
previous_pass_rate = pass_rate(previous_reviewed_documents)
|
||||
current_issue_count = issue_count(current_reviewed_documents)
|
||||
previous_issue_count = issue_count(previous_reviewed_documents)
|
||||
|
||||
return HomeDashboardStatisticsVO(
|
||||
todayPendingFiles=len([document for document in today_documents if int(document.auditStatus or 0) != 1]),
|
||||
monthlyReviewedFiles=len(current_reviewed_documents),
|
||||
monthlyReviewGrowth=growth(len(current_reviewed_documents), len(previous_reviewed_documents)),
|
||||
monthlyPassRate=current_pass_rate,
|
||||
passRateGrowth=growth(current_pass_rate, previous_pass_rate),
|
||||
issuesDetected=current_issue_count,
|
||||
issuesGrowth=growth(current_issue_count, previous_issue_count),
|
||||
)
|
||||
|
||||
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
|
||||
@@ -345,6 +467,75 @@ class HomeServiceImpl(IHomeService):
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _entry_module_menu_select_sql(HasMenuColumns: bool) -> str:
|
||||
if HasMenuColumns:
|
||||
return "em.menu_profile, em.features"
|
||||
return "'document_review'::varchar AS menu_profile, '[]'::jsonb AS features"
|
||||
|
||||
@classmethod
|
||||
def _normalizeMenuProfile(cls, MenuProfile: str | None) -> str:
|
||||
value = str(MenuProfile or "document_review").strip() or "document_review"
|
||||
if value not in cls._DEFAULT_FEATURES_BY_PROFILE:
|
||||
return "document_review"
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def _parseFeatures(cls, RawFeatures: Any, MenuProfile: str | None) -> list[str]:
|
||||
menu_profile = cls._normalizeMenuProfile(MenuProfile)
|
||||
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",
|
||||
}
|
||||
if isinstance(RawFeatures, list):
|
||||
parsed = RawFeatures
|
||||
elif isinstance(RawFeatures, str) and RawFeatures.strip():
|
||||
try:
|
||||
parsed = json.loads(RawFeatures)
|
||||
except json.JSONDecodeError:
|
||||
parsed = []
|
||||
else:
|
||||
parsed = []
|
||||
normalized: list[str] = []
|
||||
for item in parsed:
|
||||
feature = str(item or "").strip()
|
||||
if feature in allowed_features and feature not in normalized:
|
||||
normalized.append(feature)
|
||||
return normalized or list(cls._DEFAULT_FEATURES_BY_PROFILE[menu_profile])
|
||||
|
||||
async def _resolveLegacyTenantValue(self, *, RawValue: str, Source: str) -> TenantResolution:
|
||||
resolution = await self.TenantResolver.Resolve(
|
||||
RawValue=RawValue,
|
||||
|
||||
Reference in New Issue
Block a user