- 新增本体字段注册表与字段治理审计脚本 - 重构风险规则模板执行器、DSL 验证与清单分类器 - 完善票据夹服务与差旅请求详情页交互 - 优化趋势图表与总览页数据展示 - 增强报销平台风险分级与模拟公司筛选 - 补充本体字段、风险规则生成与票据夹服务测试覆盖
175 lines
6.7 KiB
Python
175 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def build_risk_rule_execution_trace(
|
|
manifest: dict[str, Any],
|
|
*,
|
|
result: dict[str, Any] | None,
|
|
) -> dict[str, Any]:
|
|
evidence = result.get("evidence") if isinstance(result, dict) else {}
|
|
if not isinstance(evidence, dict):
|
|
evidence = {}
|
|
matched = isinstance(result, dict)
|
|
severity = _risk_severity(manifest) if matched else "none"
|
|
steps = _build_condition_steps(manifest, evidence)
|
|
if not steps:
|
|
steps = [_generic_step(manifest, evidence, matched)]
|
|
path_node_ids = ["start", "evidence", *[step["node_id"] for step in steps]]
|
|
path_node_ids.append("hit" if matched else "pass")
|
|
return {
|
|
"matched": matched,
|
|
"risk_level": severity,
|
|
"risk_score": _risk_score(manifest),
|
|
"path_node_ids": _dedupe(path_node_ids),
|
|
"steps": steps,
|
|
}
|
|
|
|
|
|
def _build_condition_steps(manifest: dict[str, Any], evidence: dict[str, Any]) -> list[dict[str, Any]]:
|
|
steps: list[dict[str, Any]] = []
|
|
condition_results = evidence.get("condition_results")
|
|
condition_evidence = evidence.get("conditions")
|
|
if isinstance(condition_results, dict):
|
|
evidence_by_id = {
|
|
str(item.get("id") or ""): item
|
|
for item in condition_evidence
|
|
if isinstance(item, dict)
|
|
} if isinstance(condition_evidence, list) else {}
|
|
for condition_id, passed in condition_results.items():
|
|
item = evidence_by_id.get(str(condition_id), {})
|
|
steps.append(
|
|
{
|
|
"node_id": str(condition_id),
|
|
"title": _condition_title(manifest, str(condition_id)),
|
|
"result": bool(passed),
|
|
"operator": str(item.get("operator") or ""),
|
|
"inputs": _compact_inputs(item),
|
|
}
|
|
)
|
|
return steps
|
|
|
|
city_consistency = evidence.get("city_consistency")
|
|
if isinstance(city_consistency, dict):
|
|
steps.append(
|
|
{
|
|
"node_id": "city_consistency",
|
|
"title": "城市一致性判断",
|
|
"result": bool(
|
|
city_consistency.get("unexpected_route_cities")
|
|
or not _has_overlap(
|
|
city_consistency.get("attachment_values"),
|
|
city_consistency.get("reference_values"),
|
|
)
|
|
),
|
|
"operator": "route_city_consistency",
|
|
"inputs": {
|
|
"application_reference_values": city_consistency.get("application_reference_values") or [],
|
|
"claim_reference_values": city_consistency.get("claim_reference_values") or [],
|
|
"attachment_values": city_consistency.get("attachment_values") or [],
|
|
"reference_values": city_consistency.get("reference_values") or [],
|
|
"unexpected_route_cities": city_consistency.get("unexpected_route_cities") or [],
|
|
"explanation_hits": city_consistency.get("explanation_hits") or [],
|
|
},
|
|
}
|
|
)
|
|
return steps
|
|
|
|
failed_conditions = evidence.get("failed_conditions")
|
|
if isinstance(failed_conditions, list):
|
|
for index, item in enumerate(failed_conditions, start=1):
|
|
if not isinstance(item, dict):
|
|
continue
|
|
steps.append(
|
|
{
|
|
"node_id": str(item.get("id") or f"condition_{index}"),
|
|
"title": _condition_title(manifest, str(item.get("id") or f"condition_{index}")),
|
|
"result": True,
|
|
"operator": str(item.get("operator") or ""),
|
|
"inputs": _compact_inputs(item),
|
|
}
|
|
)
|
|
return steps
|
|
|
|
|
|
def _generic_step(
|
|
manifest: dict[str, Any],
|
|
evidence: dict[str, Any],
|
|
matched: bool,
|
|
) -> dict[str, Any]:
|
|
params = manifest.get("params") if isinstance(manifest.get("params"), dict) else {}
|
|
return {
|
|
"node_id": "decision",
|
|
"title": "规则判断",
|
|
"result": matched,
|
|
"operator": str(params.get("template_key") or manifest.get("template_key") or ""),
|
|
"inputs": {
|
|
"condition_summary": evidence.get("condition_summary") or params.get("condition_summary") or "",
|
|
"missing_fields": evidence.get("missing_fields") or [],
|
|
"keyword_hits": evidence.get("keyword_hits") or [],
|
|
},
|
|
}
|
|
|
|
|
|
def _condition_title(manifest: dict[str, Any], condition_id: str) -> str:
|
|
params = manifest.get("params") if isinstance(manifest.get("params"), dict) else {}
|
|
conditions = params.get("conditions") if isinstance(params.get("conditions"), list) else []
|
|
for index, condition in enumerate(conditions, start=1):
|
|
if not isinstance(condition, dict):
|
|
continue
|
|
current_id = str(condition.get("id") or f"condition_{index}")
|
|
if current_id == condition_id:
|
|
return str(condition.get("title") or condition.get("operator") or condition_id)
|
|
return condition_id
|
|
|
|
|
|
def _compact_inputs(item: dict[str, Any]) -> dict[str, Any]:
|
|
keys = (
|
|
"fields",
|
|
"left_fields",
|
|
"right_fields",
|
|
"left_values",
|
|
"right_values",
|
|
"values",
|
|
"missing_fields",
|
|
"keyword_hits",
|
|
"dates",
|
|
"range_start",
|
|
"range_end",
|
|
"outside_dates",
|
|
)
|
|
return {key: item.get(key) for key in keys if item.get(key) not in (None, "", [])}
|
|
|
|
|
|
def _risk_severity(manifest: dict[str, Any]) -> str:
|
|
outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {}
|
|
fail = outcomes.get("fail") if isinstance(outcomes.get("fail"), dict) else {}
|
|
return str(fail.get("severity") or "medium")
|
|
|
|
|
|
def _risk_score(manifest: dict[str, Any]) -> int | None:
|
|
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {}
|
|
fail = outcomes.get("fail") if isinstance(outcomes.get("fail"), dict) else {}
|
|
for value in (fail.get("risk_score"), metadata.get("risk_score")):
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
return None
|
|
|
|
|
|
def _has_overlap(left: Any, right: Any) -> bool:
|
|
left_set = {str(item).strip().lower() for item in left or [] if str(item).strip()}
|
|
right_set = {str(item).strip().lower() for item in right or [] if str(item).strip()}
|
|
return bool(left_set & right_set)
|
|
|
|
|
|
def _dedupe(values: list[str]) -> list[str]:
|
|
rows: list[str] = []
|
|
for value in values:
|
|
if value and value not in rows:
|
|
rows.append(value)
|
|
return rows
|