feat(search): 库存搜索支持拼音/首字母,按回车触发

- 新增 util.ToPinyin(),使用 go-pinyin 生成全拼和首字母
- Product model 新增 name_pinyin / name_initials 列(AutoMigrate)
- 启动时自动回填存量商品拼音
- Create / Update / FindOrCreate 写入时同步生成拼音
- 库存搜索 SQL 加入拼音/首字母 LIKE 条件
- 前端去掉 300ms 防抖,改为回车/点击搜索按钮触发

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-05-25 21:27:54 +08:00
parent 5f2248001d
commit e9a6543a8b
9 changed files with 105 additions and 23 deletions
+2 -2
View File
@@ -64,9 +64,9 @@ func (h *InventoryHandler) List(c *gin.Context) {
inStock := c.Query("in_stock")
if keyword != "" {
baseWhere += " AND (COALESCE(NULLIF(p.name,''), inv.product_name) LIKE ? OR COALESCE(NULLIF(p.code,''), inv.product_code) LIKE ?)"
baseWhere += " AND (COALESCE(NULLIF(p.name,''), inv.product_name) LIKE ? OR COALESCE(NULLIF(p.code,''), inv.product_code) LIKE ? OR p.name_pinyin LIKE ? OR p.name_initials LIKE ?)"
like := "%" + keyword + "%"
args = append(args, like, like)
args = append(args, like, like, like, like)
}
if warehouseIDStr != "" {
baseWhere += " AND inv.warehouse_id = ?"
+14 -6
View File
@@ -15,6 +15,7 @@ import (
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/internal/util"
)
type ProductHandler struct {
@@ -70,6 +71,7 @@ func (h *ProductHandler) Create(c *gin.Context) {
}
product.ShopID = shopID
product.PublicID = uuid.New().String()
product.NamePinyin, product.NameInitials = util.ToPinyin(product.Name)
// Auto-generate product code if not provided (e.g. P001, P002)
// Retry up to 5 times on duplicate key to handle concurrent creates
@@ -122,6 +124,7 @@ func (h *ProductHandler) Update(c *gin.Context) {
return
}
namePinyin, nameInitials := util.ToPinyin(req.Name)
// 只更新业务字段,防止 Save() 覆盖 shop_id / created_at 等系统字段
if err := h.db.Model(&product).Updates(map[string]interface{}{
"code": req.Code,
@@ -138,6 +141,8 @@ func (h *ProductHandler) Update(c *gin.Context) {
"description": req.Description,
"remark": req.Remark,
"custom_fields": req.CustomFields,
"name_pinyin": namePinyin,
"name_initials": nameInitials,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -226,13 +231,16 @@ func (h *ProductHandler) FindOrCreate(c *gin.Context) {
var count int64
h.db.Model(&model.Product{}).Where("shop_id = ? AND deleted_at IS NULL", shopID).Count(&count)
namePinyin, nameInitials := util.ToPinyin(req.Name)
product = model.Product{
TenantBase: model.TenantBase{ShopID: shopID},
PublicID: uuid.New().String(),
Name: req.Name,
Series: req.Series,
Spec: req.Spec,
Code: fmt.Sprintf("P%03d", count+1),
TenantBase: model.TenantBase{ShopID: shopID},
PublicID: uuid.New().String(),
Name: req.Name,
Series: req.Series,
Spec: req.Spec,
Code: fmt.Sprintf("P%03d", count+1),
NamePinyin: namePinyin,
NameInitials: nameInitials,
}
if createErr := h.db.Create(&product).Error; createErr != nil {
// Race condition: try to find the record created by another request
+2
View File
@@ -24,6 +24,8 @@ type Product struct {
Description string `gorm:"type:text" json:"description"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
NamePinyin string `gorm:"size:400;index" json:"-"`
NameInitials string `gorm:"size:100;index" json:"-"`
Category *ProductCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Images []ProductImage `gorm:"foreignKey:ProductID" json:"images,omitempty"`
+48
View File
@@ -0,0 +1,48 @@
package util
import (
"strings"
gp "github.com/mozillazg/go-pinyin"
)
var (
fullArgs = func() gp.Args {
a := gp.NewArgs()
a.Style = gp.Normal
a.Fallback = func(r rune, _ gp.Args) []string {
return []string{strings.ToLower(string(r))}
}
return a
}()
initialArgs = func() gp.Args {
a := gp.NewArgs()
a.Style = gp.FirstLetter
a.Fallback = func(r rune, _ gp.Args) []string {
s := strings.ToLower(string(r))
if s == "" {
return nil
}
return []string{string(s[0])}
}
return a
}()
)
// ToPinyin converts a Chinese name to full pinyin and initials.
// Non-Chinese characters are kept as-is (lowercased).
// Example: "茅台酒" → ("maotaijiu", "mtj")
func ToPinyin(s string) (full, initials string) {
var fb, ib strings.Builder
for _, row := range gp.Pinyin(s, fullArgs) {
if len(row) > 0 {
fb.WriteString(row[0])
}
}
for _, row := range gp.Pinyin(s, initialArgs) {
if len(row) > 0 {
ib.WriteString(row[0])
}
}
return fb.String(), ib.String()
}