开发者日记:2023年11月25日 周六 阴
项目名称:跨平台大文件传输系统(WebUploader+Vue3+SpringBoot+腾讯云COS)
项目背景与核心挑战
作为河南独立开发者,近期承接了一个高复杂度外包项目,客户要求实现20G级文件/文件夹跨浏览器上传下载,技术栈涉及:
- 前端:Vue3(TypeScript)+ WebUploader(兼容IE8)
- 后端:SpringBoot 3.0 + Oracle 21c
- 存储:腾讯云COS(需处理分片上传与合并)
- 兼容性:从IE8到现代浏览器(Chrome/Firefox/Safari/Edge/Opera)
现存痛点:
- 网上开源方案仅支持单文件上传,无完整文件夹层级解析逻辑
- IE8的ActiveX控件在Windows 11上频繁崩溃
- Oracle数据库与MySQL的序列化差异导致进度存储失败
- 20G文件传输时,SpringBoot默认超时时间(1分钟)严重不足
技术方案设计
前端架构(Vue3 + WebUploader)
后端架构(SpringBoot + Oracle)
关键数据库设计(Oracle)
-- 上传任务表CREATETABLEUPLOAD_TASK(TASK_ID VARCHAR2(36)PRIMARYKEY,FILE_ETAG VARCHAR2(64)NOTNULL,RELATIVE_PATH VARCHAR2(1024),-- 保留文件夹层级(如 /project/src/)TOTAL_CHUNKS NUMBER,UPLOADED_CHUNKS NUMBERDEFAULT0,STATUSVARCHAR2(20)CHECK(STATUSIN('PENDING','UPLOADING','COMPLETED','FAILED')),COS_KEY VARCHAR2(1024),CREATE_TIMETIMESTAMPDEFAULTSYSTIMESTAMP);-- 分片存储表(Oracle BLOB优化)CREATETABLEUPLOAD_CHUNK(CHUNK_ID VARCHAR2(72)PRIMARYKEY,-- TASK_ID+CHUNK_INDEXTASK_ID VARCHAR2(36)REFERENCESUPLOAD_TASK(TASK_ID),CHUNK_INDEX NUMBER,CHUNK_DATABLOB,CONSTRAINTUK_TASK_CHUNKUNIQUE(TASK_ID,CHUNK_INDEX));核心代码实现
前端:文件夹上传与断点续传(Vue3 + TypeScript)
// src/components/FolderUploader.vueimport{ref,onMounted}from'vue';importWebUploaderfrom'webuploader';import{calculateFileETag}from'@/utils/fileHash';// 自定义ETag计算(兼容IE8)exportdefault{setup(){consttaskList=ref>([]);constuploader=ref(null);// 初始化上传器(兼容IE8)constinitUploader=()=>{constisIE8=document.documentMode===8;uploader.value=WebUploader.create({swf:'/static/Uploader.swf',// IE8回退server:'/api/upload/chunk',chunked:true,chunkSize:isIE8?4*1024*1024:20*1024*1024,// IE8限制4MB分片threads:isIE8?1:5,formData:{taskId:localStorage.getItem('currentTaskId')||''},timeout:0// 禁用超时(由后端控制)});// 恢复未完成任务(从IndexedDB)restoreTasksFromDB();};// 递归解析文件夹(跨浏览器)consthandleFolderSelect=async(e:Event)=>{constinput=e.targetasHTMLInputElement;constfiles=input.files;if(!files?.length)return;constparseFolder=async(entries:FileSystemEntry[],parentPath='')=>{for(letentryofentries){if(entry.isFile){constfile=(entryasFileSystemFileEntry).file!;constrelativePath=parentPath?`${parentPath}/${entry.name}`:entry.name;awaitaddUploadTask(file,relativePath);}elseif(entry.isDirectory){constdirReader=(entryasFileSystemDirectoryEntry).createReader();constnewEntries=awaitnewPromise(resolve=>{dirReader.readEntries(resolve);});awaitparseFolder(newEntries,parentPath?`${parentPath}/${entry.name}`:entry.name);}}};// Chrome/Firefox使用showDirectoryPickerif(files[0].webkitGetAsEntry){constentry=files[0].webkitGetAsEntry();if(entry?.isDirectory){constdirReader=entry.createReader();constentries=awaitnewPromise(resolve=>{dirReader.readEntries(resolve);});awaitparseFolder(entries);}else{awaitaddUploadTask(files[0],files[0].name);}}// IE8使用ActiveX(需用户授权)elseif(window.ActiveXObject){// ActiveX实现代码省略(需处理权限弹窗)}};// 添加上传任务constaddUploadTask=async(file:File,relativePath:string)=>{constetag=awaitcalculateFileETag(file);// 使用CRC32+文件大小替代MD5consttask={file,relativePath,etag,chunkCount:Math.ceil(file.size/uploader.value!.options.chunkSize),uploadedChunks:0};// 检查本地是否有未完成记录constexistingTask=awaitgetTaskFromDB(etag);if(existingTask){task.uploadedChunks=existingTask.uploadedChunks;}taskList.value.push(task);saveTaskToDB(task);startUpload(task);};onMounted(()=>{initUploader();document.getElementById('folderInput')?.addEventListener('change',handleFolderSelect);});return{taskList,uploader};}};后端:SpringBoot分片处理与COS上传
// src/main/java/com/example/uploader/controller/UploadController.java@RestController@RequestMapping("/api/upload")publicclassUploadController{@AutowiredprivateUploadTaskRepositorytaskRepository;@AutowiredprivateUploadChunkRepositorychunkRepository;@AutowiredprivateCOSClientcosClient;// 腾讯云COS客户端// 分片上传接口@PostMapping("/chunk")publicResponseEntityhandleChunk(@RequestParam("file")MultipartFilefile,@RequestParam("taskId")StringtaskId,@RequestParam("chunkIndex")intchunkIndex,@RequestParam("totalChunks")inttotalChunks,@RequestParam("etag")Stringetag,@RequestParam("relativePath")StringrelativePath){// 1. 查询或创建任务UploadTasktask=taskRepository.findById(taskId).orElseGet(()->{UploadTasknewTask=newUploadTask();newTask.setTaskId(taskId);newTask.setFileEtag(etag);newTask.setRelativePath(relativePath);newTask.setTotalChunks(totalChunks);newTask.setStatus("UPLOADING");returntaskRepository.save(newTask);});// 2. 保存分片到Oracle BLOBUploadChunkchunk=newUploadChunk();chunk.setChunkId(taskId+"_"+chunkIndex);chunk.setTaskId(taskId);chunk.setChunkIndex(chunkIndex);try{chunk.setChunkData(file.getBytes());chunkRepository.save(chunk);}catch(IOExceptione){returnResponseEntity.status(500).body("分片保存失败");}// 3. 更新任务进度task.setUploadedChunks(chunkIndex+1);taskRepository.save(task);// 4. 检查是否全部上传完成if(task.getUploadedChunks()>=task.getTotalChunks()){// 启动异步合并任务mergeAndUploadToCOS(task);returnResponseEntity.ok("{\"status\":\"COMPLETED\"}");}returnResponseEntity.ok("{\"status\":\"SUCCESS\"}");}// 异步合并并上传COS@AsyncpublicvoidmergeAndUploadToCOS(UploadTasktask){try{// 1. 从Oracle读取所有分片Listchunks=chunkRepository.findByTaskIdOrderByChunkIndex(task.getTaskId());// 2. 合并文件(流式处理避免内存溢出)PathtempFile=Files.createTempFile("upload_",".tmp");try(OutputStreamout=Files.newOutputStream(tempFile)){for(UploadChunkchunk:chunks){out.write(chunk.getChunkData());}}// 3. 上传到腾讯云COSStringcosKey="uploads/"+task.getTaskId()+"/"+task.getRelativePath();cosClient.putObject(newPutObjectRequest("your-bucket",cosKey,tempFile.toFile()));// 4. 更新任务状态task.setStatus("COMPLETED");task.setCosKey(cosKey);taskRepository.save(task);// 5. 清理临时文件和分片Files.deleteIfExists(tempFile);chunkRepository.deleteByTaskId(task.getTaskId());}catch(Exceptione){task.setStatus("FAILED");taskRepository.save(task);}}}关键问题解决
IE8兼容性:
- 使用
标签替代已废弃的加载Uploader.swf - 通过
document.execCommand('SaveAs')实现IE8的下载功能
- 使用
Oracle性能优化:
- 对
UPLOAD_CHUNK表的TASK_ID字段建立索引 - 使用JDBC批处理更新进度(
addBatch()+executeBatch())
- 对
大文件传输超时:
- 在SpringBoot配置中增加:
server:tomcat:connection-timeout:0# 禁用连接超时spring:servlet:multipart:max-file-size:20GBmax-request-size:20GB
- 在SpringBoot配置中增加:
文件夹层级保留:
- 前端传递
relativePath参数(如/docs/2023/report.pdf) - 后端直接存储该路径,上传COS时保持原样
- 前端传递
求助与社区支持
目前遇到以下难题,已在QQ群(374992201)发布详细日志:
- IE8的ActiveX控件在Windows 11上频繁弹出权限警告
- Oracle BLOB存储20G文件时出现
ORA-01653表空间不足错误 - SpringBoot异步任务在合并大文件时被K8s容器终止
完整代码仓库:
- 前端:https://gitee.com/yourname/vue3-folder-uploader
- 后端:https://gitee.com/yourname/springboot-cos-uploader
明日计划:
- 实现WebUploader的IE8进度条显示
- 编写Oracle分片表的分区策略(按任务ID哈希分区)
- 测试20G文件在低带宽(2Mbps)下的传输稳定性
(日记结束)
附:技术栈对比表
| 模块 | 原方案 | 当前方案 | 优化点 |
|---|---|---|---|
| 前端框架 | jQuery | Vue3 + TypeScript | 类型安全+组件化 |
| 上传组件 | WebUploader基础版 | 定制版(支持文件夹+ETag) | 递归解析文件夹结构 |
| 后端语言 | JSP | SpringBoot 3.0 | 响应式编程+Oracle优化 |
| 数据库 | MySQL | Oracle 21c | BLOB存储优化+分区表 |
| 对象存储 | 百度OBS | 腾讯云COS | 适配不同的分片API规范 |
如需完整项目或调试协助,请联系QQ群或留言获取测试账号!
SQL示例
创建数据库
配置数据库连接
自动下载maven依赖
启动项目
启动成功
访问及测试
默认页面接口定义
在浏览器中访问
数据表中的数据
效果预览
文件上传
文件刷新续传
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件夹上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
批量下载
支持文件批量下载
下载续传
文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。
文件夹下载
支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。
示例下载
下载完整示例