后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块, 优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强 报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图 组件,重构审计页面和风险规则测试对话框交互,完善文档中心 和报销创建页面细节,补充单元测试覆盖。
120 lines
5.0 KiB
Python
120 lines
5.0 KiB
Python
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,
|
||
}
|
||
],
|
||
}
|