|
|
@@ -0,0 +1,223 @@
|
|
|
+"""CRM 客户列表列头/导出共用过滤条件拼装。
|
|
|
+
|
|
|
+不含 tenant_no / del_flag / view_mode(由 list_customers 负责)。
|
|
|
+create_by_name / update_by_name / create_dept_name 为展示字段(join 补全),此处不筛。
|
|
|
+"""
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+from datetime import datetime, timedelta
|
|
|
+from decimal import Decimal, InvalidOperation
|
|
|
+from typing import Any, List, Optional
|
|
|
+
|
|
|
+from sqlalchemy import func, or_
|
|
|
+
|
|
|
+from app.models.crm_models import CrmCustomer
|
|
|
+
|
|
|
+_ENUM_FIELDS = (
|
|
|
+ "customer_type",
|
|
|
+ "level",
|
|
|
+ "source",
|
|
|
+ "status",
|
|
|
+ "deal_status",
|
|
|
+ "park_area",
|
|
|
+ "industry",
|
|
|
+ "scale",
|
|
|
+)
|
|
|
+
|
|
|
+_TEXT_FIELDS = (
|
|
|
+ "customer_no",
|
|
|
+ "short_name",
|
|
|
+ "mobile",
|
|
|
+ "telephone",
|
|
|
+ "email",
|
|
|
+ "website",
|
|
|
+ "address",
|
|
|
+ "province",
|
|
|
+ "city",
|
|
|
+ "district",
|
|
|
+ "owner_user_name",
|
|
|
+ "remark",
|
|
|
+)
|
|
|
+
|
|
|
+_DATE_FIELDS = (
|
|
|
+ "last_time",
|
|
|
+ "next_time",
|
|
|
+ "deal_time",
|
|
|
+ "into_pool_time",
|
|
|
+ "claim_time",
|
|
|
+ "protect_time",
|
|
|
+ "update_time",
|
|
|
+)
|
|
|
+
|
|
|
+_NUMBER_FIELDS = (
|
|
|
+ "deal_amount",
|
|
|
+ "follow_count",
|
|
|
+ "contact_count",
|
|
|
+ "opportunity_count",
|
|
|
+ "contract_count",
|
|
|
+)
|
|
|
+
|
|
|
+_OPS = frozenset({"between", "eq", "gt", "gte", "lt", "lte"})
|
|
|
+
|
|
|
+_FMT = "%Y-%m-%d %H:%M:%S"
|
|
|
+_DATE_FMT = "%Y-%m-%d"
|
|
|
+
|
|
|
+
|
|
|
+def _split_csv(raw: Optional[str]) -> List[str]:
|
|
|
+ if not raw or not str(raw).strip():
|
|
|
+ return []
|
|
|
+ return [s.strip() for s in str(raw).split(",") if s.strip()]
|
|
|
+
|
|
|
+
|
|
|
+def _parse_number(raw: Optional[str], *, as_int: bool = False):
|
|
|
+ text = str(raw or "").strip()
|
|
|
+ if not text:
|
|
|
+ return None
|
|
|
+ try:
|
|
|
+ if as_int:
|
|
|
+ return int(Decimal(text))
|
|
|
+ return Decimal(text)
|
|
|
+ except (InvalidOperation, ValueError, TypeError):
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _parse_day_start(raw: Optional[str]) -> Optional[datetime]:
|
|
|
+ if raw is None:
|
|
|
+ return None
|
|
|
+ if isinstance(raw, datetime):
|
|
|
+ return raw
|
|
|
+ text = str(raw).strip()
|
|
|
+ if not text:
|
|
|
+ return None
|
|
|
+ for fmt in (_FMT, _DATE_FMT):
|
|
|
+ try:
|
|
|
+ return datetime.strptime(text, fmt)
|
|
|
+ except ValueError:
|
|
|
+ continue
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _append_enum_filters(filters: list, params: dict) -> None:
|
|
|
+ for field in _ENUM_FIELDS:
|
|
|
+ values = _split_csv(params.get(f"{field}_in"))
|
|
|
+ if not values:
|
|
|
+ single = params.get(field)
|
|
|
+ if single is not None and str(single).strip():
|
|
|
+ values = [str(single).strip()]
|
|
|
+ if values:
|
|
|
+ col = getattr(CrmCustomer, field)
|
|
|
+ filters.append(col.in_(values))
|
|
|
+
|
|
|
+
|
|
|
+def _append_text_filters(filters: list, params: dict) -> None:
|
|
|
+ name = (params.get("customer_name") or "").strip()
|
|
|
+ if name:
|
|
|
+ filters.append(
|
|
|
+ or_(
|
|
|
+ CrmCustomer.customer_name.like(f"%{name}%"),
|
|
|
+ func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
|
|
|
+ f"%{name.lower()}%"
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ for field in _TEXT_FIELDS:
|
|
|
+ raw = params.get(field)
|
|
|
+ kw = str(raw).strip() if raw is not None else ""
|
|
|
+ if kw:
|
|
|
+ col = getattr(CrmCustomer, field)
|
|
|
+ filters.append(col.like(f"%{kw}%"))
|
|
|
+
|
|
|
+
|
|
|
+def _append_create_time_filters(filters: list, params: dict) -> None:
|
|
|
+ start_raw = (
|
|
|
+ params.get("begin_time")
|
|
|
+ or params.get("create_time_from")
|
|
|
+ or params.get("create_time_start")
|
|
|
+ )
|
|
|
+ end_raw = (
|
|
|
+ params.get("end_time")
|
|
|
+ or params.get("create_time_to")
|
|
|
+ or params.get("create_time_end")
|
|
|
+ )
|
|
|
+ start = _parse_day_start(start_raw)
|
|
|
+ if start is not None:
|
|
|
+ filters.append(CrmCustomer.create_time >= start)
|
|
|
+
|
|
|
+ if end_raw is None or not str(end_raw).strip():
|
|
|
+ return
|
|
|
+ end_text = str(end_raw).strip()
|
|
|
+ # 仅日期 → 闭区间到当天结束(次日不含)
|
|
|
+ try:
|
|
|
+ day = datetime.strptime(end_text, _DATE_FMT)
|
|
|
+ filters.append(CrmCustomer.create_time < day + timedelta(days=1))
|
|
|
+ return
|
|
|
+ except ValueError:
|
|
|
+ pass
|
|
|
+ end = _parse_day_start(end_raw)
|
|
|
+ if end is not None:
|
|
|
+ filters.append(CrmCustomer.create_time <= end)
|
|
|
+
|
|
|
+
|
|
|
+def _append_date_range_filters(filters: list, params: dict) -> None:
|
|
|
+ for field in _DATE_FIELDS:
|
|
|
+ col = getattr(CrmCustomer, field)
|
|
|
+ start = _parse_day_start(params.get(f"{field}_from"))
|
|
|
+ end_raw = params.get(f"{field}_to")
|
|
|
+ if start is not None:
|
|
|
+ filters.append(col >= start)
|
|
|
+ if end_raw is not None and str(end_raw).strip():
|
|
|
+ end_text = str(end_raw).strip()
|
|
|
+ try:
|
|
|
+ day = datetime.strptime(end_text, _DATE_FMT)
|
|
|
+ filters.append(col < day + timedelta(days=1))
|
|
|
+ except ValueError:
|
|
|
+ end = _parse_day_start(end_raw)
|
|
|
+ if end is not None:
|
|
|
+ filters.append(col <= end)
|
|
|
+
|
|
|
+
|
|
|
+def _append_number_filters(filters: list, params: dict) -> None:
|
|
|
+ for field in _NUMBER_FIELDS:
|
|
|
+ op = (params.get(f"{field}_op") or "").strip() or "between"
|
|
|
+ if op not in _OPS:
|
|
|
+ continue
|
|
|
+ col = getattr(CrmCustomer, field)
|
|
|
+ as_int = field != "deal_amount"
|
|
|
+ if op == "between":
|
|
|
+ vmin = _parse_number(params.get(f"{field}_min"), as_int=as_int)
|
|
|
+ vmax = _parse_number(params.get(f"{field}_max"), as_int=as_int)
|
|
|
+ if vmin is not None:
|
|
|
+ filters.append(col >= vmin)
|
|
|
+ if vmax is not None:
|
|
|
+ filters.append(col <= vmax)
|
|
|
+ else:
|
|
|
+ val = _parse_number(params.get(field), as_int=as_int)
|
|
|
+ if val is None:
|
|
|
+ continue
|
|
|
+ if op == "eq":
|
|
|
+ filters.append(col == val)
|
|
|
+ elif op == "gt":
|
|
|
+ filters.append(col > val)
|
|
|
+ elif op == "gte":
|
|
|
+ filters.append(col >= val)
|
|
|
+ elif op == "lt":
|
|
|
+ filters.append(col < val)
|
|
|
+ elif op == "lte":
|
|
|
+ filters.append(col <= val)
|
|
|
+
|
|
|
+
|
|
|
+def build_customer_column_filters(**params: Any) -> list:
|
|
|
+ """根据列筛参数返回 SQLAlchemy 二元表达式列表(不含 tenant/del_flag/view_mode)。"""
|
|
|
+ filters: list = []
|
|
|
+ _append_enum_filters(filters, params)
|
|
|
+ _append_text_filters(filters, params)
|
|
|
+ _append_create_time_filters(filters, params)
|
|
|
+ _append_date_range_filters(filters, params)
|
|
|
+ _append_number_filters(filters, params)
|
|
|
+
|
|
|
+ owner_user_id = params.get("owner_user_id")
|
|
|
+ if owner_user_id is not None and str(owner_user_id).strip():
|
|
|
+ filters.append(CrmCustomer.owner_user_id == str(owner_user_id).strip())
|
|
|
+
|
|
|
+ return filters
|