Files
X-Financial/server/src/app/api/v1/endpoints/employees.py
caoxiaozhu 678f64d772 feat: 统一后端分页查询与前端服务层适配
后端新增通用分页模块,为报销单、员工、预算、agent 资产等
端点统一接入分页参数和游标查询,优化 repository 层分页实
现,前端服务层适配分页响应结构,完善预算图表和全局样式,
优化侧边栏和企业选择器组件,引入 Element Plus 插件注册。
2026-05-29 14:11:06 +08:00

228 lines
7.4 KiB
Python

from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from fastapi.responses import Response
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.api.pagination import PageNumber, PageSize, page_payload, wants_page
from app.schemas.common import ErrorResponse, PaginatedResponse
from app.schemas.employee import (
EmployeeCreate,
EmployeeImportResultRead,
EmployeeMetaRead,
EmployeeRead,
EmployeeUpdate,
)
from app.services.employee import EmployeeService
from app.services.employee_pagination import EmployeePaginationService
router = APIRouter()
DbSession = Annotated[Session, Depends(get_db)]
@router.get(
"/meta",
response_model=EmployeeMetaRead,
summary="读取员工目录元数据",
description="返回员工总数、状态汇总和可选角色列表,供员工管理页面初始化使用。",
)
def get_employee_meta(db: DbSession) -> EmployeeMetaRead:
return EmployeeService(db).get_employee_meta()
@router.get(
"",
response_model=list[EmployeeRead] | PaginatedResponse[EmployeeRead],
summary="查询员工列表",
description="按状态和关键字筛选员工目录。",
)
def list_employees(
db: DbSession,
status_filter: Annotated[
str | None,
Query(alias="status", description="员工状态筛选值。"),
] = None,
keyword: Annotated[
str | None,
Query(description="姓名、工号、邮箱等关键字模糊查询。"),
] = None,
page: PageNumber = None,
page_size: PageSize = None,
) -> list[EmployeeRead] | PaginatedResponse[EmployeeRead]:
if wants_page(page, page_size):
return page_payload(
EmployeePaginationService(db).list_employees_page(
status=status_filter,
keyword=keyword,
page=page,
page_size=page_size,
)
)
return EmployeeService(db).list_employees(status=status_filter, keyword=keyword)
@router.get(
"/import-template",
summary="下载员工导入模板",
description="下载固定格式的员工 Excel 导入模板。",
)
def download_employee_import_template(db: DbSession) -> Response:
content = EmployeeService(db).build_import_template()
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": 'attachment; filename="employee-import-template.xlsx"'
},
)
@router.get(
"/export",
summary="导出员工 Excel",
description="按筛选条件导出员工目录 Excel 文件。",
)
def export_employees(
db: DbSession,
status_filter: Annotated[
str | None,
Query(alias="status", description="员工状态筛选值。"),
] = None,
keyword: Annotated[
str | None,
Query(description="姓名、工号、邮箱等关键字模糊查询。"),
] = None,
) -> Response:
content = EmployeeService(db).export_employees(status=status_filter, keyword=keyword)
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": 'attachment; filename="employee-export.xlsx"'},
)
@router.post(
"/import",
response_model=EmployeeImportResultRead,
summary="导入员工 Excel",
description="按模板批量导入员工。全部校验通过后才写入数据库,任一行有错则整批不导入。",
)
async def import_employees(
db: DbSession,
file: Annotated[UploadFile, File(description="待导入的员工 Excel 文件。")],
) -> EmployeeImportResultRead:
filename = (file.filename or "").lower()
if not filename.endswith(".xlsx"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="当前仅支持上传 .xlsx 格式的员工表格。",
)
content = await file.read()
return EmployeeService(db).import_employees(content)
@router.post(
"",
response_model=EmployeeRead,
status_code=status.HTTP_201_CREATED,
summary="创建员工",
description="创建新的员工目录记录,并初始化基础角色与组织归属。",
responses={
status.HTTP_400_BAD_REQUEST: {
"model": ErrorResponse,
"description": "员工数据校验失败。",
}
},
)
def create_employee(payload: EmployeeCreate, db: DbSession) -> EmployeeRead:
try:
return EmployeeService(db).create_employee(payload)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
@router.get(
"/{employee_id}",
response_model=EmployeeRead,
summary="读取员工详情",
description="根据员工主键读取员工完整档案信息。",
responses={
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponse,
"description": "员工不存在。",
}
},
)
def get_employee(employee_id: str, db: DbSession) -> EmployeeRead:
employee = EmployeeService(db).get_employee(employee_id)
if employee is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Employee not found")
return employee
@router.patch(
"/{employee_id}",
response_model=EmployeeRead,
summary="更新员工",
description="更新员工基础信息、角色、密码等可维护字段。",
responses={
status.HTTP_400_BAD_REQUEST: {
"model": ErrorResponse,
"description": "请求字段不合法。",
},
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponse,
"description": "员工不存在。",
},
},
)
def update_employee(employee_id: str, payload: EmployeeUpdate, db: DbSession) -> EmployeeRead:
try:
return EmployeeService(db).update_employee(employee_id, payload)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
@router.post(
"/{employee_id}/disable",
response_model=EmployeeRead,
summary="停用员工",
description="将员工状态切换为停用,阻止其继续登录系统。",
responses={
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponse,
"description": "员工不存在。",
}
},
)
def disable_employee(employee_id: str, db: DbSession) -> EmployeeRead:
try:
return EmployeeService(db).disable_employee(employee_id)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
@router.post(
"/{employee_id}/enable",
response_model=EmployeeRead,
summary="启用员工",
description="将停用员工恢复为在职状态,使其可以重新登录系统。",
responses={
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponse,
"description": "员工不存在。",
}
},
)
def enable_employee(employee_id: str, db: DbSession) -> EmployeeRead:
try:
return EmployeeService(db).enable_employee(employee_id)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc