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")