157 lines
4.6 KiB
Go
157 lines
4.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"kefu-cloud/server/internal/config"
|
|
"kefu-cloud/server/internal/media"
|
|
"kefu-cloud/server/internal/middleware"
|
|
"kefu-cloud/server/internal/storage"
|
|
)
|
|
|
|
type UploadHandler struct {
|
|
store storage.ObjectStorage
|
|
cfg config.StorageConfig
|
|
}
|
|
|
|
func NewUploadHandler(store storage.ObjectStorage, cfg config.StorageConfig) *UploadHandler {
|
|
return &UploadHandler{store: store, cfg: cfg}
|
|
}
|
|
|
|
// UploadImage 客服端上传:multipart file → 处理 → 存对象存储
|
|
func (h *UploadHandler) UploadImage(c *gin.Context) {
|
|
h.handleUpload(c, fmt.Sprintf("tenants/%d/chat", middleware.GetTenantID(c)))
|
|
}
|
|
|
|
// WidgetUploadImage 访客端上传:需 visitor token + session_id
|
|
func (h *UploadHandler) WidgetUploadImage(c *gin.Context) {
|
|
sessionID := c.PostForm("session_id")
|
|
if sessionID == "" {
|
|
sessionID = c.Query("session_id")
|
|
}
|
|
if sessionID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "缺少 session_id"})
|
|
return
|
|
}
|
|
token := visitorTokenFromRequest(c, c.PostForm("visitor_token"))
|
|
var sid uint
|
|
if _, err := fmt.Sscanf(sessionID, "%d", &sid); err != nil || sid == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "会话参数错误"})
|
|
return
|
|
}
|
|
session, ok := loadVisitorSession(c, sid, token)
|
|
if !ok {
|
|
return
|
|
}
|
|
if session.Status == "ended" || session.Status == "archived" {
|
|
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "会话已结束"})
|
|
return
|
|
}
|
|
h.handleUpload(c, fmt.Sprintf("tenants/%d/widget/%d", session.TenantID, session.ID))
|
|
}
|
|
|
|
func (h *UploadHandler) handleUpload(c *gin.Context, keyPrefix string) {
|
|
if h.store == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "对象存储未配置"})
|
|
return
|
|
}
|
|
|
|
maxBytes := int64(h.cfg.MaxUploadMB) * 1024 * 1024
|
|
if maxBytes <= 0 {
|
|
maxBytes = 10 * 1024 * 1024
|
|
}
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBytes+512)
|
|
file, header, err := c.Request.FormFile("file")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请上传文件字段 file"})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
if header.Size > maxBytes {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": fmt.Sprintf("文件不能超过 %d MB", h.cfg.MaxUploadMB)})
|
|
return
|
|
}
|
|
|
|
// 嗅探 MIME
|
|
head := make([]byte, 512)
|
|
n, _ := io.ReadFull(file, head)
|
|
contentType := http.DetectContentType(head[:n])
|
|
// 复位读取:拼回 head + 剩余
|
|
reader := io.MultiReader(bytes.NewReader(head[:n]), file)
|
|
|
|
// 部分浏览器 Content-Type 不准,也允许 header 中的 type
|
|
if !media.AllowedUploadMIME(contentType) {
|
|
ct := header.Header.Get("Content-Type")
|
|
if media.AllowedUploadMIME(ct) {
|
|
contentType = ct
|
|
} else {
|
|
// 按扩展名兜底
|
|
ext := strings.ToLower(path.Ext(header.Filename))
|
|
switch ext {
|
|
case ".jpg", ".jpeg":
|
|
contentType = "image/jpeg"
|
|
case ".png":
|
|
contentType = "image/png"
|
|
case ".gif":
|
|
contentType = "image/gif"
|
|
case ".webp":
|
|
contentType = "image/webp"
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "仅支持 jpg/png/gif/webp 图片"})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
_ = contentType
|
|
|
|
raw, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "读取文件失败"})
|
|
return
|
|
}
|
|
if int64(len(raw)) > maxBytes {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "文件过大"})
|
|
return
|
|
}
|
|
|
|
processed, err := media.ProcessImage(bytes.NewReader(raw), media.ProcessOptions{
|
|
MaxEdge: h.cfg.MaxImageEdge,
|
|
ThumbEdge: 400,
|
|
WebPQuality: h.cfg.WebPQuality,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
|
return
|
|
}
|
|
|
|
mainKey := storage.NewObjectKey(keyPrefix, processed.MainExt)
|
|
thumbKey := storage.NewObjectKey(keyPrefix+"/thumbs", processed.ThumbExt)
|
|
|
|
mainURL, err := h.store.Put(c.Request.Context(), mainKey, bytes.NewReader(processed.Main), int64(len(processed.Main)), processed.MainType)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "上传失败: " + err.Error()})
|
|
return
|
|
}
|
|
thumbURL, err := h.store.Put(c.Request.Context(), thumbKey, bytes.NewReader(processed.Thumb), int64(len(processed.Thumb)), processed.ThumbType)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "缩略图上传失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
middleware.JSON(c, gin.H{
|
|
"url": mainURL,
|
|
"thumb_url": thumbURL,
|
|
"content_type": processed.MainType,
|
|
"width": processed.Width,
|
|
"height": processed.Height,
|
|
"size": len(processed.Main),
|
|
})
|
|
}
|