diff --git a/CHANGELOG-server.md b/CHANGELOG-server.md index e46daf1..22d6419 100644 --- a/CHANGELOG-server.md +++ b/CHANGELOG-server.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.77] - 2026-06-22 + +### 新功能 +- 出库单列表支持按「商品编码」精确反查:可查出包含某个商品编码的所有出库单。同时为出库明细新增 (门店, 商品编码) 复合索引,在数万条明细下仍保持毫秒级查询,不影响写入。 + ## [1.0.76] - 2026-06-22 ### 修复 diff --git a/backend/internal/handler/stock_out.go b/backend/internal/handler/stock_out.go index 1b7cb9c..f23f010 100644 --- a/backend/internal/handler/stock_out.go +++ b/backend/internal/handler/stock_out.go @@ -50,6 +50,13 @@ func (h *StockOutHandler) List(c *gin.Context) { like, shopID, like, ) } + // 按商品编码反查单据:精确匹配,走 idx_so_items_shop_code(shop_id, product_code) 索引 + if code := strings.TrimSpace(c.Query("product_code")); code != "" { + query = query.Where( + "id IN (SELECT order_id FROM stock_out_items WHERE shop_id = ? AND product_code = ? AND deleted_at IS NULL)", + shopID, code, + ) + } var total int64 query.Count(&total) diff --git a/backend/internal/handler/stock_out_test.go b/backend/internal/handler/stock_out_test.go index ab00688..9187270 100644 --- a/backend/internal/handler/stock_out_test.go +++ b/backend/internal/handler/stock_out_test.go @@ -133,6 +133,40 @@ func TestStockOutHandler_List(t *testing.T) { assert.Equal(t, float64(2), resp["total"].(float64)) } +// 按商品编码精确反查出库单(明细 product_code = ?) +func TestStockOutHandler_List_FilterByProductCode(t *testing.T) { + db := testutil.SetupTestDB() + shop := testutil.CreateTestShop(db, "SO_CODE") + user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") + warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse") + product := testutil.CreateTestProduct(db, shop.ID, "Beer") + token := getAuthToken(user.ID, shop.ID, "admin") + r := setupProtectedRouter(db) + + 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": product.ID, "product_code": "ZXZ-FIND", "quantity": 1.0}, + }, + }) + 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": product.ID, "product_code": "OTHER-CODE", "quantity": 1.0}, + }, + }) + + // 精确命中 1 单 + w := makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=ZXZ-FIND", token, nil) + require.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, float64(1), parseResponse(w)["total"].(float64)) + + // 不存在的编码 → 0;前缀不算精确(不命中) + w = makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=ZXZ", token, nil) + require.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, float64(0), parseResponse(w)["total"].(float64)) +} + func TestStockOutHandler_List_FilterByKeyword(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "SO011") diff --git a/backend/main.go b/backend/main.go index 04f52fb..4e9a7a1 100644 --- a/backend/main.go +++ b/backend/main.go @@ -144,5 +144,12 @@ func autoMigrate(db *gorm.DB) { log.Fatalf("create unique index uk_shop_code failed: %v", err) } } + // stock_out_items(shop_id, product_code) 复合索引:支撑出库列表「按商品编码反查单据」, + // 避免在 2.6 万+ 明细行上全表扫。ShopID 在嵌入字段上无法用 struct tag 表达,故显式幂等建。 + if !db.Migrator().HasIndex(&model.StockOutItem{}, "idx_so_items_shop_code") { + if err := db.Exec("CREATE INDEX idx_so_items_shop_code ON stock_out_items (shop_id, product_code)").Error; err != nil { + log.Fatalf("create index idx_so_items_shop_code failed: %v", err) + } + } log.Println("AutoMigrate completed") } diff --git a/backend/schema/schema.sql b/backend/schema/schema.sql index e29824a..ab574b7 100644 --- a/backend/schema/schema.sql +++ b/backend/schema/schema.sql @@ -369,7 +369,8 @@ CREATE TABLE IF NOT EXISTS `stock_out_items` ( `remark` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), - KEY `idx_shop_product` (`shop_id`, `product_id`) + KEY `idx_shop_product` (`shop_id`, `product_id`), + KEY `idx_so_items_shop_code` (`shop_id`, `product_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库单明细'; -- ------------------------------------------------------------