feat: 增加差旅报销标准测算和财务终审流程

新增差旅报销测算接口及 Spreadsheet 规则解析,审批流程拆分
直属领导审批与财务终审两阶段并细分权限,修复 PDF 文本层
缺失时自动回退 OCR,提交后清理关联会话,前端适配审批流
交互并补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-21 09:28:33 +08:00
parent 002bf4f756
commit 8f65661809
43 changed files with 4366 additions and 410 deletions

View File

@@ -39,6 +39,7 @@ class AgentConversationService:
normalized_id = str(conversation_id or "").strip()
normalized_user_id = str(user_id or "").strip() or None
incoming_session_type = str(context_json.get("session_type") or "").strip() or "expense"
incoming_draft_claim_id = self._resolve_draft_claim_id(context_json)
conversation = self.get_conversation(normalized_id) if normalized_id else None
if conversation is not None and conversation.user_id != normalized_user_id:
normalized_id = ""
@@ -56,6 +57,7 @@ class AgentConversationService:
source=source,
entry_source=str(context_json.get("entry_source") or "").strip() or None,
title=self._resolve_title(context_json),
draft_claim_id=incoming_draft_claim_id or None,
state_json=self._extract_state_json(context_json),
)
self.db.add(conversation)
@@ -69,6 +71,8 @@ class AgentConversationService:
conversation.entry_source = str(context_json.get("entry_source") or "").strip() or None
if not conversation.title:
conversation.title = self._resolve_title(context_json)
if incoming_draft_claim_id:
conversation.draft_claim_id = incoming_draft_claim_id
conversation.state_json = self._merge_state_json(
conversation.state_json,
self._extract_state_json(context_json),
@@ -354,6 +358,38 @@ class AgentConversationService:
self.db.commit()
return len(conversations)
def delete_conversations_for_draft_claim(
self,
*,
claim_id: str | None,
source: str | None = "user_message",
session_type: str | None = "expense",
) -> int:
normalized_claim_id = str(claim_id or "").strip()
if not normalized_claim_id:
return 0
stmt = select(AgentConversation).where(AgentConversation.draft_claim_id == normalized_claim_id)
if source:
stmt = stmt.where(AgentConversation.source == source)
conversations = list(self.db.scalars(stmt).all())
normalized_session_type = str(session_type or "").strip()
if normalized_session_type:
conversations = [
conversation
for conversation in conversations
if (str((conversation.state_json or {}).get("session_type") or "").strip() or "expense")
== normalized_session_type
]
if not conversations:
return 0
for conversation in conversations:
self.db.delete(conversation)
self.db.commit()
return len(conversations)
def delete_conversation(
self,
*,
@@ -478,11 +514,28 @@ class AgentConversationService:
continue
state_json[key] = value
draft_claim_id = str(context_json.get("draft_claim_id") or "").strip()
draft_claim_id = AgentConversationService._resolve_draft_claim_id(context_json)
if draft_claim_id:
state_json["draft_claim_id"] = draft_claim_id
return state_json
@staticmethod
def _resolve_draft_claim_id(context_json: dict[str, Any]) -> str:
draft_claim_id = str((context_json or {}).get("draft_claim_id") or "").strip()
if draft_claim_id:
return draft_claim_id
request_context = (context_json or {}).get("request_context")
if isinstance(request_context, dict):
return str(
request_context.get("claim_id")
or request_context.get("claimId")
or request_context.get("draft_claim_id")
or request_context.get("draftClaimId")
or ""
).strip()
return ""
@staticmethod
def _merge_state_json(
current_state: dict[str, Any] | None,