Browse Source

feat: store and search customer name pinyin initials.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 months ago
parent
commit
659fc89179

+ 55 - 2
backend/app/crud/crm/customer.py

@@ -17,6 +17,7 @@ from app.crud.crm.data_scope_helpers import (
     apply_crm_owner_scope,
     ensure_crm_owner_row_in_scope,
 )
+from app.utils.pinyin_abbr import resolve_name_pinyin_abbr
 
 
 def _ensure_customer_in_scope(db: Session, customer: CrmCustomer, user_info, tenant_no: str) -> None:
@@ -109,6 +110,7 @@ def _row_to_dict(row: CrmCustomer) -> Dict[str, Any]:
         "id": row.id,
         "customerNo": row.customer_no or "",
         "customerName": row.customer_name or "",
+        "namePinyinAbbr": getattr(row, "name_pinyin_abbr", None) or "",
         "shortName": row.short_name or "",
         "customerType": row.customer_type or "",
         "mobile": row.mobile or "",
@@ -179,11 +181,21 @@ def list_customer_options(
                 CrmCustomer.customer_name.like(like),
                 CrmCustomer.customer_no.like(like),
                 CrmCustomer.short_name.like(like),
+                func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
+                    f"%{kw.lower()}%"
+                ),
             )
         )
     name = (customer_name or "").strip()
     if name:
-        q = q.filter(CrmCustomer.customer_name.like(f"%{name}%"))
+        q = q.filter(
+            or_(
+                CrmCustomer.customer_name.like(f"%{name}%"),
+                func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
+                    f"%{name.lower()}%"
+                ),
+            )
+        )
 
     limit = min(max(1, int(limit or 50)), 200)
     rows = q.order_by(CrmCustomer.create_time.desc()).limit(limit).all()
@@ -237,6 +249,9 @@ def list_customers(
                 CrmCustomer.customer_no.like(like),
                 CrmCustomer.mobile.like(like),
                 CrmCustomer.short_name.like(like),
+                func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
+                    f"%{kw.lower()}%"
+                ),
             )
         )
 
@@ -266,7 +281,16 @@ def list_customers(
         )
 
     if customer_name:
-        q = q.filter(CrmCustomer.customer_name.like(f"%{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}%"))
 
