from __future__ import annotations import json import os from datetime import datetime from decimal import Decimal from typing import Any from urllib.parse import quote_plus from sqlalchemy import bindparam, text from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from fastapi_admin.config import DB_HOST, DB_PASSWORD, DB_PORT, DB_USER 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.evaluationPointDto import ( EvaluationPointCreateDTO, EvaluationPointUpdateDTO, ) from fastapi_modules.fastapi_leaudit.domian.vo.evaluationPointVo import ( AttributeTypeListVO, AttributeTypeVO, EvaluationPointDeleteVO, EvaluationPointListVO, EvaluationPointVO, ) from fastapi_modules.fastapi_leaudit.services.evaluationPointService import IEvaluationPointService from fastapi_modules.fastapi_leaudit.services.impl.permissionServiceImpl import PermissionServiceImpl from fastapi_modules.fastapi_leaudit.services.impl.ssoUserCompat import SsoUserCompat from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolver _LEGACY_DB_NAME = os.getenv("LEGACY_RULE_DB_NAME", "docauditai") _LEGACY_DB_URL = ( f"postgresql+asyncpg://{quote_plus(str(DB_USER))}:{quote_plus(str(DB_PASSWORD))}" f"@{DB_HOST}:{DB_PORT}/{quote_plus(_LEGACY_DB_NAME)}" ) _LEGACY_ENGINE = create_async_engine(_LEGACY_DB_URL, pool_pre_ping=True) _LegacySession = async_sessionmaker(_LEGACY_ENGINE, expire_on_commit=False) class EvaluationPointServiceImpl(IEvaluationPointService): """评查点服务实现。""" _WRITE_PERMISSION_KEYS: tuple[str, ...] = ( "evaluation_point:create:write", "evaluation_point:update:write", "evaluation_point:delete:delete", ) def __init__(self) -> None: self.PermissionService = PermissionServiceImpl() self.TenantResolver = TenantResolver() self._legacy_columns_cache: dict[str, set[str]] = {} async def ListPoints( self, CurrentUserId: int, UserArea: str | None, UserRole: str | None, CurrentTenantCode: str | None, CurrentTenantName: str | None, Name: str | None, Code: str | None, Risk: str | None, IsEnabled: bool | None, GroupPid: int | None, GroupId: int | None, DocumentAttributeType: str | None, FilterArea: str | None, FilterTenantCode: str | None, FilterTenantName: str | None, Page: int, PageSize: int, ) -> EvaluationPointListVO: offset = max(Page - 1, 0) * PageSize current_user = await self._get_current_user_context( CurrentUserId=CurrentUserId, UserArea=UserArea, UserRole=UserRole, TenantCode=CurrentTenantCode, TenantName=CurrentTenantName, ) requested_scope = await self._resolve_requested_scope( Area=FilterArea, TenantCode=FilterTenantCode, TenantName=FilterTenantName, Source="evaluation_point_list", ) where_clause, params = self._build_list_filters( Name=Name, Code=Code, Risk=Risk, IsEnabled=IsEnabled, GroupPid=GroupPid, GroupId=GroupId, DocumentAttributeType=DocumentAttributeType, RequestedScope=requested_scope, CurrentUser=current_user, EvaluationPointColumns=await self._load_legacy_table_columns("evaluation_points"), ) params.update({"limit": PageSize, "offset": offset}) async with _LegacySession() as session: evaluation_point_columns = await self._load_legacy_table_columns("evaluation_points", Session=session) tenant_code_select = self._evaluation_point_tenant_code_select(evaluation_point_columns, alias="ep") tenant_name_select = self._evaluation_point_tenant_name_select(evaluation_point_columns, alias="ep") count_sql = text( f""" SELECT COUNT(*) FROM evaluation_points ep LEFT JOIN evaluation_point_groups child_group ON child_group.id = ep.evaluation_point_groups_id LEFT JOIN evaluation_point_groups parent_group ON parent_group.id = COALESCE(ep.evaluation_point_groups_pid, child_group.pid) WHERE {where_clause} """ ) list_sql = text( f""" SELECT ep.id, ep.code, ep.name, ep.evaluation_point_groups_id, ep.evaluation_point_groups_pid, ep.risk, ep.description, ep.is_enabled, ep.document_attribute_type, ep.references_laws, ep.extraction_config, ep.evaluation_config, ep.pass_message, ep.fail_message, ep.suggestion_message, ep.suggestion_message_type, ep.post_action, ep.action_config, ep.score, ep.area, {tenant_code_select}, {tenant_name_select}, ep.created_at, ep.updated_at, child_group.name AS group_name, parent_group.name AS rule_type FROM evaluation_points ep LEFT JOIN evaluation_point_groups child_group ON child_group.id = ep.evaluation_point_groups_id LEFT JOIN evaluation_point_groups parent_group ON parent_group.id = COALESCE(ep.evaluation_point_groups_pid, child_group.pid) WHERE {where_clause} ORDER BY COALESCE(ep.sort, 0) ASC, ep.updated_at DESC, ep.id DESC LIMIT :limit OFFSET :offset """ ) total = int( ( await session.execute( count_sql, params, ) ).scalar_one() ) rows = ( await session.execute( list_sql, params, ) ).mappings().all() return EvaluationPointListVO( data=[await self._to_point_vo(row) for row in rows], total=total, page=Page, page_size=PageSize, ) async def GetPoint( self, CurrentUserId: int, UserArea: str | None, UserRole: str | None, TenantCode: str | None, TenantName: str | None, PointId: int, ) -> EvaluationPointVO: current_user = await self._get_current_user_context( CurrentUserId=CurrentUserId, UserArea=UserArea, UserRole=UserRole, TenantCode=TenantCode, TenantName=TenantName, ) row = await self._get_point_row(PointId=PointId, CurrentUser=current_user) if not row: raise LeauditException(StatusCodeEnum.NOT_FOUND, "评查点不存在") return await self._to_point_vo(row) async def CreatePoint( self, CurrentUserId: int, UserArea: str | None, UserRole: str | None, TenantCode: str | None, TenantName: str | None, Body: EvaluationPointCreateDTO, ) -> EvaluationPointVO: current_user = await self._get_current_user_context( CurrentUserId=CurrentUserId, UserArea=UserArea, UserRole=UserRole, TenantCode=TenantCode, TenantName=TenantName, ) await self._validate_group_relation(Body.evaluation_point_groups_pid, Body.evaluation_point_groups_id) await self._ensure_code_unique(str(Body.code).strip()) now = datetime.utcnow() insert_params = self._build_write_params(Body, now) requested_scope = await self._resolve_requested_scope( Area=Body.area, TenantCode=Body.tenant_code, TenantName=Body.tenant_name, Source="evaluation_point_create", ) writable_scope = self._resolve_writable_scope( current_user, RequestedScope=requested_scope, ) insert_params["area"] = writable_scope["area"] insert_params["created_at"] = now insert_params["updated_at"] = now async with _LegacySession() as session: evaluation_point_columns = await self._load_legacy_table_columns("evaluation_points", Session=session) insert_fields = [ "code", "name", "evaluation_point_groups_id", "evaluation_point_groups_pid", "risk", "description", "is_enabled", "document_attribute_type", "references_laws", "extraction_config", "evaluation_config", "pass_message", "fail_message", "suggestion_message", "suggestion_message_type", "post_action", "action_config", "score", "area", "created_at", "updated_at", ] insert_values = [ ":code", ":name", ":evaluation_point_groups_id", ":evaluation_point_groups_pid", ":risk", ":description", ":is_enabled", ":document_attribute_type", "CAST(:references_laws AS jsonb)", "CAST(:extraction_config AS jsonb)", "CAST(:evaluation_config AS jsonb)", ":pass_message", ":fail_message", ":suggestion_message", ":suggestion_message_type", ":post_action", ":action_config", ":score", ":area", ":created_at", ":updated_at", ] if "tenant_code" in evaluation_point_columns: insert_params["tenant_code"] = writable_scope["tenant_code"] insert_fields.append("tenant_code") insert_values.append(":tenant_code") if "tenant_name" in evaluation_point_columns: insert_params["tenant_name"] = writable_scope["tenant_name"] insert_fields.append("tenant_name") insert_values.append(":tenant_name") async with session.begin(): new_id = await session.scalar( text( f""" INSERT INTO evaluation_points ( {", ".join(insert_fields)} ) VALUES ( {", ".join(insert_values)} ) RETURNING id """ ), insert_params, ) await session.commit() return await self.GetPoint(CurrentUserId, UserArea, UserRole, TenantCode, TenantName, int(new_id)) async def UpdatePoint( self, CurrentUserId: int, UserArea: str | None, UserRole: str | None, TenantCode: str | None, TenantName: str | None, PointId: int, Body: EvaluationPointUpdateDTO, ) -> EvaluationPointVO: current_user = await self._get_current_user_context( CurrentUserId=CurrentUserId, UserArea=UserArea, UserRole=UserRole, TenantCode=TenantCode, TenantName=TenantName, ) await self.GetPoint(CurrentUserId, UserArea, UserRole, TenantCode, TenantName, PointId) payload = Body.model_dump(exclude_unset=True) if not payload: return await self.GetPoint(CurrentUserId, UserArea, UserRole, TenantCode, TenantName, PointId) group_pid = payload.get("evaluation_point_groups_pid") group_id = payload.get("evaluation_point_groups_id") if group_pid is not None or group_id is not None: current = await self.GetPoint(CurrentUserId, UserArea, UserRole, TenantCode, TenantName, PointId) await self._validate_group_relation( group_pid if group_pid is not None else current.evaluation_point_groups_pid, group_id if group_id is not None else current.evaluation_point_groups_id, ) if "code" in payload and payload["code"]: await self._ensure_code_unique(str(payload["code"]).strip(), PointId) updates: list[str] = [] params: dict[str, Any] = {"point_id": PointId, "updated_at": datetime.utcnow()} simple_fields = [ "code", "name", "evaluation_point_groups_id", "evaluation_point_groups_pid", "risk", "description", "is_enabled", "document_attribute_type", "pass_message", "fail_message", "suggestion_message", "suggestion_message_type", "post_action", "action_config", "score", ] json_fields = ["references_laws", "extraction_config", "evaluation_config"] for field in simple_fields: if field not in payload: continue params[field] = self._normalize_scalar_field(field, payload[field]) updates.append(f"{field} = :{field}") if any(key in payload for key in ("area", "tenant_code", "tenant_name")): requested_scope = await self._resolve_requested_scope( Area=payload.get("area"), TenantCode=payload.get("tenant_code"), TenantName=payload.get("tenant_name"), Source="evaluation_point_update", ) writable_scope = self._resolve_writable_scope( current_user, RequestedScope=requested_scope, ) params["area"] = writable_scope["area"] if "area = :area" not in updates: updates.append("area = :area") for field in json_fields: if field not in payload: continue params[field] = json.dumps(payload[field] if payload[field] is not None else self._default_json(field), ensure_ascii=False) updates.append(f"{field} = CAST(:{field} AS jsonb)") updates.append("updated_at = :updated_at") async with _LegacySession() as session: evaluation_point_columns = await self._load_legacy_table_columns("evaluation_points", Session=session) if "tenant_code" in evaluation_point_columns and "tenant_code" not in params and "writable_scope" in locals(): params["tenant_code"] = writable_scope["tenant_code"] updates.append("tenant_code = :tenant_code") if "tenant_name" in evaluation_point_columns and "tenant_name" not in params and "writable_scope" in locals(): params["tenant_name"] = writable_scope["tenant_name"] updates.append("tenant_name = :tenant_name") async with session.begin(): await session.execute( text(f"UPDATE evaluation_points SET {', '.join(updates)} WHERE id = :point_id"), params, ) await session.commit() return await self.GetPoint(CurrentUserId, UserArea, UserRole, TenantCode, TenantName, PointId) async def DeletePoint( self, CurrentUserId: int, UserArea: str | None, UserRole: str | None, TenantCode: str | None, TenantName: str | None, PointId: int, ) -> EvaluationPointDeleteVO: current_user = await self._get_current_user_context( CurrentUserId=CurrentUserId, UserArea=UserArea, UserRole=UserRole, TenantCode=TenantCode, TenantName=TenantName, ) self._assert_can_write(current_user) await self.GetPoint(CurrentUserId, UserArea, UserRole, TenantCode, TenantName, PointId) async with _LegacySession() as session: async with session.begin(): await session.execute(text("DELETE FROM evaluation_points WHERE id = :point_id"), {"point_id": PointId}) await session.commit() return EvaluationPointDeleteVO(success=True, message="评查点删除成功") async def GetAttributeTypes(self) -> AttributeTypeListVO: async with _LegacySession() as session: rows = ( await session.execute( text( """ SELECT DISTINCT TRIM(document_attribute_type) AS code FROM evaluation_points WHERE document_attribute_type IS NOT NULL AND TRIM(document_attribute_type) <> '' ORDER BY TRIM(document_attribute_type) ASC """ ) ) ).scalars().all() types = [AttributeTypeVO(code=str(item), label=str(item)) for item in rows if item] if not any(item.code == "ALL" for item in types): types.insert(0, AttributeTypeVO(code="ALL", label="通用")) return AttributeTypeListVO(types=types) def _build_list_filters( self, Name: str | None, Code: str | None, Risk: str | None, IsEnabled: bool | None, GroupPid: int | None, GroupId: int | None, DocumentAttributeType: str | None, RequestedScope: dict[str, str], CurrentUser: dict[str, Any], EvaluationPointColumns: set[str], ) -> tuple[str, dict[str, Any]]: filters = ["1=1"] params: dict[str, Any] = {} name = (Name or "").strip() code = (Code or "").strip() if name and code: if name == code: filters.append("(ep.name ILIKE :keyword OR ep.code ILIKE :keyword)") params["keyword"] = f"%{name}%" else: filters.append("ep.name ILIKE :name") filters.append("ep.code ILIKE :code") params["name"] = f"%{name}%" params["code"] = f"%{code}%" elif name: filters.append("ep.name ILIKE :name") params["name"] = f"%{name}%" elif code: filters.append("ep.code ILIKE :code") params["code"] = f"%{code}%" if Risk: filters.append("ep.risk = :risk") params["risk"] = Risk if IsEnabled is not None: filters.append("ep.is_enabled = :is_enabled") params["is_enabled"] = IsEnabled if GroupPid is not None: filters.append("ep.evaluation_point_groups_pid = :group_pid") params["group_pid"] = GroupPid if GroupId is not None: filters.append("ep.evaluation_point_groups_id = :group_id") params["group_id"] = GroupId if DocumentAttributeType: if DocumentAttributeType == "ALL": filters.append("(ep.document_attribute_type = 'ALL' OR ep.document_attribute_type = '通用' OR ep.document_attribute_type IS NULL OR TRIM(ep.document_attribute_type) = '')") else: filters.append("ep.document_attribute_type = :document_attribute_type") params["document_attribute_type"] = DocumentAttributeType filters.extend( self._build_scope_filters( CurrentUser=CurrentUser, Params=params, RequestedScope=RequestedScope, EvaluationPointColumns=EvaluationPointColumns, ) ) return " AND ".join(filters), params async def _validate_group_relation(self, GroupPid: int | None, GroupId: int | None) -> None: if not GroupPid: raise LeauditException(StatusCodeEnum.BAD_REQUEST, "评查点类型不能为空") if not GroupId: raise LeauditException(StatusCodeEnum.BAD_REQUEST, "所属规则组不能为空") async with _LegacySession() as session: parent = ( await session.execute( text("SELECT id, pid, is_enabled FROM evaluation_point_groups WHERE id = :group_pid"), {"group_pid": GroupPid}, ) ).mappings().first() child = ( await session.execute( text("SELECT id, pid, is_enabled FROM evaluation_point_groups WHERE id = :group_id"), {"group_id": GroupId}, ) ).mappings().first() if not parent: raise LeauditException(StatusCodeEnum.BAD_REQUEST, "评查点类型不存在") if not child: raise LeauditException(StatusCodeEnum.BAD_REQUEST, "所属规则组不存在") if int(child.get("pid") or 0) != int(GroupPid): raise LeauditException(StatusCodeEnum.BAD_REQUEST, "所属规则组与评查点类型不匹配") async def _ensure_code_unique(self, Code: str, PointId: int | None = None) -> None: params: dict[str, Any] = {"code": Code} sql = "SELECT id FROM evaluation_points WHERE LOWER(code) = LOWER(:code)" if PointId is not None: sql += " AND id <> :point_id" params["point_id"] = PointId async with _LegacySession() as session: exists = ( await session.execute(text(sql), params) ).scalar_one_or_none() if exists: raise LeauditException(StatusCodeEnum.BAD_REQUEST, "评查点编码已存在") def _build_write_params(self, Body: EvaluationPointCreateDTO, Now: datetime) -> dict[str, Any]: return { "code": str(Body.code).strip(), "name": str(Body.name).strip(), "evaluation_point_groups_id": Body.evaluation_point_groups_id, "evaluation_point_groups_pid": Body.evaluation_point_groups_pid, "risk": Body.risk, "description": Body.description or "", "is_enabled": bool(Body.is_enabled), "document_attribute_type": self._normalize_document_attribute_type(Body.document_attribute_type), "references_laws": json.dumps(Body.references_laws or self._default_json("references_laws"), ensure_ascii=False), "extraction_config": json.dumps(Body.extraction_config or self._default_json("extraction_config"), ensure_ascii=False), "evaluation_config": json.dumps(Body.evaluation_config or self._default_json("evaluation_config"), ensure_ascii=False), "pass_message": Body.pass_message or "", "fail_message": Body.fail_message or "", "suggestion_message": Body.suggestion_message or "", "suggestion_message_type": Body.suggestion_message_type or "warning", "post_action": Body.post_action or "none", "action_config": Body.action_config or "", "score": float(Body.score or 0), "area": None, "created_at": Now, "updated_at": Now, } async def _to_point_vo(self, row: dict[str, Any]) -> EvaluationPointVO: group_id = row.get("evaluation_point_groups_id") tenant = await self._resolve_record_tenant( Area=row.get("area"), TenantCode=row.get("tenant_code"), TenantName=row.get("tenant_name"), ) return EvaluationPointVO( id=int(row["id"]), code=str(row.get("code") or ""), name=str(row.get("name") or ""), evaluation_point_groups_id=int(group_id) if group_id is not None else None, evaluation_point_groups_pid=int(row["evaluation_point_groups_pid"]) if row.get("evaluation_point_groups_pid") is not None else None, ruleType=str(row.get("rule_type") or ""), groupName=str(row.get("group_name") or ""), groupId=str(group_id) if group_id is not None else "", risk=str(row.get("risk") or ""), description=str(row.get("description") or ""), is_enabled=bool(row.get("is_enabled")), document_attribute_type=self._normalize_document_attribute_type(row.get("document_attribute_type")), references_laws=self._parse_json(row.get("references_laws"), self._default_json("references_laws")), extraction_config=self._parse_json(row.get("extraction_config"), self._default_json("extraction_config")), evaluation_config=self._parse_json(row.get("evaluation_config"), self._default_json("evaluation_config")), pass_message=str(row.get("pass_message") or ""), fail_message=str(row.get("fail_message") or ""), suggestion_message=str(row.get("suggestion_message") or ""), suggestion_message_type=str(row.get("suggestion_message_type") or "warning"), post_action=str(row.get("post_action") or "none"), action_config=str(row.get("action_config") or ""), score=self._normalize_score(row.get("score")), area=str(row.get("area") or ""), tenantCode=tenant.tenant_code or "", tenantName=tenant.tenant_name or str(row.get("tenant_name") or row.get("area") or ""), created_at=self._format_datetime(row.get("created_at")), updated_at=self._format_datetime(row.get("updated_at")), ) def _parse_json(self, value: Any, default: Any) -> Any: if value is None: return default if isinstance(value, (dict, list)): return value if isinstance(value, str): try: return json.loads(value) except json.JSONDecodeError: return default return value def _format_datetime(self, value: Any) -> str | None: if value is None: return None if isinstance(value, datetime): return value.isoformat() return str(value) def _normalize_document_attribute_type(self, value: Any) -> str: normalized = str(value or "").strip() if not normalized: return "通用" if normalized == "ALL": return "通用" return normalized def _normalize_score(self, value: Any) -> float: if value is None: return 0 if isinstance(value, Decimal): return float(value) if isinstance(value, (int, float)): return float(value) try: return float(value) except (TypeError, ValueError): return 0 def _normalize_scalar_field(self, field: str, value: Any) -> Any: if field in {"code", "name"}: return str(value or "").strip() if field == "document_attribute_type": return self._normalize_document_attribute_type(value) if field == "score": return self._normalize_score(value) return value async def _resolve_area_value( self, *, Area: str | None, TenantCode: str | None, TenantName: str | None, Source: str, ) -> str | None: resolution = await self.TenantResolver.Resolve( RawValue=Area, Source=Source, PreferredTenantCode=str(TenantCode or "").strip() or None, FallbackTenantName=TenantName, ) normalized = resolution.tenant_name or resolution.normalized_value or str(Area or "").strip() normalized = normalized.strip() return normalized or None async def _resolve_requested_scope( self, *, Area: str | None, TenantCode: str | None, TenantName: str | None, Source: str, ) -> dict[str, str]: resolution = await self.TenantResolver.Resolve( RawValue=Area, Source=Source, PreferredTenantCode=str(TenantCode or "").strip() or None, FallbackTenantName=TenantName, ) requested_scope = { "tenant_code": str(resolution.tenant_code or TenantCode or "").strip(), "tenant_name": str(resolution.tenant_name or TenantName or "").strip(), "normalized_value": str(resolution.normalized_value or Area or "").strip(), } shared_scope = self._normalize_shared_writable_scope(requested_scope) if shared_scope: return { "tenant_code": shared_scope["tenant_code"], "tenant_name": shared_scope["tenant_name"], "normalized_value": requested_scope["normalized_value"], } return requested_scope async def _resolve_record_tenant( self, Area: str | None, TenantCode: str | None = None, TenantName: str | None = None, ): return await self.TenantResolver.Resolve( RawValue=Area, Source="evaluation_point_record", PreferredTenantCode=str(TenantCode or "").strip() or None, FallbackTenantName=TenantName, ) async def _get_current_user_context( self, *, CurrentUserId: int, UserArea: str | None, UserRole: str | None, TenantCode: str | None, TenantName: str | None, ) -> dict[str, Any]: async with GetAsyncSession() as session: sso_user_columns = await SsoUserCompat.get_columns(session) tenant_code_select = SsoUserCompat.optional_coalesce_as( sso_user_columns, alias="u", column="tenant_code", fallback_sql="''", ) tenant_name_select = SsoUserCompat.optional_coalesce_as( sso_user_columns, alias="u", column="tenant_name", fallback_sql="''", ) row = ( await session.execute( text( f""" SELECT u.id, COALESCE(u.area, '') AS area, {tenant_code_select}, {tenant_name_select}, COALESCE(bool_or(COALESCE(r.data_scope, '') = 'ALL'), FALSE) AS is_global FROM sso_users u LEFT JOIN user_role ur ON ur.user_id = u.id LEFT JOIN roles r ON r.id = ur.role_id WHERE u.id = :user_id GROUP BY u.id, u.area """ ), {"user_id": CurrentUserId}, ) ).mappings().first() if row: tenant = await self.TenantResolver.ResolveUserContext( Area=str(row["area"] or ""), TenantCode=str(row.get("tenant_code") or "") or None, TenantName=str(row.get("tenant_name") or "") or None, Source="evaluation_point_user_context", ) can_manage = await self.PermissionService.HasAnyPermission(CurrentUserId, list(self._WRITE_PERMISSION_KEYS)) return { "id": int(row["id"]), "area": str(row["area"] or ""), "tenant_code": tenant.tenant_code or (str(row.get("tenant_code") or "") or None), "tenant_name": tenant.tenant_name or (str(row.get("tenant_name") or "") or None), "tenant_scope_value": tenant.tenant_name or tenant.normalized_value or str(row["area"] or ""), "is_global": bool(row["is_global"]), "can_manage": can_manage, } tenant = await self.TenantResolver.ResolveUserContext( Area=UserArea, TenantCode=TenantCode, TenantName=TenantName, Source="evaluation_point_user_context_fallback", ) can_manage = await self.PermissionService.HasAnyPermission(CurrentUserId, list(self._WRITE_PERMISSION_KEYS)) return { "id": int(CurrentUserId), "area": str(UserArea or ""), "tenant_code": tenant.tenant_code or (str(TenantCode or "").strip() or None), "tenant_name": tenant.tenant_name or (str(TenantName or "").strip() or None), "tenant_scope_value": tenant.tenant_name or tenant.normalized_value or str(UserArea or ""), "is_global": False, "can_manage": can_manage, } def _build_scope_filters( self, *, CurrentUser: dict[str, Any], Params: dict[str, Any], RequestedScope: dict[str, str], EvaluationPointColumns: set[str], ) -> list[str]: current_tenant_code = str(CurrentUser.get("tenant_code") or "").strip() current_tenant_name = str(CurrentUser.get("tenant_name") or CurrentUser.get("tenant_scope_value") or CurrentUser.get("area") or "").strip() requested_tenant_code = str(RequestedScope.get("tenant_code") or "").strip() requested_tenant_name = str( RequestedScope.get("tenant_name") or RequestedScope.get("normalized_value") or "" ).strip() shared_scope_codes = {"PUBLIC", "PROVINCIAL"} shared_scope_names = {"公共", "省级"} if CurrentUser.get("is_global"): if requested_tenant_code or requested_tenant_name: return [ self._tenant_scope_match_sql( alias="ep", columns=EvaluationPointColumns, params=Params, prefix="requested_scope", tenant_code=requested_tenant_code or None, tenant_name=requested_tenant_name or requested_tenant_code, ) ] return ["1=1"] if not current_tenant_code and not current_tenant_name: return ["1=0"] if requested_tenant_code or requested_tenant_name: allowed = False effective_tenant_code = requested_tenant_code effective_tenant_name = requested_tenant_name or requested_tenant_code if requested_tenant_code in shared_scope_codes or effective_tenant_name in shared_scope_names: allowed = True elif current_tenant_code and requested_tenant_code == current_tenant_code: allowed = True effective_tenant_code = current_tenant_code effective_tenant_name = effective_tenant_name or current_tenant_name elif requested_tenant_name and requested_tenant_name == current_tenant_name: allowed = True effective_tenant_code = effective_tenant_code or current_tenant_code effective_tenant_name = current_tenant_name if not allowed: return ["1=0"] return [ self._tenant_scope_match_sql( alias="ep", columns=EvaluationPointColumns, params=Params, prefix="requested_scope", tenant_code=effective_tenant_code or None, tenant_name=effective_tenant_name, ) ] visible_scopes: list[tuple[str | None, str]] = [ ("PROVINCIAL", "省级"), ("PUBLIC", "公共"), ] if current_tenant_code or current_tenant_name: visible_scopes.append((current_tenant_code or None, current_tenant_name)) scope_clauses = [ self._tenant_scope_match_sql( alias="ep", columns=EvaluationPointColumns, params=Params, prefix=f"visible_scope_{index}", tenant_code=tenant_code, tenant_name=tenant_name, ) for index, (tenant_code, tenant_name) in enumerate(visible_scopes) if tenant_name ] return [f"({' OR '.join(scope_clauses)})"] if scope_clauses else ["1=0"] def _assert_can_write(self, current_user: dict[str, Any]) -> None: if current_user.get("can_manage"): return raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户没有跨租户评查点管理权限") def _normalize_shared_writable_scope(self, requested_scope: dict[str, str]) -> dict[str, str] | None: requested_tenant_code = str(requested_scope.get("tenant_code") or "").strip() requested_tenant_name = str(requested_scope.get("tenant_name") or "").strip() requested_normalized_value = str(requested_scope.get("normalized_value") or "").strip() if requested_tenant_code == "PROVINCIAL": return {"area": "省级", "tenant_code": "PROVINCIAL", "tenant_name": requested_tenant_name or "省级"} if requested_tenant_code == "PUBLIC": return {"area": "公共", "tenant_code": "PUBLIC", "tenant_name": requested_tenant_name or "公共"} if requested_tenant_name == "省级" or requested_normalized_value == "省级": return {"area": "省级", "tenant_code": "PROVINCIAL", "tenant_name": "省级"} if requested_tenant_name == "公共" or requested_normalized_value in {"公共", "default"}: return {"area": "公共", "tenant_code": "PUBLIC", "tenant_name": "公共"} return None def _resolve_writable_scope( self, current_user: dict[str, Any], *, RequestedScope: dict[str, str], ) -> dict[str, str]: self._assert_can_write(current_user) current_tenant_code = str(current_user.get("tenant_code") or "").strip() current_tenant_name = str( current_user.get("tenant_name") or current_user.get("tenant_scope_value") or current_user.get("area") or "" ).strip() current_area = str(current_user.get("tenant_scope_value") or current_user.get("area") or current_tenant_name).strip() requested_tenant_code = str(RequestedScope.get("tenant_code") or "").strip() requested_tenant_name = str(RequestedScope.get("tenant_name") or "").strip() requested_normalized_value = str(RequestedScope.get("normalized_value") or "").strip() shared_scope = self._normalize_shared_writable_scope(RequestedScope) if current_user.get("is_global"): if not requested_tenant_code and not requested_tenant_name and not requested_normalized_value: raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "请显式指定评查点所属租户或共享域") if shared_scope: return shared_scope effective_tenant_code = requested_tenant_code effective_tenant_name = requested_tenant_name or requested_normalized_value or effective_tenant_code if not effective_tenant_code: raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "请使用标准 tenant_code 维护非公共评查点") return { "area": effective_tenant_name, "tenant_code": effective_tenant_code, "tenant_name": effective_tenant_name, } if not current_tenant_code and not current_tenant_name: raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前租户管理员未配置所属租户,无法写入评查点") current_scope = self._normalize_shared_writable_scope( { "tenant_code": current_tenant_code, "tenant_name": current_tenant_name, "normalized_value": current_area, } ) or { "area": current_tenant_name or current_area or current_tenant_code, "tenant_code": current_tenant_code, "tenant_name": current_tenant_name or current_area or current_tenant_code, } if not requested_tenant_code and not requested_tenant_name and not requested_normalized_value: return current_scope if shared_scope and shared_scope["tenant_code"] != current_scope["tenant_code"]: raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前仅允许维护本人所属租户的评查点") if requested_tenant_code and current_tenant_code and requested_tenant_code != current_tenant_code: raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前仅允许维护本人所属租户的评查点") if requested_tenant_name and not requested_tenant_code and requested_tenant_name != current_tenant_name: raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前仅允许维护本人所属租户的评查点") if ( requested_normalized_value and not requested_tenant_code and not requested_tenant_name and requested_normalized_value != current_area and requested_normalized_value != current_tenant_name ): raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前仅允许维护本人所属租户的评查点") return current_scope async def _get_point_row(self, *, PointId: int, CurrentUser: dict[str, Any]) -> Any | None: evaluation_point_columns = await self._load_legacy_table_columns("evaluation_points") where_clause, params = self._build_list_filters( Name=None, Code=None, Risk=None, IsEnabled=None, GroupPid=None, GroupId=None, DocumentAttributeType=None, RequestedScope={"tenant_code": "", "tenant_name": "", "normalized_value": ""}, CurrentUser=CurrentUser, EvaluationPointColumns=evaluation_point_columns, ) params["point_id"] = PointId async with _LegacySession() as session: tenant_code_select = self._evaluation_point_tenant_code_select(evaluation_point_columns, alias="ep") tenant_name_select = self._evaluation_point_tenant_name_select(evaluation_point_columns, alias="ep") point_sql = text( f""" SELECT ep.id, ep.code, ep.name, ep.evaluation_point_groups_id, ep.evaluation_point_groups_pid, ep.risk, ep.description, ep.is_enabled, ep.document_attribute_type, ep.references_laws, ep.extraction_config, ep.evaluation_config, ep.pass_message, ep.fail_message, ep.suggestion_message, ep.suggestion_message_type, ep.post_action, ep.action_config, ep.score, ep.area, {tenant_code_select}, {tenant_name_select}, ep.created_at, ep.updated_at, child_group.name AS group_name, parent_group.name AS rule_type FROM evaluation_points ep LEFT JOIN evaluation_point_groups child_group ON child_group.id = ep.evaluation_point_groups_id LEFT JOIN evaluation_point_groups parent_group ON parent_group.id = COALESCE(ep.evaluation_point_groups_pid, child_group.pid) WHERE ep.id = :point_id AND {where_clause} """ ) row = ( await session.execute( point_sql, params, ) ).mappings().first() return row async def _load_legacy_table_columns(self, table_name: str, Session=None) -> set[str]: cached = self._legacy_columns_cache.get(table_name) if cached is not None: return cached if Session is not None: rows = ( await Session.execute( text( """ SELECT column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = :table_name """ ), {"table_name": table_name}, ) ).mappings().all() columns = {str(row["column_name"]) for row in rows if row.get("column_name")} self._legacy_columns_cache[table_name] = columns return columns async with _LegacySession() as session: return await self._load_legacy_table_columns(table_name, Session=session) @staticmethod def _evaluation_point_tenant_code_expr(columns: set[str], *, alias: str) -> str: if "tenant_code" in columns: return f"COALESCE(NULLIF(BTRIM({alias}.tenant_code), ''), '')" return "''" @staticmethod def _evaluation_point_tenant_name_expr(columns: set[str], *, alias: str) -> str: if "tenant_name" in columns: return f"COALESCE(NULLIF(BTRIM({alias}.tenant_name), ''), NULLIF(BTRIM({alias}.area), ''), '')" return f"COALESCE(NULLIF(BTRIM({alias}.area), ''), '')" def _evaluation_point_tenant_code_select(self, columns: set[str], *, alias: str) -> str: return f"{self._evaluation_point_tenant_code_expr(columns, alias=alias)} AS tenant_code" def _evaluation_point_tenant_name_select(self, columns: set[str], *, alias: str) -> str: return f"{self._evaluation_point_tenant_name_expr(columns, alias=alias)} AS tenant_name" def _tenant_scope_match_sql( self, *, alias: str, columns: set[str], params: dict[str, Any], prefix: str, tenant_code: str | None, tenant_name: str, ) -> str: tenant_code_expr = self._evaluation_point_tenant_code_expr(columns, alias=alias) tenant_name_expr = self._evaluation_point_tenant_name_expr(columns, alias=alias) normalized_tenant_code = str(tenant_code or "").strip() normalized_tenant_name = str(tenant_name or "").strip() fallback_names = self._shared_scope_legacy_names(normalized_tenant_code, normalized_tenant_name) if normalized_tenant_code: params[f"{prefix}_tenant_code"] = normalized_tenant_code if fallback_names: fallback_clauses: list[str] = [] for index, fallback_name in enumerate(fallback_names): key = f"{prefix}_tenant_name_{index}" params[key] = fallback_name fallback_clauses.append(f"{tenant_name_expr} = :{key}") return ( "(" f"{tenant_code_expr} = :{prefix}_tenant_code " "OR (" f"{tenant_code_expr} = '' " f"AND ({' OR '.join(fallback_clauses)})" ")" ")" ) params[f"{prefix}_tenant_name"] = normalized_tenant_name return ( "(" f"{tenant_code_expr} = :{prefix}_tenant_code " "OR (" f"{tenant_code_expr} = '' " f"AND {tenant_name_expr} = :{prefix}_tenant_name" ")" ")" ) params[f"{prefix}_tenant_name"] = normalized_tenant_name return f"{tenant_name_expr} = :{prefix}_tenant_name" @staticmethod def _shared_scope_legacy_names(tenant_code: str, tenant_name: str) -> list[str]: if tenant_code == "PUBLIC": return ["公共", "default", ""] if tenant_code == "PROVINCIAL": return ["省级", "省局"] if tenant_name == "公共": return ["公共", "default", ""] if tenant_name == "省级": return ["省级", "省局"] return [] def _default_json(self, field: str) -> dict[str, Any]: if field == "references_laws": return {"name": "", "content": "", "articles": []} if field == "evaluation_config": return {"logicType": "and", "customLogic": "", "rules": []} return { "llm": {"fields": [], "prompt_setting": {"type": "system", "template": ""}}, "vlm": {"fields": [], "prompt_setting": {"type": "system", "template": ""}}, "regex": {"fields": []}, }