package handler import ( "fmt" "image" _ "image/jpeg" _ "image/png" "net/http" "os" "path/filepath" "strconv" "github.com/disintegration/imaging" "github.com/gin-gonic/gin" "github.com/google/uuid" "gorm.io/gorm" "github.com/wangjia/jiu/backend/config" "github.com/wangjia/jiu/backend/internal/middleware" "github.com/wangjia/jiu/backend/internal/model" "github.com/wangjia/jiu/backend/internal/util" ) type ProductImageHandler struct { db *gorm.DB } func NewProductImageHandler(db *gorm.DB) *ProductImageHandler { return &ProductImageHandler{db: db} } // Upload POST /api/v1/products/:id/images func (h *ProductImageHandler) Upload(c *gin.Context) { shopID := middleware.GetShopID(c) productID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product id"}) return } // Verify product belongs to shop var product model.Product if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", productID, shopID). First(&product).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "product not found"}) return } // Check image count limit var count int64 h.db.Model(&model.ProductImage{}).Where("product_id = ? AND shop_id = ?", productID, shopID).Count(&count) if count >= 5 { c.JSON(http.StatusBadRequest, gin.H{"error": "最多上传 5 张图片"}) return } // Parse multipart (1MB limit) if err := c.Request.ParseMultipartForm(1 << 20); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "文件超过 1MB 限制"}) return } file, _, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请上传文件(field: file)"}) return } defer file.Close() // Validate image format via decoding img, _, err := image.Decode(file) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "仅支持 JPEG/PNG 图片"}) return } // Resize if needed (max 1200px on either dimension, preserve aspect ratio) resized := imaging.Fit(img, 1200, 1200, imaging.Lanczos) // Prepare output path filename := uuid.New().String() + ".jpg" subdir := fmt.Sprintf("%s/products/%d", config.C.Storage.UploadDir, productID) if err := os.MkdirAll(subdir, 0755); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "存储目录创建失败"}) return } fullPath := filepath.Join(subdir, filename) // Save as JPEG with quality 85 if err := imaging.Save(resized, fullPath, imaging.JPEGQuality(85)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "图片保存失败"}) return } // Relative URL served by Nginx / dev static handler relURL := fmt.Sprintf("/images/products/%d/%s", productID, filename) pi := model.ProductImage{ ProductID: productID, ShopID: shopID, URL: relURL, } if err := h.db.Create(&pi).Error; err != nil { os.Remove(fullPath) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } util.RespondCreated(c, pi) } // Delete DELETE /api/v1/products/:id/images/:image_id func (h *ProductImageHandler) Delete(c *gin.Context) { shopID := middleware.GetShopID(c) productID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product id"}) return } imageID, err := strconv.ParseUint(c.Param("image_id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid image id"}) return } var pi model.ProductImage if err := h.db.Where("id = ? AND product_id = ? AND shop_id = ?", imageID, productID, shopID). First(&pi).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } // Delete file from disk filename := filepath.Base(pi.URL) fullPath := filepath.Join(config.C.Storage.UploadDir, fmt.Sprintf("products/%d/%s", productID, filename)) os.Remove(fullPath) if err := h.db.Delete(&pi).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) }