435 lines
23 KiB
Python
435 lines
23 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 CustomerColumnFilterMixin(BaseModel):
|
||
"""列头漏斗参数:前端 camelCase ↔ 后端 snake_case,供 build_customer_column_filters。"""
|
||
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
# 文本
|
||
customer_name: Optional[str] = Field(default=None, alias="customerName", description="客户名称")
|
||
customer_no: Optional[str] = Field(default=None, alias="customerNo", description="客户编号")
|
||
short_name: Optional[str] = Field(default=None, alias="shortName", 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="客户网站")
|
||
address: 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="区")
|
||
owner_user_name: Optional[str] = Field(default=None, alias="ownerUserName", description="负责人姓名")
|
||
remark: Optional[str] = Field(default=None, description="备注")
|
||
|
||
# 枚举单值(兼容旧入参)+ 多选 *In
|
||
customer_type: Optional[str] = Field(default=None, alias="customerType", description="客户类型")
|
||
customer_type_in: Optional[str] = Field(default=None, alias="customerTypeIn", description="客户类型多选(逗号)")
|
||
level: Optional[str] = Field(default=None, description="客户级别")
|
||
level_in: Optional[str] = Field(default=None, alias="levelIn", description="客户级别多选(逗号)")
|
||
source: Optional[str] = Field(default=None, description="客户来源")
|
||
source_in: Optional[str] = Field(default=None, alias="sourceIn", description="客户来源多选(逗号)")
|
||
industry: Optional[str] = Field(default=None, description="所属行业")
|
||
industry_in: Optional[str] = Field(default=None, alias="industryIn", description="所属行业多选(逗号)")
|
||
park_area: Optional[str] = Field(default=None, alias="parkArea", description="所在园区")
|
||
park_area_in: Optional[str] = Field(default=None, alias="parkAreaIn", description="所在园区多选(逗号)")
|
||
scale: Optional[str] = Field(default=None, description="企业规模")
|
||
scale_in: Optional[str] = Field(default=None, alias="scaleIn", description="企业规模多选(逗号)")
|
||
status: Optional[str] = Field(default=None, description="客户状态")
|
||
status_in: Optional[str] = Field(default=None, alias="statusIn", description="客户状态多选(逗号)")
|
||
deal_status: Optional[str] = Field(default=None, alias="dealStatus", description="成交状态")
|
||
deal_status_in: Optional[str] = Field(default=None, alias="dealStatusIn", description="成交状态多选(逗号)")
|
||
|
||
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
|
||
|
||
# 创建时间(begin/end 与 createTimeFrom/To 并存;range 由 CustomerListBody 解析)
|
||
begin_time: Optional[str] = Field(default=None, alias="beginTime", description="创建时间起")
|
||
end_time: Optional[str] = Field(default=None, alias="endTime", description="创建时间止")
|
||
create_time_from: Optional[str] = Field(default=None, alias="createTimeFrom", description="创建时间起")
|
||
create_time_to: Optional[str] = Field(default=None, alias="createTimeTo", description="创建时间止")
|
||
|
||
# 其它日期 From/To
|
||
last_time_from: Optional[str] = Field(default=None, alias="lastTimeFrom")
|
||
last_time_to: Optional[str] = Field(default=None, alias="lastTimeTo")
|
||
next_time_from: Optional[str] = Field(default=None, alias="nextTimeFrom")
|
||
next_time_to: Optional[str] = Field(default=None, alias="nextTimeTo")
|
||
deal_time_from: Optional[str] = Field(default=None, alias="dealTimeFrom")
|
||
deal_time_to: Optional[str] = Field(default=None, alias="dealTimeTo")
|
||
into_pool_time_from: Optional[str] = Field(default=None, alias="intoPoolTimeFrom")
|
||
into_pool_time_to: Optional[str] = Field(default=None, alias="intoPoolTimeTo")
|
||
claim_time_from: Optional[str] = Field(default=None, alias="claimTimeFrom")
|
||
claim_time_to: Optional[str] = Field(default=None, alias="claimTimeTo")
|
||
protect_time_from: Optional[str] = Field(default=None, alias="protectTimeFrom")
|
||
protect_time_to: Optional[str] = Field(default=None, alias="protectTimeTo")
|
||
update_time_from: Optional[str] = Field(default=None, alias="updateTimeFrom")
|
||
update_time_to: Optional[str] = Field(default=None, alias="updateTimeTo")
|
||
|
||
# 数字 Op / Min / Max / 单值
|
||
deal_amount_op: Optional[str] = Field(default=None, alias="dealAmountOp")
|
||
deal_amount: Optional[str] = Field(default=None, alias="dealAmount")
|
||
deal_amount_min: Optional[str] = Field(default=None, alias="dealAmountMin")
|
||
deal_amount_max: Optional[str] = Field(default=None, alias="dealAmountMax")
|
||
follow_count_op: Optional[str] = Field(default=None, alias="followCountOp")
|
||
follow_count: Optional[str] = Field(default=None, alias="followCount")
|
||
follow_count_min: Optional[str] = Field(default=None, alias="followCountMin")
|
||
follow_count_max: Optional[str] = Field(default=None, alias="followCountMax")
|
||
contact_count_op: Optional[str] = Field(default=None, alias="contactCountOp")
|
||
contact_count: Optional[str] = Field(default=None, alias="contactCount")
|
||
contact_count_min: Optional[str] = Field(default=None, alias="contactCountMin")
|
||
contact_count_max: Optional[str] = Field(default=None, alias="contactCountMax")
|
||
opportunity_count_op: Optional[str] = Field(default=None, alias="opportunityCountOp")
|
||
opportunity_count: Optional[str] = Field(default=None, alias="opportunityCount")
|
||
opportunity_count_min: Optional[str] = Field(default=None, alias="opportunityCountMin")
|
||
opportunity_count_max: Optional[str] = Field(default=None, alias="opportunityCountMax")
|
||
contract_count_op: Optional[str] = Field(default=None, alias="contractCountOp")
|
||
contract_count: Optional[str] = Field(default=None, alias="contractCount")
|
||
contract_count_min: Optional[str] = Field(default=None, alias="contractCountMin")
|
||
contract_count_max: Optional[str] = Field(default=None, alias="contractCountMax")
|
||
|
||
def column_filter_kwargs(self) -> dict:
|
||
"""snake_case 列筛参数,供 CRUD / build_customer_column_filters。"""
|
||
return {
|
||
"customer_name": self.customer_name,
|
||
"customer_no": self.customer_no,
|
||
"short_name": self.short_name,
|
||
"mobile": self.mobile,
|
||
"telephone": self.telephone,
|
||
"email": self.email,
|
||
"website": self.website,
|
||
"address": self.address,
|
||
"province": self.province,
|
||
"city": self.city,
|
||
"district": self.district,
|
||
"owner_user_name": self.owner_user_name,
|
||
"remark": self.remark,
|
||
"customer_type": self.customer_type,
|
||
"customer_type_in": self.customer_type_in,
|
||
"level": self.level,
|
||
"level_in": self.level_in,
|
||
"source": self.source,
|
||
"source_in": self.source_in,
|
||
"industry": self.industry,
|
||
"industry_in": self.industry_in,
|
||
"park_area": self.park_area,
|
||
"park_area_in": self.park_area_in,
|
||
"scale": self.scale,
|
||
"scale_in": self.scale_in,
|
||
"status": self.status,
|
||
"status_in": self.status_in,
|
||
"deal_status": self.deal_status,
|
||
"deal_status_in": self.deal_status_in,
|
||
"owner_user_id": self.owner_user_id,
|
||
"begin_time": self.begin_time,
|
||
"end_time": self.end_time,
|
||
"create_time_from": self.create_time_from,
|
||
"create_time_to": self.create_time_to,
|
||
"last_time_from": self.last_time_from,
|
||
"last_time_to": self.last_time_to,
|
||
"next_time_from": self.next_time_from,
|
||
"next_time_to": self.next_time_to,
|
||
"deal_time_from": self.deal_time_from,
|
||
"deal_time_to": self.deal_time_to,
|
||
"into_pool_time_from": self.into_pool_time_from,
|
||
"into_pool_time_to": self.into_pool_time_to,
|
||
"claim_time_from": self.claim_time_from,
|
||
"claim_time_to": self.claim_time_to,
|
||
"protect_time_from": self.protect_time_from,
|
||
"protect_time_to": self.protect_time_to,
|
||
"update_time_from": self.update_time_from,
|
||
"update_time_to": self.update_time_to,
|
||
"deal_amount_op": self.deal_amount_op,
|
||
"deal_amount": self.deal_amount,
|
||
"deal_amount_min": self.deal_amount_min,
|
||
"deal_amount_max": self.deal_amount_max,
|
||
"follow_count_op": self.follow_count_op,
|
||
"follow_count": self.follow_count,
|
||
"follow_count_min": self.follow_count_min,
|
||
"follow_count_max": self.follow_count_max,
|
||
"contact_count_op": self.contact_count_op,
|
||
"contact_count": self.contact_count,
|
||
"contact_count_min": self.contact_count_min,
|
||
"contact_count_max": self.contact_count_max,
|
||
"opportunity_count_op": self.opportunity_count_op,
|
||
"opportunity_count": self.opportunity_count,
|
||
"opportunity_count_min": self.opportunity_count_min,
|
||
"opportunity_count_max": self.opportunity_count_max,
|
||
"contract_count_op": self.contract_count_op,
|
||
"contract_count": self.contract_count,
|
||
"contract_count_min": self.contract_count_min,
|
||
"contract_count_max": self.contract_count_max,
|
||
}
|
||
|
||
|
||
# ── 客户列表 ──────────────────────────────────────────────────
|
||
|
||
|
||
class CustomerListBody(CustomerColumnFilterMixin):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
page: int = Field(default=1, ge=1, description="页码")
|
||
pageSize: int = Field(default=10, ge=1, le=500, description="每页条数")
|
||
keyword: Optional[str] = Field(default=None, description="搜索关键词(客户名称/编号/手机号)")
|
||
search_keyword: Optional[str] = Field(
|
||
default=None, alias="searchKeyword", description="快速搜索关键词(与 keyword 等价)"
|
||
)
|
||
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]"
|
||
)
|
||
|
||
ids: Optional[List[str]] = Field(default=None, description="导出指定ID列表")
|
||
columns: Optional[List[str]] = Field(default=None, description="导出列(前端 visibleColumns)")
|
||
|
||
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
|
||
view_mode = view_mode_override or self.view_mode or self.view_type or "all"
|
||
kwargs = {
|
||
"keyword": keyword,
|
||
"view_mode": view_mode,
|
||
**self.column_filter_kwargs(),
|
||
}
|
||
# createTimeRange 优先于 begin/end / createTimeFrom/To
|
||
if self.create_time_range and len(self.create_time_range) == 2:
|
||
kwargs["begin_time"] = self.create_time_range[0]
|
||
kwargs["end_time"] = self.create_time_range[1]
|
||
kwargs["create_time_from"] = None
|
||
kwargs["create_time_to"] = None
|
||
return kwargs
|
||
|
||
|
||
# ── 客户新增 ──────────────────────────────────────────────────
|
||
|
||
|
||
class CustomerCreateBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
customer_name: str = Field(..., alias="customerName", description="客户名称")
|
||
name_pinyin_abbr: Optional[str] = Field(
|
||
default=None, alias="namePinyinAbbr", 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="客户名称")
|
||
name_pinyin_abbr: Optional[str] = Field(
|
||
default=None, alias="namePinyinAbbr", 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(编辑时使用)")
|