285 lines
15 KiB
Python
285 lines
15 KiB
Python
"""CRM 线索管理 Pydantic Schemas"""
|
||
|
||
from typing import List, Optional
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
|
||
|
||
# ── 线索列表查询 ──────────────────────────────────────────────────
|
||
|
||
|
||
class ClueColumnFilterMixin(BaseModel):
|
||
"""列头漏斗参数:前端 camelCase ↔ 后端 snake_case。"""
|
||
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
clue_name: Optional[str] = Field(default=None, alias="clueName", description="线索名称")
|
||
clue_name_in: Optional[str] = Field(
|
||
default=None, alias="clueNameIn", description="线索名称多选(逗号)"
|
||
)
|
||
company: Optional[str] = Field(default=None, description="公司名称")
|
||
company_in: Optional[str] = Field(default=None, alias="companyIn", description="公司名称多选(逗号)")
|
||
mobile: Optional[str] = Field(default=None, description="手机号")
|
||
telephone: Optional[str] = Field(default=None, description="固定电话")
|
||
email: Optional[str] = Field(default=None, description="邮箱")
|
||
owner_user_name: Optional[str] = Field(default=None, alias="ownerUserName")
|
||
remark: Optional[str] = Field(default=None, description="备注")
|
||
|
||
customer_type: Optional[str] = Field(default=None, alias="customerType")
|
||
customer_type_in: Optional[str] = Field(default=None, alias="customerTypeIn")
|
||
source: Optional[str] = Field(default=None, description="线索来源")
|
||
source_in: Optional[str] = Field(default=None, alias="sourceIn")
|
||
level: Optional[str] = Field(default=None, description="线索级别")
|
||
level_in: Optional[str] = Field(default=None, alias="levelIn")
|
||
status: Optional[str] = Field(default=None, description="线索状态")
|
||
status_in: Optional[str] = Field(default=None, alias="statusIn")
|
||
industry: Optional[str] = Field(default=None, description="所属行业")
|
||
industry_in: Optional[str] = Field(default=None, alias="industryIn")
|
||
park_area: Optional[str] = Field(default=None, alias="parkArea", description="所在园区")
|
||
park_area_in: Optional[str] = Field(default=None, alias="parkAreaIn")
|
||
|
||
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
|
||
|
||
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")
|
||
create_time_to: Optional[str] = Field(default=None, alias="createTimeTo")
|
||
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")
|
||
update_time_from: Optional[str] = Field(default=None, alias="updateTimeFrom")
|
||
update_time_to: Optional[str] = Field(default=None, alias="updateTimeTo")
|
||
|
||
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")
|
||
|
||
def column_filter_kwargs(self) -> dict:
|
||
return {
|
||
"clue_name": self.clue_name,
|
||
"clue_name_in": self.clue_name_in,
|
||
"company": self.company,
|
||
"company_in": self.company_in,
|
||
"mobile": self.mobile,
|
||
"telephone": self.telephone,
|
||
"email": self.email,
|
||
"owner_user_name": self.owner_user_name,
|
||
"remark": self.remark,
|
||
"customer_type": self.customer_type,
|
||
"customer_type_in": self.customer_type_in,
|
||
"source": self.source,
|
||
"source_in": self.source_in,
|
||
"level": self.level,
|
||
"level_in": self.level_in,
|
||
"status": self.status,
|
||
"status_in": self.status_in,
|
||
"industry": self.industry,
|
||
"industry_in": self.industry_in,
|
||
"park_area": self.park_area,
|
||
"park_area_in": self.park_area_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,
|
||
"update_time_from": self.update_time_from,
|
||
"update_time_to": self.update_time_to,
|
||
"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,
|
||
}
|
||
|
||
|
||
class ClueListBody(ClueColumnFilterMixin):
|
||
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/todayNew/converted)"
|
||
)
|
||
view_mode: Optional[str] = Field(default=None, alias="viewMode", description="视图模式(mine/all)")
|
||
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_type_override: Optional[str] = None) -> dict:
|
||
keyword = (self.keyword or self.search_keyword or "").strip() or None
|
||
kwargs = {
|
||
"keyword": keyword,
|
||
"view_type": view_type_override or self.view_type,
|
||
"view_mode": self.view_mode,
|
||
**self.column_filter_kwargs(),
|
||
}
|
||
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 ClueColumnFacetBody(ClueColumnFilterMixin):
|
||
"""线索列表列头可选值(去重;继承列筛 Mixin 支持 Excel 级联)"""
|
||
|
||
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
||
|
||
field: str = Field(..., description="clueName | company")
|
||
keyword: Optional[str] = Field(default=None, description="列表搜索关键词")
|
||
limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
|
||
view_type: Optional[str] = Field(
|
||
default=None, alias="viewType", description="视图类型(mine/all/toFollow/todayNew/converted)"
|
||
)
|
||
view_mode: Optional[str] = Field(default=None, alias="viewMode", description="视图模式(mine/all)")
|
||
|
||
|
||
# ── 线索新增/编辑 ──────────────────────────────────────────────────
|
||
|
||
|
||
class ClueCreateBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
clue_name: str = Field(..., alias="clueName", min_length=1, max_length=100, description="线索名称")
|
||
mobile: str = Field(..., min_length=1, max_length=20, description="手机号")
|
||
company: Optional[str] = Field(default=None, max_length=200, description="公司名称")
|
||
customer_type: Optional[str] = Field(default=None, alias="customerType", max_length=20, description="客户类型")
|
||
telephone: Optional[str] = Field(default=None, max_length=30, description="座机")
|
||
email: Optional[str] = Field(default=None, max_length=100, description="邮箱")
|
||
title: Optional[str] = Field(default=None, max_length=10, description="称谓")
|
||
position: Optional[str] = Field(default=None, max_length=50, description="职位")
|
||
source: str = Field(..., max_length=20, description="线索来源")
|
||
level: Optional[str] = Field(default="unrated", max_length=10, description="线索级别")
|
||
industry: Optional[str] = Field(default=None, max_length=50, description="所属行业")
|
||
park_area: Optional[str] = Field(default=None, alias="parkArea", max_length=100, description="所在园区")
|
||
province: Optional[str] = Field(default=None, max_length=50, description="省份")
|
||
city: Optional[str] = Field(default=None, max_length=50, description="城市")
|
||
district: Optional[str] = Field(default=None, max_length=50, description="区县")
|
||
address: Optional[str] = Field(default=None, max_length=255, description="详细地址")
|
||
website: Optional[str] = Field(default=None, max_length=500, description="网站")
|
||
intent_products: Optional[str] = Field(
|
||
default=None, alias="intentProducts", max_length=500, description="意向产品"
|
||
)
|
||
expect_time: Optional[str] = Field(default=None, alias="expectTime", description="预计购买时间")
|
||
budget: Optional[str] = Field(default=None, max_length=20, description="预算")
|
||
requirement: Optional[str] = Field(default=None, description="需求描述")
|
||
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
|
||
next_time: Optional[str] = Field(default=None, alias="nextTime", description="下次联系时间")
|
||
remark: Optional[str] = Field(default=None, max_length=512, description="备注")
|
||
|
||
|
||
class ClueUpdateBody(ClueCreateBody):
|
||
id: str = Field(..., min_length=1, description="线索ID")
|
||
|
||
|
||
# ── 线索操作 ──────────────────────────────────────────────────
|
||
|
||
|
||
class ClueDeleteBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
ids: List[str] = Field(..., min_length=1, description="线索ID列表")
|
||
|
||
|
||
class ClueAssignBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
ids: List[str] = Field(..., min_length=1, description="线索ID列表")
|
||
owner_user_id: str = Field(..., alias="ownerUserId", description="新负责人ID")
|
||
remark: Optional[str] = Field(default=None, description="分配说明")
|
||
|
||
|
||
class ClueConvertBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
id: str = Field(..., min_length=1, description="线索ID")
|
||
customer_name: Optional[str] = Field(default=None, alias="customerName", description="客户名称")
|
||
customer_type: Optional[str] = Field(default=None, alias="customerType", description="客户类型")
|
||
contact_name: Optional[str] = Field(default=None, alias="contactName", description="联系人姓名")
|
||
contact_mobile: Optional[str] = Field(default=None, alias="contactMobile", description="联系人手机号")
|
||
remark: Optional[str] = Field(default=None, description="转化说明")
|
||
|
||
|
||
class ClueStatusBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
id: str = Field(..., min_length=1, description="线索ID")
|
||
status: str = Field(..., max_length=20, description="目标状态")
|
||
|
||
|
||
class ClueLevelBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
ids: List[str] = Field(..., min_length=1, description="线索ID列表")
|
||
level: str = Field(..., max_length=10, description="目标级别")
|
||
|
||
|
||
class ClueInvalidBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
id: Optional[str] = Field(default=None, description="线索ID")
|
||
ids: Optional[List[str]] = Field(default=None, description="线索ID列表")
|
||
remark: Optional[str] = Field(default=None, description="作废原因")
|
||
|
||
|
||
# ── 跟进记录 ──────────────────────────────────────────────────
|
||
|
||
|
||
class ClueFollowListBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
clue_id: str = Field(..., alias="clueId", description="线索ID")
|
||
page: int = Field(default=1, ge=1, description="页码")
|
||
pageSize: int = Field(default=10, ge=1, le=500, description="每页条数")
|
||
|
||
|
||
class ClueFollowCreateBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
clue_id: str = Field(..., alias="clueId", description="线索ID")
|
||
follow_type: str = Field(..., alias="followType", max_length=20, description="跟进方式")
|
||
follow_content: str = Field(..., alias="followContent", description="跟进内容")
|
||
next_time: Optional[str] = Field(default=None, alias="nextTime", description="下次联系时间")
|
||
attachments: Optional[str] = Field(default=None, max_length=1000, description="附件URL(逗号分隔)")
|
||
remark: Optional[str] = Field(default=None, description="备注")
|
||
|
||
|
||
class ClueFollowDeleteBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
ids: List[str] = Field(..., min_length=1, description="跟进记录ID列表")
|
||
|
||
|
||
class ClueFollowUpdateBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
id: str = Field(..., min_length=1, description="跟进记录ID")
|
||
follow_type: Optional[str] = Field(default=None, alias="followType", description="跟进方式")
|
||
follow_content: Optional[str] = Field(default=None, alias="followContent", description="跟进内容")
|
||
next_time: Optional[str] = Field(default=None, alias="nextTime", description="下次联系时间")
|
||
attachments: Optional[str] = Field(default=None, max_length=1000, description="附件URL(逗号分隔)")
|
||
remark: Optional[str] = Field(default=None, description="备注")
|
||
|
||
|
||
class ClueStatisticsBody(BaseModel):
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
begin_time: Optional[str] = Field(default=None, alias="beginTime", description="开始日期")
|
||
end_time: Optional[str] = Field(default=None, alias="endTime", description="结束日期")
|
||
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
|
||
time_unit: Optional[str] = Field(default="day", alias="timeUnit", description="趋势粒度 day/week/month")
|
||
type: Optional[str] = Field(default=None, description="趋势粒度别名")
|