package handler import ( "fmt" "net/http" "strconv" "github.com/gin-gonic/gin" "gorm.io/gorm" "github.com/wangjia/jiu/backend/internal/middleware" "github.com/wangjia/jiu/backend/internal/model" ) type InventoryHandler struct { db *gorm.DB } func NewInventoryHandler(db *gorm.DB) *InventoryHandler { return &InventoryHandler{db: db} } // List GET /api/v1/inventory func (h *InventoryHandler) List(c *gin.Context) { shopID := middleware.GetShopID(c) page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) query := h.db.Model(&model.Inventory{}).Where("shop_id = ?", shopID) if warehouseID := c.Query("warehouse_id"); warehouseID != "" { query = query.Where("warehouse_id = ?", warehouseID) } if productID := c.Query("product_id"); productID != "" { query = query.Where("product_id = ?", productID) } if c.Query("in_stock") == "1" { query = query.Where("quantity > 0") } var total int64 query.Count(&total) var inventory []model.Inventory query.Preload("Product").Preload("Warehouse"). Offset((page - 1) * pageSize).Limit(pageSize). Find(&inventory) c.JSON(http.StatusOK, gin.H{"data": inventory, "total": total, "page": page, "page_size": pageSize}) } // Logs GET /api/v1/inventory/logs func (h *InventoryHandler) Logs(c *gin.Context) { shopID := middleware.GetShopID(c) page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) query := h.db.Model(&model.InventoryLog{}).Where("shop_id = ?", shopID) if productID := c.Query("product_id"); productID != "" { query = query.Where("product_id = ?", productID) } var total int64 query.Count(&total) var logs []model.InventoryLog query.Preload("Product").Preload("Warehouse").Offset((page-1)*pageSize).Limit(pageSize).Order("id DESC").Find(&logs) c.JSON(http.StatusOK, gin.H{"data": logs, "total": total, "page": page, "page_size": pageSize}) } // productTrackingItem is the response shape for GET /api/v1/inventory/products type productTrackingItem struct { model.StockInItem CurrentQty float64 `json:"current_qty"` Status string `json:"status"` // in_stock | sold_out BuyerName string `json:"buyer_name,omitempty"` SoldAt string `json:"sold_at,omitempty"` } // Products GET /api/v1/inventory/products — 商品追踪:已审核入库单的明细行,附库存状态和买家 func (h *InventoryHandler) Products(c *gin.Context) { shopID := middleware.GetShopID(c) page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) query := h.db.Model(&model.StockInItem{}). Joins("JOIN stock_in_orders ON stock_in_orders.id = stock_in_items.order_id"). Where("stock_in_orders.shop_id = ? AND stock_in_orders.status = 'approved'", shopID) if productID := c.Query("product_id"); productID != "" { query = query.Where("stock_in_items.product_id = ?", productID) } if warehouseID := c.Query("warehouse_id"); warehouseID != "" { query = query.Where("stock_in_orders.warehouse_id = ?", warehouseID) } var total int64 query.Count(&total) var items []model.StockInItem query. Preload("Product"). Preload("Order", func(db *gorm.DB) *gorm.DB { return db.Preload("Warehouse").Preload("Partner") }). Select("stock_in_items.*"). Order("stock_in_orders.order_date DESC, stock_in_items.id DESC"). Offset((page-1)*pageSize).Limit(pageSize). Find(&items) // 一次查出所有库存,构建 product+warehouse → qty 的 map var inventories []model.Inventory h.db.Where("shop_id = ?", shopID).Find(&inventories) invMap := make(map[string]float64, len(inventories)) for _, inv := range inventories { key := fmt.Sprintf("%d:%d", inv.ProductID, inv.WarehouseID) invMap[key] = inv.Quantity } // 查询每个 product+warehouse 最新的出库买家信息 type soldRow struct { ProductID uint64 `gorm:"column:product_id"` WarehouseID uint64 `gorm:"column:warehouse_id"` BuyerName string `gorm:"column:buyer_name"` SoldAt string `gorm:"column:sold_at"` } var soldRows []soldRow h.db.Raw(` SELECT sooi.product_id, soo.warehouse_id, COALESCE(p.name, '') AS buyer_name, CAST(MAX(soo.order_date) AS CHAR) AS sold_at FROM stock_out_orders soo JOIN stock_out_items sooi ON sooi.order_id = soo.id LEFT JOIN partners p ON p.id = soo.partner_id AND p.shop_id = ? WHERE soo.shop_id = ? AND soo.status = 'approved' GROUP BY sooi.product_id, soo.warehouse_id `, shopID, shopID).Scan(&soldRows) soldMap := make(map[string]soldRow, len(soldRows)) for _, r := range soldRows { key := fmt.Sprintf("%d:%d", r.ProductID, r.WarehouseID) soldMap[key] = r } result := make([]productTrackingItem, 0, len(items)) for _, item := range items { var warehouseID uint64 if item.Order != nil { warehouseID = item.Order.WarehouseID } key := fmt.Sprintf("%d:%d", item.ProductID, warehouseID) currentQty := invMap[key] status := "in_stock" buyerName := "" soldAt := "" if currentQty <= 0 { status = "sold_out" if si, ok := soldMap[key]; ok { buyerName = si.BuyerName if len(si.SoldAt) > 10 { soldAt = si.SoldAt[:10] } else { soldAt = si.SoldAt } } } result = append(result, productTrackingItem{ StockInItem: item, CurrentQty: currentQty, Status: status, BuyerName: buyerName, SoldAt: soldAt, }) } c.JSON(http.StatusOK, gin.H{"data": result, "total": total, "page": page, "page_size": pageSize}) } // CreateCheck POST /api/v1/inventory/checks func (h *InventoryHandler) CreateCheck(c *gin.Context) { shopID := middleware.GetShopID(c) operatorID := middleware.GetUserID(c) var req model.InventoryCheck if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } req.ShopID = shopID req.OperatorID = operatorID req.Status = "draft" // 自动填入系统库存数量 for i := range req.Items { req.Items[i].ShopID = shopID var inv model.Inventory if err := h.db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?", shopID, req.WarehouseID, req.Items[i].ProductID).First(&inv).Error; err == nil { req.Items[i].SystemQty = inv.Quantity } } if err := h.db.Create(&req).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"data": req}) } // GetCheck GET /api/v1/inventory/checks/:id func (h *InventoryHandler) GetCheck(c *gin.Context) { shopID := middleware.GetShopID(c) var check model.InventoryCheck if err := h.db.Preload("Items.Product"). Where("id = ? AND shop_id = ?", c.Param("id"), shopID). First(&check).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } c.JSON(http.StatusOK, gin.H{"data": check}) }