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 } // buildObjectKey 生成 /<日期>/<随机>.ext 的对象键 func buildObjectKey(prefix, 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("%s/%s/%s%s", prefix, date, hex.EncodeToString(buf), ext) return key, ext, nil } // buildImageKey 生成 knowledge/<日期>/<随机>.ext 的对象键 func buildImageKey(filename string) (string, string, error) { return buildObjectKey("knowledge", filename) } // buildDataKey 生成 /<日期>/<随机>(不校验扩展名,由调用方校验) func buildDataKey(prefix, filename string) (string, string, error) { ext := strings.ToLower(filepath.Ext(filename)) buf := make([]byte, 8) if _, err := rand.Read(buf); err != nil { return "", "", err } date := time.Now().Format("20060102") return fmt.Sprintf("%s/%s/%s%s", prefix, date, hex.EncodeToString(buf), ext), 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)) }