feat(crm): wire customer and pool list filters to column params.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
3bb905bc20
commit
fbf830172f
4 files changed
+201
-104
No files matched your search
+9
-12
@@ -1403,15 +1403,15 @@ def pool_customer_list(
|
||||
):
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
page = body.page if body else 1
|
||||
page_size = body.pageSize if body else 10
|
||||
list_body = body or PoolCustomerListBody()
|
||||
page = list_body.page
|
||||
page_size = list_body.pageSize
|
||||
rows, total = list_pool_customers(
|
||||
db, tenant_no,
|
||||
page=page, page_size=page_size,
|
||||
keyword=body.keyword if body else None,
|
||||
level=body.level if body else None,
|
||||
industry=body.industry if body else None,
|
||||
park_area=body.parkArea if body else None,
|
||||
db,
|
||||
tenant_no,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
**list_body.to_list_kwargs(),
|
||||
)
|
||||
return success_response({
|
||||
"list": rows,
|
||||
@@ -1445,10 +1445,7 @@ def pool_customer_export(
|
||||
page=1,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
max_page_size=EXPORT_MAX_ROWS,
|
||||
keyword=list_body.keyword,
|
||||
level=list_body.level,
|
||||
industry=list_body.industry,
|
||||
park_area=list_body.parkArea,
|
||||
**list_body.to_list_kwargs(),
|
||||
)
|
||||
rows = _filter_export_rows_by_ids(rows, list_body.ids)
|
||||
# map telephone -> phone for shared column if needed
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.crud.crm.data_scope_helpers import (
|
||||
apply_crm_owner_scope,
|
||||
ensure_crm_owner_row_in_scope,
|
||||
)
|
||||
from app.services.crm.customer_list_filters import build_customer_column_filters
|
||||
from app.utils.pinyin_abbr import resolve_name_pinyin_abbr
|
||||
|
||||
|
||||
@@ -220,21 +221,10 @@ def list_customers(
|
||||
page_size: int = 10,
|
||||
keyword: str = None,
|
||||
view_mode: str = None,
|
||||
customer_name: str = None,
|
||||
customer_no: str = None,
|
||||
status: str = None,
|
||||
deal_status: str = None,
|
||||
level: str = None,
|
||||
source: str = None,
|
||||
customer_type: str = None,
|
||||
industry: str = None,
|
||||
park_area: str = None,
|
||||
owner_user_id: str = None,
|
||||
create_time_start: str = None,
|
||||
create_time_end: str = None,
|
||||
user_info=None,
|
||||
skip_data_scope: bool = False,
|
||||
max_page_size: Optional[int] = None,
|
||||
**filter_params,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
q = db.query(CrmCustomer).filter(
|
||||
CrmCustomer.tenant_no == tenant_no,
|
||||
@@ -281,41 +271,11 @@ def list_customers(
|
||||
)
|
||||
)
|
||||
|
||||
if customer_name:
|
||||
cn = customer_name.strip()
|
||||
if cn:
|
||||
q = q.filter(
|
||||
or_(
|
||||
CrmCustomer.customer_name.like(f"%{cn}%"),
|
||||
func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
|
||||
f"%{cn.lower()}%"
|
||||
),
|
||||
)
|
||||
)
|
||||
if customer_no:
|
||||
q = q.filter(CrmCustomer.customer_no.like(f"%{customer_no}%"))
|
||||
|
||||
if status:
|
||||
q = q.filter(CrmCustomer.status == status)
|
||||
if deal_status:
|
||||
q = q.filter(CrmCustomer.deal_status == deal_status)
|
||||
if level:
|
||||
q = q.filter(CrmCustomer.level == level)
|
||||
if source:
|
||||
q = q.filter(CrmCustomer.source == source)
|
||||
if customer_type:
|
||||
q = q.filter(CrmCustomer.customer_type == customer_type)
|
||||
if industry:
|
||||
q = q.filter(CrmCustomer.industry == industry)
|
||||
if park_area:
|
||||
q = q.filter(CrmCustomer.park_area == park_area)
|
||||
if owner_user_id:
|
||||
q = q.filter(CrmCustomer.owner_user_id == owner_user_id)
|
||||
if create_time_start:
|
||||
q = q.filter(CrmCustomer.create_time >= create_time_start)
|
||||
if create_time_end:
|
||||
q = q.filter(CrmCustomer.create_time <= create_time_end)
|
||||
# 列头 / 旧单值 / 日期 / 数字:统一走 builder(与 export 共用)
|
||||
for expr in build_customer_column_filters(**filter_params):
|
||||
q = q.filter(expr)
|
||||
|
||||
status = filter_params.get("status")
|
||||
pool_scope_exempt = skip_data_scope or view_mode == "pool" or status == "pool"
|
||||
if user_info and not pool_scope_exempt:
|
||||
q = apply_crm_owner_scope(q, CrmCustomer, user_info, db, tenant_no)
|
||||
@@ -347,25 +307,22 @@ def list_pool_customers(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
keyword: str = None,
|
||||
level: str = None,
|
||||
industry: str = None,
|
||||
park_area: str = None,
|
||||
user_info=None,
|
||||
max_page_size: Optional[int] = None,
|
||||
**filter_params,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
params = dict(filter_params)
|
||||
params["status"] = "pool"
|
||||
return list_customers(
|
||||
db,
|
||||
tenant_no,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
keyword=keyword,
|
||||
status="pool",
|
||||
level=level,
|
||||
industry=industry,
|
||||
park_area=park_area,
|
||||
user_info=user_info,
|
||||
skip_data_scope=True,
|
||||
max_page_size=max_page_size,
|
||||
**params,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -21,10 +21,171 @@ class CustomerOptionsBody(BaseModel):
|
||||
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(BaseModel):
|
||||
class CustomerListBody(CustomerColumnFilterMixin):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
page: int = Field(default=1, ge=1, description="页码")
|
||||
@@ -33,24 +194,11 @@ class CustomerListBody(BaseModel):
|
||||
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="创建时间止")
|
||||
|
||||
|
||||
ids: Optional[List[str]] = Field(default=None, description="导出指定ID列表")
|
||||
columns: Optional[List[str]] = Field(default=None, description="导出列(前端 visibleColumns)")
|
||||
@@ -58,31 +206,19 @@ class CustomerListBody(BaseModel):
|
||||
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 {
|
||||
kwargs = {
|
||||
"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,
|
||||
**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
|
||||
|
||||
|
||||
# ── 客户新增 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -3,19 +3,26 @@ from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.schemas.crm.customer import CustomerColumnFilterMixin
|
||||
|
||||
class PoolCustomerListBody(BaseModel):
|
||||
|
||||
class PoolCustomerListBody(CustomerColumnFilterMixin):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
page: int = Field(default=1, ge=1)
|
||||
pageSize: int = Field(default=10, ge=1, le=500)
|
||||
keyword: Optional[str] = Field(default=None)
|
||||
level: Optional[str] = Field(default=None)
|
||||
industry: Optional[str] = Field(default=None)
|
||||
parkArea: Optional[str] = Field(default=None, alias="parkArea")
|
||||
ids: Optional[List[str]] = Field(default=None, description="导出指定ID列表")
|
||||
columns: Optional[List[str]] = Field(default=None, description="导出列(前端 visibleColumns)")
|
||||
|
||||
def to_list_kwargs(self) -> dict:
|
||||
"""列筛 + keyword;公海范围由 list_pool_customers 强制 status=pool。"""
|
||||
keyword = (self.keyword or "").strip() or None
|
||||
return {
|
||||
"keyword": keyword,
|
||||
**self.column_filter_kwargs(),
|
||||
}
|
||||
|
||||
|
||||
class PoolAssignBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
Reference in new issue
Block a user