feat: 本体字段治理与风险规则模板执行器重构

- 新增本体字段注册表与字段治理审计脚本
- 重构风险规则模板执行器、DSL 验证与清单分类器
- 完善票据夹服务与差旅请求详情页交互
- 优化趋势图表与总览页数据展示
- 增强报销平台风险分级与模拟公司筛选
- 补充本体字段、风险规则生成与票据夹服务测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 15:46:56 +08:00
parent e12b140508
commit 34457f9c3e
81 changed files with 4858 additions and 1073 deletions

View File

@@ -38,6 +38,7 @@ from app.services.expense_rule_runtime import ExpenseRuleRuntimeService, Runtime
from app.services.risk_ontology_bridge import resolve_rule_codes_for_risk_check
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
from app.services.expense_type_keywords import resolve_expense_type_label_from_text
from app.services.ontology_field_registry import normalize_ontology_form_values
from app.services.user_agent_constants import *
@@ -151,10 +152,9 @@ class UserAgentReviewSlotMixin:
def _resolve_location_value(self, payload: UserAgentRequest) -> str:
review_form_values = self._resolve_review_form_values(payload)
for key in ("business_location", "location"):
value = str(review_form_values.get(key) or "").strip()
if value:
return value
value = str(review_form_values.get("location") or "").strip()
if value:
return value
if str(payload.context_json.get("entry_source") or "").strip() == "detail":
request_context = payload.context_json.get("request_context")
@@ -181,21 +181,7 @@ class UserAgentReviewSlotMixin:
@staticmethod
def _resolve_review_form_values(payload: UserAgentRequest) -> dict[str, str]:
values = payload.context_json.get("review_form_values")
if not isinstance(values, dict):
return {}
normalized: dict[str, str] = {}
for key, value in values.items():
cleaned_key = str(key or "").strip()
if not cleaned_key:
continue
normalized[cleaned_key] = str(value or "").strip()
if not normalized.get("transport_mode"):
for alias in ("transportMode", "application_transport_mode", "applicationTransportMode"):
if normalized.get(alias):
normalized["transport_mode"] = normalized[alias]
break
return normalized
return normalize_ontology_form_values(payload.context_json.get("review_form_values"))
@staticmethod
@@ -220,12 +206,7 @@ class UserAgentReviewSlotMixin:
def _build_time_slot(self, payload: UserAgentRequest) -> dict[str, str | float]:
review_form_values = self._resolve_review_form_values(payload)
edited_value = str(
review_form_values.get("time_range")
or review_form_values.get("business_time")
or review_form_values.get("occurred_date")
or ""
).strip()
edited_value = str(review_form_values.get("time_range") or "").strip()
if edited_value:
raw_value = str(review_form_values.get("time_range_raw") or edited_value).strip()
return self._build_slot_value(
@@ -237,17 +218,6 @@ class UserAgentReviewSlotMixin:
evidence="来源于用户修改后的结构化表单。",
)
application_time = str(review_form_values.get("application_business_time") or "").strip()
if application_time:
return self._build_slot_value(
value=application_time,
raw_value=application_time,
normalized_value=application_time,
source="detail_context",
confidence=0.86,
evidence="来源于已关联申请单,作为本次报销草稿的发生时间依据。",
)
time_range = payload.ontology.time_range
if time_range.start_date and time_range.end_date:
normalized_value = (
@@ -270,25 +240,14 @@ class UserAgentReviewSlotMixin:
def _build_location_slot(self, payload: UserAgentRequest) -> dict[str, str | float]:
review_form_values = self._resolve_review_form_values(payload)
for key in ("business_location", "location"):
value = str(review_form_values.get(key) or "").strip()
if value:
return self._build_slot_value(
value=value,
normalized_value=value,
source="user_form",
confidence=1.0,
evidence="来源于用户修改后的结构化表单。",
)
application_location = str(review_form_values.get("application_location") or "").strip()
if application_location:
value = str(review_form_values.get("location") or "").strip()
if value:
return self._build_slot_value(
value=application_location,
normalized_value=application_location,
source="detail_context",
confidence=0.86,
evidence="来源于已关联申请单,作为本次报销草稿的地点依据",
value=value,
normalized_value=value,
source="user_form",
confidence=1.0,
evidence="来源于用户修改后的结构化表单",
)
if str(payload.context_json.get("entry_source") or "").strip() == "detail":
@@ -396,17 +355,6 @@ class UserAgentReviewSlotMixin:
evidence="来源于用户修改后的结构化表单。",
)
application_reason = str(review_form_values.get("application_reason") or "").strip()
if application_reason:
return self._build_slot_value(
value=application_reason,
raw_value=application_reason,
normalized_value=application_reason,
source="detail_context",
confidence=0.9,
evidence="来源于已关联申请单,作为本次报销草稿的事由依据。",
)
inferred_reason = self._infer_reason_from_claim_groups(
claim_groups=claim_groups,
)
@@ -457,22 +405,6 @@ class UserAgentReviewSlotMixin:
evidence="来源于用户修改后的结构化表单。",
)
application_amount = str(
review_form_values.get("application_amount")
or review_form_values.get("application_amount_label")
or ""
).strip()
if application_amount:
normalized = self._normalize_amount_text(application_amount)
return self._build_slot_value(
value=normalized,
raw_value=application_amount,
normalized_value=normalized,
source="detail_context",
confidence=0.86,
evidence="来源于已关联申请单,作为本次报销草稿的金额依据。",
)
amount_value = entity_map.get("amount", "")
if amount_value:
normalized = self._normalize_amount_text(amount_value)
@@ -506,7 +438,7 @@ class UserAgentReviewSlotMixin:
ocr_documents: list[dict[str, object]],
) -> dict[str, str | float]:
review_form_values = self._resolve_review_form_values(payload)
edited_value = str(review_form_values.get("expense_type") or review_form_values.get("reimbursement_type") or "").strip()
edited_value = str(review_form_values.get("expense_type") or "").strip()
if edited_value:
normalized_code, normalized_label = self._normalize_expense_type_input(edited_value)
return self._build_slot_value(
@@ -581,7 +513,7 @@ class UserAgentReviewSlotMixin:
def _build_attachment_slot(self, payload: UserAgentRequest) -> dict[str, str | float]:
review_form_values = self._resolve_review_form_values(payload)
attachment_names = str(review_form_values.get("attachment_names") or "").strip()
attachment_names = str(review_form_values.get("attachments") or "").strip()
if attachment_names:
return self._build_slot_value(
value=attachment_names,