package handler import ( "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "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" ) func timeNow() *time.Time { t := time.Now() return &t } type StockInHandler struct { db *gorm.DB stockSvc *service.StockService } func NewStockInHandler(db *gorm.DB, svc *service.StockService) *StockInHandler { return &StockInHandler{db: db, stockSvc: svc} } // List GET /api/v1/stock-in/orders func (h *StockInHandler) 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.StockInOrder{}). Where("shop_id = ? AND deleted_at IS NULL", shopID) if status := c.Query("status"); status != "" { query = query.Where("status IN ?", strings.Split(status, ",")) } if startDate := c.Query("start_date"); startDate != "" { query = query.Where("order_date >= ?", startDate) } if endDate := c.Query("end_date"); endDate != "" { query = query.Where("order_date <= ?", endDate) } if kw := strings.TrimSpace(c.Query("keyword")); kw != "" { like := "%" + kw + "%" query = query.Where( "order_no LIKE ? OR partner_id IN (SELECT id FROM partners WHERE shop_id = ? AND name LIKE ?)", like, shopID, like, ) } var total int64 query.Count(&total) orders := make([]model.StockInOrder, 0) query.Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer"). Offset((page - 1) * pageSize).Limit(pageSize). Order("order_date DESC, id DESC").Find(&orders) c.JSON(http.StatusOK, gin.H{"data": orders, "total": total, "page": page, "page_size": pageSize}) } // Get GET /api/v1/stock-in/orders/:id func (h *StockInHandler) Get(c *gin.Context) { shopID := middleware.GetShopID(c) var order model.StockInOrder if err := h.db.Preload("Items.Product").Preload("Items.Product.Origin"). Preload("Items.Product.ShelfLife").Preload("Items.Product.Storage"). Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer"). Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID). First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } util.RespondSuccess(c, order) } // Create POST /api/v1/stock-in/orders func (h *StockInHandler) Create(c *gin.Context) { shopID := middleware.GetShopID(c) operatorID := middleware.GetUserID(c) var req model.StockInOrder 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" // 生成单号 orderNo, err := h.stockSvc.GenerateOrderNo(shopID, "stock_in") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } req.OrderNo = orderNo // 事务内:每条明细新建一个独立产品(特有产品/序列号,不按名称复用),回填 product_id,再建单。 err = h.db.Transaction(func(tx *gorm.DB) error { var total float64 for i := range req.Items { it := &req.Items[i] it.ShopID = shopID 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) if e != nil { return e } it.ProductID = prod.ID it.ProductCode = prod.Code // 保留快照,兼容现有查询(瘦身阶段再去) it.TotalPrice = it.Quantity * it.UnitPrice total += it.TotalPrice } req.TotalAmount = total return tx.Create(&req).Error }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } util.RespondCreated(c, req) } // Update PUT /api/v1/stock-in/orders/:id (只允许草稿状态) func (h *StockInHandler) Update(c *gin.Context) { shopID := middleware.GetShopID(c) var order model.StockInOrder if err := h.db.Where("id = ? AND shop_id = ? AND status = 'draft' AND deleted_at IS NULL", c.Param("id"), shopID). First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "入库单不存在或不可修改"}) return } var req model.StockInOrder if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err := h.db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("order_id = ?", order.ID).Delete(&model.StockInItem{}).Error; err != nil { return err } var total float64 for i := range req.Items { it := &req.Items[i] it.ShopID = shopID it.OrderID = order.ID if it.BatchNo == "" { 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) if e != nil { return e } it.ProductID = prod.ID it.ProductCode = prod.Code it.TotalPrice = it.Quantity * it.UnitPrice total += it.TotalPrice } updates := map[string]interface{}{ "warehouse_id": req.WarehouseID, "partner_id": req.PartnerID, "order_date": req.OrderDate, "remark": req.Remark, "total_amount": total, } if err := tx.Model(&order).Updates(updates).Error; err != nil { return err } if len(req.Items) > 0 { if err := tx.Create(&req.Items).Error; err != nil { return err } } return nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "updated"}) } // Delete DELETE /api/v1/stock-in/orders/:id (只允许草稿状态) func (h *StockInHandler) Delete(c *gin.Context) { shopID := middleware.GetShopID(c) now := timeNow() result := h.db.Model(&model.StockInOrder{}). Where("id = ? AND shop_id = ? AND status = 'draft' AND deleted_at IS NULL", c.Param("id"), shopID). Update("deleted_at", now) if result.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "入库单不存在或不可删除"}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } // Submit PUT /api/v1/stock-in/orders/:id/submit (草稿→待审核) func (h *StockInHandler) Submit(c *gin.Context) { shopID := middleware.GetShopID(c) result := h.db.Model(&model.StockInOrder{}). Where("id = ? AND shop_id = ? AND status = 'draft'", c.Param("id"), shopID). Update("status", "pending") if result.RowsAffected == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in draft status"}) return } c.JSON(http.StatusOK, gin.H{"message": "submitted"}) } // Approve PUT /api/v1/stock-in/orders/:id/approve func (h *StockInHandler) Approve(c *gin.Context) { shopID := middleware.GetShopID(c) reviewerID := middleware.GetUserID(c) id, _ := strconv.ParseUint(c.Param("id"), 10, 64) if err := h.stockSvc.ApproveStockIn(shopID, id, reviewerID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "approved"}) } // Reject PUT /api/v1/stock-in/orders/:id/reject func (h *StockInHandler) Reject(c *gin.Context) { shopID := middleware.GetShopID(c) reviewerID := middleware.GetUserID(c) now := timeNow() result := h.db.Model(&model.StockInOrder{}). Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID). Updates(map[string]interface{}{ "status": "rejected", "reviewer_id": reviewerID, "reviewed_at": now, }) if result.RowsAffected == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"}) return } c.JSON(http.StatusOK, gin.H{"message": "rejected"}) } // Withdraw PUT /api/v1/stock-in/orders/:id/withdraw // 审核中(pending)撤回为草稿(draft),修改后可重新提交。仅 pending 可撤回; // 已审核(approved)单据只读、库存已变动,不可撤回。 // 权限:管理员/超管可撤回任意单;普通操作员只能撤回本人提交的单(operator_id 为本人)。 func (h *StockInHandler) Withdraw(c *gin.Context) { shopID := middleware.GetShopID(c) userID := middleware.GetUserID(c) role := middleware.GetRole(c) var order model.StockInOrder if err := h.db.Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID). First(&order).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"}) return } isAdmin := role == "admin" || role == "superadmin" if !isAdmin && order.OperatorID != userID { c.JSON(http.StatusForbidden, gin.H{"error": "只能撤回本人提交的单据"}) return } h.db.Model(&model.StockInOrder{}). Where("id = ? AND shop_id = ?", order.ID, shopID). Update("status", "draft") c.JSON(http.StatusOK, gin.H{"message": "withdrawn"}) } // Return POST /api/v1/stock-in/orders/:id/return // 入库退单:退掉指定明细,对应库存从库存删除、冲减应付。仅 approved 单、管理员/超管。 func (h *StockInHandler) Return(c *gin.Context) { shopID := middleware.GetShopID(c) userID := middleware.GetUserID(c) role := middleware.GetRole(c) id, _ := strconv.ParseUint(c.Param("id"), 10, 64) var req struct { ItemIDs []uint64 `json:"item_ids"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if len(req.ItemIDs) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "未选择退单明细"}) return } if err := h.stockSvc.ReturnStockIn(shopID, id, userID, role, req.ItemIDs); err != nil { if errors.Is(err, service.ErrForbidden) { c.JSON(http.StatusForbidden, gin.H{"error": "无权退单"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "returned"}) }