"""客户管理接口(需求 2.1.3)。 归属调整有专属端点 ``PUT /{id}/ownership``:它是唯一写归属留痕的入口, 编辑端点的 schema 不含归属字段 —— 防止出现「改了归属却无留痕」的旁路 (原因详见 `schemas.customer.CustomerUpdateIn`)。 与 `api/v1/system` 的权限分工:基础数据是**业务操作**,登录即可访问 (不要求管理员);写操作同样经 `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, PageQuery, PageResult, ok from app.models import User from app.schemas.customer import ( CustomerCreateIn, CustomerOut, CustomerOwnershipIn, CustomerQuery, CustomerUpdateIn, OwnershipChangeLogOut, ) from app.schemas.rating import CustomerRatingOut, CustomerRatingSaveIn from app.services import customer_service, rating_service router = APIRouter(prefix="/customers", tags=["客户管理"]) # 基础数据不要求管理员(见模块 docstring),登录用户即可操作 CurrentUser = Annotated[User, Depends(get_current_user)] @router.get("", response_model=ApiResponse[PageResult[CustomerOut]], summary="客户列表") def list_customers( session: DbSession, _: CurrentUser, query: Annotated[CustomerQuery, Query()], ) -> ApiResponse[PageResult[CustomerOut]]: """分页查询客户(keyword 对公司名称/客户负责人模糊匹配,附归属人员姓名)。""" return ok(customer_service.list_customers(session, query)) @router.post("", response_model=ApiResponse[CustomerOut], summary="新增客户") def create_customer( session: DbSession, current_user: CurrentUser, payload: CustomerCreateIn, meta: RequestMetaDep, ) -> ApiResponse[CustomerOut]: """新增客户。客户分类须在注册表内;「公司名称+负责人」组合唯一。""" return ok( customer_service.create_customer(session, payload, operator=current_user, meta=meta), message="客户已创建", ) @router.get("/{customer_id}", response_model=ApiResponse[CustomerOut], summary="客户详情") def get_customer( session: DbSession, _: CurrentUser, customer_id: Annotated[int, Path(ge=1)], ) -> ApiResponse[CustomerOut]: """按主键查询客户详情。""" return ok(customer_service.get_customer(session, customer_id)) @router.put("/{customer_id}", response_model=ApiResponse[CustomerOut], summary="编辑客户") def update_customer( session: DbSession, current_user: CurrentUser, customer_id: Annotated[int, Path(ge=1)], payload: CustomerUpdateIn, meta: RequestMetaDep, ) -> ApiResponse[CustomerOut]: """编辑客户档案。 **不含归属** —— 归属调整统一走 ``PUT /{id}/ownership``;编辑表单即使 提交 ownerPersonnelId 也会被忽略,归属以归属端点为准。 """ return ok( customer_service.update_customer( session, customer_id, payload, operator=current_user, meta=meta ), message="已保存", ) @router.delete("/{customer_id}", response_model=ApiResponse[None], summary="删除客户") def delete_customer( session: DbSession, current_user: CurrentUser, customer_id: Annotated[int, Path(ge=1)], meta: RequestMetaDep, ) -> ApiResponse[None]: """删除客户(物理删除)。 已存在关联业务时返回 400(先处理相关业务单据)。 """ customer_service.delete_customer(session, customer_id, operator=current_user, meta=meta) return ok(message="客户已删除") @router.put( "/{customer_id}/ownership", response_model=ApiResponse[CustomerOut], summary="归属变更", ) def change_ownership( session: DbSession, current_user: CurrentUser, customer_id: Annotated[int, Path(ge=1)], payload: CustomerOwnershipIn, meta: RequestMetaDep, ) -> ApiResponse[CustomerOut]: """调整客户归属(唯一写归属留痕的入口)。 只影响变更后核算,不回溯历史(对照表 #19);新旧归属人员ID任一为 null 表示清空归属。新旧归属相同报「归属未变化」。 """ return ok( customer_service.change_ownership( session, customer_id, payload, operator=current_user, meta=meta ), message="归属已调整", ) @router.get( "/{customer_id}/ownership-logs", response_model=ApiResponse[PageResult[OwnershipChangeLogOut]], summary="归属变更留痕", ) def list_ownership_logs( session: DbSession, _: CurrentUser, customer_id: Annotated[int, Path(ge=1)], query: Annotated[PageQuery, Query()], ) -> ApiResponse[PageResult[OwnershipChangeLogOut]]: """查某客户的归属变更留痕(分页,按变更时间倒序,附新旧归属人员姓名)。""" return ok(customer_service.list_ownership_logs(session, customer_id, query)) @router.get( "/{customer_id}/rating", response_model=ApiResponse[CustomerRatingOut], summary="客户当前评级", ) def get_customer_rating( session: DbSession, _: CurrentUser, customer_id: Annotated[int, Path(ge=1)], ) -> ApiResponse[CustomerRatingOut]: """查某客户当前评级。 未评级返回 ``data: null``(这是正常态而非错误);客户不存在才 404。 """ return ok(rating_service.get_customer_rating(session, customer_id)) @router.put( "/{customer_id}/rating", response_model=ApiResponse[CustomerRatingOut], summary="保存客户评级", ) def save_customer_rating( session: DbSession, current_user: CurrentUser, customer_id: Annotated[int, Path(ge=1)], payload: CustomerRatingSaveIn, meta: RequestMetaDep, ) -> ApiResponse[CustomerRatingOut]: """保存客户评级(首次创建、再次保存整体覆盖)。 五项分值 0-20 由服务层在审计上下文内校验,总分与等级由服务端重算; 等级按保存时点的映射快照固化,等级表后续增删改不影响该客户结论。 """ return ok( rating_service.save_customer_rating( session, customer_id, payload, operator=current_user, meta=meta ), message="评级已保存", )