Просмотр исходного кода

feat(crm): add whitelist datetime list sort helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong 1 месяц назад
Родитель
Сommit
e6af68a2c3
2 измененных файлов с 79 добавлено и 0 удалено
  1. 27 0
      backend/app/services/crm/list_sort.py
  2. 52 0
      backend/tests/test_crm_list_sort.py

+ 27 - 0
backend/app/services/crm/list_sort.py

@@ -0,0 +1,27 @@
+"""CRM 列表日期列排序:白名单 prop → SQLAlchemy order 表达式。"""
+from __future__ import annotations
+
+from typing import Any, Mapping, Optional
+
+
+def resolve_datetime_order_by(
+    sort_field: Optional[str],
+    sort_order: Optional[str],
+    whitelist: Mapping[str, Any],
+    default,
+):
+    """
+    sort_field: 前端 camelCase prop(如 createTime)
+    sort_order: asc | desc(大小写不敏感)
+    whitelist: prop → Column / InstrumentedAttribute
+    非法或缺参 → 返回 default(通常 create_time.desc())
+    MySQL 对 NULL:ASC 前、DESC 后,与产品决策一致,无需额外 IS NULL。
+    """
+    field = (sort_field or "").strip()
+    order = (sort_order or "").strip().lower()
+    if not field or order not in ("asc", "desc"):
+        return default
+    col = whitelist.get(field)
+    if col is None:
+        return default
+    return col.asc() if order == "asc" else col.desc()

+ 52 - 0
backend/tests/test_crm_list_sort.py

@@ -0,0 +1,52 @@
+# backend/tests/test_crm_list_sort.py
+from sqlalchemy import Column, DateTime, MetaData, String, Table
+
+from app.services.crm.list_sort import resolve_datetime_order_by
+
+
+def _fake_columns():
+    md = MetaData()
+    t = Table(
+        "t",
+        md,
+        Column("id", String(64)),
+        Column("create_time", DateTime),
+        Column("deal_time", DateTime),
+    )
+    return {
+        "createTime": t.c.create_time,
+        "dealTime": t.c.deal_time,
+    }, t.c.create_time.desc()
+
+
+def test_valid_asc():
+    wl, default = _fake_columns()
+    expr = resolve_datetime_order_by("createTime", "asc", wl, default)
+    assert "ASC" in str(expr).upper() or getattr(expr, "modifier", None) is not None
+    # 更稳:比较 clause 指向白名单列
+    assert expr.element is wl["createTime"] or list(expr.element.base_columns)[0] is list(wl["createTime"].base_columns)[0]
+
+
+def test_valid_desc():
+    wl, default = _fake_columns()
+    expr = resolve_datetime_order_by("dealTime", "desc", wl, default)
+    assert expr.element.key == "deal_time" or "deal_time" in str(expr)
+
+
+def test_invalid_field_falls_back():
+    wl, default = _fake_columns()
+    expr = resolve_datetime_order_by("hack", "asc", wl, default)
+    assert expr is default
+
+
+def test_invalid_order_falls_back():
+    wl, default = _fake_columns()
+    expr = resolve_datetime_order_by("createTime", "up", wl, default)
+    assert expr is default
+
+
+def test_missing_falls_back():
+    wl, default = _fake_columns()
+    assert resolve_datetime_order_by(None, None, wl, default) is default
+    assert resolve_datetime_order_by("createTime", None, wl, default) is default
+    assert resolve_datetime_order_by(None, "asc", wl, default) is default