feat: 增强风险规则生成引擎与预算中心页面

后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块,
优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强
报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图
组件,重构审计页面和风险规则测试对话框交互,完善文档中心
和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-26 09:15:14 +08:00
parent d0e946cf47
commit 0e861d8fa6
150 changed files with 14953 additions and 4099 deletions

View File

@@ -0,0 +1,119 @@
from __future__ import annotations
from typing import Any
TRAVEL_ROUTE_CITY_SEMANTIC_TYPE = "travel_route_city_consistency"
LEGACY_CITY_CONSISTENCY_SEMANTIC_TYPE = "travel_city_consistency"
CITY_CONSISTENCY_SEMANTIC_TYPES = {
TRAVEL_ROUTE_CITY_SEMANTIC_TYPE,
LEGACY_CITY_CONSISTENCY_SEMANTIC_TYPE,
}
CITY_CONSISTENCY_SEMANTIC_TYPE = TRAVEL_ROUTE_CITY_SEMANTIC_TYPE
RISK_LEVEL_LABELS = {
"low": "低风险",
"medium": "中风险",
"high": "高风险",
"critical": "极高风险",
}
CITY_ATTACHMENT_FIELDS = ("attachment.route_cities", "attachment.hotel_city")
CITY_REFERENCE_FIELDS = ("claim.location", "item.item_location")
CITY_HOME_FIELDS = ("employee.location",)
CITY_EXCEPTION_FIELDS = ("claim.reason", "item.item_reason")
CITY_EXCEPTION_KEYWORDS = ("绕行", "跨城办事", "跨城", "临时改签", "改签", "变更")
def is_city_consistency_rule(text: str) -> bool:
normalized = str(text or "")
has_city_subject = any(
term in normalized
for term in ("交通票", "住宿票", "住宿发票", "票据", "附件", "行程城市", "住宿城市")
)
has_reference = any(
term in normalized
for term in ("申报目的地", "申报地点", "明细地点", "发生地点", "意图城市", "目的地")
)
has_relation = any(
term in normalized
for term in ("一致", "不一致", "形成一致关系", "匹配", "无法与", "对应")
)
has_route_anomaly = any(term in normalized for term in ("绕行", "跨城", "中转", "周转", "改签"))
return has_city_subject and has_reference and (has_relation or has_route_anomaly)
def build_city_consistency_draft(
draft: dict[str, Any],
*,
natural_language: str,
fields: list[Any],
risk_level: str,
) -> dict[str, Any]:
del natural_language
field_by_key = {field.key: field for field in fields}
field_keys = [
key
for key in (
*CITY_ATTACHMENT_FIELDS,
*CITY_REFERENCE_FIELDS,
*CITY_HOME_FIELDS,
*CITY_EXCEPTION_FIELDS,
)
if key in field_by_key
]
risk_label = RISK_LEVEL_LABELS.get(risk_level, "风险")
condition_summary = (
"判断公式A=交通票行程城市住宿发票城市B=申报目的地∪明细发生地点,"
"C=员工常驻地/合理出发地。若A或B为空则要求补充识别若A与B无交集且无合理说明"
"或票据路线中存在不属于BC的额外城市则命中目的地不一致/中途周转异常风险。"
)
flow = draft.get("flow") if isinstance(draft.get("flow"), dict) else {}
return {
**draft,
"template_key": "field_compare_v1",
"field_keys": field_keys,
"semantic_type": TRAVEL_ROUTE_CITY_SEMANTIC_TYPE,
"condition_summary": condition_summary,
"keywords": [],
"exception_keywords": list(CITY_EXCEPTION_KEYWORDS),
"flow": {
**flow,
"start": "差旅报销单据提交,并上传交通票据、住宿票据或其他可识别城市的附件",
"evidence": "读取员工常驻地、申报目的地、明细发生地点、交通票行程城市、住宿发票城市和报销事由",
"decision": "附件城市是否覆盖申报行程,且票据路线是否出现申报目的地和常驻地之外的中转城市",
"pass": "票据城市覆盖申报行程,且未出现申报目的地和常驻地之外的额外城市",
"fail": f"票据路线存在目的地不一致或额外中转城市,命中{risk_label}并要求补充说明或退回修改",
},
}
def build_city_consistency_params(draft: dict[str, Any]) -> dict[str, Any]:
exception_keywords = list(draft.get("exception_keywords") or CITY_EXCEPTION_KEYWORDS)
return {
"semantic_type": TRAVEL_ROUTE_CITY_SEMANTIC_TYPE,
"attachment_city_fields": list(CITY_ATTACHMENT_FIELDS),
"reference_city_fields": list(CITY_REFERENCE_FIELDS),
"home_city_fields": list(CITY_HOME_FIELDS),
"exception_fields": list(CITY_EXCEPTION_FIELDS),
"exception_keywords": exception_keywords,
"keywords": [],
"route_anomaly_policy": "flag_unexpected_intermediate_cities",
"exception_handling": "exception_text_is_evidence_not_auto_pass_for_route_anomaly",
"formula": (
"A=UNION(attachment.route_cities, attachment.hotel_city); "
"B=UNION(claim.location, item.item_location); "
"C=UNION(employee.location); "
"HIT WHEN (A∩B=∅ AND NOT CONTAINS_ANY(exception_fields, exception_keywords)) "
"OR EXISTS(city IN A WHERE city NOT IN BC)"
),
"conditions": [
{
"left_group": list(CITY_ATTACHMENT_FIELDS),
"operator": "route_city_consistency",
"right_group": list(CITY_REFERENCE_FIELDS),
"home_group": list(CITY_HOME_FIELDS),
"exception_fields": list(CITY_EXCEPTION_FIELDS),
"exception_keywords": exception_keywords,
}
],
}