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 }