PayrollManagementSystem/backend/app/api/v1/base/customers.py
liuwenxin 32b7940067 feat(customer): 客户评级并入客户管理并落库
- 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
2026-10-23 10:59:33 +08:00

169 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""客户管理接口(需求 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))