feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-01 17:07:14 +08:00
parent 7989f3a159
commit 92444e7eae
285 changed files with 25075 additions and 2986 deletions

View File

@@ -36,6 +36,7 @@ from app.services.document_intelligence import build_document_insight
from app.services.document_numbering import is_application_claim_no
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_approval_flow import ExpenseClaimApprovalFlowMixin
from app.services.expense_claim_approval_routing import ExpenseClaimApprovalRoutingMixin
from app.services.expense_claim_attachment_presentation import ExpenseClaimAttachmentPresentation
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
from app.services.expense_claim_application_handoff import ExpenseClaimApplicationHandoffMixin
@@ -49,8 +50,10 @@ from app.services.expense_claim_draft_flow import ExpenseClaimDraftFlowMixin
from app.services.expense_claim_draft_persistence import ExpenseClaimDraftPersistenceMixin
from app.services.expense_claim_errors import ExpenseClaimSubmissionBlockedError
from app.services.expense_claim_pagination import ExpenseClaimPaginationMixin
from app.services.expense_claim_pre_review import ExpenseClaimPreReviewMixin
from app.services.expense_claim_ontology_resolvers import ExpenseClaimOntologyResolverMixin
from app.services.expense_claim_read_model import ExpenseClaimReadModelMixin
from app.services.expense_claim_risk_stage import with_risk_business_stage
from app.services.expense_claim_review_preview import ExpenseClaimReviewPreviewMixin
from app.services.expense_claim_constants import (
EXPENSE_TYPE_LABELS,
@@ -131,7 +134,9 @@ from app.services.ocr import OcrService
class ExpenseClaimService(
ExpenseClaimPaginationMixin,
ExpenseClaimApprovalFlowMixin,
ExpenseClaimApprovalRoutingMixin,
ExpenseClaimApplicationHandoffMixin,
ExpenseClaimPreReviewMixin,
ExpenseClaimBudgetFlowMixin,
ExpenseClaimAttachmentOperationsMixin,
ExpenseClaimReviewPreviewMixin,
@@ -197,7 +202,7 @@ class ExpenseClaimService(
.order_by(ExpenseClaim.created_at.desc(), ExpenseClaim.occurred_at.desc())
)
stmt = self._access_policy.apply_claim_scope(stmt, current_user)
return list(self.db.scalars(stmt).all())
return self._access_policy.attach_budget_approval_snapshots(list(self.db.scalars(stmt).all()))
def list_approval_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]:
stmt = (
@@ -210,7 +215,7 @@ class ExpenseClaimService(
.order_by(ExpenseClaim.submitted_at.desc(), ExpenseClaim.created_at.desc())
)
stmt = self._access_policy.apply_approval_claim_scope(stmt, current_user)
return list(self.db.scalars(stmt).all())
return self._access_policy.attach_budget_approval_snapshots(list(self.db.scalars(stmt).all()))
def list_archived_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]:
stmt = (
@@ -236,7 +241,7 @@ class ExpenseClaimService(
.where(ExpenseClaim.id == claim_id)
)
stmt = self._access_policy.apply_claim_scope(stmt, current_user, include_approval_scope=True)
return self.db.scalar(stmt)
return self._access_policy.attach_budget_approval_snapshot(self.db.scalar(stmt))
def can_view_budget_analysis(self, current_user: CurrentUserContext, claim: ExpenseClaim | None = None) -> bool:
if claim is None:
@@ -468,27 +473,44 @@ class ExpenseClaimService(
for flag in list(claim.risk_flags_json or [])
if not (
isinstance(flag, dict)
and str(flag.get("source") or "").strip() in {"submission_review", "attachment_analysis"}
and str(flag.get("source") or "").strip()
in {"submission_review", "attachment_analysis"}
)
]
submit_flag = {
"source": "application_submission",
"event_type": "expense_application_submission",
"severity": "info",
"label": "申请提交",
"message": "费用申请已提交至直属领导审批,请等待审核结果。",
"previous_status": str(claim.status or "").strip(),
"previous_approval_stage": str(claim.approval_stage or "").strip(),
"next_status": "submitted",
"next_approval_stage": "直属领导审批",
"created_at": submitted_at.isoformat(),
}
platform_review = self.evaluate_platform_risk_rules(
claim,
business_stage="expense_application",
)
platform_flags = list(platform_review.get("flags") or [])
submit_flag = with_risk_business_stage(
{
"source": "application_submission",
"event_type": "expense_application_submission",
"severity": "info",
"label": "申请提交",
"message": "费用申请已提交至直属领导审批,请等待审核结果。",
"previous_status": str(claim.status or "").strip(),
"previous_approval_stage": str(claim.approval_stage or "").strip(),
"next_status": "submitted",
"next_approval_stage": "直属领导审批",
"created_at": submitted_at.isoformat(),
},
"expense_application",
)
claim.status = "submitted"
claim.approval_stage = "直属领导审批"
claim.risk_flags_json = self._append_budget_flags([*preserved_flags, submit_flag], budget_flags)
claim.risk_flags_json = self._append_budget_flags(
[*preserved_flags, submit_flag, *platform_flags],
budget_flags,
business_stage="expense_application",
)
claim.submitted_at = submitted_at
else:
claim.risk_flags_json = self._append_budget_flags(claim.risk_flags_json, budget_flags)
claim.risk_flags_json = self._append_budget_flags(
claim.risk_flags_json,
budget_flags,
business_stage="reimbursement",
)
review_result = self._run_ai_submission_review(claim)
claim.status = str(review_result.get("status") or "supplement")
@@ -681,6 +703,7 @@ class ExpenseClaimService(
claim.risk_flags_json = self._append_budget_flags(
[*list(claim.risk_flags_json or []), return_flag],
budget_flags,
business_stage="expense_application" if is_application_claim else "reimbursement",
)
self.db.commit()
@@ -717,10 +740,6 @@ class ExpenseClaimService(