feat: 重构报销单服务并完善前端提交与审核交互

重构 expense_claims 服务模块结构并优化差旅票据审核逻辑,
增强用户代理服务的票据类型识别,前端报销创建页面拆分为
附件模型和会话模型模块,重构提交编排器和草稿关联确认流
程,更新知识库索引,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-22 08:58:59 +08:00
parent f6f787ff38
commit 5fe3b201d9
42 changed files with 13697 additions and 9496 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -3354,23 +3354,23 @@ class UserAgentService:
location = slots.get("location")
customer = slots.get("customer_name")
summary = "我先根据您当前提供的信息整理出一笔报销"
summary = "我先根据您当前提供的信息整理出一笔报销"
if expense_type and expense_type.value:
summary = f"识别到您希望报销一笔“{expense_type.value}”费用"
summary = f"识别到您希望报销一笔“{expense_type.value}”费用"
details: list[str] = []
if customer and customer.value:
details.append(f"客户{customer.value}")
details.append(f"客户{customer.value}")
if time_range and time_range.value:
details.append(f"时间{time_range.value}")
details.append(f"时间{time_range.value}")
if location and location.value:
details.append(f"地点{location.value}")
details.append(f"地点{location.value}")
if amount and amount.value:
details.append(f"金额{amount.value}")
details.append(f"金额{amount.value}")
reason = slots.get("reason")
if reason and reason.value:
details.append(f"事由{reason.value}")
details.append(f"事由{reason.value}")
if details:
return f"{summary} {''.join(details)}"
return "\n\n".join([summary, "基础信息识别结果:", "\n".join(details)])
return summary
def _build_review_body_answer(
@@ -3399,6 +3399,11 @@ class UserAgentService:
slot_cards=review_payload.slot_cards,
claim_groups=review_payload.claim_groups,
)
if payload.tool_payload.get("duplicate_attachment_blocked") or payload.tool_payload.get("duplicate_invoice_blocked"):
return (
str(payload.tool_payload.get("message") or "").strip()
or "检测到本次上传票据与当前单据已有票据重复,请重新上传不同的票据后再归集。"
)
if review_action == "save_draft":
if draft_payload is not None and draft_payload.claim_no:
return (
@@ -3441,7 +3446,7 @@ class UserAgentService:
)
return str(payload.tool_payload.get("message") or "").strip() or "当前报销单暂时还不能提交审批。"
return (
f"{self._build_review_intent_summary(payload, slot_cards=review_payload.slot_cards, claim_groups=review_payload.claim_groups)} "
f"{self._build_review_intent_summary(payload, slot_cards=review_payload.slot_cards, claim_groups=review_payload.claim_groups)}\n\n"
"当前关键信息已基本齐全,您确认无误后可以继续下一步。"
)
return review_payload.body_message or None
@@ -3497,7 +3502,7 @@ class UserAgentService:
expense_type_slot = next((item for item in slot_cards if item.key == "expense_type"), None)
if expense_type_slot is not None and not str(expense_type_slot.value or "").strip():
return (
f"{self._build_review_intent_summary(payload, slot_cards=slot_cards, claim_groups=[])} "
f"{self._build_review_intent_summary(payload, slot_cards=slot_cards, claim_groups=[])}\n\n"
"我已经先保留了当前识别出的时间、地点和事由,但还不能确定这张单据应该走哪类报销流程。"
"请先点击“选择报销类型”,在差旅费、交通费、住宿费等选项中选定;"
"选定后,后续上传的票据都会作为这张单据的补充继续核对,不会重新改判报销类型。"
@@ -3616,17 +3621,17 @@ class UserAgentService:
[
"报销测算参考:",
"",
(
f"职级 {calculation.grade},目的地 {destination},匹配城市 {calculation.matched_city}"
"补齐交通、酒店等票据后,我会按真实票据金额和规则中心标准重新复核。"
),
"",
"| 项目 | 测算口径 | 金额 |",
"| --- | --- | ---: |",
f"| 交通票据 | {ticket_basis} | {self._format_decimal_money(ticket_amount)} 元 |",
f"| 住宿标准 | {self._format_decimal_money(calculation.hotel_rate)} 元/天 × {calculation.days} 天 | {self._format_decimal_money(calculation.hotel_amount)} 元 |",
f"| 出差补贴 | {self._format_decimal_money(calculation.total_allowance_rate)} 元/天 × {calculation.days} 天 | {self._format_decimal_money(calculation.allowance_amount)} 元 |",
f"| 参考合计 | 交通票据 + 住宿标准 + 出差补贴 | {self._format_decimal_money(total_amount)} 元 |",
"",
(
f"测算依据:职级 {calculation.grade},目的地 {destination},匹配城市 {calculation.matched_city}"
"补齐交通、酒店等票据后,我会按真实票据金额和规则中心标准重新复核。"
),
]
)
@@ -3850,7 +3855,6 @@ class UserAgentService:
*,
mention_save_draft: bool,
) -> str:
missing_count = len(review_payload.missing_slots)
reminder_count = len(review_payload.risk_briefs)
if review_payload.can_proceed:
@@ -3861,18 +3865,7 @@ class UserAgentService:
)
return "当前关键信息已基本齐全,您确认无误后可以继续下一步。"
issue_parts: list[str] = []
if missing_count:
issue_parts.append(f"{missing_count} 项信息待补充")
if reminder_count:
issue_parts.append(f"{reminder_count} 条提醒")
issue_summary = "".join(issue_parts) if issue_parts else "一些细节还需要进一步确认"
suffix = ";如果想先暂存,也可以点击对话文字中的“草稿”。" if mention_save_draft else ""
return (
f"当前还有 {issue_summary}"
f"请核查对话中的文字说明{suffix}"
)
return ""
@staticmethod
def _can_proceed_review(