731 lines
34 KiB
Python
731 lines
34 KiB
Python
"""规则配置页聚合服务实现。"""
|
|
|
|
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
|