"""人员管理接口(需求 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="已保存")