Files
shanxia2.0/backend/app/schemas/crm/customer.py
T
2026-08-04 14:02:01 +08:00

289 lines
14 KiB
Python

"""CRM 客户管理 Pydantic Schemas"""
from decimal import Decimal
from typing import List, Optional
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, model_validator
# ── 客户下拉选项(主数据选用,不做数据权限)────────────────────
class CustomerOptionsBody(BaseModel):
"""客户下拉选项请求体(租户内只读搜索,不做 owner 数据权限)"""
model_config = ConfigDict(populate_by_name=True)
keyword: Optional[str] = Field(default=None, description="搜索关键词(客户名称/编号)")
customer_name: Optional[str] = Field(
default=None, alias="customerName", description="客户名称(模糊)"
)
limit: int = Field(default=50, ge=1, le=200, description="返回条数上限")
# ── 客户列表 ──────────────────────────────────────────────────
class CustomerListBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
page: int = Field(default=1, ge=1, description="页码")
pageSize: int = Field(default=10, ge=1, le=100, description="每页条数")
keyword: Optional[str] = Field(default=None, description="搜索关键词(客户名称/编号/手机号)")
search_keyword: Optional[str] = Field(
default=None, alias="searchKeyword", description="快速搜索关键词(与 keyword 等价)"
)
customer_name: Optional[str] = Field(default=None, alias="customerName", description="客户名称")
customer_no: Optional[str] = Field(default=None, alias="customerNo", description="客户编号")
customer_type: Optional[str] = Field(default=None, alias="customerType", description="客户类型")
level: Optional[str] = Field(default=None, description="客户级别")
source: Optional[str] = Field(default=None, description="客户来源")
industry: Optional[str] = Field(default=None, description="所属行业")
park_area: Optional[str] = Field(default=None, alias="parkArea", description="所在园区")
status: Optional[str] = Field(default=None, description="客户状态")
deal_status: Optional[str] = Field(default=None, alias="dealStatus", description="成交状态")
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
view_type: Optional[str] = Field(default=None, alias="viewType", description="视图类型(mine/all/toFollow/key)")
view_mode: Optional[str] = Field(default=None, alias="viewMode", description="视图模式(mine/all/toFollow/key)")
create_time_range: Optional[List[str]] = Field(
default=None, alias="createTimeRange", description="创建时间范围 [start, end]"
)
begin_time: Optional[str] = Field(default=None, alias="beginTime", description="创建时间起")
end_time: Optional[str] = Field(default=None, alias="endTime", description="创建时间止")
def to_list_kwargs(self, *, view_mode_override: Optional[str] = None) -> dict:
"""解析列表查询参数,供 API 与 CRUD 共用。"""
keyword = (self.keyword or self.search_keyword or "").strip() or None
create_time_start = None
create_time_end = None
if self.create_time_range and len(self.create_time_range) == 2:
create_time_start = self.create_time_range[0]
create_time_end = self.create_time_range[1]
else:
create_time_start = self.begin_time
create_time_end = self.end_time
view_mode = view_mode_override or self.view_mode or self.view_type or "all"
return {
"keyword": keyword,
"view_mode": view_mode,
"customer_name": self.customer_name,
"customer_no": self.customer_no,
"customer_type": self.customer_type,
"level": self.level,
"source": self.source,
"industry": self.industry,
"park_area": self.park_area,
"status": self.status,
"deal_status": self.deal_status,
"owner_user_id": self.owner_user_id,
"create_time_start": create_time_start,
"create_time_end": create_time_end,
}
# ── 客户新增 ──────────────────────────────────────────────────
class CustomerCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
customer_name: str = Field(..., alias="customerName", description="客户名称")
short_name: Optional[str] = Field(default=None, alias="shortName", description="客户简称")
customer_type: str = Field(default="company", alias="customerType", description="客户类型")
mobile: Optional[str] = Field(default=None, description="手机号")
telephone: Optional[str] = Field(default=None, description="固定电话")
email: Optional[str] = Field(default=None, description="邮箱")
website: Optional[str] = Field(default=None, description="客户网站")
level: str = Field(default="normal", description="客户级别")
source: Optional[str] = Field(default=None, description="客户来源")
industry: Optional[str] = Field(default=None, description="所属行业")
park_area: Optional[str] = Field(default=None, alias="parkArea", description="所在园区")
payment_terms: Optional[str] = Field(default=None, alias="paymentTerms", description="收付款条件")
scale: Optional[str] = Field(default=None, description="企业规模")
province: Optional[str] = Field(default=None, description="省")
city: Optional[str] = Field(default=None, description="市")
district: Optional[str] = Field(default=None, description="区")
address: Optional[str] = Field(default=None, description="详细地址")
next_time: Optional[str] = Field(default=None, alias="nextTime", description="下次联系时间")
remark: Optional[str] = Field(default=None, description="备注")
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
business_license_img: Optional[str] = Field(
default=None,
validation_alias=AliasChoices("businessLicenseImages", "businessLicenseImg"),
serialization_alias="businessLicenseImages",
description="营业执照图片URL",
)
business_permit_images: Optional[str] = Field(
default=None, alias="businessPermitImages", description="经营许可证图片URL"
)
other_cert_images: Optional[str] = Field(
default=None, alias="otherCertImages", description="其他证件图片URL"
)
@model_validator(mode="before")
@classmethod
def _forbid_legacy_invoice_fields(cls, data):
if not isinstance(data, dict):
return data
legacy_keys = {
"invoiceTitle",
"taxNo",
"invoiceBank",
"invoiceAccount",
"invoiceAddress",
"invoicePhone",
"invoice_title",
"tax_no",
"invoice_bank",
"invoice_account",
"invoice_address",
"invoice_phone",
}
if any(k in data for k in legacy_keys):
raise ValueError("客户开票信息已收敛到“开票信息”模块,请通过 /invoice-info/* 维护。")
return data
# ── 客户更新 ──────────────────────────────────────────────────
class CustomerUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str = Field(..., description="客户ID")
customer_name: Optional[str] = Field(default=None, alias="customerName", description="客户名称")
short_name: Optional[str] = Field(default=None, alias="shortName", description="客户简称")
customer_type: Optional[str] = Field(default=None, alias="customerType", description="客户类型")
mobile: Optional[str] = Field(default=None, description="手机号")
telephone: Optional[str] = Field(default=None, description="固定电话")
email: Optional[str] = Field(default=None, description="邮箱")
website: Optional[str] = Field(default=None, description="客户网站")
level: Optional[str] = Field(default=None, description="客户级别")
source: Optional[str] = Field(default=None, description="客户来源")
industry: Optional[str] = Field(default=None, description="所属行业")
park_area: Optional[str] = Field(default=None, alias="parkArea", description="所在园区")
payment_terms: Optional[str] = Field(default=None, alias="paymentTerms", description="收付款条件")
scale: Optional[str] = Field(default=None, description="企业规模")
province: Optional[str] = Field(default=None, description="省")
city: Optional[str] = Field(default=None, description="市")
district: Optional[str] = Field(default=None, description="区")
address: Optional[str] = Field(default=None, description="详细地址")
next_time: Optional[str] = Field(default=None, alias="nextTime", description="下次联系时间")
remark: Optional[str] = Field(default=None, description="备注")
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
business_license_img: Optional[str] = Field(
default=None,
validation_alias=AliasChoices("businessLicenseImages", "businessLicenseImg"),
serialization_alias="businessLicenseImages",
description="营业执照图片URL",
)
business_permit_images: Optional[str] = Field(
default=None, alias="businessPermitImages", description="经营许可证图片URL"
)
other_cert_images: Optional[str] = Field(
default=None, alias="otherCertImages", description="其他证件图片URL"
)
@model_validator(mode="before")
@classmethod
def _forbid_legacy_invoice_fields(cls, data):
if not isinstance(data, dict):
return data
legacy_keys = {
"invoiceTitle",
"taxNo",
"invoiceBank",
"invoiceAccount",
"invoiceAddress",
"invoicePhone",
"invoice_title",
"tax_no",
"invoice_bank",
"invoice_account",
"invoice_address",
"invoice_phone",
}
if any(k in data for k in legacy_keys):
raise ValueError("客户开票信息已收敛到“开票信息”模块,请通过 /invoice-info/* 维护。")
return data
# ── 客户删除 ──────────────────────────────────────────────────
class CustomerDeleteBody(BaseModel):
id: Optional[str] = Field(default=None, description="客户ID(单条删除)")
ids: Optional[List[str]] = Field(default=None, description="客户ID列表(批量删除)")
# ── 客户详情 ──────────────────────────────────────────────────
class CustomerDetailBody(BaseModel):
id: str = Field(..., description="客户ID")
# ── 客户转移 ──────────────────────────────────────────────────
class CustomerTransferBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
ids: List[str] = Field(..., description="客户ID列表")
owner_user_id: str = Field(..., alias="ownerUserId", description="新负责人ID")
owner_user_name: Optional[str] = Field(default=None, alias="ownerUserName", description="新负责人姓名")
remark: Optional[str] = Field(default=None, description="转移原因")
# ── 客户放入公海池 ──────────────────────────────────────────────
class CustomerIntoPoolBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
ids: List[str] = Field(..., description="客户ID列表")
into_pool_reason: Optional[str] = Field(default=None, alias="intoPoolReason", description="放入公海原因")
# ── 客户成交 ──────────────────────────────────────────────────
class CustomerDealBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str = Field(..., description="客户ID")
deal_amount: Optional[Decimal] = Field(default=None, alias="dealAmount", description="成交金额")
deal_time: Optional[str] = Field(default=None, alias="dealTime", description="成交时间")
remark: Optional[str] = Field(default=None, description="备注")
# ── 公海池领取 ──────────────────────────────────────────────────
class CustomerClaimBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
ids: List[str] = Field(..., description="客户ID列表")
# ── 公海池分配 ──────────────────────────────────────────────────
class CustomerAssignPoolBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
ids: List[str] = Field(..., description="客户ID列表")
owner_user_id: str = Field(..., alias="ownerUserId", description="分配给的用户ID")
remark: Optional[str] = Field(default=None, description="分配原因")
# ── 客户重复检测 ──────────────────────────────────────────────
class CustomerCheckDuplicateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
mobile: Optional[str] = Field(default=None, description="手机号")
customer_name: Optional[str] = Field(default=None, alias="customerName", description="客户名称")
exclude_id: Optional[str] = Field(default=None, alias="excludeId", description="排除的客户ID(编辑时使用)")