Compare commits

...

2 Commits

Author SHA1 Message Date
wangjia af56055eef chore: release server-v1.0.83
Deploy Server / release-deploy-server (push) Successful in 2m10s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:47:13 +08:00
wangjia d0c71a9252 fix(backend): 往来单位下拉返回全部 + 入库售价写入产品
- partner List 分页上限 200→1000:避免下拉请求大页被 ValidatePageSize
  回退到默认 20(超上限返回默认值而非截断),供应商/客户下拉一次取全部
- 入库明细 sale_price 建独立产品时写入 product.SalePrice(建议售价);
  SalePrice 为 gorm:"-" 仅作建库入参,不落 stock_in_items 表
- 补 partner 大页返回全部、入库售价入产品两条回归测试

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:44:50 +08:00
7 changed files with 78 additions and 4 deletions
+8
View File
@@ -5,6 +5,14 @@
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.83] - 2026-07-02
### 修复
- 供应商 / 客户下拉修复:现在一次返回全部往来单位,不再被截断到 20 条(此前录单和筛选的下拉最多只显示 20 个)。
### 改进
- 入库录单填写的「售价」现在会写入对应商品,作为该商品的建议售价,后续出库可直接带出。
## [1.0.82] - 2026-07-02
### 新功能
+3 -1
View File
@@ -25,7 +25,9 @@ func (h *PartnerHandler) List(c *gin.Context) {
shopID := middleware.GetShopID(c)
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
pageSize = util.ValidatePageSize(pageSize, 20, 200)
// 往来单位是有界的参考数据,下拉需一次取全部;放宽上限到 1000,
// 避免前端请求大页被 ValidatePageSize 回退到默认 20(超上限即返回默认值,非截断)。
pageSize = util.ValidatePageSize(pageSize, 20, 1000)
query := h.db.Model(&model.Partner{}).
Where("shop_id = ? AND deleted_at IS NULL", shopID)
+23
View File
@@ -205,6 +205,29 @@ func TestPartnerHandler_List_KeywordSearch(t *testing.T) {
assert.Equal(t, float64(0), resp["total"].(float64))
}
// 下拉需一次取全部:请求大页(>默认 20)应返回全部,而非被 ValidatePageSize
// 回退到默认 20(超上限即返回默认值的历史坑)。
func TestPartnerHandler_List_LargePageSizeReturnsAll(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "PT007")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
for i := 0; i < 25; i++ {
makeRequest(r, "POST", "/api/v1/partners", token, map[string]interface{}{
"name": fmt.Sprintf("Partner %02d", i),
"type": "supplier",
})
}
w := makeRequest(r, "GET", "/api/v1/partners?page_size=1000", token, nil)
resp := parseResponse(w)
assert.Equal(t, float64(25), resp["total"].(float64))
assert.Equal(t, float64(1000), resp["page_size"].(float64))
assert.Len(t, resp["data"].([]interface{}), 25)
}
func TestPartnerHandler_ShopIDFromToken(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "PT007")
+2 -1
View File
@@ -88,7 +88,7 @@ func nextProductCode(tx *gorm.DB, shopID uint64) (string, error) {
// createIndependentProduct 为入库明细新建一个独立产品(特有产品/序列号),返回新 product。
// "入库每行 = 一个特有产品"模型:每条明细建一个独立 product、发新序列号,不按名称复用。
// 含 nextProductCode 自增 + 撞 uk_shop_code 唯一约束时重试。必须在事务内调用。
func createIndependentProduct(tx *gorm.DB, shopID uint64, name, series, spec, batchNo string, prodDate *model.Date, price float64) (model.Product, error) {
func createIndependentProduct(tx *gorm.DB, shopID uint64, name, series, spec, batchNo string, prodDate *model.Date, price, salePrice float64) (model.Product, error) {
namePinyin, nameInitials := util.ToPinyin(name)
var prod model.Product
var err error
@@ -107,6 +107,7 @@ func createIndependentProduct(tx *gorm.DB, shopID uint64, name, series, spec, ba
BatchNo: batchNo,
ProductionDate: prodDate,
PurchasePrice: price,
SalePrice: salePrice,
NamePinyin: namePinyin,
NameInitials: nameInitials,
}
+2 -2
View File
@@ -228,7 +228,7 @@ func (h *StockInHandler) Create(c *gin.Context) {
if it.BatchNo == "" {
it.BatchNo = fmt.Sprintf("%s-%02d", req.OrderNo, i+1)
}
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice)
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice, it.SalePrice)
if e != nil {
return e
}
@@ -277,7 +277,7 @@ func (h *StockInHandler) Update(c *gin.Context) {
it.BatchNo = fmt.Sprintf("%s-%02d", order.OrderNo, i+1)
}
// 编辑草稿:旧明细已删,每条按新模型重建独立产品(旧草稿 product 无库存,暂留待后续清理)
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice)
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice, it.SalePrice)
if e != nil {
return e
}
+38
View File
@@ -75,6 +75,44 @@ func TestStockInHandler_FullFlow(t *testing.T) {
assert.Equal(t, float64(10), logs[0].Quantity)
}
// 入库明细带 sale_price 时,为该行新建的独立产品应写入 product.SalePrice(建议售价)。
func TestStockInHandler_Create_SalePriceToProduct(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SISALE")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Main")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
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": "茅台飞天",
"series": "53度",
"spec": "500ml",
"quantity": 3.0,
"unit_price": 2680.0,
"sale_price": 2980.0,
},
},
})
require.Equal(t, http.StatusCreated, w.Code)
orderID := extractID(w)
// 找到该单据明细新建的独立产品,核对进价/售价
var item model.StockInItem
require.NoError(t, db.Where("order_id = ?", orderID).First(&item).Error)
require.NotZero(t, item.ProductID)
var prod model.Product
require.NoError(t, db.Where("id = ? AND shop_id = ?", item.ProductID, shop.ID).First(&prod).Error)
assert.Equal(t, 2680.0, prod.PurchasePrice)
assert.Equal(t, 2980.0, prod.SalePrice)
// sale_price 不落 stock_in_itemsgorm:"-"),仅作产品建库入参
assert.Equal(t, "茅台飞天", prod.Name)
}
func TestStockInHandler_List(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI002")
+2
View File
@@ -42,6 +42,8 @@ type StockInItem struct {
Spec string `gorm:"size:100" json:"spec"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
UnitPrice float64 `gorm:"type:decimal(16,2);default:0" json:"unit_price"`
// 建议售价:仅用于入库建产品时写入 product.SalePrice,不落 stock_in_items 表(gorm:"-")。
SalePrice float64 `gorm:"-" json:"sale_price"`
TotalPrice float64 `gorm:"type:decimal(16,2);default:0" json:"total_price"`
// 已退数量:0=未退,=Quantity 表示整行已退单(整行退,不做部分数量)
ReturnedQuantity float64 `gorm:"type:decimal(12,3);default:0" json:"returned_quantity"`