From e8f416ddeac3071fb8e34f7bad4e4ecd7536765f Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 23 Jun 2026 23:57:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E5=85=A5=E5=BA=93/=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=88=97=E8=A1=A8=20keyword=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=85=92=E5=90=8D=E6=A8=A1=E7=B3=8A=E5=8F=8D=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - keyword 在单号/往来单位基础上,新增按酒名反查含该酒明细的单据 - 酒名匹配复用拼音基建:name(优先 product 回退快照) + name_pinyin + name_initials - 子查询带 shop_id,多租户隔离不破 - 补 stock_in/stock_out List 按酒名+拼音命中的测试用例 Co-Authored-By: Claude Opus 4.8 --- backend/internal/handler/stock_in.go | 12 ++++++-- backend/internal/handler/stock_in_test.go | 36 ++++++++++++++++++++++ backend/internal/handler/stock_out.go | 12 ++++++-- backend/internal/handler/stock_out_test.go | 36 ++++++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/backend/internal/handler/stock_in.go b/backend/internal/handler/stock_in.go index b96ad8d..fa41239 100644 --- a/backend/internal/handler/stock_in.go +++ b/backend/internal/handler/stock_in.go @@ -52,9 +52,17 @@ func (h *StockInHandler) List(c *gin.Context) { } if kw := strings.TrimSpace(c.Query("keyword")); kw != "" { like := "%" + kw + "%" + // keyword 同时命中:单号 / 往来单位 / 酒名(含全拼+首字母,反查含该酒明细的单据)。 + // 酒名优先取 product 主数据、回退快照列;拼音只在 products 上(快照无拼音列)。 query = query.Where( - "order_no LIKE ? OR partner_id IN (SELECT id FROM partners WHERE shop_id = ? AND name LIKE ?)", - like, shopID, like, + "order_no LIKE ?"+ + " OR partner_id IN (SELECT id FROM partners WHERE shop_id = ? AND name LIKE ?)"+ + " OR id IN (SELECT it.order_id FROM stock_in_items it"+ + " LEFT JOIN products p ON p.id = it.product_id AND p.shop_id = it.shop_id"+ + " WHERE it.shop_id = ? AND it.deleted_at IS NULL"+ + " AND (COALESCE(NULLIF(p.name,''), it.product_name) LIKE ?"+ + " OR p.name_pinyin LIKE ? OR p.name_initials LIKE ?))", + like, shopID, like, shopID, like, like, like, ) } diff --git a/backend/internal/handler/stock_in_test.go b/backend/internal/handler/stock_in_test.go index eb869e7..20afb2a 100644 --- a/backend/internal/handler/stock_in_test.go +++ b/backend/internal/handler/stock_in_test.go @@ -451,6 +451,42 @@ func TestStockInHandler_List_FilterByKeyword(t *testing.T) { assert.Equal(t, float64(2), parseResponse(w)["total"].(float64)) } +func TestStockInHandler_List_FilterByProductName(t *testing.T) { + db := testutil.SetupTestDB() + shop := testutil.CreateTestShop(db, "SI012") + user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") + warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse") + token := getAuthToken(user.ID, shop.ID, "admin") + r := setupProtectedRouter(db) + + // 单据1:明细酒名「飞天茅台」(入库按 product_name 新建独立 product) + w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{ + "warehouse_id": warehouse.ID, + "order_date": time.Now().Format(time.RFC3339), + "items": []map[string]interface{}{{"product_name": "飞天茅台", "quantity": 5.0}}, + }) + require.Equal(t, http.StatusCreated, w.Code) + + // 单据2:明细酒名「拉菲红酒」 + w = makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{ + "warehouse_id": warehouse.ID, + "order_date": time.Now().Format(time.RFC3339), + "items": []map[string]interface{}{{"product_name": "拉菲红酒", "quantity": 3.0}}, + }) + require.Equal(t, http.StatusCreated, w.Code) + + // 按酒名模糊反查 → 各命中 1 单 + w = makeRequest(r, "GET", "/api/v1/stock-in/orders?keyword=茅台", token, nil) + assert.Equal(t, float64(1), parseResponse(w)["total"].(float64)) + + w = makeRequest(r, "GET", "/api/v1/stock-in/orders?keyword=红酒", token, nil) + assert.Equal(t, float64(1), parseResponse(w)["total"].(float64)) + + // 无关键词 → 两单都在 + w = makeRequest(r, "GET", "/api/v1/stock-in/orders", token, nil) + assert.Equal(t, float64(2), parseResponse(w)["total"].(float64)) +} + func TestStockInHandler_List_FilterByStatus(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "SI010") diff --git a/backend/internal/handler/stock_out.go b/backend/internal/handler/stock_out.go index f23f010..99d9292 100644 --- a/backend/internal/handler/stock_out.go +++ b/backend/internal/handler/stock_out.go @@ -45,9 +45,17 @@ func (h *StockOutHandler) List(c *gin.Context) { } if kw := strings.TrimSpace(c.Query("keyword")); kw != "" { like := "%" + kw + "%" + // keyword 同时命中:单号 / 往来单位 / 酒名(含全拼+首字母,反查含该酒明细的单据)。 + // 酒名优先取 product 主数据、回退快照列;拼音只在 products 上(快照无拼音列)。 query = query.Where( - "order_no LIKE ? OR partner_id IN (SELECT id FROM partners WHERE shop_id = ? AND name LIKE ?)", - like, shopID, like, + "order_no LIKE ?"+ + " OR partner_id IN (SELECT id FROM partners WHERE shop_id = ? AND name LIKE ?)"+ + " OR id IN (SELECT it.order_id FROM stock_out_items it"+ + " LEFT JOIN products p ON p.id = it.product_id AND p.shop_id = it.shop_id"+ + " WHERE it.shop_id = ? AND it.deleted_at IS NULL"+ + " AND (COALESCE(NULLIF(p.name,''), it.product_name) LIKE ?"+ + " OR p.name_pinyin LIKE ? OR p.name_initials LIKE ?))", + like, shopID, like, shopID, like, like, like, ) } // 按商品编码反查单据:精确匹配,走 idx_so_items_shop_code(shop_id, product_code) 索引 diff --git a/backend/internal/handler/stock_out_test.go b/backend/internal/handler/stock_out_test.go index 9187270..af5148f 100644 --- a/backend/internal/handler/stock_out_test.go +++ b/backend/internal/handler/stock_out_test.go @@ -212,6 +212,42 @@ func TestStockOutHandler_List_FilterByKeyword(t *testing.T) { assert.Equal(t, float64(2), parseResponse(w)["total"].(float64)) } +func TestStockOutHandler_List_FilterByProductName(t *testing.T) { + db := testutil.SetupTestDB() + shop := testutil.CreateTestShop(db, "SO012") + user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") + warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse") + maotai := testutil.CreateTestProduct(db, shop.ID, "飞天茅台") + lafei := testutil.CreateTestProduct(db, shop.ID, "拉菲红酒") + token := getAuthToken(user.ID, shop.ID, "admin") + r := setupProtectedRouter(db) + + w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{ + "warehouse_id": warehouse.ID, + "order_date": time.Now().Format(time.RFC3339), + "items": []map[string]interface{}{{"product_id": maotai.ID, "quantity": 1.0}}, + }) + require.Equal(t, http.StatusCreated, w.Code) + + w = makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{ + "warehouse_id": warehouse.ID, + "order_date": time.Now().Format(time.RFC3339), + "items": []map[string]interface{}{{"product_id": lafei.ID, "quantity": 1.0}}, + }) + require.Equal(t, http.StatusCreated, w.Code) + + // 按酒名模糊反查 → 各命中 1 单 + w = makeRequest(r, "GET", "/api/v1/stock-out/orders?keyword=茅台", token, nil) + assert.Equal(t, float64(1), parseResponse(w)["total"].(float64)) + + w = makeRequest(r, "GET", "/api/v1/stock-out/orders?keyword=红酒", token, nil) + assert.Equal(t, float64(1), parseResponse(w)["total"].(float64)) + + // 无关键词 → 两单都在 + w = makeRequest(r, "GET", "/api/v1/stock-out/orders", token, nil) + assert.Equal(t, float64(2), parseResponse(w)["total"].(float64)) +} + func TestStockOutHandler_Reject(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "SO004")