feat: 新增员工行为画像算法与费用风险标签体系

后端新增员工行为画像算法模块,支持标签规则引擎和评分计算,
完善员工模型、银行信息、序列化和导入逻辑,优化报销审批流
和工作流常量,增强 Hermes 同步和知识同步能力,前端新增费
用画像详情弹窗、雷达图和风险卡片组件,完善登录页和工作台
样式,优化文档中心和归档中心交互,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-28 12:09:49 +08:00
parent 04cd6d0f81
commit 8a4a777be7
96 changed files with 9835 additions and 704 deletions

View File

@@ -7,10 +7,13 @@ from typing import Any
from app.api.deps import CurrentUserContext
from app.services.expense_claim_workflow_constants import (
APPROVAL_DONE_STAGE,
ARCHIVE_ACCOUNTING_STAGE,
BUDGET_MANAGER_APPROVAL_STAGE,
DIRECT_MANAGER_APPROVAL_STAGE,
FINANCE_APPROVAL_STAGE,
PAYMENT_PAID_STAGE,
PAYMENT_PAID_STATUS,
PAYMENT_PENDING_STAGE,
PAYMENT_PENDING_STATUS,
)
@@ -67,9 +70,9 @@ class ExpenseClaimApprovalFlowMixin:
approval_source = "finance_approval"
event_type = "expense_claim_finance_approval"
label = "财务审核通过"
next_status = "approved"
next_stage = ARCHIVE_ACCOUNTING_STAGE
default_message = "{operator} 已完成财务审核,进入归档入账"
next_status = PAYMENT_PENDING_STATUS
next_stage = PAYMENT_PENDING_STAGE
default_message = "{operator} 已完成财务审核,进入待付款"
else:
raise ValueError("当前节点不支持审批通过。")
@@ -160,6 +163,65 @@ class ExpenseClaimApprovalFlowMixin:
return claim
def mark_claim_paid(
self,
claim_id: str,
current_user: CurrentUserContext,
):
claim = self.get_claim(claim_id, current_user)
if claim is None:
return None
normalized_status = str(claim.status or "").strip().lower()
if normalized_status == PAYMENT_PAID_STATUS:
raise ValueError("该报销单已付款,无需重复确认。")
if normalized_status != PAYMENT_PENDING_STATUS:
raise ValueError("只有待付款状态的报销单可以确认已付款。")
if not self._access_policy.can_mark_claim_paid(current_user, claim):
raise ValueError("只有财务人员或高级财务人员可以确认付款,且不能处理本人单据。")
before_json = self._serialize_claim(claim)
operator = self._access_policy.resolve_current_user_display_name(current_user)
previous_stage = str(claim.approval_stage or "").strip()
payment_flag = {
"source": "payment",
"event_type": "expense_claim_payment_completed",
"payment_event_id": str(uuid.uuid4()),
"severity": "info",
"label": "付款完成",
"message": f"{operator} 已确认付款,报销单进入已付款。",
"operator": operator,
"operator_username": current_user.username,
"operator_role_codes": [
str(item).strip().lower()
for item in current_user.role_codes
if str(item).strip()
],
"previous_status": str(claim.status or "").strip(),
"previous_approval_stage": previous_stage,
"next_status": PAYMENT_PAID_STATUS,
"next_approval_stage": PAYMENT_PAID_STAGE,
"created_at": datetime.now(UTC).isoformat(),
}
claim.status = PAYMENT_PAID_STATUS
claim.approval_stage = PAYMENT_PAID_STAGE
claim.risk_flags_json = [*list(claim.risk_flags_json or []), payment_flag]
self.db.commit()
self.db.refresh(claim)
self.audit_service.log_action(
actor=operator,
action="expense_claim.mark_paid",
resource_type="expense_claim",
resource_id=claim.id,
before_json=before_json,
after_json=self._serialize_claim(claim),
)
return claim
@staticmethod
def _resolve_latest_approval_opinion(claim, *, source: str) -> str:
for flag in reversed(list(claim.risk_flags_json or [])):