feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -15,12 +15,14 @@ from app.schemas.user_agent import (
|
||||
UserAgentSuggestedAction,
|
||||
)
|
||||
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
|
||||
from app.services.expense_claim_risk_stage import with_risk_business_stage
|
||||
from app.services.document_numbering import (
|
||||
build_document_number,
|
||||
generate_unique_expense_claim_no,
|
||||
)
|
||||
from app.services.user_agent_application_dates import expand_application_time_with_days
|
||||
from app.services.user_agent_application_locations import normalize_application_location
|
||||
from app.services.application_system_estimate import apply_application_system_estimate_to_facts
|
||||
|
||||
APPLICATION_CONTEXT_VALUES = {
|
||||
"application",
|
||||
@@ -152,7 +154,7 @@ class UserAgentApplicationMixin:
|
||||
"我已按「费用申请 / 事前审批」来处理这条内容。",
|
||||
"已识别信息:\n" + recognized_table,
|
||||
f"当前还需要补充:{missing_text}。",
|
||||
"请一次性补齐上述字段,我会继续生成模拟申请结果并让你确认是否提交。",
|
||||
"请一次性补齐上述字段,我会继续生成申请核对结果并让你确认是否提交。",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -170,7 +172,7 @@ class UserAgentApplicationMixin:
|
||||
|
||||
return "\n\n".join(
|
||||
[
|
||||
"这是模拟的费用申请结果,请核对:",
|
||||
"这是费用申请核对结果,请核对:",
|
||||
self._build_application_summary_table(facts),
|
||||
"请核对上述信息无误,确认无误后 [确认](#application-submit) 提交至审批流程。",
|
||||
]
|
||||
@@ -185,7 +187,11 @@ class UserAgentApplicationMixin:
|
||||
"transport_mode": "",
|
||||
"amount": "",
|
||||
"application_type": "",
|
||||
"applicant": "",
|
||||
"grade": "",
|
||||
"department": "",
|
||||
"position": "",
|
||||
"manager_name": "",
|
||||
"lodging_daily_cap": "",
|
||||
"subsidy_daily_cap": "",
|
||||
"transport_policy": "",
|
||||
@@ -193,6 +199,12 @@ class UserAgentApplicationMixin:
|
||||
"matched_city": "",
|
||||
"rule_name": "",
|
||||
"rule_version": "",
|
||||
"hotel_amount": "",
|
||||
"allowance_amount": "",
|
||||
"transport_estimated_amount": "",
|
||||
"transport_estimate_source": "",
|
||||
"transport_estimate_confidence": "",
|
||||
"policy_total_amount": "",
|
||||
}
|
||||
for message, is_current in self._iter_application_user_messages(payload):
|
||||
partial = {
|
||||
@@ -212,6 +224,41 @@ class UserAgentApplicationMixin:
|
||||
if value:
|
||||
facts[key] = value
|
||||
|
||||
context_json = payload.context_json or {}
|
||||
current_user = getattr(payload, "current_user", None)
|
||||
if not facts["applicant"]:
|
||||
facts["applicant"] = str(
|
||||
context_json.get("name")
|
||||
or context_json.get("user_name")
|
||||
or context_json.get("applicant")
|
||||
or getattr(current_user, "name", "")
|
||||
or ""
|
||||
).strip()
|
||||
if not facts["department"]:
|
||||
facts["department"] = str(
|
||||
context_json.get("department")
|
||||
or context_json.get("department_name")
|
||||
or context_json.get("departmentName")
|
||||
or getattr(current_user, "department_name", "")
|
||||
or ""
|
||||
).strip()
|
||||
if not facts["position"]:
|
||||
facts["position"] = str(
|
||||
context_json.get("position")
|
||||
or context_json.get("employee_position")
|
||||
or context_json.get("employeePosition")
|
||||
or ""
|
||||
).strip()
|
||||
if not facts["manager_name"]:
|
||||
facts["manager_name"] = str(
|
||||
context_json.get("manager_name")
|
||||
or context_json.get("managerName")
|
||||
or context_json.get("direct_manager_name")
|
||||
or context_json.get("directManagerName")
|
||||
or getattr(current_user, "manager_name", "")
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
if not facts["application_type"]:
|
||||
facts["application_type"] = self._infer_application_type(facts)
|
||||
facts["time"] = self._expand_application_time_with_days(
|
||||
@@ -219,6 +266,7 @@ class UserAgentApplicationMixin:
|
||||
facts.get("days", ""),
|
||||
payload.context_json or {},
|
||||
)
|
||||
apply_application_system_estimate_to_facts(facts)
|
||||
return facts
|
||||
|
||||
@staticmethod
|
||||
@@ -245,7 +293,11 @@ class UserAgentApplicationMixin:
|
||||
"days": pick("days"),
|
||||
"transport_mode": pick("transportMode", "transport_mode"),
|
||||
"amount": pick("amount"),
|
||||
"applicant": pick("applicant", "name", "userName", "user_name"),
|
||||
"grade": pick("grade"),
|
||||
"department": pick("department", "departmentName", "department_name"),
|
||||
"position": pick("position", "employeePosition", "employee_position"),
|
||||
"manager_name": pick("managerName", "manager_name", "directManagerName", "direct_manager_name"),
|
||||
"lodging_daily_cap": pick("lodgingDailyCap", "lodging_daily_cap"),
|
||||
"subsidy_daily_cap": pick("subsidyDailyCap", "subsidy_daily_cap"),
|
||||
"transport_policy": pick("transportPolicy", "transport_policy"),
|
||||
@@ -253,6 +305,12 @@ class UserAgentApplicationMixin:
|
||||
"matched_city": pick("matchedCity", "matched_city"),
|
||||
"rule_name": pick("ruleName", "rule_name"),
|
||||
"rule_version": pick("ruleVersion", "rule_version"),
|
||||
"hotel_amount": pick("hotelAmount", "hotel_amount"),
|
||||
"allowance_amount": pick("allowanceAmount", "allowance_amount"),
|
||||
"transport_estimated_amount": pick("transportEstimatedAmount", "transport_estimated_amount"),
|
||||
"transport_estimate_source": pick("transportEstimateSource", "transport_estimate_source"),
|
||||
"transport_estimate_confidence": pick("transportEstimateConfidence", "transport_estimate_confidence"),
|
||||
"policy_total_amount": pick("policyTotalAmount", "policy_total_amount"),
|
||||
}
|
||||
|
||||
def _resolve_expense_application_step(
|
||||
@@ -294,7 +352,7 @@ class UserAgentApplicationMixin:
|
||||
def _resolve_application_missing_followup_fields(facts: dict[str, str]) -> list[str]:
|
||||
return [
|
||||
field
|
||||
for field in ("transport_mode", "amount")
|
||||
for field in ("transport_mode",)
|
||||
if not str(facts.get(field) or "").strip()
|
||||
]
|
||||
|
||||
@@ -558,7 +616,7 @@ class UserAgentApplicationMixin:
|
||||
def _display_application_slot_label(slot: str) -> str:
|
||||
return {
|
||||
"expense_type": "申请类型",
|
||||
"amount": "用户预估费用",
|
||||
"amount": "系统预估费用",
|
||||
"time_range": "发生时间",
|
||||
"time": "发生时间",
|
||||
"location": "地点",
|
||||
@@ -603,7 +661,7 @@ class UserAgentApplicationMixin:
|
||||
"reason": ("补充申请事由", "事由:"),
|
||||
"days": ("补充天数", "天数:"),
|
||||
"transport_mode": ("补充出行方式", "出行方式:"),
|
||||
"amount": ("补充预估费用", "用户预估费用:"),
|
||||
"amount": ("补充系统预估费用", "系统预估费用:"),
|
||||
}
|
||||
return config.get(field, ("补充申请信息", ""))
|
||||
|
||||
@@ -646,17 +704,21 @@ class UserAgentApplicationMixin:
|
||||
f"{label}:{value or '待补充'}"
|
||||
for label, value in (
|
||||
("申请类型", facts.get("application_type", "")),
|
||||
("姓名", facts.get("applicant", "")),
|
||||
("部门", facts.get("department", "")),
|
||||
("岗位", facts.get("position", "")),
|
||||
("职级", facts.get("grade", "")),
|
||||
("直属领导", facts.get("manager_name", "")),
|
||||
("发生时间", facts.get("time", "")),
|
||||
("地点", facts.get("location", "")),
|
||||
("事由", facts.get("reason", "")),
|
||||
("天数", facts.get("days", "")),
|
||||
("出行方式", facts.get("transport_mode", "")),
|
||||
("职级", facts.get("grade", "")),
|
||||
("住宿上限/天", facts.get("lodging_daily_cap", "")),
|
||||
("补贴标准/天", facts.get("subsidy_daily_cap", "")),
|
||||
("交通费用口径", facts.get("transport_policy", "")),
|
||||
("规则测算参考", facts.get("policy_estimate", "")),
|
||||
("用户预估费用", facts.get("amount", "")),
|
||||
("系统预估费用", facts.get("amount", "")),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -668,17 +730,21 @@ class UserAgentApplicationMixin:
|
||||
) -> str:
|
||||
rows = [
|
||||
("申请类型", facts.get("application_type", "")),
|
||||
("姓名", facts.get("applicant", "")),
|
||||
("部门", facts.get("department", "")),
|
||||
("岗位", facts.get("position", "")),
|
||||
("职级", facts.get("grade", "")),
|
||||
("直属领导", facts.get("manager_name", "")),
|
||||
("发生时间", facts.get("time", "")),
|
||||
("地点", facts.get("location", "")),
|
||||
("事由", facts.get("reason", "")),
|
||||
("天数", facts.get("days", "")),
|
||||
("出行方式", facts.get("transport_mode", "")),
|
||||
("职级", facts.get("grade", "")),
|
||||
("住宿上限/天", facts.get("lodging_daily_cap", "")),
|
||||
("补贴标准/天", facts.get("subsidy_daily_cap", "")),
|
||||
("交通费用口径", facts.get("transport_policy", "")),
|
||||
("规则测算参考", facts.get("policy_estimate", "")),
|
||||
("用户预估费用", facts.get("amount", "")),
|
||||
("系统预估费用", facts.get("amount", "")),
|
||||
]
|
||||
visible_rows = rows if include_empty else [(label, value) for label, value in rows if str(value or "").strip()]
|
||||
if not visible_rows:
|
||||
@@ -736,34 +802,53 @@ class UserAgentApplicationMixin:
|
||||
risk_flags_json=[self._build_application_detail_flag(facts)],
|
||||
)
|
||||
self.db.add(claim)
|
||||
self.db.flush()
|
||||
from app.services.expense_claims import ExpenseClaimService
|
||||
|
||||
platform_review = ExpenseClaimService(self.db).evaluate_platform_risk_rules(
|
||||
claim,
|
||||
business_stage="expense_application",
|
||||
)
|
||||
platform_flags = list(platform_review.get("flags") or [])
|
||||
if platform_flags:
|
||||
claim.risk_flags_json = [*list(claim.risk_flags_json or []), *platform_flags]
|
||||
self.db.commit()
|
||||
self.db.refresh(claim)
|
||||
return claim
|
||||
|
||||
@staticmethod
|
||||
def _build_application_detail_flag(facts: dict[str, str]) -> dict[str, object]:
|
||||
return {
|
||||
"source": "application_detail",
|
||||
"severity": "info",
|
||||
"label": "申请详情",
|
||||
"application_detail": {
|
||||
"application_type": str(facts.get("application_type") or "").strip(),
|
||||
"time": str(facts.get("time") or "").strip(),
|
||||
"location": str(facts.get("location") or "").strip(),
|
||||
"reason": str(facts.get("reason") or "").strip(),
|
||||
"days": str(facts.get("days") or "").strip(),
|
||||
"transport_mode": str(facts.get("transport_mode") or "").strip(),
|
||||
"amount": str(facts.get("amount") or "").strip(),
|
||||
"grade": str(facts.get("grade") or "").strip(),
|
||||
"lodging_daily_cap": str(facts.get("lodging_daily_cap") or "").strip(),
|
||||
"subsidy_daily_cap": str(facts.get("subsidy_daily_cap") or "").strip(),
|
||||
"transport_policy": str(facts.get("transport_policy") or "").strip(),
|
||||
"policy_estimate": str(facts.get("policy_estimate") or "").strip(),
|
||||
"matched_city": str(facts.get("matched_city") or "").strip(),
|
||||
"rule_name": str(facts.get("rule_name") or "").strip(),
|
||||
"rule_version": str(facts.get("rule_version") or "").strip(),
|
||||
return with_risk_business_stage(
|
||||
{
|
||||
"source": "application_detail",
|
||||
"severity": "info",
|
||||
"label": "申请详情",
|
||||
"application_detail": {
|
||||
"application_type": str(facts.get("application_type") or "").strip(),
|
||||
"time": str(facts.get("time") or "").strip(),
|
||||
"location": str(facts.get("location") or "").strip(),
|
||||
"reason": str(facts.get("reason") or "").strip(),
|
||||
"days": str(facts.get("days") or "").strip(),
|
||||
"transport_mode": str(facts.get("transport_mode") or "").strip(),
|
||||
"amount": str(facts.get("amount") or "").strip(),
|
||||
"grade": str(facts.get("grade") or "").strip(),
|
||||
"lodging_daily_cap": str(facts.get("lodging_daily_cap") or "").strip(),
|
||||
"subsidy_daily_cap": str(facts.get("subsidy_daily_cap") or "").strip(),
|
||||
"transport_policy": str(facts.get("transport_policy") or "").strip(),
|
||||
"policy_estimate": str(facts.get("policy_estimate") or "").strip(),
|
||||
"matched_city": str(facts.get("matched_city") or "").strip(),
|
||||
"rule_name": str(facts.get("rule_name") or "").strip(),
|
||||
"rule_version": str(facts.get("rule_version") or "").strip(),
|
||||
"hotel_amount": str(facts.get("hotel_amount") or "").strip(),
|
||||
"allowance_amount": str(facts.get("allowance_amount") or "").strip(),
|
||||
"transport_estimated_amount": str(facts.get("transport_estimated_amount") or "").strip(),
|
||||
"transport_estimate_source": str(facts.get("transport_estimate_source") or "").strip(),
|
||||
"transport_estimate_confidence": str(facts.get("transport_estimate_confidence") or "").strip(),
|
||||
"policy_total_amount": str(facts.get("policy_total_amount") or "").strip(),
|
||||
},
|
||||
},
|
||||
}
|
||||
"expense_application",
|
||||
)
|
||||
|
||||
def _resolve_application_manager_name(
|
||||
self,
|
||||
@@ -810,6 +895,13 @@ class UserAgentApplicationMixin:
|
||||
or context_json.get("departmentName")
|
||||
or ""
|
||||
).strip(),
|
||||
manager_name=str(
|
||||
context_json.get("manager_name")
|
||||
or context_json.get("managerName")
|
||||
or context_json.get("direct_manager_name")
|
||||
or context_json.get("directManagerName")
|
||||
or ""
|
||||
).strip(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user