Files
X-Financial/server/src/app/api/v1/endpoints/employee_profiles.py
caoxiaozhu 7989f3a159 feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
2026-05-30 15:46:51 +08:00

140 lines
4.8 KiB
Python

from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session
from app.api.deps import CurrentUserContext, get_current_user, get_db
from app.models.employee import Employee
from app.schemas.employee_profile import EmployeeProfileLatestRead
from app.services.account_behavior_profile import AccountBehaviorProfileService
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(
"/me/latest",
response_model=EmployeeProfileLatestRead,
summary="读取当前登录人的最新用户画像",
description="按当前登录用户的邮箱或姓名匹配员工目录,返回个人工作台使用的综合用户画像。",
)
def get_current_employee_latest_profile(
db: DbSession,
current_user: CurrentUser,
scene: Annotated[str, Query(max_length=50)] = "operations",
window_days: Annotated[int, Query(ge=1, le=365)] = 90,
expense_type_scope: Annotated[str, Query(max_length=50)] = "overall",
) -> EmployeeProfileLatestRead:
employee = _resolve_current_employee(db, current_user)
if employee is None:
return AccountBehaviorProfileService(db).get_latest_account_profile(
account_id=current_user.username,
account_name=current_user.name,
identifiers=_current_account_identifiers(current_user),
scene=scene,
window_days=window_days,
expense_type_scope=expense_type_scope,
)
service = EmployeeBehaviorProfileService(db)
latest = service.get_latest_profile(
employee_id=employee.id,
scene=scene,
window_days=window_days,
expense_type_scope=expense_type_scope,
)
if latest.empty_reason or _missing_usage_duration_metric(latest):
service.refresh_employee_profiles(
employee_id=employee.id,
window_days=(window_days,),
expense_type_scope=expense_type_scope,
source_task_type="workbench_on_demand",
)
latest = service.get_latest_profile(
employee_id=employee.id,
scene=scene,
window_days=window_days,
expense_type_scope=expense_type_scope,
)
return latest
@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,
)
def _resolve_current_employee(
db: Session,
current_user: CurrentUserContext,
) -> Employee | None:
identities = [
str(current_user.username or "").strip(),
str(current_user.name or "").strip(),
]
normalized = [item for item in dict.fromkeys(identities) if item]
if not normalized:
return None
email_values = [item.lower() for item in normalized if "@" in item]
exact_values = [item for item in normalized if "@" not in item]
conditions = []
if email_values:
conditions.append(func.lower(Employee.email).in_(email_values))
if exact_values:
conditions.append(Employee.name.in_(exact_values))
conditions.append(Employee.employee_no.in_(exact_values))
if not conditions:
return None
stmt = select(Employee).where(or_(*conditions)).order_by(Employee.created_at.asc()).limit(1)
return db.scalars(stmt).first()
def _missing_usage_duration_metric(latest: EmployeeProfileLatestRead) -> bool:
if latest.scene != "operations":
return False
for profile in latest.profiles:
if profile.profile_type == "ai_usage":
return "usage_duration_ms" not in profile.metrics
return False
def _current_account_identifiers(current_user: CurrentUserContext) -> set[str]:
return {
item
for item in (
current_user.username,
current_user.name,
)
if str(item or "").strip()
}