# 微信小程序/UniApp 分块上传实现说明
## 📋 概述
分块上传不是固定的"三次请求",而是 **1 + N + 1** 次请求:
- **1次** 初始化请求 - 获取 `uploadId`
- **N次** 上传分块请求 - 每个分块一次请求(N = 文件大小 ÷ 分块大小)
- **1次** 完成请求 - 合并所有分块并保存到数据库
**示例:** 上传一个 10MB 的文件,分块大小为 2MB
- 1次初始化
- 5次上传分块(10MB ÷ 2MB = 5个分块)
- 1次完成
- **总共 7 次请求**
## 🔄 完整流程
```
1. 用户选择文件
↓
2. 调用初始化接口 → 获取 uploadId 和 fileName
↓
3. 将文件分成 N 个分块
↓
4. 循环上传每个分块 → 收集 PartETag
├─ 分块1 → PartETag1
├─ 分块2 → PartETag2
├─ ...
└─ 分块N → PartETagN
↓
5. 调用完成接口 → 传入所有 PartETag → 合并文件并保存
↓
6. 上传完成
```
## 📱 微信小程序特殊处理
微信小程序的 `wx.uploadFile` API 只能上传文件路径,不能直接上传 ArrayBuffer 或 Blob。因此需要:
1. **读取文件** → 使用 `wx.getFileSystemManager().readFile()` 读取整个文件
2. **切分文件** → 使用 `ArrayBuffer.slice()` 切分成多个分块
3. **保存临时文件** → 将每个分块保存为临时文件(`wx.env.USER_DATA_PATH`)
4. **上传分块** → 使用 `wx.uploadFile()` 上传临时文件
5. **清理临时文件** → 上传完成后删除临时文件
## 🚀 核心实现代码
### 1. 分块上传工具函数
创建文件:`utils/multipartUpload.js`
```javascript
/**
* 微信小程序/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 };
```
### 2. 页面使用示例
```vue
{{ progress }}%
```
## 🔧 后端接口说明
### 1. 初始化分块上传
**接口:** `POST /wechat/file/multipart/init/{caseId}/{type}`
**参数:** `originalName` (String)
**返回:**
```json
{
"code": 200,
"data": {
"uploadId": "xxx",
"fileName": "123/document/uuid.ext",
"randomUUID": "uuid",
"ext": "jpg"
}
}
```
### 2. 上传分块
**接口:** `POST /wechat/file/multipart/upload`
**参数(FormData):**
- `fileName` (String)
- `uploadId` (String)
- `partNumber` (Integer)
- `chunk` (File)
**返回:**
```json
{
"code": 200,
"data": {
"partNumber": 1,
"eTag": "xxx"
}
}
```
### 3. 完成分块上传
**接口:** `POST /wechat/file/multipart/complete/{caseId}/{type}`
**请求体(JSON):**
```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"}
]
}
```
## ⚙️ 配置参数
- **分块大小:** 默认 2MB,可根据需要调整
- **重试次数:** 默认每个分块最多重试 3 次
## ⚠️ 注意事项
1. 微信小程序需要将分块保存为临时文件
2. 上传完成后记得清理临时文件
3. 建议添加错误处理和重试机制
4. 大文件建议使用更大的分块大小