package handler import ( "fmt" "image" _ "image/jpeg" _ "image/png" "net/http" "os" "path/filepath" "strconv" "strings" "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" ) type FeedbackHandler struct{ db *gorm.DB } func NewFeedbackHandler(db *gorm.DB) *FeedbackHandler { return &FeedbackHandler{db: db} } type submitFeedbackRequest struct { Type string `json:"type" binding:"required,oneof=bug suggestion"` Content string `json:"content"` Images []string `json:"images"` AppVersion string `json:"app_version"` Platform string `json:"platform"` } // Submit POST /api/v1/feedback (需登录) func (h *FeedbackHandler) Submit(c *gin.Context) { shopID := middleware.GetShopID(c) userID := middleware.GetUserID(c) role := c.GetString(middleware.CtxRole) var req submitFeedbackRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if strings.TrimSpace(req.Content) == "" && len(req.Images) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "请填写反馈内容或添加图片"}) return } if len(req.Images) > 9 { req.Images = req.Images[:9] } // 去规范化:从 JWT 身份补全 username / shop_code,便于后台查看 var username string var u model.User if err := h.db.Select("username").First(&u, userID).Error; err == nil { username = u.Username } var shopCode string var shop model.Shop if err := h.db.Select("code").First(&shop, shopID).Error; err == nil { shopCode = shop.Code } fb := model.Feedback{ ShopID: shopID, ShopCode: shopCode, UserID: userID, Username: username, Role: role, Type: req.Type, Content: strings.TrimSpace(req.Content), Images: req.Images, AppVersion: req.AppVersion, Platform: req.Platform, ClientIP: c.ClientIP(), Status: "new", } if err := h.db.Create(&fb).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "submit failed"}) return } c.JSON(http.StatusCreated, gin.H{"id": fb.ID}) } // UploadImage POST /api/v1/feedback/images (需登录) func (h *FeedbackHandler) UploadImage(c *gin.Context) { shopID := middleware.GetShopID(c) // 限制 5MB if err := c.Request.ParseMultipartForm(5 << 20); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "文件超过 5MB 限制"}) return } file, _, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请上传文件(field: file)"}) return } defer file.Close() img, _, err := image.Decode(file) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "仅支持 JPEG/PNG 图片"}) return } resized := imaging.Fit(img, 1600, 1600, imaging.Lanczos) filename := uuid.New().String() + ".jpg" subdir := fmt.Sprintf("%s/feedback/%d", config.C.Storage.UploadDir, shopID) if err := os.MkdirAll(subdir, 0755); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "存储目录创建失败"}) return } fullPath := filepath.Join(subdir, filename) if err := imaging.Save(resized, fullPath, imaging.JPEGQuality(85)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "图片保存失败"}) return } relURL := fmt.Sprintf("/images/feedback/%d/%s", shopID, filename) c.JSON(http.StatusCreated, gin.H{"url": relURL}) } // List GET /api/v1/admin/feedback (仅超级管理员) func (h *FeedbackHandler) List(c *gin.Context) { var q struct { Type string `form:"type"` Status string `form:"status"` ShopID uint64 `form:"shop_id"` Page int `form:"page,default=1"` PageSize int `form:"page_size,default=50"` } if err := c.ShouldBindQuery(&q); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if q.PageSize <= 0 || q.PageSize > 200 { q.PageSize = 50 } if q.Page <= 0 { q.Page = 1 } db := h.db.Model(&model.Feedback{}) if q.Type != "" { db = db.Where("type = ?", q.Type) } if q.Status != "" { db = db.Where("status = ?", q.Status) } if q.ShopID != 0 { db = db.Where("shop_id = ?", q.ShopID) } var total int64 db.Count(&total) var list []model.Feedback db.Order("id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Find(&list) c.JSON(http.StatusOK, gin.H{ "data": list, "total": total, "page": q.Page, "page_size": q.PageSize, }) } // UpdateStatus PATCH /api/v1/admin/feedback/:id (仅超级管理员) func (h *FeedbackHandler) UpdateStatus(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var req struct { Status string `json:"status" binding:"required,oneof=new handled"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } res := h.db.Model(&model.Feedback{}).Where("id = ?", id).Update("status", req.Status) if res.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()}) return } if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "updated"}) }