修复入库单/出库单按商品名搜不到历史导入单:搜索酒名条件由 COALESCE(被占位名劫持) 改为 p.name OR it.product_name 任一命中,并加回归测试。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
This commit is contained in:
@@ -53,16 +53,18 @@ func (h *StockInHandler) List(c *gin.Context) {
|
||||
if kw := strings.TrimSpace(c.Query("keyword")); kw != "" {
|
||||
like := "%" + kw + "%"
|
||||
// keyword 同时命中:单号 / 往来单位 / 酒名(含全拼+首字母,反查含该酒明细的单据)。
|
||||
// 酒名优先取 product 主数据、回退快照列;拼音只在 products 上(快照无拼音列)。
|
||||
// 酒名「product 主数据名 OR 明细快照名」任一命中——不能用 COALESCE 二选一:历史导入单的
|
||||
// 明细 product_id 统一指向占位商品「历史导入占位」(p.name 非空),COALESCE 会被占位名劫持,
|
||||
// 永远取不到真实快照 product_name,导致导入单按真实酒名搜不到。拼音只在 products 上(快照无拼音列)。
|
||||
query = query.Where(
|
||||
"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 ?"+
|
||||
" AND (p.name LIKE ? OR it.product_name LIKE ?"+
|
||||
" OR p.name_pinyin LIKE ? OR p.name_initials LIKE ?))",
|
||||
like, shopID, like, shopID, like, like, like,
|
||||
like, shopID, like, shopID, like, like, like, like,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ func TestStockInHandler_TotalAmount(t *testing.T) {
|
||||
"warehouse_id": warehouse.ID,
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": product1.ID, "quantity": 10.0, "unit_price": 5.0}, // 50
|
||||
{"product_id": product1.ID, "quantity": 10.0, "unit_price": 5.0}, // 50
|
||||
{"product_id": product2.ID, "quantity": 3.0, "unit_price": 20.0}, // 60
|
||||
},
|
||||
})
|
||||
@@ -487,6 +487,49 @@ func TestStockInHandler_List_FilterByProductName(t *testing.T) {
|
||||
assert.Equal(t, float64(2), parseResponse(w)["total"].(float64))
|
||||
}
|
||||
|
||||
// 回归:历史导入单的明细 product_id 统一指向占位商品「历史导入占位」(p.name 非空),
|
||||
// 真实酒名只在快照列 product_name。修复前搜索用 COALESCE(NULLIF(p.name,”), product_name)
|
||||
// 被占位名劫持,导入单按真实酒名搜不到;修复后「p.name OR product_name」任一命中。
|
||||
func TestStockInHandler_List_FilterByImportedSnapshotName(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI012B")
|
||||
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)
|
||||
|
||||
// 占位商品:名「历史导入占位」,编码 HIST-PLACEHOLDER(模拟 import-history)
|
||||
placeholder := &model.Product{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
Name: "历史导入占位",
|
||||
Code: "HIST-PLACEHOLDER",
|
||||
}
|
||||
require.NoError(t, db.Create(placeholder).Error)
|
||||
|
||||
// 导入单:明细 product_id 指向占位商品,真实酒名「飞天茅台」仅在快照 product_name
|
||||
order := &model.StockInOrder{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
OrderNo: "RK-IMPORT-001",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "approved",
|
||||
OrderDate: model.Date{Time: time.Now()},
|
||||
}
|
||||
require.NoError(t, db.Create(order).Error)
|
||||
require.NoError(t, db.Create(&model.StockInItem{
|
||||
OrderID: order.ID,
|
||||
ShopID: shop.ID,
|
||||
ProductID: placeholder.ID,
|
||||
ProductCode: "ZXZ000010",
|
||||
ProductName: "飞天茅台",
|
||||
Quantity: 5.0,
|
||||
}).Error)
|
||||
|
||||
// 按真实酒名搜 → 命中导入单(修复前为 0)
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-in/orders?keyword=茅台", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
}
|
||||
|
||||
func TestStockInHandler_List_FilterByProductPinyin(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI013")
|
||||
|
||||
@@ -46,16 +46,18 @@ func (h *StockOutHandler) List(c *gin.Context) {
|
||||
if kw := strings.TrimSpace(c.Query("keyword")); kw != "" {
|
||||
like := "%" + kw + "%"
|
||||
// keyword 同时命中:单号 / 往来单位 / 酒名(含全拼+首字母,反查含该酒明细的单据)。
|
||||
// 酒名优先取 product 主数据、回退快照列;拼音只在 products 上(快照无拼音列)。
|
||||
// 酒名「product 主数据名 OR 明细快照名」任一命中——不能用 COALESCE 二选一:历史导入单的
|
||||
// 明细 product_id 统一指向占位商品「历史导入占位」(p.name 非空),COALESCE 会被占位名劫持,
|
||||
// 永远取不到真实快照 product_name,导致导入单按真实酒名搜不到。拼音只在 products 上(快照无拼音列)。
|
||||
query = query.Where(
|
||||
"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 ?"+
|
||||
" AND (p.name LIKE ? OR it.product_name LIKE ?"+
|
||||
" OR p.name_pinyin LIKE ? OR p.name_initials LIKE ?))",
|
||||
like, shopID, like, shopID, like, like, like,
|
||||
like, shopID, like, shopID, like, like, like, like,
|
||||
)
|
||||
}
|
||||
// 按商品编码反查单据:精确匹配,走 idx_so_items_shop_code(shop_id, product_code) 索引
|
||||
|
||||
@@ -248,6 +248,46 @@ func TestStockOutHandler_List_FilterByProductName(t *testing.T) {
|
||||
assert.Equal(t, float64(2), parseResponse(w)["total"].(float64))
|
||||
}
|
||||
|
||||
// 回归:历史导入出库单的明细 product_id 指向占位商品「历史导入占位」,真实酒名只在快照列。
|
||||
// 修复前 COALESCE 被占位名劫持搜不到,修复后「p.name OR product_name」任一命中。
|
||||
func TestStockOutHandler_List_FilterByImportedSnapshotName(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SO012B")
|
||||
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)
|
||||
|
||||
placeholder := &model.Product{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
Name: "历史导入占位",
|
||||
Code: "HIST-PLACEHOLDER",
|
||||
}
|
||||
require.NoError(t, db.Create(placeholder).Error)
|
||||
|
||||
order := &model.StockOutOrder{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
OrderNo: "CK-IMPORT-001",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "approved",
|
||||
OrderDate: model.Date{Time: time.Now()},
|
||||
}
|
||||
require.NoError(t, db.Create(order).Error)
|
||||
require.NoError(t, db.Create(&model.StockOutItem{
|
||||
OrderID: order.ID,
|
||||
ShopID: shop.ID,
|
||||
ProductID: placeholder.ID,
|
||||
ProductCode: "ZXZ000010",
|
||||
ProductName: "飞天茅台",
|
||||
Quantity: 5.0,
|
||||
}).Error)
|
||||
|
||||
// 按真实酒名搜 → 命中导入单(修复前为 0)
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-out/orders?keyword=茅台", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
}
|
||||
|
||||
func TestStockOutHandler_Reject(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SO004")
|
||||
|
||||
Reference in New Issue
Block a user