补齐源快照、资料预演、正式导入及工资核算的依赖链路。 新增版本化输入、逐段 FIFO、生成锁和结构化当月调整, 并提供默认关闭的周期采集、有限重试及 Windows 托管。 原始客户资料、私有配置及首页改动不纳入本次提交。 BREAKING CHANGE: 升级前需应用新增迁移,并通过 PAYROLL_MAKER_USER_ID 绑定实际工资制作人,否则业务只读。 旧单批次核算默认关闭;历史兼容需显式启用 PAYROLL_ALLOW_LEGACY_CALCULATION。生产启用和真实金额待验收。
144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
"""人员管理接口(需求 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="已保存")
|