一、技术流程概述
- 初始化分片上传:后端生成唯一
uploadId - 获取预签名URL:后端为每个分片生成带签名的上传URL
- 前端分片上传:uni-app将文件切片后直传COS
- 分片合并:后端合并所有分片为完整文件
二、后端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>
四、关键注意事项
-
安全优化:
- 使用临时密钥代替永久密钥
- 在预签名URL中限制Content-Type
opt.Header = &http.Header{"Content-Type": []string{"image/*"}} -
分片大小建议:
- 小文件(100MB):5MB分片
- 大文件(1GB+):10-20MB分片
- 最大支持10000分片
-
断点续传实现:
// 本地存储上传进度 const saveProgress = () => { uni.setStorageSync(uploadId, { fileName, uploadedParts, chunkCount }); } -
错误处理:
- 分片失败时重试3次
- 合并失败时尝试重新合并
- 超时设置(建议30秒)
通过此方案,可以在保证安全性的前提下实现高效的大文件分片上传,前端直传COS减轻服务器压力,后端仅负责签名和合并操作,充分发挥云存储优势。