267 lines
7.7 KiB
Go
267 lines
7.7 KiB
Go
// 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)
|
||
}
|