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

@@ -0,0 +1,39 @@
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import CurrentUserContext, get_current_user, get_db
from app.schemas.employee_profile import EmployeeProfileLatestRead
from app.services.employee_behavior_profile_service import EmployeeBehaviorProfileService
router = APIRouter(prefix="/employee-profiles")
DbSession = Annotated[Session, Depends(get_db)]
CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)]
@router.get(
"/{employee_id}/latest",
response_model=EmployeeProfileLatestRead,
summary="读取员工最新业务行为画像",
description="返回员工在指定场景下的最新画像快照,审批场景默认只展示费用支出和流程质量画像。",
)
def get_employee_latest_profile(
employee_id: str,
db: DbSession,
current_user: CurrentUser,
scene: Annotated[str, Query(max_length=50)] = "approval",
claim_id: Annotated[str | None, Query(max_length=80)] = None,
window_days: Annotated[int, Query(ge=1, le=365)] = 90,
expense_type_scope: Annotated[str, Query(max_length=50)] = "overall",
) -> EmployeeProfileLatestRead:
del current_user
return EmployeeBehaviorProfileService(db).get_latest_profile(
employee_id=employee_id,
scene=scene,
claim_id=claim_id,
window_days=window_days,
expense_type_scope=expense_type_scope,
)

View File

@@ -601,6 +601,38 @@ def approve_expense_claim(
return claim
@router.post(
"/claims/{claim_id}/pay",
response_model=ExpenseClaimRead,
summary="确认报销单已付款",
description="财务人员或高级财务人员确认待付款报销单已完成付款。",
responses={
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponse,
"description": "单据不存在。",
},
status.HTTP_400_BAD_REQUEST: {
"model": ErrorResponse,
"description": "当前用户或单据状态不允许确认付款。",
},
},
)
def pay_expense_claim(
claim_id: str,
db: DbSession,
current_user: CurrentUser,
) -> ExpenseClaimRead:
service = ExpenseClaimService(db)
try:
claim = service.mark_claim_paid(claim_id, current_user)
except ValueError as error:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
if claim is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Claim not found")
return claim
@router.delete(
"/claims/{claim_id}",
response_model=ExpenseClaimActionResponse,