Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ff41cc505 | |||
| 33b0f6c30c |
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.78] - 2026-06-24
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
- 维护性发布:发布与部署流程修复,应用功能无变更。
|
||||||
|
|
||||||
## [1.0.77] - 2026-06-24
|
## [1.0.77] - 2026-06-24
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.81] - 2026-06-24
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 入库单 / 出库单按商品名搜索:历史导入的单据现在也能按商品名搜到(此前明细统一挂占位商品,搜索被占位名顶替,只能搜到新建单据)。
|
||||||
|
|
||||||
## [1.0.80] - 2026-06-24
|
## [1.0.80] - 2026-06-24
|
||||||
|
|
||||||
### 改进
|
### 改进
|
||||||
|
|||||||
@@ -53,16 +53,18 @@ func (h *StockInHandler) List(c *gin.Context) {
|
|||||||
if kw := strings.TrimSpace(c.Query("keyword")); kw != "" {
|
if kw := strings.TrimSpace(c.Query("keyword")); kw != "" {
|
||||||
like := "%" + kw + "%"
|
like := "%" + kw + "%"
|
||||||
// keyword 同时命中:单号 / 往来单位 / 酒名(含全拼+首字母,反查含该酒明细的单据)。
|
// keyword 同时命中:单号 / 往来单位 / 酒名(含全拼+首字母,反查含该酒明细的单据)。
|
||||||
// 酒名优先取 product 主数据、回退快照列;拼音只在 products 上(快照无拼音列)。
|
// 酒名「product 主数据名 OR 明细快照名」任一命中——不能用 COALESCE 二选一:历史导入单的
|
||||||
|
// 明细 product_id 统一指向占位商品「历史导入占位」(p.name 非空),COALESCE 会被占位名劫持,
|
||||||
|
// 永远取不到真实快照 product_name,导致导入单按真实酒名搜不到。拼音只在 products 上(快照无拼音列)。
|
||||||
query = query.Where(
|
query = query.Where(
|
||||||
"order_no LIKE ?"+
|
"order_no LIKE ?"+
|
||||||
" OR partner_id IN (SELECT id FROM partners WHERE shop_id = ? AND name 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"+
|
" 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"+
|
" 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"+
|
" 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 ?))",
|
" 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,
|
"warehouse_id": warehouse.ID,
|
||||||
"order_date": time.Now().Format(time.RFC3339),
|
"order_date": time.Now().Format(time.RFC3339),
|
||||||
"items": []map[string]interface{}{
|
"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
|
{"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))
|
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) {
|
func TestStockInHandler_List_FilterByProductPinyin(t *testing.T) {
|
||||||
db := testutil.SetupTestDB()
|
db := testutil.SetupTestDB()
|
||||||
shop := testutil.CreateTestShop(db, "SI013")
|
shop := testutil.CreateTestShop(db, "SI013")
|
||||||
|
|||||||
@@ -46,16 +46,18 @@ func (h *StockOutHandler) List(c *gin.Context) {
|
|||||||
if kw := strings.TrimSpace(c.Query("keyword")); kw != "" {
|
if kw := strings.TrimSpace(c.Query("keyword")); kw != "" {
|
||||||
like := "%" + kw + "%"
|
like := "%" + kw + "%"
|
||||||
// keyword 同时命中:单号 / 往来单位 / 酒名(含全拼+首字母,反查含该酒明细的单据)。
|
// keyword 同时命中:单号 / 往来单位 / 酒名(含全拼+首字母,反查含该酒明细的单据)。
|
||||||
// 酒名优先取 product 主数据、回退快照列;拼音只在 products 上(快照无拼音列)。
|
// 酒名「product 主数据名 OR 明细快照名」任一命中——不能用 COALESCE 二选一:历史导入单的
|
||||||
|
// 明细 product_id 统一指向占位商品「历史导入占位」(p.name 非空),COALESCE 会被占位名劫持,
|
||||||
|
// 永远取不到真实快照 product_name,导致导入单按真实酒名搜不到。拼音只在 products 上(快照无拼音列)。
|
||||||
query = query.Where(
|
query = query.Where(
|
||||||
"order_no LIKE ?"+
|
"order_no LIKE ?"+
|
||||||
" OR partner_id IN (SELECT id FROM partners WHERE shop_id = ? AND name 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"+
|
" 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"+
|
" 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"+
|
" 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 ?))",
|
" 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) 索引
|
// 按商品编码反查单据:精确匹配,走 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))
|
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) {
|
func TestStockOutHandler_Reject(t *testing.T) {
|
||||||
db := testutil.SetupTestDB()
|
db := testutil.SetupTestDB()
|
||||||
shop := testutil.CreateTestShop(db, "SO004")
|
shop := testutil.CreateTestShop(db, "SO004")
|
||||||
|
|||||||
Reference in New Issue
Block a user