feat: store and search product name pinyin initials.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-07 17:18:59 +08:00
1 parent e056dfaac1
commit ba3f2c19bb
4 files changed
+83 -6

No files matched your search

+49 -5
View File
@@ -23,6 +23,7 @@ from app.models.crm_models import (
SysOss,
)
from app.models.pl_models import PlInventory, PlPlan, PlPlanProduct
from app.utils.pinyin_abbr import resolve_name_pinyin_abbr
from fastapi import HTTPException
@@ -522,6 +523,7 @@ def _product_row_to_dict(
"id": row.id,
"productNo": row.product_no,
"productName": row.product_name,
"namePinyinAbbr": getattr(row, "name_pinyin_abbr", None) or "",
"categoryId": row.category_id or "",
"categoryName": row.category_name or "",
"model": row.model or "",
@@ -578,11 +580,18 @@ def list_products(
if user_info:
q = apply_crm_create_by_scope(q, CrmProduct, user_info, db, tenant_no)
# keyword 筛选
# keyword 筛选:名称 / 编号 / 拼音首字母
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q = q.filter(or_(CrmProduct.product_name.like(like), CrmProduct.product_no.like(like)))
pinyin_like = f"%{kw.lower()}%"
q = q.filter(
or_(
CrmProduct.product_name.like(like),
CrmProduct.product_no.like(like),
func.lower(func.coalesce(CrmProduct.name_pinyin_abbr, "")).like(pinyin_like),
)
)
# category_id 筛选
if category_id:
@@ -751,10 +760,21 @@ def create_product(
if cat:
category_name = cat.category_name
# 拼音首字母:新增始终按提交值解析(空则由产品名称生成);兼容 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("product_name"),
submitted=submitted,
submitted_provided=True,
)
obj = CrmProduct(
id=generate_uuid(),
product_no=product_no,
product_name=kwargs.get("product_name"),
name_pinyin_abbr=name_pinyin_abbr,
category_id=category_id,
category_name=category_name or "",
model=kwargs.get("model"),
@@ -815,7 +835,7 @@ def update_product(
db, obj, user_info, obj.tenant_no, message="无权访问该产品"
)
# 逐字段更新
# 逐字段更新(name_pinyin_abbr 单独处理,避免双重赋值)
for field in (
"product_no",
"product_name",
@@ -838,6 +858,23 @@ def update_product(
if field in kwargs and kwargs[field] is not None:
setattr(obj, field, kwargs[field])
# 拼音首字母:提交了则用提交值(空则重生);未提交则保留已有,已有空则按名称生成
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 "product_name" in kwargs and kwargs.get("product_name") is not None:
new_name = kwargs.get("product_name")
else:
new_name = obj.product_name
obj.name_pinyin_abbr = resolve_name_pinyin_abbr(
name=new_name,
submitted=submitted,
existing=obj.name_pinyin_abbr,
submitted_provided=submitted_provided,
)
if "barcode" in kwargs:
barcode = _normalize_text(kwargs.get("barcode"))
if barcode and barcode_exists(db, barcode, tenant_no=obj.tenant_no, exclude_id=product_id):
@@ -941,11 +978,18 @@ def list_product_options(
CrmProduct.status == "on",
)
# 下拉选项供合同/报价等选用,不做 create_by 过滤(产品管理分页列表仍保留数据权限)
# keyword 仅按产品名称模糊匹配(不匹配编号,避免短数字如「32」误命中 SXWL00732x)
# keyword:产品名称 + 拼音首字母(不匹配编号,避免短数字如「32」误命中 SXWL00732x)
_ = user_info
kw = (keyword or "").strip()
if kw:
q = q.filter(CrmProduct.product_name.like(f"%{kw}%"))
q = q.filter(
or_(
CrmProduct.product_name.like(f"%{kw}%"),
func.lower(func.coalesce(CrmProduct.name_pinyin_abbr, "")).like(
f"%{kw.lower()}%"
),
)
)
if category_id:
q = q.filter(CrmProduct.category_id == category_id)
q = q.order_by(CrmProduct.create_time.desc())
+6
View File
@@ -104,6 +104,9 @@ class ProductCreateBody(BaseModel):
product_no: Optional[str] = Field(default=None, alias="productNo", description="产品编号(不传则自动生成,格式 SXWL000001)")
product_name: str = Field(..., alias="productName", description="产品名称")
name_pinyin_abbr: Optional[str] = Field(
default=None, alias="namePinyinAbbr", description="产品名称拼音首字母(小写)"
)
category_id: Optional[str] = Field(default=None, alias="categoryId", description="分类ID")
category_name: Optional[str] = Field(default=None, alias="categoryName", description="分类名称")
model: Optional[str] = Field(default=None, description="规格型号")
@@ -139,6 +142,9 @@ class ProductUpdateBody(BaseModel):
id: str = Field(..., description="产品ID")
product_no: Optional[str] = Field(default=None, alias="productNo", description="产品编号")
product_name: Optional[str] = Field(default=None, alias="productName", description="产品名称")
name_pinyin_abbr: Optional[str] = Field(
default=None, alias="namePinyinAbbr", description="产品名称拼音首字母(小写)"
)
category_id: Optional[str] = Field(default=None, alias="categoryId", description="分类ID")
category_name: Optional[str] = Field(default=None, alias="categoryName", description="分类名称")
model: Optional[str] = Field(default=None, description="规格型号")
@@ -0,0 +1,26 @@
"""产品列表 keyword:名称/编号/拼音首字母 OR 匹配。"""
from __future__ import annotations
from unittest.mock import MagicMock
from app.crud.crm.product import list_products
def test_list_products_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_products(db, "shanxia2.0", keyword="gyls")
assert q.filter.call_count >= 2
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
@@ -1,4 +1,4 @@
"""产品下拉 options:keyword 仅按产品名称模糊匹配,不匹配产品编号。"""
"""产品下拉 options:keyword 按产品名称/拼音首字母模糊匹配,不匹配产品编号。"""
from __future__ import annotations
from unittest.mock import MagicMock
@@ -20,4 +20,5 @@ def test_list_product_options_keyword_only_filters_product_name():
assert q.filter.call_count >= 2
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
assert "product_no" not in keyword_expr