2026-10-12 22:35:04 -04:00
|
|
|
|
"""客户管理接口(需求 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.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]:
|
|
|
|
|
|
"""删除客户(物理删除)。
|
|
|
|
|
|
|
2026-10-14 06:48:32 -04:00
|
|
|
|
已存在关联业务时返回 400(先处理相关业务单据)。
|
2026-10-12 22:35:04 -04:00
|
|
|
|
"""
|
|
|
|
|
|
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))
|