refactor(server): split oversized backend services
This commit is contained in:
177
server/src/app/services/expense_claim_risk_review.py
Normal file
177
server/src/app/services/expense_claim_risk_review.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
|
||||
from app.models.financial_record import ExpenseClaim
|
||||
from app.services.expense_claim_constants import (
|
||||
AI_REVIEW_LOOKBACK_DAYS,
|
||||
AI_REVIEW_REPEAT_RISK_BLOCK_COUNT,
|
||||
AI_REVIEW_REPEAT_RISK_WARNING_COUNT,
|
||||
)
|
||||
from app.services.expense_claim_item_sync import ExpenseClaimItemSyncMixin
|
||||
from app.services.expense_claim_platform_risk import ExpenseClaimPlatformRiskMixin
|
||||
from app.services.expense_claim_policy_review import ExpenseClaimPolicyReviewMixin
|
||||
|
||||
|
||||
class ExpenseClaimRiskReviewMixin(
|
||||
ExpenseClaimPlatformRiskMixin,
|
||||
ExpenseClaimPolicyReviewMixin,
|
||||
ExpenseClaimItemSyncMixin,
|
||||
):
|
||||
def _run_ai_submission_review(self, claim: ExpenseClaim) -> dict[str, Any]:
|
||||
base_flags = list(claim.risk_flags_json or [])
|
||||
attachment_flags = [
|
||||
flag
|
||||
for flag in base_flags
|
||||
if isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis"
|
||||
]
|
||||
preserved_flags = [
|
||||
flag
|
||||
for flag in base_flags
|
||||
if not (isinstance(flag, dict) and str(flag.get("source") or "").strip() == "submission_review")
|
||||
]
|
||||
|
||||
review_flags: list[dict[str, Any]] = []
|
||||
attention_reasons: list[str] = []
|
||||
|
||||
high_attachment_flags = [
|
||||
flag
|
||||
for flag in attachment_flags
|
||||
if str(flag.get("severity") or "").strip().lower() == "high"
|
||||
]
|
||||
medium_attachment_flags = [
|
||||
flag
|
||||
for flag in attachment_flags
|
||||
if str(flag.get("severity") or "").strip().lower() == "medium"
|
||||
]
|
||||
if high_attachment_flags:
|
||||
attention_reasons.append("存在高风险票据,需审批人重点复核。")
|
||||
review_flags.append(
|
||||
{
|
||||
"source": "submission_review",
|
||||
"severity": "high",
|
||||
"label": "AI预审重点复核",
|
||||
"message": (
|
||||
f"AI预审发现 {len(high_attachment_flags)} 条高风险附件,"
|
||||
"已随单流转给审批人重点复核。"
|
||||
),
|
||||
}
|
||||
)
|
||||
elif medium_attachment_flags:
|
||||
review_flags.append(
|
||||
{
|
||||
"source": "submission_review",
|
||||
"severity": "medium",
|
||||
"label": "AI预审提醒",
|
||||
"message": f"AI预审发现 {len(medium_attachment_flags)} 条中风险附件,已随单流转给审批人复核。",
|
||||
}
|
||||
)
|
||||
|
||||
manager_name = self._resolve_claim_manager_name(claim)
|
||||
if not manager_name:
|
||||
attention_reasons.append("未识别到该员工的直属领导,需审批环节补充分配。")
|
||||
review_flags.append(
|
||||
{
|
||||
"source": "submission_review",
|
||||
"severity": "medium",
|
||||
"label": "审批链待分配",
|
||||
"message": "AI预审发现直属领导缺失,已提交到审批环节等待分配或复核。",
|
||||
}
|
||||
)
|
||||
|
||||
historical_risk_count = self._count_recent_risky_claims(claim)
|
||||
if historical_risk_count >= AI_REVIEW_REPEAT_RISK_BLOCK_COUNT:
|
||||
review_flags.append(
|
||||
{
|
||||
"source": "submission_review",
|
||||
"severity": "medium",
|
||||
"label": "历史风险偏高",
|
||||
"message": (
|
||||
f"近 {AI_REVIEW_LOOKBACK_DAYS} 天内该员工已有 {historical_risk_count} 笔带风险标记的报销,"
|
||||
"本次已追加到审批链重点关注。"
|
||||
),
|
||||
}
|
||||
)
|
||||
elif historical_risk_count >= AI_REVIEW_REPEAT_RISK_WARNING_COUNT:
|
||||
review_flags.append(
|
||||
{
|
||||
"source": "submission_review",
|
||||
"severity": "low",
|
||||
"label": "历史风险提醒",
|
||||
"message": (
|
||||
f"近 {AI_REVIEW_LOOKBACK_DAYS} 天内该员工已有 {historical_risk_count} 笔带风险标记的报销,"
|
||||
"建议直属领导重点复核。"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
travel_review = self._run_travel_policy_review(claim)
|
||||
attention_reasons.extend(travel_review["blocking_reasons"])
|
||||
review_flags.extend(travel_review["flags"])
|
||||
|
||||
scene_policy_review = self._run_scene_policy_review(claim)
|
||||
attention_reasons.extend(scene_policy_review["blocking_reasons"])
|
||||
review_flags.extend(scene_policy_review["flags"])
|
||||
|
||||
platform_risk_review = self.evaluate_platform_risk_rules(claim)
|
||||
attention_reasons.extend(platform_risk_review["blocking_reasons"])
|
||||
review_flags.extend(platform_risk_review["flags"])
|
||||
|
||||
if attention_reasons:
|
||||
summary_message = "AI预审发现需审批重点关注事项:" + ";".join(
|
||||
dict.fromkeys(attention_reasons)
|
||||
)
|
||||
review_flags.insert(
|
||||
0,
|
||||
{
|
||||
"source": "submission_review",
|
||||
"severity": "medium",
|
||||
"label": "AI预审重点复核",
|
||||
"message": summary_message,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "submitted",
|
||||
"approval_stage": "直属领导审批",
|
||||
"risk_flags": preserved_flags + review_flags,
|
||||
"message": (
|
||||
f"报销单 {claim.claim_no} 已完成 AI预审,"
|
||||
f"现已提交给直属领导 {manager_name or '审批人'} 审批。"
|
||||
),
|
||||
"passed": True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _resolve_claim_manager_name(claim: ExpenseClaim) -> str:
|
||||
if claim.employee is not None:
|
||||
if claim.employee.manager is not None and claim.employee.manager.name:
|
||||
return str(claim.employee.manager.name).strip()
|
||||
if claim.employee.organization_unit is not None and claim.employee.organization_unit.manager_name:
|
||||
return str(claim.employee.organization_unit.manager_name).strip()
|
||||
return ""
|
||||
|
||||
def _count_recent_risky_claims(self, claim: ExpenseClaim) -> int:
|
||||
filters = []
|
||||
if claim.employee_id:
|
||||
filters.append(ExpenseClaim.employee_id == claim.employee_id)
|
||||
elif claim.employee_name:
|
||||
filters.append(ExpenseClaim.employee_name == claim.employee_name)
|
||||
if not filters:
|
||||
return 0
|
||||
|
||||
since = datetime.now(UTC) - timedelta(days=AI_REVIEW_LOOKBACK_DAYS)
|
||||
stmt = (
|
||||
select(ExpenseClaim)
|
||||
.where(or_(*filters))
|
||||
.where(ExpenseClaim.id != claim.id)
|
||||
.where(ExpenseClaim.occurred_at >= since)
|
||||
)
|
||||
recent_claims = list(self.db.scalars(stmt).all())
|
||||
return sum(1 for item in recent_claims if list(item.risk_flags_json or []))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user