分块上传不是固定的"三次请求",而是 1 + N + 1 次请求:
uploadId示例: 上传一个 10MB 的文件,分块大小为 2MB
1. 用户选择文件
↓
2. 调用初始化接口 → 获取 uploadId 和 fileName
↓
3. 将文件分成 N 个分块
↓
4. 循环上传每个分块 → 收集 PartETag
├─ 分块1 → PartETag1
├─ 分块2 → PartETag2
├─ ...
└─ 分块N → PartETagN
↓
5. 调用完成接口 → 传入所有 PartETag → 合并文件并保存
↓
6. 上传完成
微信小程序的 wx.uploadFile API 只能上传文件路径,不能直接上传 ArrayBuffer 或 Blob。因此需要:
wx.getFileSystemManager().readFile() 读取整个文件ArrayBuffer.slice() 切分成多个分块wx.env.USER_DATA_PATH)wx.uploadFile() 上传临时文件创建文件:utils/multipartUpload.js
/**
* 微信小程序/UniApp 分块上传实现
*/
const CHUNK_SIZE = 2 * 1024 * 1024; // 每个分块大小 2MB
const BASE_URL = 'https://your-api-domain.com'; // 替换为你的后端API地址
/**
* 分块上传主函数
*/
async function multipartUpload(filePathOrFile, caseId, fileType, originalName, isDelete = {}, onProgress) {
try {
// 1. 获取文件信息
let fileSize, filePath, fileObject;
// #ifdef H5
if (filePathOrFile instanceof File) {
fileObject = filePathOrFile;
fileSize = fileObject.size;
filePath = null;
} else {
const fileInfo = await getFileInfo(filePathOrFile);
fileSize = fileInfo.size;
filePath = filePathOrFile;
}
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
const fileInfo = await getFileInfo(filePathOrFile);
fileSize = fileInfo.size;
filePath = filePathOrFile;
fileObject = null;
// #endif
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
// 2. 初始化分块上传
const initResult = await initMultipartUpload(caseId, fileType, originalName);
if (!initResult.success) {
throw new Error('初始化分块上传失败: ' + initResult.message);
}
const { uploadId, fileName, randomUUID, ext } = initResult.data;
// 3. 上传所有分块
const partETags = [];
let uploadedSize = 0;
const tempFiles = [];
for (let partNumber = 1; partNumber <= totalChunks; partNumber++) {
const start = (partNumber - 1) * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, fileSize);
const chunkSize = end - start;
// 读取分块数据
let chunkData;
// #ifdef H5
if (fileObject) {
chunkData = await readFileChunkFromFile(fileObject, start, end);
} else {
chunkData = await readFileChunkToTempFile(filePath, start, end, partNumber);
}
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
chunkData = await readFileChunkToTempFile(filePath, start, end, partNumber);
// #endif
if (chunkData.tempFilePath) {
tempFiles.push(chunkData.tempFilePath);
}
// 上传分块(支持重试)
const partETag = await uploadPartWithRetry(
fileName, uploadId, partNumber, chunkData, chunkSize, 3
);
partETags.push({
partNumber: partETag.partNumber,
eTag: partETag.eTag
});
// 清理临时文件
if (chunkData.tempFilePath) {
try {
// #ifdef MP-WEIXIN
wx.getFileSystemManager().unlinkSync(chunkData.tempFilePath);
// #endif
// #ifdef APP-PLUS
uni.getFileSystemManager().unlinkSync(chunkData.tempFilePath);
// #endif
} catch (e) {
console.warn('清理临时文件失败:', e);
}
}
// 更新进度
uploadedSize += chunkSize;
const progress = Math.round((uploadedSize / fileSize) * 100);
if (onProgress) {
onProgress({
progress: progress,
uploaded: uploadedSize,
total: fileSize,
currentChunk: partNumber,
totalChunks: totalChunks
});
}
}
// 4. 完成分块上传
const completeResult = await completeMultipartUpload(
caseId, fileType, fileName, uploadId, randomUUID, ext,
originalName, fileSize, isDelete, partETags
);
if (!completeResult.success) {
await abortMultipartUpload(fileName, uploadId);
throw new Error('完成分块上传失败: ' + completeResult.message);
}
return { success: true, data: completeResult.data };
} catch (error) {
console.error('分块上传失败:', error);
return { success: false, message: error.message || '上传失败' };
}
}
// 辅助函数实现...
// (完整代码请参考项目中的实现)
export default { multipartUpload, CHUNK_SIZE };
<template>
<view class="upload-container">
<button @click="chooseFile">选择文件</button>
<button @click="startUpload" :disabled="uploading">开始上传</button>
<view v-if="uploading" class="progress">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
</view>
<text>{{ progress }}%</text>
</view>
</view>
</template>
<script>
import multipartUpload from '@/utils/multipartUpload.js';
export default {
data() {
return {
selectedFile: null,
uploading: false,
progress: 0
};
},
methods: {
chooseFile() {
// #ifdef MP-WEIXIN
wx.chooseMedia({
count: 1,
success: (res) => {
this.selectedFile = {
path: res.tempFiles[0].tempFilePath,
name: res.tempFiles[0].name,
size: res.tempFiles[0].size
};
}
});
// #endif
// #ifdef H5
const input = document.createElement('input');
input.type = 'file';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
this.selectedFile = {
path: file,
name: file.name,
size: file.size,
file: file
};
}
};
input.click();
// #endif
},
async startUpload() {
if (!this.selectedFile) return;
this.uploading = true;
this.progress = 0;
const fileParam = this.selectedFile.file || this.selectedFile.path;
const result = await multipartUpload.multipartUpload(
fileParam,
123, // caseId
'document', // fileType
this.selectedFile.name,
{}, // isDelete
(progressInfo) => {
this.progress = progressInfo.progress;
}
);
this.uploading = false;
if (result.success) {
uni.showToast({ title: '上传成功', icon: 'success' });
} else {
uni.showToast({ title: result.message, icon: 'none' });
}
}
}
};
</script>
接口: POST /wechat/file/multipart/init/{caseId}/{type}
参数: originalName (String)
返回:
{
"code": 200,
"data": {
"uploadId": "xxx",
"fileName": "123/document/uuid.ext",
"randomUUID": "uuid",
"ext": "jpg"
}
}
接口: POST /wechat/file/multipart/upload
参数(FormData):
fileName (String)uploadId (String)partNumber (Integer)chunk (File)返回:
{
"code": 200,
"data": {
"partNumber": 1,
"eTag": "xxx"
}
}
接口: POST /wechat/file/multipart/complete/{caseId}/{type}
请求体(JSON):
{
"fileName": "123/document/uuid.ext",
"uploadId": "xxx",
"randomUUID": "uuid",
"ext": "jpg",
"originalName": "原始文件名.jpg",
"fileSize": 1024000,
"isDelete": {},
"partETagsList": [
{"partNumber": 1, "eTag": "xxx"},
{"partNumber": 2, "eTag": "yyy"}
]
}