feat(knowledge): 图片上传接口(S3 存储,限 5MB jpg/png/webp)

This commit is contained in:
weijuesen
2026-08-12 12:12:28 +08:00
parent 8bb93ad5c3
commit c8aa213626
6 changed files with 201 additions and 2 deletions
@@ -0,0 +1,101 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"
"silk-server-go/internal/middleware"
"silk-server-go/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// maxImageSize 知识库图片大小上限(5MB)
const maxImageSize = 5 << 20
// imageContentType 根据扩展名返回图片 Content-Type
func imageContentType(ext string) (string, bool) {
switch strings.ToLower(ext) {
case ".jpg", ".jpeg":
return "image/jpeg", true
case ".png":
return "image/png", true
case ".webp":
return "image/webp", true
}
return "", false
}
// validateImageFile 校验图片文件名扩展名与大小
func validateImageFile(filename string, size int64) error {
if size <= 0 || size > maxImageSize {
return errors.New("图片大小需在 1B~5MB 之间")
}
ext := strings.ToLower(filepath.Ext(filename))
if _, ok := imageContentType(ext); !ok {
return errors.New("仅支持 jpg/jpeg/png/webp 图片")
}
return nil
}
// buildImageKey 生成 knowledge/<日期>/<随机>.ext 的对象键
func buildImageKey(filename string) (string, string, error) {
ext := strings.ToLower(filepath.Ext(filename))
if _, ok := imageContentType(ext); !ok {
return "", "", errors.New("仅支持 jpg/jpeg/png/webp 图片")
}
buf := make([]byte, 8)
if _, err := rand.Read(buf); err != nil {
return "", "", err
}
date := time.Now().Format("20060102")
key := fmt.Sprintf("knowledge/%s/%s%s", date, hex.EncodeToString(buf), ext)
return key, ext, nil
}
// uploadKnowledgeImage 上传知识库图片(multipart 字段名 file
func uploadKnowledgeImage(s3 *service.S3Service, bucket string) gin.HandlerFunc {
return func(c *gin.Context) {
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择图片文件(字段名 file"})
return
}
defer file.Close()
if err := validateImageFile(header.Filename, header.Size); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
key, ext, err := buildImageKey(header.Filename)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
contentType, _ := imageContentType(ext)
if err := s3.EnsureBucket(bucket); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "图片存储不可用: " + err.Error()})
return
}
if err := s3.UploadImage(bucket, key, file, contentType); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "图片上传失败: " + err.Error()})
return
}
url := fmt.Sprintf("%s/%s/%s", s3.Endpoint(), bucket, key)
c.JSON(http.StatusOK, gin.H{"url": url, "name": header.Filename, "size": header.Size})
}
}
// uploadImageRoute 在知识库路由组中注册图片上传(由 RegisterKnowledgeRoutes 调用)
func registerKnowledgeImageRoute(rg *gin.RouterGroup, db *gorm.DB, s3 *service.S3Service, bucket string) {
rg.POST("/knowledge/images", middleware.RequirePermission(db, "knowledge:write"), uploadKnowledgeImage(s3, bucket))
}