feat(crm): wire customer and pool list filters to column params.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-09-06 08:19:48 +08:00
1 parent 3bb905bc20
commit fbf830172f
4 files changed
+201 -104

No files matched your search

+9 -12
View File
@@ -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
+10 -53
View File
@@ -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,
)
+171 -35
View File
@@ -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
# ── 客户新增 ──────────────────────────────────────────────────
+11 -4
View File
@@ -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)