腾讯云COS分片上传实现方案(Go后端 + uni-app前端)

282 阅读2分钟

𝙞𝙋𝙖𝙙|电脑横屏壁纸 𝙞𝙋𝙖𝙙|电脑横屏_1_小土豆的旷野~_来自小红书网页版.jpg

一、技术流程概述

  1. ​初始化分片上传​​:后端生成唯一uploadId
  2. ​获取预签名URL​​:后端为每个分片生成带签名的上传URL
  3. ​前端分片上传​​:uni-app将文件切片后直传COS
  4. ​分片合并​​:后端合并所有分片为完整文件

二、后端Go实现代码

1. 初始化分片上传
// 初始化分片上传任务
func InitMultipartUpload(c *gin.Context) {
    // 从配置获取密钥
    secretID := os.Getenv("COS_SECRET_ID")
    secretKey := os.Getenv("COS_SECRET_KEY")
    
    // 创建COS客户端[10](@ref)
    u, _ := url.Parse("https://<BucketName>-<APPID>.cos.<Region>.myqcloud.com")
    b := &cos.BaseURL{BucketURL: u}
    client := cos.NewClient(b, &http.Client{
        Transport: &cos.AuthorizationTransport{
            SecretID:  secretID,
            SecretKey: secretKey,
        },
    })

    // 生成唯一文件名和uploadId[9](@ref)
    fileName := "uploads/" + uuid.New().String() + filepath.Ext(c.Query("filename"))
    res, _, err := client.Object.InitiateMultipartUpload(context.Background(), fileName, nil)
    if err != nil {
        c.JSON(500, gin.H{"error": "初始化失败"})
        return
    }

    c.JSON(200, gin.H{
        "uploadId": res.UploadID,
        "fileName": fileName,
    })
}
2. 生成分片预签名URL
// 生成分片预签名URL
func GeneratePresignedURL(c *gin.Context) {
    // 获取参数
    uploadId := c.Query("uploadId")
    fileName := c.Query("fileName")
    partNumber := c.Query("partNumber") // 分片序号(从1开始)

    // 配置预签名URL参数[9,10](@ref)
    opt := &cos.PresignedURLOptions{
        Query:  &url.Values{},
        Method: http.MethodPut,
        Expire: time.Hour, // 1小时有效期
    }
    opt.Query.Add("uploadId", uploadId)
    opt.Query.Add("partNumber", partNumber)

    // 生成预签名URL
    presignedURL, err := client.Object.GetPresignedURL(
        context.Background(), 
        http.MethodPut, 
        fileName, 
        secretID, 
        secretKey, 
        time.Hour, 
        opt,
    )

    c.JSON(200, gin.H{"url": presignedURL.String()})
}
3. 合并分片
// 合并分片
func CompleteMultipartUpload(c *gin.Context) {
    var parts []cos.Object
    // 前端传回的分片信息[{PartNumber:1, ETag:"xxx"},...]
    if err := c.BindJSON(&parts); err != nil {
        c.JSON(400, gin.H{"error": "参数错误"})
        return
    }

    // 执行合并[9](@ref)
    _, _, err := client.Object.CompleteMultipartUpload(
        context.Background(),
        c.Query("fileName"),
        c.Query("uploadId"),
        &cos.CompleteMultipartUploadOptions{Parts: parts},
    )

    c.JSON(200, gin.H{"url": "https://domain.com/" + c.Query("fileName")})
}

三、前端uni-app实现

1. 文件分片上传逻辑
// 文件上传方法
async function uploadFile(file) {
    // 1. 初始化上传任务
    const initRes = await uni.request({
        url: '/api/upload/init',
        method: 'POST',
        data: { filename: file.name }
    });
    
    const { uploadId, fileName } = initRes.data;
    const chunkSize = 5 * 1024 * 1024; // 5MB分片
    const chunkCount = Math.ceil(file.size / chunkSize);
    const uploadedParts = [];

    // 2. 分片上传
    for (let i = 0; i < chunkCount; i++) {
        // 获取分片签名URL
        const signRes = await uni.request({
            url: '/api/upload/sign',
            data: { 
                uploadId, 
                fileName,
                partNumber: i + 1 
            }
        });
        
        // 文件切片[6,8](@ref)
        const start = i * chunkSize;
        const end = Math.min(file.size, start + chunkSize);
        const chunk = file.slice(start, end);
        
        // 直传COS[3](@ref)
        const uploadRes = await uni.uploadFile({
            url: signRes.data.url,
            filePath: chunk,
            name: 'file',
            method: 'PUT',
            header: { 'Content-Type': 'application/octet-stream' }
        });
        
        // 记录分片信息(ETag从响应头获取)
        uploadedParts.push({
            PartNumber: i + 1,
            ETag: JSON.parse(uploadRes.data).ETag
        });
    }

    // 3. 合并分片
    await uni.request({
        url: '/api/upload/complete',
        method: 'POST',
        data: { 
            uploadId, 
            fileName,
            parts: uploadedParts 
        }
    });
}
2. 页面调用示例
<template>
  <view>
    <button @click="selectFile">选择文件</button>
  </view>
</template>

<script>
export default {
  methods: {
    async selectFile() {
      const [file] = await uni.chooseFile({
        count: 1,
        type: 'file'
      });
      
      await this.uploadFile(file);
      uni.showToast({ title: '上传成功' });
    }
  }
}
</script>

四、关键注意事项

  1. ​安全优化​​:

    • 使用临时密钥代替永久密钥
    • 在预签名URL中限制Content-Type
    opt.Header = &http.Header{"Content-Type": []string{"image/*"}}
    
  2. ​分片大小建议​​:

    • 小文件(100MB):5MB分片
    • 大文件(1GB+):10-20MB分片
    • 最大支持10000分片
  3. ​断点续传实现​​:

    // 本地存储上传进度
    const saveProgress = () => {
      uni.setStorageSync(uploadId, {
        fileName,
        uploadedParts,
        chunkCount
      });
    }
    
  4. ​错误处理​​:

    • 分片失败时重试3次
    • 合并失败时尝试重新合并
    • 超时设置(建议30秒)

通过此方案,可以在保证安全性的前提下实现高效的大文件分片上传,前端直传COS减轻服务器压力,后端仅负责签名和合并操作,充分发挥云存储优势。