// Upload 模块:图片上传(支持上传到微信服务器和 MinIO,公开接口,无需权限) package upload import ( "context" "io" "net/http" "path/filepath" "strings" "time" "com.sclktx/m/v2/internal/common/response" "com.sclktx/m/v2/internal/pkg/minio" "com.sclktx/m/v2/internal/pkg/wechat" "github.com/gin-gonic/gin" ) // UploadHandler 上传处理器 type UploadHandler struct { wxClient *wechat.MiniProgramClient minioClient *minio.Client } // NewUploadHandler 创建上传处理器 func NewUploadHandler(wxClient *wechat.MiniProgramClient, minioClient *minio.Client) *UploadHandler { return &UploadHandler{ wxClient: wxClient, minioClient: minioClient, } } // UploadImgResponse 上传图片返回 type UploadImgResponse struct { URL string `json:"url"` // 微信返回的图片 URL } // @Summary 上传图片到微信服务器 // @Description 上传图片文件到微信服务器(公开接口,无需权限) // @Tags 文件上传 // @Accept multipart/form-data // @Produce json // @Param file formData file true "图片文件" // @Success 200 {object} response.Response{data=upload.UploadImgResponse} "success" // @Router /api/v1/upload/image [post] // UploadImage 上传图片文件到微信服务器(公开接口,无需权限) // POST /api/v1/upload/image func (h *UploadHandler) UploadImage(c *gin.Context) { // 获取上传的文件 file, header, err := c.Request.FormFile("file") if err != nil { response.BadRequest(c, "请选择要上传的图片文件") return } defer file.Close() // 读取文件内容 imageData, err := io.ReadAll(file) if err != nil { response.InternalError(c, "读取文件失败") return } // 获取文件名,如果没有则生成一个 filename := header.Filename if filename == "" { filename = "image_" + time.Now().Format("20060102150405") + ".jpg" } // 确保文件名有扩展名 ext := strings.ToLower(filepath.Ext(filename)) if ext == "" { // 根据 Content-Type 猜测扩展名 contentType := header.Header.Get("Content-Type") switch contentType { case "image/png": filename += ".png" case "image/gif": filename += ".gif" case "image/webp": filename += ".webp" default: filename += ".jpg" } } // 上传到微信服务器 result, err := h.wxClient.UploadImg(imageData, filename) if err != nil { response.InternalError(c, "上传到微信失败: "+err.Error()) return } response.Success(c, &UploadImgResponse{URL: result.URL}) } // UploadImageByURLRequest 通过链接上传请求 type UploadImageByURLRequest struct { URL string `json:"url" binding:"required"` // 图片链接 } // @Summary 通过链接上传图片到微信服务器 // @Description 通过图片链接下载后上传到微信服务器(公开接口,无需权限) // @Tags 文件上传 // @Accept json // @Produce json // @Param body body upload.UploadImageByURLRequest true "图片链接信息" // @Success 200 {object} response.Response{data=upload.UploadImgResponse} "success" // @Router /api/v1/upload/image-by-url [post] // UploadImageByURL 通过图片链接下载后上传到微信服务器(公开接口,无需权限) // POST /api/v1/upload/image-by-url func (h *UploadHandler) UploadImageByURL(c *gin.Context) { var req UploadImageByURLRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "请提供图片链接(url)") return } // 下载图片(使用自定义请求,添加 User-Agent 避免被反爬拦截) httpReq, err := http.NewRequest("GET", req.URL, nil) if err != nil { response.InternalError(c, "创建请求失败: "+err.Error()) return } httpReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(httpReq) if err != nil { response.InternalError(c, "下载图片失败: "+err.Error()) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { response.InternalError(c, "下载图片失败,HTTP状态码: "+http.StatusText(resp.StatusCode)) return } imageData, err := io.ReadAll(resp.Body) if err != nil { response.InternalError(c, "读取图片数据失败") return } // 从 URL 中提取文件名 filename := extractFilenameFromURL(req.URL) contentType := resp.Header.Get("Content-Type") ext := getExtByContentType(contentType) if !strings.HasSuffix(strings.ToLower(filename), ext) && ext != "" { filename += ext } // 上传到微信服务器 result, err := h.wxClient.UploadImg(imageData, filename) if err != nil { response.InternalError(c, "上传到微信失败: "+err.Error()) return } response.Success(c, &UploadImgResponse{URL: result.URL}) } // extractFilenameFromURL 从 URL 中提取文件名 func extractFilenameFromURL(rawURL string) string { // 去掉查询参数 if idx := strings.Index(rawURL, "?"); idx != -1 { rawURL = rawURL[:idx] } // 取最后一段作为文件名 base := filepath.Base(rawURL) if base == "." || base == "/" || base == "" { return "image_" + time.Now().Format("20060102150405") + ".jpg" } return base } // getExtByContentType 根据 Content-Type 获取文件扩展名 func getExtByContentType(contentType string) string { switch { case strings.Contains(contentType, "image/png"): return ".png" case strings.Contains(contentType, "image/gif"): return ".gif" case strings.Contains(contentType, "image/webp"): return ".webp" case strings.Contains(contentType, "image/svg"): return ".svg" case strings.Contains(contentType, "image/jpeg"): return ".jpg" default: return ".jpg" } } // UploadToMinioResponse 上传到 MinIO 返回 type UploadToMinioResponse struct { URL string `json:"url"` // MinIO 中的图片 URL } // @Summary 上传图片到MinIO对象存储 // @Description 上传图片到 MinIO 对象存储(公开接口,无需权限) // @Tags 文件上传 // @Accept multipart/form-data // @Produce json // @Param file formData file true "图片文件" // @Success 200 {object} response.Response{data=upload.UploadToMinioResponse} "success" // @Router /api/v1/upload/minio [post] // UploadImageToMinio 上传图片到 MinIO 对象存储(公开接口,无需权限) // POST /api/v1/upload/minio func (h *UploadHandler) UploadImageToMinio(c *gin.Context) { // 获取上传的文件 file, header, err := c.Request.FormFile("file") if err != nil { response.BadRequest(c, "请选择要上传的图片文件") return } defer file.Close() // 读取文件内容 imageData, err := io.ReadAll(file) if err != nil { response.InternalError(c, "读取文件失败") return } // 获取文件名,如果没有则生成一个 filename := header.Filename if filename == "" { filename = "image_" + time.Now().Format("20060102150405") + ".jpg" } // 确保文件名有扩展名 ext := strings.ToLower(filepath.Ext(filename)) if ext == "" { // 根据 Content-Type 猜测扩展名 contentType := header.Header.Get("Content-Type") switch contentType { case "image/png": filename += ".png" case "image/gif": filename += ".gif" case "image/webp": filename += ".webp" default: filename += ".jpg" } } // 上传到 MinIO ctx := context.Background() url, err := h.minioClient.UploadImage(ctx, imageData, filename) if err != nil { response.InternalError(c, "上传到 MinIO 失败:"+err.Error()) return } response.Success(c, &UploadToMinioResponse{URL: url}) } // RegisterRoutes 注册上传路由(公开接口,无需认证和权限) func (h *UploadHandler) RegisterRoutes(r *gin.RouterGroup) { r.POST("/upload/image", h.UploadImage) r.POST("/upload/image-by-url", h.UploadImageByURL) r.POST("/upload/minio", h.UploadImageToMinio) }