@@ -440,10 +464,21 @@ def create_customer(
     owner_id = (kwargs.pop("owner_user_id", None) or user_id or "").strip() or None
     owner_name = _resolve_user_display_name(db, owner_id) or (user_name or "")
 
+    # 拼音首字母:新增始终按提交值解析(空则由客户名称生成);兼容 snake / camel 键
+    submitted = kwargs.get("name_pinyin_abbr")
+    if submitted is None and "namePinyinAbbr" in kwargs:
+        submitted = kwargs.get("namePinyinAbbr")
+    name_pinyin_abbr = resolve_name_pinyin_abbr(
+        name=kwargs.get("customer_name"),
+        submitted=submitted,
+        submitted_provided=True,
+    )
+
     obj = CrmCustomer(
         id=generate_uuid(),
         customer_no=customer_no,
         customer_name=kwargs.get("customer_name"),
+        name_pinyin_abbr=name_pinyin_abbr,
         short_name=kwargs.get("short_name"),
         customer_type=kwargs.get("customer_type", "company"),
         mobile=kwargs.get("mobile"),
@@ -503,6 +538,7 @@ def update_customer(
         raise ValueError("客户不存在")
     _ensure_customer_in_scope(db, obj, user_info, obj.tenant_no)
 
+    # name_pinyin_abbr 单独处理,避免双重赋值
     _updatable = (
         "customer_name", "short_name", "customer_type", "mobile", "telephone",
         "email", "website", "level", "source", "industry", "park_area",
@@ -522,6 +558,23 @@ def update_customer(
         elif val is not None:
             setattr(obj, field, val)
 
+    # 拼音首字母:提交了则用提交值(空则重生);未提交则保留已有,已有空则按名称生成
+    name_keys = ("name_pinyin_abbr", "namePinyinAbbr")
+    submitted_provided = any(k in kwargs for k in name_keys)
+    submitted = kwargs.get("name_pinyin_abbr")
+    if submitted is None:
+        submitted = kwargs.get("namePinyinAbbr")
+    if "customer_name" in kwargs and kwargs.get("customer_name") is not None:
+        new_name = kwargs.get("customer_name")
+    else:
+        new_name = obj.customer_name
+    obj.name_pinyin_abbr = resolve_name_pinyin_abbr(
+        name=new_name,
+        submitted=submitted,
+        existing=obj.name_pinyin_abbr,
+        submitted_provided=submitted_provided,
+    )
+
     if "owner_user_id" in kwargs:
         owner_id = (kwargs.pop("owner_user_id") or "").strip() or None
         obj.owner_user_id = owner_id

+ 6 - 0
backend/app/schemas/crm/customer.py

@@ -88,6 +88,9 @@ class CustomerCreateBody(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
 
     customer_name: str = Field(..., alias="customerName", description="客户名称")
+    name_pinyin_abbr: Optional[str] = Field(
+        default=None, alias="namePinyinAbbr", description="客户名称拼音首字母(小写)"
+    )
     short_name: Optional[str] = Field(default=None, alias="shortName", description="客户简称")
     customer_type: str = Field(default="company", alias="customerType", description="客户类型")
     mobile: Optional[str] = Field(default=None, description="手机号")
@@ -152,6 +155,9 @@ class CustomerUpdateBody(BaseModel):
 
     id: str = Field(..., description="客户ID")
     customer_name: Optional[str] = Field(default=None, alias="customerName", description="客户名称")
+    name_pinyin_abbr: Optional[str] = Field(
+        default=None, alias="namePinyinAbbr", description="客户名称拼音首字母(小写)"
+    )
     short_name: Optional[str] = Field(default=None, alias="shortName", description="客户简称")
     customer_type: Optional[str] = Field(default=None, alias="customerType", description="客户类型")
     mobile: Optional[str] = Field(default=None, description="手机号")

+ 39 - 1
backend/tests/test_crm_name_pinyin_search.py

@@ -1,8 +1,9 @@
-"""产品列表 keyword:名称/编号/拼音首字母 OR 匹配。"""
+"""产品/客户列表 keyword:名称/编号/拼音首字母 OR 匹配。"""
 from __future__ import annotations
 
 from unittest.mock import MagicMock
 
+from app.crud.crm.customer import list_customer_options, list_customers
 from app.crud.crm.product import list_products
 
 
@@ -24,3 +25,40 @@ def test_list_products_keyword_includes_name_and_pinyin():
     keyword_expr = " ".join(str(a) for a in q.filter.call_args_list[1][0])
     assert "product_name" in keyword_expr
     assert "name_pinyin_abbr" in keyword_expr
+
+
+def test_list_customers_keyword_includes_name_and_pinyin():
+    db = MagicMock()
+    q = MagicMock()
+    db.query.return_value = q
+    q.filter.return_value = q
+    q.order_by.return_value = q
+    q.with_entities.return_value = q
+    q.scalar.return_value = 0
+    q.offset.return_value = q
+    q.limit.return_value = q
+    q.all.return_value = []
+
+    list_customers(db, "shanxia2.0", keyword="sxhg")
+
+    assert q.filter.call_count >= 2
+    keyword_expr = " ".join(str(a) for a in q.filter.call_args_list[1][0])
+    assert "customer_name" in keyword_expr
+    assert "name_pinyin_abbr" in keyword_expr
+
+
+def test_list_customer_options_keyword_includes_name_and_pinyin():
+    db = MagicMock()
+    q = MagicMock()
+    db.query.return_value = q
+    q.filter.return_value = q
+    q.order_by.return_value = q
+    q.limit.return_value = q
+    q.all.return_value = []
+
+    list_customer_options(db, "shanxia2.0", keyword="sxhg")
+
+    assert q.filter.call_count >= 2
+    keyword_expr = " ".join(str(a) for a in q.filter.call_args_list[1][0])
+    assert "customer_name" in keyword_expr
+    assert "name_pinyin_abbr" in keyword_expr