"""规则配置页聚合服务实现。""" from __future__ import annotations import asyncio from collections import defaultdict import re import time from typing import Any 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 sqlalchemy import text from fastapi_modules.fastapi_leaudit.domian.vo.ruleConfigVo import ( RuleConfigPackListVO, RuleConfigPackRuleSummaryVO, RuleConfigPackVO, ) from fastapi_modules.fastapi_leaudit.leaudit_bridge.ruleValidator import RuleValidator from fastapi_modules.fastapi_leaudit.services import IOssService from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import GetRuleServiceSingleton from fastapi_modules.fastapi_leaudit.services.impl.ruleTenantScope import normalize_scoped_tenant_code, pick_effective_scoped_row from fastapi_modules.fastapi_leaudit.services.ruleConfigService import IRuleConfigService class RuleConfigServiceImpl(IRuleConfigService): """规则配置页聚合服务实现。""" _GLOBAL_YAML_SUMMARY_CACHE: dict[int, tuple[float, dict[str, Any]]] = {} _GLOBAL_PACK_SUMMARY_CACHE: dict[str, tuple[float, list[RuleConfigPackListVO]]] = {} _GLOBAL_WARM_LOCK: asyncio.Lock | None = None def __init__(self, OssService: IOssService | None = None) -> None: self.OssService = OssService or OssServiceImpl() self.RuleService = GetRuleServiceSingleton() self.Validator = RuleValidator() self._yaml_summary_cache = self.__class__._GLOBAL_YAML_SUMMARY_CACHE self._pack_summary_cache = self.__class__._GLOBAL_PACK_SUMMARY_CACHE @classmethod def _set_pack_summary_cache(cls, cache_key: str, value: tuple[float, list[RuleConfigPackListVO]] | None) -> None: if value is None: cls._GLOBAL_PACK_SUMMARY_CACHE.pop(cache_key, None) return cls._GLOBAL_PACK_SUMMARY_CACHE[cache_key] = value @classmethod def _get_warm_lock(cls) -> asyncio.Lock: if cls._GLOBAL_WARM_LOCK is None: cls._GLOBAL_WARM_LOCK = asyncio.Lock() return cls._GLOBAL_WARM_LOCK def InvalidateSummaryCaches(self, version_ids: list[int] | None = None) -> None: """清理规则摘要缓存;version_ids 为空时清空全部。""" self.__class__._GLOBAL_PACK_SUMMARY_CACHE.clear() if version_ids is None: self._yaml_summary_cache.clear() return for version_id in {int(item) for item in version_ids if item is not None}: self._yaml_summary_cache.pop(version_id, None) async def WarmPackSummaries(self, force: bool = False, CurrentUserId: int | None = None) -> list[RuleConfigPackListVO]: """预热规则列表摘要缓存。""" async with self.__class__._get_warm_lock(): if force: self.InvalidateSummaryCaches() return await self.ListPackSummaries(CurrentUserId=CurrentUserId) async def ListPacks(self, CurrentUserId: int | None = None) -> list[RuleConfigPackVO]: """列出规则配置页所需的全部 pack。""" rows = await self._load_pack_rows() rule_set_map = await self._load_rule_set_meta_map(CurrentUserId=CurrentUserId) return [await self._build_pack_vo(row, rule_set_map, CurrentUserId=CurrentUserId) for row in rows] async def ListPackSummaries(self, CurrentUserId: int | None = None) -> list[RuleConfigPackListVO]: """列出规则列表页所需的轻量 pack。""" cache_key = self._summary_cache_key(CurrentUserId) cached = self.__class__._GLOBAL_PACK_SUMMARY_CACHE.get(cache_key) now = time.monotonic() if cached and now - cached[0] <= 60: return cached[1] rows = await self._load_pack_rows() if not rows: return [] current_user = await self._load_current_user(CurrentUserId) group_ids = [int(row["group_id"]) for row in rows] binding_map = await self._load_effective_binding_map(group_ids, CurrentUserId=CurrentUserId) rule_set_ids = sorted({int(item["rule_set_id"]) for item in binding_map.values() if item.get("rule_set_id") is not None}) rule_set_map = await self._load_rule_set_meta_map_by_ids(rule_set_ids, CurrentUserId=CurrentUserId) latest_version_map = await self._load_latest_version_map(rule_set_ids) base_items: list[dict[str, Any]] = [] resolved_version_ids: set[int] = set() for row in rows: group_id = int(row["group_id"]) binding = binding_map.get(group_id) item = self._build_pack_summary_item( row=row, binding=binding, rule_set_map=rule_set_map, latest_version_map=latest_version_map, current_user=current_user, ) if item.get("resolvedVersionId") is not None: resolved_version_ids.add(int(item["resolvedVersionId"])) base_items.append(item) version_oss_map = await self._load_version_oss_map(sorted(resolved_version_ids)) yaml_summary_map = await self._load_yaml_summaries(version_oss_map) packs: list[RuleConfigPackListVO] = [] for item in base_items: summary = yaml_summary_map.get(item["resolvedVersionId"]) if item.get("resolvedVersionId") else None rules = summary["rules"] if summary else [] source_status = "empty" yaml_name = item.get("ruleName") or "" if item.get("resolvedVersionId") is not None: source_status = "ready" if summary and summary.get("loaded") else "missing" yaml_name = str(summary.get("yaml_name") or yaml_name or "") item["sourceStatus"] = source_status item["yamlName"] = yaml_name item["rules"] = [RuleConfigPackRuleSummaryVO(**rule) for rule in rules] item["usableRuleCount"] = len(rules) packs.append(RuleConfigPackListVO(**item)) cache_value = (time.monotonic(), packs) self.__class__._set_pack_summary_cache(cache_key, cache_value) self._pack_summary_cache = cache_value return packs async def GetPack(self, PackId: int, CurrentUserId: int | None = None) -> RuleConfigPackVO: """获取单个规则配置 pack。""" async with GetAsyncSession() as session: row = ( await session.execute( text( """ SELECT child.id AS group_id, child.name AS subtype, COALESCE(child.document_type_id, root.document_type_id) AS document_type_id, dt.name AS document_type_name, root.id AS root_group_id, COALESCE(root.name, child.name) AS main_type, em.name AS entry_module_name FROM leaudit_evaluation_point_groups child LEFT JOIN leaudit_evaluation_point_groups root ON root.id = child.pid AND root.deleted_at IS NULL LEFT JOIN leaudit_document_types dt ON dt.id = COALESCE(child.document_type_id, root.document_type_id) LEFT JOIN leaudit_entry_modules em ON em.id = COALESCE(child.entry_module_id, root.entry_module_id, dt.entry_module_id) WHERE child.id = :group_id AND child.deleted_at IS NULL AND COALESCE(child.pid, 0) <> 0 LIMIT 1 """ ), {"group_id": PackId}, ) ).mappings().first() if not row: raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则配置 pack 不存在") rule_set_map = await self._load_rule_set_meta_map(CurrentUserId=CurrentUserId) return await self._build_pack_vo(row, rule_set_map, CurrentUserId=CurrentUserId) async def _build_pack_vo( self, row, rule_set_map: dict[int, dict[str, object]], *, CurrentUserId: int | None = None, ) -> RuleConfigPackVO: """构建单个 pack 聚合对象。""" group_id = int(row["group_id"]) binding = await self._load_effective_binding(group_id, CurrentUserId=CurrentUserId) current_user = await self._load_current_user(CurrentUserId) document_type = str(row["document_type_name"] or "").strip() main_type = str(row["main_type"] or "").strip() subtype = str(row["subtype"] or "").strip() or "通用" module_type = str(row["entry_module_name"] or "").strip() or (f"{document_type}评查" if document_type else "规则配置") source_status = "empty" yaml_text = "" rule_set_id: int | None = None rule_type: str | None = None rule_name: str | None = None current_version_id: int | None = None fallback_version_id: int | None = None resolved_version_id: int | None = None has_usable_version = False usable_rule_count = 0 binding_id: int | None = None rules: list[RuleConfigPackRuleSummaryVO] = [] source_fields = self._resolve_source_fields(binding=binding, current_user=current_user) if binding: binding_id = int(binding["id"]) bound_rule_set_id = int(binding["rule_set_id"]) rule_set_meta = rule_set_map.get(bound_rule_set_id) if rule_set_meta: rule_set_id = bound_rule_set_id rule_type = str(rule_set_meta.get("rule_type") or "") or None rule_name = str(rule_set_meta.get("rule_name") or "") or None current_version_id = self._to_int(rule_set_meta.get("current_version_id")) fallback_version_id = self._to_int(rule_set_meta.get("fallback_version_id")) resolved_version_id = current_version_id or fallback_version_id has_usable_version = bool(rule_set_meta.get("has_usable_version")) usable_rule_count = int(rule_set_meta.get("usable_rule_count") or 0) if resolved_version_id is None: resolved_version_id = await self._load_latest_version_id(rule_set_id) if resolved_version_id is not None: yaml_text = await self._load_yaml_text_by_version_id(resolved_version_id) source_status = "ready" if yaml_text.strip() else "missing" version_oss_map = await self._load_version_oss_map([resolved_version_id]) summary = (await self._load_yaml_summaries(version_oss_map)).get(resolved_version_id) if summary and summary.get("loaded"): summary_rules = list(summary.get("rules") or []) rules = [RuleConfigPackRuleSummaryVO(**rule) for rule in summary_rules] usable_rule_count = len(rules) return RuleConfigPackVO( packId=group_id, groupId=group_id, rootGroupId=self._to_int(row.get("root_group_id")), bindingId=binding_id, ruleSetId=rule_set_id, effectiveTenantCode=source_fields["effectiveTenantCode"], effectiveScopeType=source_fields["effectiveScopeType"], isInherited=source_fields["isInherited"], sourceRuleSetId=self._to_int(rule_set_map.get(rule_set_id, {}).get("source_rule_set_id")) if rule_set_id is not None else None, ruleType=rule_type, ruleName=rule_name, currentVersionId=current_version_id, fallbackVersionId=fallback_version_id, resolvedVersionId=resolved_version_id, hasUsableVersion=has_usable_version, usableRuleCount=usable_rule_count, documentTypeId=self._to_int(row.get("document_type_id")), documentType=document_type, moduleType=module_type, mainType=main_type or document_type, subtype=subtype, yamlText=yaml_text, sourceStatus=source_status, rules=rules, ) async def _load_pack_rows(self): async with GetAsyncSession() as session: return ( await session.execute( text( """ SELECT child.id AS group_id, child.name AS subtype, COALESCE(child.document_type_id, root.document_type_id) AS document_type_id, dt.name AS document_type_name, root.id AS root_group_id, COALESCE(root.name, child.name) AS main_type, em.name AS entry_module_name FROM leaudit_evaluation_point_groups child LEFT JOIN leaudit_evaluation_point_groups root ON root.id = child.pid AND root.deleted_at IS NULL LEFT JOIN leaudit_document_types dt ON dt.id = COALESCE(child.document_type_id, root.document_type_id) LEFT JOIN leaudit_entry_modules em ON em.id = COALESCE(child.entry_module_id, root.entry_module_id, dt.entry_module_id) WHERE child.deleted_at IS NULL AND COALESCE(child.pid, 0) <> 0 ORDER BY COALESCE(root.sort_order, 0) ASC, root.id ASC, child.sort_order ASC, child.id ASC """ ) ) ).mappings().all() async def _load_effective_binding(self, group_id: int, CurrentUserId: int | None = None): """读取当前二级分组实际生效的规则集绑定。""" return (await self._load_effective_binding_map([group_id], CurrentUserId=CurrentUserId)).get(group_id) async def _load_effective_binding_map( self, group_ids: list[int], CurrentUserId: int | None = None, ) -> dict[int, dict[str, Any]]: if not group_ids: return {} async with GetAsyncSession() as session: current_user = await self.RuleService._get_current_user_context(session, CurrentUserId) binding_tenant_expr = ( "COALESCE(NULLIF(BTRIM(tenant_code), ''), 'PROVINCIAL')" if await self.RuleService._column_exists(session, "leaudit_rule_group_bindings", "tenant_code") else "'PROVINCIAL'" ) rows = ( await session.execute( text( f""" SELECT id, group_id, rule_set_id, priority, {binding_tenant_expr} AS tenant_code FROM leaudit_rule_group_bindings WHERE deleted_at IS NULL AND is_active = TRUE AND group_id = ANY(:group_ids) ORDER BY group_id ASC, priority DESC, id ASC """ ), {"group_ids": group_ids}, ) ).mappings().all() grouped: dict[int, list[dict[str, Any]]] = defaultdict(list) for row in rows: grouped[int(row["group_id"])].append(dict(row)) binding_map: dict[int, dict[str, Any]] = {} tenant_code = str(current_user.get("tenant_code") or "") or None if current_user else None for group_id, group_rows in grouped.items(): effective = pick_effective_scoped_row(group_rows, tenant_code) if effective is not None: binding_map[group_id] = dict(effective) return binding_map async def _load_current_user(self, CurrentUserId: int | None = None) -> dict[str, object] | None: async with GetAsyncSession() as session: return await self.RuleService._get_current_user_context(session, CurrentUserId) async def _load_rule_set_meta_map(self, CurrentUserId: int | None = None) -> dict[int, dict[str, object]]: items = await self.RuleService.ListSets(CurrentUserId=CurrentUserId) return { item.id: { "rule_type": item.ruleType, "rule_name": item.ruleName, "current_version_id": item.currentVersionId, "fallback_version_id": item.fallbackVersionId, "has_usable_version": item.hasUsableVersion, "usable_rule_count": item.usableRuleCount, "source_rule_set_id": getattr(item, "sourceRuleSetId", None), } for item in items } async def _load_rule_set_meta_map_by_ids( self, rule_set_ids: list[int], CurrentUserId: int | None = None, ) -> dict[int, dict[str, object]]: if not rule_set_ids: return {} items = await self.RuleService.ListSets(CurrentUserId=CurrentUserId) return { item.id: { "rule_type": item.ruleType, "rule_name": item.ruleName, "current_version_id": item.currentVersionId, "fallback_version_id": item.fallbackVersionId, "has_usable_version": item.hasUsableVersion, "usable_rule_count": item.usableRuleCount, "source_rule_set_id": getattr(item, "sourceRuleSetId", None), } for item in items if item.id in set(rule_set_ids) } def _build_pack_summary_item( self, *, row, binding: dict[str, Any] | None, rule_set_map: dict[int, dict[str, object]], latest_version_map: dict[int, int], current_user: dict[str, object] | None, ) -> dict[str, Any]: document_type = str(row["document_type_name"] or "").strip() main_type = str(row["main_type"] or "").strip() subtype = str(row["subtype"] or "").strip() or "通用" module_type = str(row["entry_module_name"] or "").strip() or (f"{document_type}评查" if document_type else "规则配置") binding_id: int | None = None rule_set_id: int | None = None rule_type: str | None = None rule_name: str | None = None current_version_id: int | None = None fallback_version_id: int | None = None resolved_version_id: int | None = None has_usable_version = False usable_rule_count = 0 source_rule_set_id: int | None = None source_fields = self._resolve_source_fields(binding=binding, current_user=current_user) if binding: binding_id = int(binding["id"]) bound_rule_set_id = int(binding["rule_set_id"]) rule_set_meta = rule_set_map.get(bound_rule_set_id) if rule_set_meta: rule_set_id = bound_rule_set_id rule_type = str(rule_set_meta.get("rule_type") or "") or None rule_name = str(rule_set_meta.get("rule_name") or "") or None current_version_id = self._to_int(rule_set_meta.get("current_version_id")) fallback_version_id = self._to_int(rule_set_meta.get("fallback_version_id")) has_usable_version = bool(rule_set_meta.get("has_usable_version")) source_rule_set_id = self._to_int(rule_set_meta.get("source_rule_set_id")) resolved_version_id = current_version_id or fallback_version_id or latest_version_map.get(rule_set_id) return { "packId": int(row["group_id"]), "groupId": int(row["group_id"]), "rootGroupId": self._to_int(row.get("root_group_id")), "bindingId": binding_id, "ruleSetId": rule_set_id, "effectiveTenantCode": source_fields["effectiveTenantCode"], "effectiveScopeType": source_fields["effectiveScopeType"], "isInherited": source_fields["isInherited"], "sourceRuleSetId": source_rule_set_id, "ruleType": rule_type, "ruleName": rule_name, "currentVersionId": current_version_id, "fallbackVersionId": fallback_version_id, "resolvedVersionId": resolved_version_id, "hasUsableVersion": has_usable_version, "usableRuleCount": usable_rule_count, "documentTypeId": self._to_int(row.get("document_type_id")), "documentType": document_type, "moduleType": module_type, "mainType": main_type or document_type, "subtype": subtype, } def _resolve_source_fields( self, *, binding: dict[str, Any] | None, current_user: dict[str, object] | None, ) -> dict[str, Any]: if not binding: return { "effectiveTenantCode": None, "effectiveScopeType": None, "isInherited": False, } effective_tenant_code = normalize_scoped_tenant_code(str(binding.get("tenant_code") or "")) effective_scope_type = self._scope_type_from_tenant_code(effective_tenant_code) is_tenant_user = bool(current_user) and not bool(current_user.get("is_global")) and bool(str(current_user.get("tenant_code") or "").strip()) is_inherited = is_tenant_user and effective_scope_type in {"PROVINCIAL", "PUBLIC"} return { "effectiveTenantCode": effective_tenant_code, "effectiveScopeType": effective_scope_type, "isInherited": is_inherited, } def _scope_type_from_tenant_code(self, tenant_code: str | None) -> str | None: normalized = normalize_scoped_tenant_code(tenant_code) if tenant_code is not None else None if not normalized: return None if normalized == "PUBLIC": return "PUBLIC" if normalized == "PROVINCIAL": return "PROVINCIAL" return "TENANT" async def _load_version_oss_map(self, version_ids: list[int]) -> dict[int, str]: if not version_ids: return {} async with GetAsyncSession() as session: rows = ( await session.execute( text( """ SELECT id, oss_url FROM leaudit_rule_versions WHERE id = ANY(:version_ids) AND deleted_at IS NULL """ ), {"version_ids": version_ids}, ) ).mappings().all() return {int(row["id"]): str(row["oss_url"] or "") for row in rows if row.get("oss_url")} async def _load_latest_version_map(self, rule_set_ids: list[int]) -> dict[int, int]: if not rule_set_ids: return {} async with GetAsyncSession() as session: rows = ( await session.execute( text( """ SELECT DISTINCT ON (rule_set_id) rule_set_id, id FROM leaudit_rule_versions WHERE rule_set_id = ANY(:rule_set_ids) AND deleted_at IS NULL ORDER BY rule_set_id, version_seq DESC, id DESC """ ), {"rule_set_ids": rule_set_ids}, ) ).mappings().all() return {int(row["rule_set_id"]): int(row["id"]) for row in rows} async def _load_yaml_text_by_version_id(self, version_id: int) -> str: async with GetAsyncSession() as session: row = ( await session.execute( text( """ SELECT oss_url FROM leaudit_rule_versions WHERE id = :version_id AND deleted_at IS NULL LIMIT 1 """ ), {"version_id": version_id}, ) ).mappings().first() if not row or not row["oss_url"]: return "" try: return (await self.OssService.DownloadBytes(row["oss_url"])).decode("utf-8") except Exception: return "" async def _load_yaml_summaries(self, version_oss_map: dict[int, str]) -> dict[int, dict[str, Any]]: def _extract_rule_dependencies(rule: Any) -> list[str]: dependencies: list[str] = [] for item in list(getattr(rule, "dependencies", []) or []): value = str(item or "").strip() if value: dependencies.append(value) for stage in list(getattr(rule, "stages", []) or []): field = str(getattr(stage, "field", "") or "").strip() if field: dependencies.append(field) for attr_name in ("seal_id", "signature_id", "element"): attr_value = str(getattr(stage, attr_name, "") or "").strip() if attr_value: dependencies.append(attr_value) for item in list(getattr(stage, "fields", []) or []): value = str(item or "").strip() if value: dependencies.append(value) prompt = str(getattr(stage, "prompt", "") or "") if prompt: dependencies.extend( match.strip() for match in re.findall(r"\{\{\s*([^}]+?)\s*\}\}", prompt) if match.strip() ) deduped: list[str] = [] seen: set[str] = set() for item in dependencies: if item in seen: continue seen.add(item) deduped.append(item) return deduped async def _load_one(version_id: int, oss_url: str): if not oss_url: return version_id, {"loaded": False, "yaml_name": "", "rules": []} cached = self._yaml_summary_cache.get(version_id) now = time.monotonic() if cached and now - cached[0] <= 120: return version_id, cached[1] try: yaml_text = (await self.OssService.DownloadBytes(oss_url)).decode("utf-8") if not yaml_text.strip(): summary = {"loaded": False, "yaml_name": "", "rules": []} self._yaml_summary_cache[version_id] = (time.monotonic(), summary) return version_id, summary rules_file = self.Validator.ParseValidated(yaml_text) groups: dict[str, str] = {} try: for group in getattr(rules_file, "rules", []) or []: group_name = getattr(group, "group", "") or "" for rule in getattr(group, "rules", []) or []: groups[getattr(rule, "rule_id", "") or ""] = group_name except Exception: groups = {} summaries = [] for rule in getattr(rules_file, "flat_rules", []) or []: stage_items = [] check_types = [] for idx, stage in enumerate(getattr(rule, "stages", []) or [], start=1): check = str(getattr(stage, "check", "") or getattr(stage, "type", "") or "") check_types.append(check) content = check if check == "ai": content = str(getattr(stage, "prompt", "") or "") elif hasattr(stage, "fields") and getattr(stage, "fields"): content = "、".join(str(item) for item in getattr(stage, "fields")[:3]) stage_items.append({"id": str(idx), "check": check, "content": content}) summaries.append({ "id": getattr(rule, "rule_id", "") or getattr(rule, "name", "") or "-", "ruleId": getattr(rule, "rule_id", "") or "-", "name": getattr(rule, "name", "") or getattr(rule, "rule_id", "") or "未命名规则", "group": groups.get(getattr(rule, "rule_id", "") or "", getattr(rule, "group", "") or "未分组"), "risk": str(getattr(rule, "risk", "medium") or "medium"), "score": str(getattr(rule, "score", "0") or "0"), "type": str(getattr(rule, "type", "deterministic") or "deterministic"), "checkTypes": [item for item in check_types if item], "logic": str(getattr(rule, "logic", "") or ""), "subRules": stage_items, "subRuleIds": list(getattr(rule, "rules", []) or []), "scope": list(getattr(rule, "scope", []) or []), "dependencies": _extract_rule_dependencies(rule), "stageCount": len(stage_items), "appliesIn": list(getattr(rule, "applies_in", []) or []), "prompt": stage_items[0]["content"] if len(stage_items) == 1 and stage_items[0]["check"] == "ai" else "", "description": str(getattr(rule, "desc", "") or ""), }) summary_map = { str(item.get("ruleId") or item.get("id") or ""): item for item in summaries if str(item.get("ruleId") or item.get("id") or "") } for item in summaries: if item.get("dependencies"): continue sub_rule_ids = [str(sub_rule_id or "").strip() for sub_rule_id in list(item.get("subRuleIds") or []) if str(sub_rule_id or "").strip()] if not sub_rule_ids: continue merged_dependencies: list[str] = [] seen_dependencies: set[str] = set() for sub_rule_id in sub_rule_ids: child_summary = summary_map.get(sub_rule_id) if not child_summary: continue for dependency in list(child_summary.get("dependencies") or []): normalized_dependency = str(dependency or "").strip() if not normalized_dependency or normalized_dependency in seen_dependencies: continue seen_dependencies.add(normalized_dependency) merged_dependencies.append(normalized_dependency) item["dependencies"] = merged_dependencies summary = { "loaded": True, "yaml_name": str(getattr(getattr(rules_file, "metadata", None), "name", "") or ""), "rules": summaries, } self._yaml_summary_cache[version_id] = (time.monotonic(), summary) return version_id, summary except Exception: summary = {"loaded": False, "yaml_name": "", "rules": []} self._yaml_summary_cache[version_id] = (time.monotonic(), summary) return version_id, summary results = await asyncio.gather(*[_load_one(version_id, oss_url) for version_id, oss_url in version_oss_map.items()]) return dict(results) async def _load_latest_version_id(self, rule_set_id: int) -> int | None: async with GetAsyncSession() as session: row = ( await session.execute( text( """ SELECT id FROM leaudit_rule_versions WHERE rule_set_id = :rule_set_id AND deleted_at IS NULL ORDER BY version_seq DESC, id DESC LIMIT 1 """ ), {"rule_set_id": rule_set_id}, ) ).mappings().first() return self._to_int(row.get("id")) if row else None async def _load_version_seq_by_id(self, version_id: int | None) -> int: if version_id is None: return -1 async with GetAsyncSession() as session: row = ( await session.execute( text( """ SELECT version_seq FROM leaudit_rule_versions WHERE id = :version_id AND deleted_at IS NULL LIMIT 1 """ ), {"version_id": version_id}, ) ).mappings().first() return int(row.get("version_seq") or -1) if row else -1 def _to_int(self, value) -> int | None: if value is None: return None return int(value) @staticmethod def _summary_cache_key(CurrentUserId: int | None) -> str: return f"user:{CurrentUserId or 0}" _RULE_CONFIG_SERVICE_SINGLETON: RuleConfigServiceImpl | None = None def GetRuleConfigServiceSingleton() -> RuleConfigServiceImpl: """返回共享的规则配置服务实例,供控制器和预热任务共用缓存。""" global _RULE_CONFIG_SERVICE_SINGLETON if _RULE_CONFIG_SERVICE_SINGLETON is None: _RULE_CONFIG_SERVICE_SINGLETON = RuleConfigServiceImpl() return _RULE_CONFIG_SERVICE_SINGLETON