- customers 表新增 rating_score/rating_time(V13 迁移 + V10 SQL + init-dev 同步)
- 新端点 PUT /customers/{id}/rating:评分是档案属性,只存总分与评级时间(五项明细不落库)
- 列表排序 order_by=rating_score,未评分客户升序/降序都垫底,id desc 兜底稳定分页
- 前端 RatingCalculatorDialog 并入客户管理(评分列 + 评级计算按钮),删除独立页与路由
- 测试:评级保存/越界/排序垫底 3 例,test_customers.py 18 passed;schema 同步 216 passed
169 lines
5.7 KiB
Python
169 lines
5.7 KiB
Python
"""客户管理接口(需求 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,
|
||
CustomerRatingIn,
|
||
CustomerUpdateIn,
|
||
OwnershipChangeLogOut,
|
||
)
|
||
from app.services import customer_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}/rating",
|
||
response_model=ApiResponse[CustomerOut],
|
||
summary="保存客户评级",
|
||
)
|
||
def rate_customer(
|
||
session: DbSession,
|
||
current_user: CurrentUser,
|
||
customer_id: Annotated[int, Path(ge=1)],
|
||
payload: CustomerRatingIn,
|
||
meta: RequestMetaDep,
|
||
) -> ApiResponse[CustomerOut]:
|
||
"""保存评级总分(评级计算器「保存评分」)。
|
||
|
||
评分是客户档案属性:存总分与评级时间,五项明细不落库(对照表 #25);
|
||
编辑端点不含评分 —— 评分只走本端点,由计算器统一入口写入。
|
||
"""
|
||
return ok(
|
||
customer_service.rate_customer(
|
||
session, customer_id, payload, operator=current_user, meta=meta
|
||
),
|
||
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))
|