Files
shanxia2.0/backend/app/schemas/crm/customer.py
T

435 lines
23 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.
"""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(编辑时使用)")