diff --git a/backend/app/crud/crm/product.py b/backend/app/crud/crm/product.py index c1749cdfd..9083ac781 100644 --- a/backend/app/crud/crm/product.py +++ b/backend/app/crud/crm/product.py @@ -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()) diff --git a/backend/app/schemas/crm/product.py b/backend/app/schemas/crm/product.py index 78a09812a..6532c7b01 100644 --- a/backend/app/schemas/crm/product.py +++ b/backend/app/schemas/crm/product.py @@ -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="规格型号") diff --git a/backend/tests/test_crm_name_pinyin_search.py b/backend/tests/test_crm_name_pinyin_search.py new file mode 100644 index 000000000..bac553d80 --- /dev/null +++ b/backend/tests/test_crm_name_pinyin_search.py @@ -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 diff --git a/backend/tests/test_crm_product_options_keyword.py b/backend/tests/test_crm_product_options_keyword.py index 188c64cbe..85f94071a 100644 --- a/backend/tests/test_crm_product_options_keyword.py +++ b/backend/tests/test_crm_product_options_keyword.py @@ -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