# 微信小程序/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 ``` ## 🔧 后端接口说明 ### 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. 大文件建议使用更大的分块大小