feat: store and search product name pinyin initials.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e056dfaac1
commit
ba3f2c19bb
4 files changed
+83
-6
No files matched your search
@@ -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())
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user