Files
wangjia e0bee00ff4 fix: 批量改进 (#2/#23/#38/#39/#40/#47)
- Web: 隐藏 ICP 备案号(条件渲染),联系入口改为微信咨询(条件渲染,配置 wechat 字段后生效)
- backend: 拼音回填抽取为独立 cmd/backfill-pinyin 工具,不再在启动时运行
- backend: DB 连接池参数(max_idle_conns/max_open_conns)改为配置可控,默认 10/100
- backend: Inventory 反范式字段添加注释说明快照语义
- backend: 全量 handler 采用 util.RespondSuccess/RespondCreated 统一响应格式(51 处)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 09:33:43 +08:00

145 lines
4.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"})
}