Files
toutoukan/controllers/file/picUpload.go

204 lines
5.8 KiB
Go
Raw Normal View History

2025-09-29 02:18:45 +08:00
package file
import (
"errors"
"fmt"
"io"
"mime/multipart"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/gin-gonic/gin"
)
// 阿里云OSS配置 - 请替换为你的实际配置
const (
ossEndpoint = "oss-cn-shanghai.aliyuncs.com" // OSS地域节点
ossAccessKeyID = "LTAI5tQDJnnajQS8uCXJLKfP" // 你的AccessKeyID
ossAccessKeySecret = "4wmJNcnYg5wKgBWePTQ9x4CPfUqgzn" // 你的AccessKeySecret
ossBucketName = "ttk-userdata" // 你的Bucket名称
ossBasePath = "/" // 图片存储基础路径
)
// 允许的图片文件扩展名
var allowedImageExts = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".webp": true,
}
// 上传响应结构
type UploadResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
URL string `json:"url"` // 图片访问URL
Filename string `json:"filename"` // OSS中的文件名
Size int64 `json:"size"` // 图片大小(字节)
} `json:"data"`
}
// PicUpload 处理图片上传请求
func PicUpload(c *gin.Context) {
// 1. 获取上传的文件
file, err := c.FormFile("image")
if err != nil {
c.JSON(400, UploadResponse{
Code: 400,
Message: "获取上传文件失败: " + err.Error(),
})
return
}
// 2. 验证文件
if err := validateImageFile(file); err != nil {
c.JSON(400, UploadResponse{
Code: 400,
Message: err.Error(),
})
return
}
// 3. 打开文件
srcFile, err := file.Open()
if err != nil {
c.JSON(500, UploadResponse{
Code: 500,
Message: "打开文件失败: " + err.Error(),
})
return
}
defer srcFile.Close()
// 4. 生成OSS中的文件名
objectName, err := generateObjectName(file.Filename)
if err != nil {
c.JSON(500, UploadResponse{
Code: 500,
Message: "生成文件名失败: " + err.Error(),
})
return
}
// 5. 上传到OSS
ossURL, err := uploadToOSS(srcFile, objectName, file.Size)
if err != nil {
c.JSON(500, UploadResponse{
Code: 500,
Message: "上传到OSS失败: " + err.Error(),
})
return
}
// 6. 返回成功响应
c.JSON(200, UploadResponse{
Code: 200,
Message: "上传成功",
Data: struct {
URL string `json:"url"`
Filename string `json:"filename"`
Size int64 `json:"size"`
}{
URL: ossURL,
Filename: objectName,
Size: file.Size,
},
})
}
// 验证图片文件
func validateImageFile(file *multipart.FileHeader) error {
// 验证大小10MB
if file.Size > 10*1024*1024 {
return errors.New("文件大小不能超过10MB")
}
// 验证扩展名
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedImageExts[ext] {
return errors.New("不支持的图片格式仅支持jpg、jpeg、png、gif、webp")
}
return nil
}
// 生成唯一文件名
func generateObjectName(originalName string) (string, error) {
// 步骤1提取原始文件的扩展名如 .jpg .png
ext := strings.ToLower(filepath.Ext(originalName))
if ext == "" {
return "", errors.New("文件没有扩展名,无法识别文件类型")
}
// 验证扩展名是否为允许的图片格式(避免非法扩展名)
if !allowedImageExts[ext] {
return "", errors.New("不支持的图片格式仅支持jpg、jpeg、png、gif、webp")
}
// 步骤2过滤原始文件名中的非法字符仅保留 a-z A-Z 0-9 - _ .
// 先移除扩展名,只处理文件名部分
rawFileName := strings.TrimSuffix(originalName, ext)
// 正则表达式:保留合法字符,其他替换为空
reg := regexp.MustCompile(`[^a-zA-Z0-9_\-.]`)
filteredFileName := reg.ReplaceAllString(rawFileName, "")
// 若过滤后文件名为空,用"unknown"代替(避免生成空文件名)
if filteredFileName == "" {
filteredFileName = "unknown"
}
// 步骤3生成唯一文件名时间戳 + 过滤后的文件名 + 扩展名)
// 时间戳用秒级+毫秒级,确保唯一性(避免同一秒上传相同文件名)
timestamp := time.Now().Format("20060102150405.000") // 格式:年月日时分秒.毫秒
uniqueFileName := fmt.Sprintf("%s-%s%s", timestamp, filteredFileName, ext)
// 步骤4拼接最终 ObjectName确保路径规范无首尾/、无连续/
// 清理 ossBasePath如 "images/" 转为 "images",避免结尾/
cleanBasePath := strings.TrimSuffix(ossBasePath, "/")
// 拼接路径(如 "images" + "20240520143000.123-unknown.jpg" → "images/20240520143000.123-unknown.jpg"
objectName := fmt.Sprintf("%s/%s", cleanBasePath, uniqueFileName)
// 最终验证:确保 ObjectName 无首尾/、无连续/
objectName = strings.Trim(objectName, "/")
objectName = regexp.MustCompile(`/+`).ReplaceAllString(objectName, "/") // 多/替换为单/
return objectName, nil
}
// 上传到OSS
func uploadToOSS(reader io.Reader, objectName string, fileSize int64) (string, error) {
// 1. 创建OSS客户端旧版SDK的创建方式
client, err := oss.New(ossEndpoint, ossAccessKeyID, ossAccessKeySecret)
if err != nil {
return "", fmt.Errorf("创建OSS客户端失败: %w", err)
}
// 2. 获取Bucket
bucket, err := client.Bucket(ossBucketName)
if err != nil {
return "", fmt.Errorf("获取Bucket失败: %w", err)
}
// 3. 上传文件旧版SDK的PutObject方法
err = bucket.PutObject(objectName, reader)
if err != nil {
return "", fmt.Errorf("上传文件失败: %w", err)
}
// 4. 生成访问URL
// 公开Bucket可以直接使用此URL
url := fmt.Sprintf("https://%s.%s/%s", ossBucketName, ossEndpoint, objectName)
// 如果是私有Bucket需要生成带签名的URL有效期1小时
// signedURL, err := bucket.SignURL(objectName, oss.HTTPGet, 3600)
// if err != nil {
// return "", fmt.Errorf("生成签名URL失败: %w", err)
// }
// return signedURL, nil
return url, nil
}