feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -257,6 +257,130 @@ def build_risk_rule_flow_diagram_details(
}
def build_risk_rule_flow_diagram_spec(
payload: dict[str, Any],
*,
fields: tuple[RiskRuleFlowDiagramField, ...],
domain_label: str,
severity: str,
severity_label: str,
flow_model: dict[str, Any] | None = None,
) -> RiskRuleFlowDiagramSpec:
model_spec = _spec_from_flow_model(
payload,
fields=fields,
domain_label=domain_label,
severity=severity,
severity_label=severity_label,
flow_model=flow_model or {},
)
if model_spec:
return model_spec
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
flow = metadata.get("flow") if isinstance(metadata.get("flow"), dict) else {}
details = build_risk_rule_flow_diagram_details(payload, list(fields))
summary = str(metadata.get("condition_summary") or "").strip()
return RiskRuleFlowDiagramSpec(
title=str(payload.get("name") or "").strip() or "风险规则判断流程",
domain_label=domain_label,
severity=severity,
severity_label=severity_label,
fields=fields,
start=str(flow.get("start") or "").strip() or "业务单据提交",
evidence=str(flow.get("evidence") or "").strip() or "读取规则字段",
decision=str(flow.get("decision") or "").strip() or summary or "判断是否命中风险",
basis=summary or str(flow.get("decision") or "").strip() or "根据规则字段判断",
pass_text=str(flow.get("pass") or "").strip() or "未命中风险,继续流转",
fail_text=str(flow.get("fail") or "").strip() or f"命中{severity_label},进入人工复核",
fact_lines=details["fact_lines"],
condition_lines=details["condition_lines"],
hit_logic=str(details["hit_logic"] or ""),
)
def _spec_from_flow_model(
payload: dict[str, Any],
*,
fields: tuple[RiskRuleFlowDiagramField, ...],
domain_label: str,
severity: str,
severity_label: str,
flow_model: dict[str, Any],
) -> RiskRuleFlowDiagramSpec | None:
nodes = flow_model.get("nodes") if isinstance(flow_model, dict) else []
if not isinstance(nodes, list) or not nodes:
return None
by_type: dict[str, list[dict[str, Any]]] = {}
for node in nodes:
if isinstance(node, dict):
by_type.setdefault(str(node.get("type") or "").strip(), []).append(node)
decisions = by_type.get("decision") or []
if not decisions:
return None
start = _node_description(by_type.get("start"), "业务单据提交")
evidence = _node_description(by_type.get("evidence"), "读取规则字段")
pass_text = _node_description(by_type.get("pass"), "未命中风险,继续流转")
fail_text = _node_description(by_type.get("risk"), f"命中{severity_label},进入人工复核")
condition_lines = _condition_lines_from_flow_nodes(decisions)
basis = condition_lines[0] if condition_lines else _node_description(decisions, "判断是否命中风险")
return RiskRuleFlowDiagramSpec(
title=str(payload.get("name") or "").strip() or "风险规则判断流程",
domain_label=domain_label,
severity=severity,
severity_label=severity_label,
fields=fields,
start=start,
evidence=evidence,
decision=_node_description(decisions, basis),
basis=basis,
pass_text=pass_text,
fail_text=fail_text,
fact_lines=tuple(_field_lines_from_flow_nodes(by_type.get("evidence"), fields)),
condition_lines=tuple(condition_lines),
hit_logic=_hit_logic_from_flow_model(flow_model, condition_lines),
)
def _node_description(nodes: list[dict[str, Any]] | None, fallback: str) -> str:
node = nodes[0] if nodes else {}
return str(node.get("description") or node.get("title") or fallback).strip()
def _condition_lines_from_flow_nodes(nodes: list[dict[str, Any]]) -> list[str]:
visible = [
f"{str(node.get('title') or node.get('id') or '判断').strip()}: {str(node.get('description') or '').strip()}"
for node in nodes[:4]
]
if len(nodes) > 4:
visible[-1] = f"{visible[-1]};另有 {len(nodes) - 4} 个判断节点按命中逻辑汇总"
return visible
def _field_lines_from_flow_nodes(
nodes: list[dict[str, Any]] | None,
fields: tuple[RiskRuleFlowDiagramField, ...],
) -> list[str]:
field_keys = _read_string_list((nodes[0] if nodes else {}).get("fields"))
if not field_keys:
return [
f"{chr(65 + index)}={field.label or field.key}[{field.key}]"
for index, field in enumerate(fields[:4])
]
label_by_key = {field.key: field.label or field.key for field in fields}
return [
f"{chr(65 + index)}={label_by_key.get(key, key)}[{key}]"
for index, key in enumerate(field_keys[:4])
]
def _hit_logic_from_flow_model(flow_model: dict[str, Any], condition_lines: list[str]) -> str:
metadata = flow_model.get("metadata") if isinstance(flow_model.get("metadata"), dict) else {}
logic = str(metadata.get("hit_logic") or "").strip()
if logic:
return logic
return " AND ".join(line.split(":", 1)[0] for line in condition_lines[:4] if line)
def _build_fact_lines(
facts: list[Any],
fields: list[RiskRuleFlowDiagramField],
@@ -313,6 +437,15 @@ def _format_condition(condition: dict[str, Any], label_by_key: dict[str, str], i
start = _field_group(condition.get("range_start_fields"), label_by_key)
end = _field_group(condition.get("range_end_fields"), label_by_key)
return f"{prefix}{dates} 不在 [{start}, {end}]"
if operator == "numeric_compare":
left = _field_group(condition.get("left_fields") or condition.get("fields"), label_by_key)
right = _field_group(condition.get("right_fields"), label_by_key)
compare = str(condition.get("compare") or "gt").strip().upper()
target = right or str(condition.get("threshold") or condition.get("value") or "阈值").strip()
return f"{prefix}{left} {compare} {target}"
if operator == "duplicate_value":
fields = _field_group(condition.get("fields"), label_by_key)
return f"{prefix}{fields} 出现重复值"
if operator in {"contains_any", "not_contains_any"}:
fields = _field_group(condition.get("fields"), label_by_key)
keywords = "".join(_read_string_list(condition.get("keywords"))[:4])