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

@@ -17,6 +17,8 @@ from app.services.expense_claim_workflow_constants import (
BUDGET_MANAGER_APPROVAL_STAGE,
DIRECT_MANAGER_APPROVAL_STAGE,
FINANCE_APPROVAL_STAGE,
PAYMENT_PAID_STAGE,
PAYMENT_PENDING_STATUS,
)
@@ -29,6 +31,7 @@ BUDGET_MONITOR_APPROVAL_GRADE = "P8"
CLAIM_DELETE_ROLE_CODES = {"executive"}
ARCHIVED_CLAIM_STATUSES = ("approved", "completed", "paid")
APPLICATION_ARCHIVED_STAGES = (APPROVAL_DONE_STAGE, "申请归档", "completed")
ARCHIVED_REIMBURSEMENT_STAGES = (ARCHIVE_ACCOUNTING_STAGE, PAYMENT_PAID_STAGE, "completed")
class ExpenseClaimAccessPolicy:
@@ -60,7 +63,7 @@ class ExpenseClaimAccessPolicy:
normalized_type.like("%\\_application", escape="\\"),
)
return or_(
stage == ARCHIVE_ACCOUNTING_STAGE,
stage.in_(ARCHIVED_REIMBURSEMENT_STAGES),
stage == "completed",
and_(
application_condition,
@@ -72,7 +75,7 @@ class ExpenseClaimAccessPolicy:
or_(
stage == "",
stage.is_(None),
stage == ARCHIVE_ACCOUNTING_STAGE,
stage.in_(ARCHIVED_REIMBURSEMENT_STAGES),
stage == "completed",
),
),
@@ -88,7 +91,7 @@ class ExpenseClaimAccessPolicy:
def is_archived_claim(claim: ExpenseClaim) -> bool:
normalized_status = str(claim.status or "").strip().lower()
stage = str(claim.approval_stage or "").strip()
if stage in {ARCHIVE_ACCOUNTING_STAGE, "completed"}:
if stage in set(ARCHIVED_REIMBURSEMENT_STAGES):
return True
normalized_type = str(claim.expense_type or "").strip().lower()
claim_no = str(claim.claim_no or "").strip().upper()
@@ -103,7 +106,7 @@ class ExpenseClaimAccessPolicy:
and stage in APPLICATION_ARCHIVED_STAGES
):
return True
return normalized_status in ARCHIVED_CLAIM_STATUSES and stage in {"", ARCHIVE_ACCOUNTING_STAGE, "completed"}
return normalized_status in ARCHIVED_CLAIM_STATUSES and stage in {"", *ARCHIVED_REIMBURSEMENT_STAGES}
def can_return_claim(self, current_user: CurrentUserContext, claim: ExpenseClaim) -> bool:
normalized_status = str(claim.status or "").strip().lower()
@@ -136,6 +139,15 @@ class ExpenseClaimAccessPolicy:
)
return False
def can_mark_claim_paid(self, current_user: CurrentUserContext, claim: ExpenseClaim) -> bool:
if str(claim.status or "").strip().lower() != PAYMENT_PENDING_STATUS:
return False
if self.is_claim_owned_by_current_user(claim, current_user):
return False
if current_user.is_admin:
return True
return bool(self.normalize_role_codes(current_user) & PRIVILEGED_CLAIM_ROLE_CODES)
def is_current_direct_manager_approver(self, current_user: CurrentUserContext, claim: ExpenseClaim) -> bool:
role_codes = self.normalize_role_codes(current_user)
if not (role_codes & APPROVAL_VISIBLE_CLAIM_ROLE_CODES):