289 lines
14 KiB
Python
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(编辑时使用)")
|