PayrollManagementSystem/backend/app/api/v1/base/personnel.py
liuwenxin 108bf87225 feat(核算)!: 添加 T+ 正式核算与采集托管
补齐源快照、资料预演、正式导入及工资核算的依赖链路。
新增版本化输入、逐段 FIFO、生成锁和结构化当月调整,
并提供默认关闭的周期采集、有限重试及 Windows 托管。

原始客户资料、私有配置及首页改动不纳入本次提交。

BREAKING CHANGE: 升级前需应用新增迁移,并通过
PAYROLL_MAKER_USER_ID 绑定实际工资制作人,否则业务只读。
旧单批次核算默认关闭;历史兼容需显式启用
PAYROLL_ALLOW_LEGACY_CALCULATION。生产启用和真实金额待验收。
2026-11-02 09:42:53 +08:00

144 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""人员管理接口(需求 2.1.1:人员档案 + 辐射区域 + 拓展计划)。
基础数据属业务操作,**登录即可访问**(不要求管理员,对照系统管理模块)。
校验规则(岗位/经理合法性、删除保护、子表全量替换的防御写法)见
`personnel_service` 模块 docstring —— 前端提示之外,后端校验是唯一防线。
写操作经 `RequestMetaDep` 带入请求元信息由 service 层写入操作日志。
"""
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from app.api.deps import DbSession, PayrollWriter, RequestMetaDep, get_current_user
from app.core.response import ApiResponse, PageResult, ok
from app.models import User
from app.schemas.personnel import (
PersonnelCreateIn,
PersonnelDetailOut,
PersonnelOut,
PersonnelQuery,
PersonnelStatusIn,
PlanReplaceIn,
RegionReplaceIn,
)
from app.services import personnel_service
# 基础数据是业务操作:登录即可,不要求管理员(区别于系统管理的 AdminUser)
CurrentUser = Annotated[User, Depends(get_current_user)]
router = APIRouter(prefix="/personnel", tags=["人员管理"])
@router.get("", response_model=ApiResponse[PageResult[PersonnelOut]], summary="人员列表")
def list_personnel(
session: DbSession,
_: CurrentUser,
query: Annotated[PersonnelQuery, Query()],
) -> ApiResponse[PageResult[PersonnelOut]]:
"""分页查询人员(含离职)。姓名模糊匹配,可按状态与岗位筛选。"""
return ok(personnel_service.list_personnel(session, query))
@router.post("", response_model=ApiResponse[PersonnelOut], summary="新增人员")
def create_personnel(
session: DbSession,
current_user: PayrollWriter,
payload: PersonnelCreateIn,
meta: RequestMetaDep,
) -> ApiResponse[PersonnelOut]:
"""新增人员,默认在职。岗位须存在且启用;直属经理不能是自己且须在职。"""
return ok(
personnel_service.create_personnel(session, payload, operator=current_user, meta=meta),
message="人员已创建",
)
@router.get("/{personnel_id}", response_model=ApiResponse[PersonnelDetailOut], summary="人员详情")
def get_personnel(
session: DbSession,
_: CurrentUser,
personnel_id: Annotated[int, Path(ge=1)],
) -> ApiResponse[PersonnelDetailOut]:
"""人员详情,附辐射区域与拓展计划,供编辑抽屉一次回显。"""
return ok(personnel_service.get_detail(session, personnel_id))
@router.put("/{personnel_id}", response_model=ApiResponse[PersonnelOut], summary="编辑人员")
def update_personnel(
session: DbSession,
current_user: PayrollWriter,
personnel_id: Annotated[int, Path(ge=1)],
payload: PersonnelCreateIn,
meta: RequestMetaDep,
) -> ApiResponse[PersonnelOut]:
"""编辑人员(全量提交,不含状态)。校验规则同新增。"""
return ok(
personnel_service.update_personnel(
session, personnel_id, payload, operator=current_user, meta=meta
),
message="已保存",
)
@router.delete("/{personnel_id}", response_model=ApiResponse[None], summary="删除人员")
def delete_personnel(
session: DbSession,
current_user: PayrollWriter,
personnel_id: Annotated[int, Path(ge=1)],
meta: RequestMetaDep,
) -> ApiResponse[None]:
"""删除人员。被团队成员或客户归属引用时禁删,应改为离职。"""
personnel_service.delete_personnel(session, personnel_id, operator=current_user, meta=meta)
return ok(message="人员已删除")
@router.put(
"/{personnel_id}/status",
response_model=ApiResponse[PersonnelOut],
summary="设为在职/离职",
)
def set_status(
session: DbSession,
current_user: PayrollWriter,
personnel_id: Annotated[int, Path(ge=1)],
payload: PersonnelStatusIn,
meta: RequestMetaDep,
) -> ApiResponse[PersonnelOut]:
"""在职/离职切换。离职人员不可再被业务与客户归属引用。"""
result = personnel_service.set_status(
session, personnel_id, payload.status, operator=current_user, meta=meta
)
return ok(result, message="已设为在职" if payload.status == 1 else "已设为离职")
@router.put("/{personnel_id}/regions", response_model=ApiResponse[None], summary="维护辐射区域")
def replace_regions(
session: DbSession,
current_user: PayrollWriter,
personnel_id: Annotated[int, Path(ge=1)],
payload: RegionReplaceIn,
meta: RequestMetaDep,
) -> ApiResponse[None]:
"""全量替换辐射区域,空列表即清空。"""
personnel_service.replace_regions(
session, personnel_id, payload, operator=current_user, meta=meta
)
return ok(message="已保存")
@router.put("/{personnel_id}/plans", response_model=ApiResponse[None], summary="维护拓展计划")
def replace_plans(
session: DbSession,
current_user: PayrollWriter,
personnel_id: Annotated[int, Path(ge=1)],
payload: PlanReplaceIn,
meta: RequestMetaDep,
) -> ApiResponse[None]:
"""全量替换拓展计划,空列表即清空。"""
personnel_service.replace_plans(
session, personnel_id, payload, operator=current_user, meta=meta
)
return ok(message="已保存")