from __future__ import annotations from typing import Any BUDGET_RISK_STAGES = {"budget_execution", "budget_control", "budget_review"} def is_budget_risk_manifest(manifest: dict[str, Any]) -> bool: """判断规则是否属于预算治理风险,而不是普通费用行为风险。""" if not isinstance(manifest, dict): return False metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {} applies_to = manifest.get("applies_to") if isinstance(manifest.get("applies_to"), dict) else {} rule_code = str(manifest.get("rule_code") or "").strip().lower() finance_rule_code = str( manifest.get("finance_rule_code") or metadata.get("finance_rule_code") or "" ).strip().lower() if rule_code.startswith("risk.budget.") or rule_code.startswith("budget."): return True if finance_rule_code.startswith("budget."): return True if _normalized_text(manifest.get("risk_domain") or metadata.get("risk_domain")) == "budget": return True domains = {_normalized_text(value) for value in _as_list(applies_to.get("domains"))} if "budget" in domains and not domains.difference({"budget"}): return True stages = { _normalized_text(value) for value in [ *_as_list(manifest.get("business_stage")), *_as_list(metadata.get("business_stage")), *_as_list(applies_to.get("business_stages")), ] } if stages & BUDGET_RISK_STAGES: return True category_text = " ".join( str(value or "") for value in ( manifest.get("risk_category"), metadata.get("risk_category"), manifest.get("name"), ) ) if "预算" in category_text and any(key.startswith("budget.") for key in _iter_field_keys(manifest)): return True return any(key.startswith("budget.") for key in _iter_field_keys(manifest)) def _iter_field_keys(value: Any) -> list[str]: keys: list[str] = [] def visit(node: Any) -> None: if isinstance(node, dict): for key, item in node.items(): normalized_key = str(key or "").strip() if normalized_key in { "key", "field", "left", "right", "field_key", "fieldKey", }: _append_key(item) elif normalized_key in { "fields", "field_keys", "fieldKeys", "search_fields", "searchFields", "left_fields", "leftFields", "right_fields", "rightFields", "left_group", "leftGroup", "right_group", "rightGroup", "date_fields", "range_start_fields", "range_end_fields", }: for child in _as_list(item): _append_key(child) visit(item) return if isinstance(node, list): for item in node: visit(item) def _append_key(item: Any) -> None: text = str(item or "").strip().lower() if text and text not in keys: keys.append(text) visit(value) return keys def _as_list(value: Any) -> list[Any]: if isinstance(value, list): return value if isinstance(value, (tuple, set)): return list(value) if value in (None, ""): return [] return [value] def _normalized_text(value: Any) -> str: return str(value or "").strip().lower()