feat: 增强风险规则生成引擎与预算中心页面
后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块, 优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强 报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图 组件,重构审计页面和风险规则测试对话框交互,完善文档中心 和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
119
server/src/app/services/risk_rule_generation_semantics.py
Normal file
119
server/src/app/services/risk_rule_generation_semantics.py
Normal 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无交集且无合理说明,"
|
||||
"或票据路线中存在不属于B∪C的额外城市,则命中目的地不一致/中途周转异常风险。"
|
||||
)
|
||||
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 B∪C)"
|
||||
),
|
||||
"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,
|
||||
}
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user