fix(backend): 架构质量改进批次二 (#32-37)
- #32 License 激活迁移到 license_devices 表:Activate/Verify/Deactivate 全部改用 license_devices,新增 max_devices 校验和 GET /license/devices 端点; Activate 现在校验 shop_id 防跨租户激活 - #33 checkInventory 从 StockOutHandler 移到 StockService.CheckInventoryAvailability - #34 新增 util/response.go 统一错误响应工具(RespondError/RespondSuccess/RespondCreated) - #35 生产模式 CORS Origin='*' 启动时 Fatal - #36 生产模式 License 私钥未配置启动时 Fatal - #37 新增 util/page.go ValidatePageSize,应用到 partner/product/stock_in/stock_out handler Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"github.com/wangjia/jiu/backend/internal/middleware"
|
||||
"github.com/wangjia/jiu/backend/internal/model"
|
||||
"github.com/wangjia/jiu/backend/internal/service"
|
||||
"github.com/wangjia/jiu/backend/internal/util"
|
||||
)
|
||||
|
||||
type StockOutHandler struct {
|
||||
@@ -27,6 +27,7 @@ func (h *StockOutHandler) 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)
|
||||
|
||||
query := h.db.Model(&model.StockOutOrder{}).
|
||||
Where("shop_id = ? AND deleted_at IS NULL", shopID)
|
||||
@@ -65,51 +66,6 @@ func (h *StockOutHandler) Get(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"data": order})
|
||||
}
|
||||
|
||||
// checkInventory validates that warehouse has enough stock for each item (SUM aggregate).
|
||||
// warehouseID is the order's warehouse; items are the stock-out line items.
|
||||
func (h *StockOutHandler) checkInventory(shopID, warehouseID uint64, items []model.StockOutItem) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect product IDs
|
||||
productIDs := make([]uint64, 0, len(items))
|
||||
for _, item := range items {
|
||||
productIDs = append(productIDs, item.ProductID)
|
||||
}
|
||||
|
||||
type inventorySum struct {
|
||||
ProductID uint64
|
||||
Total float64
|
||||
}
|
||||
var sums []inventorySum
|
||||
h.db.Model(&model.Inventory{}).
|
||||
Select("product_id, COALESCE(SUM(quantity), 0) AS total").
|
||||
Where("shop_id = ? AND warehouse_id = ? AND product_id IN ? AND deleted_at IS NULL",
|
||||
shopID, warehouseID, productIDs).
|
||||
Group("product_id").Scan(&sums)
|
||||
|
||||
// Build map for quick lookup
|
||||
sumMap := make(map[uint64]float64, len(sums))
|
||||
for _, s := range sums {
|
||||
sumMap[s.ProductID] = s.Total
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
have := sumMap[item.ProductID]
|
||||
if have < item.Quantity {
|
||||
// Get product name for a clearer error message
|
||||
var p model.Product
|
||||
h.db.Where("id = ?", item.ProductID).First(&p)
|
||||
name := p.Name
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("商品ID %d", item.ProductID)
|
||||
}
|
||||
return fmt.Errorf("库存不足:%s 当前库存 %.0f,需要 %.0f", name, have, item.Quantity)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create POST /api/v1/stock-out/orders
|
||||
func (h *StockOutHandler) Create(c *gin.Context) {
|
||||
@@ -127,7 +83,7 @@ func (h *StockOutHandler) Create(c *gin.Context) {
|
||||
|
||||
// 状态只允许 draft 或 pending;直接提交审核时校验库存
|
||||
if req.Status == "pending" {
|
||||
if err := h.checkInventory(shopID, req.WarehouseID, req.Items); err != nil {
|
||||
if err := h.stockSvc.CheckInventoryAvailability(shopID, req.WarehouseID, req.Items); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -235,7 +191,7 @@ func (h *StockOutHandler) Submit(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in draft status"})
|
||||
return
|
||||
}
|
||||
if err := h.checkInventory(shopID, order.WarehouseID, order.Items); err != nil {
|
||||
if err := h.stockSvc.CheckInventoryAvailability(shopID, order.WarehouseID, order.Items); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user