feat: store and search customer name pinyin initials.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-07 17:24:28 +08:00
1 parent ba3f2c19bb
commit 659fc89179
3 files changed
+100 -3

No files matched your search

+55 -2
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.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
View File
@@ -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
View File
@@ -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