feat: 增强员工管理与报销单全流程功能
- 新增员工Excel导入服务(employee_spreadsheet)及导入/导出API端点 - 员工服务增加批量创建、邮箱唯一校验、组织架构关联等能力 - 报销单提交补充身份回填、部门信息透传及预审结果展示优化 - 认证流程增加部门信息(departmentName)并在schema中同步扩展 - 用户Agent服务增加部门关联与报销单回填逻辑 - 前端员工管理页面全面重构,新增导入导出、搜索过滤、分页等功能 - 前端审批中心、审计、差旅报销等视图交互与样式优化 - 新增TableLoadingState共享组件及员工导入测试用例
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.logging import get_logger
|
||||
from app.core.security import verify_password
|
||||
from app.models.employee import Employee
|
||||
from app.models.financial_record import ExpenseClaim
|
||||
from app.schemas.auth import AuthUserRead, LoginRequest, LoginResponse
|
||||
from app.services.employee import EmployeeService
|
||||
from app.services.employee_seed import ROLE_DISPLAY_ORDER
|
||||
@@ -34,6 +37,12 @@ class AuthenticatedUser:
|
||||
department: str
|
||||
position: str
|
||||
grade: str
|
||||
employee_no: str
|
||||
manager_name: str
|
||||
location: str
|
||||
cost_center: str
|
||||
finance_owner_name: str
|
||||
risk_profile: dict[str, Any]
|
||||
role_codes: list[str]
|
||||
email: str
|
||||
avatar: str
|
||||
@@ -82,6 +91,12 @@ class AuthService:
|
||||
department="",
|
||||
position="系统管理员",
|
||||
grade="",
|
||||
employee_no="",
|
||||
manager_name="",
|
||||
location="",
|
||||
cost_center="",
|
||||
finance_owner_name="",
|
||||
risk_profile={},
|
||||
role_codes=["manager"],
|
||||
email=admin_email or f"{admin_username}@local",
|
||||
avatar=display_name[:1].upper(),
|
||||
@@ -96,7 +111,11 @@ class AuthService:
|
||||
|
||||
stmt = (
|
||||
select(Employee)
|
||||
.options(selectinload(Employee.organization_unit), selectinload(Employee.roles))
|
||||
.options(
|
||||
selectinload(Employee.organization_unit),
|
||||
selectinload(Employee.manager),
|
||||
selectinload(Employee.roles),
|
||||
)
|
||||
.where(func.lower(Employee.email) == identifier.lower())
|
||||
)
|
||||
employee = self.db.execute(stmt).scalars().first()
|
||||
@@ -117,20 +136,75 @@ class AuthService:
|
||||
)
|
||||
role_codes = [role.role_code for role in sorted_roles]
|
||||
primary_role_code = role_codes[0] if role_codes else "user"
|
||||
department = employee.organization_unit.name if employee.organization_unit is not None else ""
|
||||
manager_name = self._resolve_manager_name(employee)
|
||||
|
||||
return AuthenticatedUser(
|
||||
username=employee.email,
|
||||
name=employee.name,
|
||||
role=ROLE_LABELS.get(primary_role_code, "使用者"),
|
||||
department=employee.organization_unit.name if employee.organization_unit is not None else "",
|
||||
department=department,
|
||||
position=employee.position,
|
||||
grade=employee.grade,
|
||||
employee_no=employee.employee_no,
|
||||
manager_name=manager_name,
|
||||
location=employee.location or "",
|
||||
cost_center=employee.cost_center or "",
|
||||
finance_owner_name=employee.finance_owner_name or "",
|
||||
risk_profile=self._build_risk_profile(employee),
|
||||
role_codes=role_codes or ["user"],
|
||||
email=employee.email,
|
||||
avatar=(employee.name or "?")[:1].upper(),
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_manager_name(employee: Employee) -> str:
|
||||
if employee.manager is not None and employee.manager.name:
|
||||
return str(employee.manager.name).strip()
|
||||
if employee.organization_unit is not None and employee.organization_unit.manager_name:
|
||||
return str(employee.organization_unit.manager_name).strip()
|
||||
return ""
|
||||
|
||||
def _build_risk_profile(self, employee: Employee) -> dict[str, Any]:
|
||||
since = datetime.now(UTC) - timedelta(days=90)
|
||||
identity_values = [
|
||||
str(employee.name or "").strip(),
|
||||
str(employee.email or "").strip(),
|
||||
str(employee.employee_no or "").strip(),
|
||||
]
|
||||
name_candidates = [item for item in dict.fromkeys(identity_values) if item]
|
||||
conditions = [ExpenseClaim.employee_id == employee.id]
|
||||
if name_candidates:
|
||||
conditions.append(ExpenseClaim.employee_name.in_(name_candidates))
|
||||
|
||||
stmt = (
|
||||
select(ExpenseClaim)
|
||||
.where(or_(*conditions), ExpenseClaim.occurred_at >= since)
|
||||
.order_by(ExpenseClaim.occurred_at.desc())
|
||||
.limit(30)
|
||||
)
|
||||
claims = list(self.db.scalars(stmt).all())
|
||||
recent_risk_flags: list[str] = []
|
||||
for claim in claims:
|
||||
for flag in claim.risk_flags_json or []:
|
||||
normalized = str(flag or "").strip()
|
||||
if normalized and normalized not in recent_risk_flags:
|
||||
recent_risk_flags.append(normalized)
|
||||
if len(recent_risk_flags) >= 6:
|
||||
break
|
||||
if len(recent_risk_flags) >= 6:
|
||||
break
|
||||
|
||||
return {
|
||||
"windowDays": 90,
|
||||
"totalClaimCount": len(claims),
|
||||
"riskyClaimCount": sum(1 for claim in claims if claim.risk_flags_json),
|
||||
"draftClaimCount": sum(1 for claim in claims if claim.status == "draft"),
|
||||
"recentRiskFlags": recent_risk_flags,
|
||||
"lastClaimAt": claims[0].occurred_at.isoformat() if claims and claims[0].occurred_at else "",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_user(user: AuthenticatedUser) -> AuthUserRead:
|
||||
return AuthUserRead(
|
||||
@@ -141,6 +215,12 @@ class AuthService:
|
||||
departmentName=user.department,
|
||||
position=user.position,
|
||||
grade=user.grade,
|
||||
employeeNo=user.employee_no,
|
||||
managerName=user.manager_name,
|
||||
location=user.location,
|
||||
costCenter=user.cost_center,
|
||||
financeOwnerName=user.finance_owner_name,
|
||||
riskProfile=user.risk_profile,
|
||||
roleCodes=user.role_codes,
|
||||
email=user.email,
|
||||
avatar=user.avatar,
|
||||
|
||||
Reference in New Issue
Block a user