feat: store and search customer name pinyin initials.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
ba3f2c19bb
commit
659fc89179
3 files changed
+100
-3
No files matched your search
@@ -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
|
||||
|
||||
@@ -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="手机号")
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user