feat: 评论系统补充功能完成——图片上传、后台管理、通知跳转高亮
- 评论图片上传(LV4+,经验≥1500),JPEG/PNG 自动转 WebP,二进制搜索质量优化 - 后台评论管理页面,支持关键词搜索、分页、删除,侧边栏新增入口 - 通知跳转高亮:评论通知 RelatedID 改为 postID,消息页链接到 #comments,前端自动滚动 - 新增 adminCommentUseCase/commentStore 接口(遵循 ISP),AdminCommentRow 移至 model 层 - 前端 @mention 联想/图片上传光标插入/评论展开收起/内联回复交互完善
This commit is contained in:
@ -1,9 +1,17 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
@ -177,8 +185,102 @@ func (ctrl *CommentController) SearchUsers(c *gin.Context) {
|
||||
common.Ok(c, users)
|
||||
}
|
||||
|
||||
// marshalJSON helper
|
||||
func marshalJSON(v interface{}) string {
|
||||
b, _ := json.Marshal(v)
|
||||
return string(b)
|
||||
// UploadImage 上传评论图片 POST /api/comments/upload-image
|
||||
// 需要 LV4+(Exp ≥ 1500),复用帖子图片上传的 WebP 转码逻辑
|
||||
func (ctrl *CommentController) UploadImage(c *gin.Context) {
|
||||
uid, _, ok := common.GetGinUser(c)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
// LV4+ 检查(Exp ≥ 1500)
|
||||
expVal, exists := c.Get("exp")
|
||||
if !exists {
|
||||
common.Error(c, http.StatusForbidden, "无法获取经验值")
|
||||
return
|
||||
}
|
||||
exp := expVal.(int)
|
||||
if exp < 1500 {
|
||||
common.Error(c, http.StatusForbidden, "需 Lv4 以上才能上传评论图片")
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "请选择文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 检查扩展名
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
allowedExt := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}
|
||||
if !allowedExt[ext] {
|
||||
common.Error(c, http.StatusBadRequest, "仅支持 jpg/jpeg/png/gif/webp 格式")
|
||||
return
|
||||
}
|
||||
|
||||
// 限制 5MB
|
||||
if header.Size > 5<<20 {
|
||||
common.Error(c, http.StatusBadRequest, "图片大小不能超过 5MB")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "读取文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
storageDir := "storage/uploads/posts"
|
||||
if err := os.MkdirAll(storageDir, 0755); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "存储初始化失败")
|
||||
return
|
||||
}
|
||||
|
||||
ts := time.Now().UnixMilli()
|
||||
isJPEGPNG := ext == ".jpg" || ext == ".jpeg" || ext == ".png"
|
||||
|
||||
var savePath, url string
|
||||
|
||||
// JPEG/PNG 转 WebP
|
||||
if isJPEGPNG {
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "图片格式无效")
|
||||
return
|
||||
}
|
||||
webpData := encodeWebPBinary(img, len(data))
|
||||
if webpData != nil && len(webpData) < len(data) {
|
||||
filename := strconv.FormatUint(uint64(uid), 10) + "_" + strconv.FormatInt(ts, 10) + ".webp"
|
||||
savePath = filepath.Join(storageDir, filename)
|
||||
os.WriteFile(savePath, webpData, 0644)
|
||||
url = "/uploads/posts/" + filename
|
||||
}
|
||||
}
|
||||
|
||||
// 回退存储
|
||||
if savePath == "" {
|
||||
dataOut := data
|
||||
if isJPEGPNG {
|
||||
decImg, _, decErr := image.Decode(bytes.NewReader(data))
|
||||
if decErr == nil {
|
||||
var buf bytes.Buffer
|
||||
if ext == ".png" {
|
||||
png.Encode(&buf, decImg)
|
||||
} else {
|
||||
jpeg.Encode(&buf, decImg, &jpeg.Options{Quality: 92})
|
||||
}
|
||||
dataOut = buf.Bytes()
|
||||
}
|
||||
}
|
||||
filename := strconv.FormatUint(uint64(uid), 10) + "_" + strconv.FormatInt(ts, 10) + ext
|
||||
savePath = filepath.Join(storageDir, filename)
|
||||
os.WriteFile(savePath, dataOut, 0644)
|
||||
url = "/uploads/posts/" + filename
|
||||
}
|
||||
|
||||
// 返回标准 JSON(前端将 URL 插入评论文本)
|
||||
common.Ok(c, gin.H{"url": url})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user