2026-10-12 22:35:02 -04:00
|
|
|
|
"""人员管理接口(需求 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, 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: CurrentUser,
|
|
|
|
|
|
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: CurrentUser,
|
|
|
|
|
|
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: CurrentUser,
|
|
|
|
|
|
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: CurrentUser,
|
|
|
|
|
|
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: CurrentUser,
|
|
|
|
|
|
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: CurrentUser,
|
|
|
|
|
|
personnel_id: Annotated[int, Path(ge=1)],
|
|
|
|
|
|
payload: PlanReplaceIn,
|
|
|
|
|
|
meta: RequestMetaDep,
|
|
|
|
|
|
) -> ApiResponse[None]:
|
|
|
|
|
|
"""全量替换拓展计划,空列表即清空。"""
|
2026-10-12 23:22:34 -04:00
|
|
|
|
personnel_service.replace_plans(
|
|
|
|
|
|
session, personnel_id, payload, operator=current_user, meta=meta
|
|
|
|
|
|
)
|
2026-10-12 22:35:02 -04:00
|
|
|
|
return ok(message="已保存")
|