Files
X-Financial/server/src/app/services/risk_rule_generation_semantics.py
caoxiaozhu 0e861d8fa6 feat: 增强风险规则生成引擎与预算中心页面
后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块,
优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强
报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图
组件,重构审计页面和风险规则测试对话框交互,完善文档中心
和报销创建页面细节,补充单元测试覆盖。
2026-05-26 09:15:14 +08:00

120 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
],
}