建设网站哪家专业,招远做网站公司,如何做个盈利的网站,网页建站总结报告大文件上传系统开发指南#xff08;基于原生JSSpringBoot#xff09;
项目概述
大家好#xff0c;我是一个陕西的Java程序员#xff0c;最近接了个刺激的外包项目 - 要开发一个支持20G文件上传下载的系统#xff0c;还得兼容IE9这种古董浏览器。客户要求用原…大文件上传系统开发指南基于原生JSSpringBoot项目概述大家好我是一个陕西的Java程序员最近接了个刺激的外包项目 - 要开发一个支持20G文件上传下载的系统还得兼容IE9这种古董浏览器。客户要求用原生JS实现不能借jQuery的力还要支持文件夹上传、加密传输、断点续传等高级功能。预算只有100元没问题咱们程序员最擅长的就是用爱发电技术选型分析经过深思熟虑和几根头发的牺牲我决定采用以下技术方案前端Vue3 CLI 原生JS实现WebUploader功能兼容IE9后端SpringBoot 阿里云OSS数据库MySQL主要存用户信息和文件元数据加密前端SM4国密 后端AES双重加密断点续传基于文件分片和本地存储记录前端实现Vue3 原生JS1. 兼容IE9的文件夹上传组件export default { name: FileUploader, data() { return { fileList: [], chunkSize: 5 * 1024 * 1024, // 5MB分片 concurrent: 3 // 并发上传数 } }, methods: { triggerFileInput() { document.getElementById(fileInput).click(); }, handleFileChange(e) { const files e.target.files; if (!files.length) return; // 处理文件夹结构 const fileTree this.buildFileTree(files); this.prepareUpload(fileTree); }, // 构建文件树结构保留文件夹层级 buildFileTree(files) { const tree {}; for (let i 0; i files.length; i) { const file files[i]; const path file.webkitRelativePath || file.relativePath || file.name; const parts path.split(/); let currentLevel tree; for (let j 0; j parts.length - 1; j) { const dir parts[j]; if (!currentLevel[dir]) { currentLevel[dir] { __files__: [] }; } currentLevel currentLevel[dir]; } // 添加文件信息 currentLevel.__files__.push({ file: file, relativePath: path, size: file.size, loaded: 0, progress: 0, chunks: Math.ceil(file.size / this.chunkSize), uploadedChunks: 0 }); } return tree; }, // 准备上传队列 prepareUpload(fileTree) { const flattenFiles []; // 扁平化文件树保留路径信息 const traverse (node, parentPath ) { for (const key in node) { if (key __files__) { node[key].forEach(fileItem { flattenFiles.push({ ...fileItem, relativePath: parentPath ? ${parentPath}/${fileItem.relativePath} : fileItem.relativePath }); }); } else { const newPath parentPath ? ${parentPath}/${key} : key; traverse(node[key], newPath); } } }; traverse(fileTree); this.fileList flattenFiles; // 开始上传 this.startUpload(); }, // 开始上传带并发控制 startUpload() { let activeUploads 0; const uploadNext () { if (activeUploads this.concurrent) return; const fileItem this.fileList.find(f f.progress 100); if (!fileItem) { if (activeUploads 0) { this.$emit(upload-complete); } return; } activeUploads; this.uploadFile(fileItem).finally(() { activeUploads--; uploadNext(); }); // 立即检查下一个 uploadNext(); }; // 初始启动 for (let i 0; i this.concurrent; i) { uploadNext(); } }, // 分片上传文件 async uploadFile(fileItem) { const file fileItem.file; const totalChunks Math.ceil(file.size / this.chunkSize); // 检查断点续传信息 const uploadInfo this.getUploadInfo(fileItem.relativePath); let startChunk uploadInfo ? uploadInfo.uploadedChunks : 0; for (let i startChunk; i totalChunks; i) { const start i * this.chunkSize; const end Math.min(start this.chunkSize, file.size); const chunk file.slice(start, end); // 读取分片内容兼容IE9 const chunkData await this.readFileAsArrayBuffer(chunk); // SM4加密前端加密 const encryptedChunk this.sm4Encrypt(chunkData); // 上传分片 const formData new FormData(); formData.append(file, new Blob([encryptedChunk]), file.name); formData.append(chunkIndex, i); formData.append(totalChunks, totalChunks); formData.append(relativePath, fileItem.relativePath); formData.append(fileSize, file.size); formData.append(fileMd5, await this.calculateMD5(chunk)); try { const response await fetch(/api/upload/chunk, { method: POST, body: formData }); if (!response.ok) throw new Error(Upload failed); // 更新进度 fileItem.uploadedChunks i 1; fileItem.loaded end; fileItem.progress Math.round((fileItem.uploadedChunks / totalChunks) * 100); // 保存上传进度使用localStorage this.saveUploadInfo(fileItem.relativePath, { uploadedChunks: fileItem.uploadedChunks, totalChunks: totalChunks, fileSize: file.size }); this.$forceUpdate(); } catch (error) { console.error(Chunk upload failed:, error); // 失败后重试当前分片 i--; await new Promise(resolve setTimeout(resolve, 1000)); } } // 所有分片上传完成通知合并 if (fileItem.uploadedChunks totalChunks) { await fetch(/api/upload/merge, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ relativePath: fileItem.relativePath, fileSize: file.size, totalChunks: totalChunks }) }); // 清除本地存储的上传信息 this.clearUploadInfo(fileItem.relativePath); } }, // 兼容IE9的文件读取方法 readFileAsArrayBuffer(file) { return new Promise((resolve, reject) { if (typeof FileReader undefined) { // IE9 fallback const reader new ActiveXObject(Scripting.FileSystemObject); const stream new ActiveXObject(ADODB.Stream); stream.Type 1; // binary stream.Open(); stream.LoadFromFile(file); const arrayBuffer stream.Read(); stream.Close(); resolve(arrayBuffer); } else { const reader new FileReader(); reader.onload () resolve(reader.result); reader.onerror reject; reader.readAsArrayBuffer(file); } }); }, // 简化的SM4加密实际项目中应该使用成熟的加密库 sm4Encrypt(data) { // 这里应该是真正的SM4加密实现 // 为了示例我们只是返回原始数据实际项目中不要这样做 console.warn(实际项目中请替换为真正的SM4加密实现); return data; }, // 计算MD5用于分片校验 calculateMD5(file) { return new Promise((resolve) { // 实际项目中应该使用真正的MD5计算 // 这里简化为固定值实际项目中不要这样做 resolve(dummy-md5-hash); }); }, // 断点续传相关方法使用localStorage getUploadInfo(relativePath) { const key upload_progress_${relativePath}; const data localStorage.getItem(key); return data ? JSON.parse(data) : null; }, } }后端实现SpringBoot1. 文件上传控制器RestControllerRequestMapping(/api/upload)publicclassFileUploadController{AutowiredprivateFileChunkServicefileChunkService;AutowiredprivateOSSClientossClient;Value(${oss.bucketName})privateStringbucketName;// 上传分片PostMapping(/chunk)publicResponseEntityuploadChunk(RequestParam(file)MultipartFilefile,RequestParam(chunkIndex)intchunkIndex,RequestParam(totalChunks)inttotalChunks,RequestParam(relativePath)StringrelativePath,RequestParam(fileSize)longfileSize,RequestParam(fileMd5)StringfileMd5){try{// AES解密后端解密byte[]decryptedBytesAesUtil.decrypt(file.getBytes(),your-secret-key-123);// 保存分片到临时目录StringtempDirSystem.getProperty(java.io.tmpdir)/upload_chunks/fileMd5;FilechunkFilenewFile(tempDir/chunkIndex);Files.createParentDirs(chunkFile);Files.write(decryptedBytes,chunkFile);// 记录分片信息到数据库FileChunkchunknewFileChunk();chunk.setFileMd5(fileMd5);chunk.setChunkIndex(chunkIndex);chunk.setTotalChunks(totalChunks);chunk.setRelativePath(relativePath);chunk.setFileSize(fileSize);chunk.setUploadTime(newDate());fileChunkService.save(chunk);returnResponseEntity.ok().build();}catch(Exceptione){returnResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());}}// 合并分片PostMapping(/merge)publicResponseEntitymergeChunks(RequestBodyMergeRequestrequest){try{StringfileMd5request.getFileMd5();StringtempDirSystem.getProperty(java.io.tmpdir)/upload_chunks/fileMd5;// 检查所有分片是否已上传ListchunksfileChunkService.findByFileMd5(fileMd5);if(chunks.size()!request.getTotalChunks()){returnResponseEntity.badRequest().body(Not all chunks uploaded);}// 创建临时合并文件FilemergedFilenewFile(tempDir/merged_System.currentTimeMillis());try(FileOutputStreamfosnewFileOutputStream(mergedFile);BufferedOutputStreammergingStreamnewBufferedOutputStream(fos)){// 按顺序合并分片for(inti0;irequest.getTotalChunks();i){FilechunkFilenewFile(tempDir/i);Files.copy(chunkFile,mergingStream);}}// 计算合并后文件的MD5校验用StringactualMd5DigestUtils.md5DigestAsHex(newFileInputStream(mergedFile));if(!actualMd5.equals(fileMd5)){returnResponseEntity.badRequest().body(File MD5 mismatch);}// 上传到OSS保留路径结构StringossKeyuploads/request.getRelativePath();ossClient.putObject(bucketName,ossKey,mergedFile);// 保存文件元数据到数据库FileInfofileInfonewFileInfo();fileInfo.setRelativePath(request.getRelativePath());fileInfo.setFileSize(request.getFileSize());fileInfo.setOssKey(ossKey);fileInfo.setUploadTime(newDate());fileInfo.setLastModified(newDate());fileInfoService.save(fileInfo);// 清理临时文件FileUtils.deleteDirectory(newFile(tempDir));returnResponseEntity.ok().build();}catch(Exceptione){returnResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());}}}2. 文件下载控制器RestControllerRequestMapping(/api/download)publicclassFileDownloadController{GetMapping(/info)publicResponseEntitygetFileInfo(RequestParamStringpath){ListfilesfileInfoService.findByPathPrefix(path);returnResponseEntity.ok(files.stream().map(this::convertToDTO).collect(Collectors.toList()));}// 分片下载文件大文件支持GetMapping(/file)publicResponseEntitydownloadFile(RequestParamStringossKey,RequestParam(requiredfalse)Longstart,RequestParam(requiredfalse)Longend){try{// 从OSS获取文件对象OSSObjectossObjectossClient.getObject(bucketName,ossKey);// 如果请求了范围下载if(start!nullend!null){InputStreaminputStreamossObject.getObjectContent();longcontentLengthend-start1;// 跳过前面的字节inputStream.skip(start);// 创建限制长度的输入流InputStreamlimitedStreamnewLimitedInputStream(inputStream,contentLength);returnResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,attachment; filename\URLEncoder.encode(ossKey.substring(ossKey.lastIndexOf(/)1),UTF-8)\).header(HttpHeaders.CONTENT_RANGE,bytes start-end/*).header(HttpHeaders.ACCEPT_RANGES,bytes).contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(contentLength).body(newInputStreamResource(limitedStream));}else{// 完整文件下载returnResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,attachment; filename\URLEncoder.encode(ossKey.substring(ossKey.lastIndexOf(/)1),UTF-8)\).contentType(MediaType.APPLICATION_OCTET_STREAM).body(newInputStreamResource(ossObject.getObjectContent()));}}catch(Exceptione){returnResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}}}数据库设计-- 文件信息表CREATETABLEfile_info(idBIGINTAUTO_INCREMENTPRIMARYKEY,relative_pathVARCHAR(1000)NOTNULLCOMMENT文件相对路径保留层级结构,file_sizeBIGINTNOTNULLCOMMENT文件大小字节,oss_keyVARCHAR(500)NOTNULLCOMMENTOSS存储key,upload_timeDATETIMENOTNULLCOMMENT上传时间,last_modifiedDATETIMENOTNULLCOMMENT最后修改时间,UNIQUEKEYuk_relative_path(relative_path(255)))ENGINEInnoDBDEFAULTCHARSETutf8mb4;-- 文件分片表用于断点续传CREATETABLEfile_chunk(idBIGINTAUTO_INCREMENTPRIMARYKEY,file_md5VARCHAR(32)NOTNULLCOMMENT文件MD5作为唯一标识,chunk_indexINTNOTNULLCOMMENT分片索引,total_chunksINTNOTNULLCOMMENT总分片数,relative_pathVARCHAR(1000)NOTNULLCOMMENT文件相对路径,file_sizeBIGINTNOTNULLCOMMENT文件总大小,upload_timeDATETIMENOTNULLCOMMENT上传时间,INDEXidx_file_md5(file_md5))ENGINEInnoDBDEFAULTCHARSETutf8mb4;项目部署说明前端部署使用Vue CLI构建生产版本npm run build将生成的dist目录内容部署到Nginx或Apache后端部署使用Maven打包mvn clean package生成JAR文件后上传到阿里云ECS使用java -jar命令运行或配置为系统服务Nginx配置示例支持大文件上传server { listen 80; server_name your-domain.com; client_max_body_size 102400m; # 100GB proxy_read_timeout 600s; proxy_send_timeout 600s; location / { root /path/to/frontend/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }兼容性处理要点IE9兼容性使用ActiveXObject替代FileReader避免使用ES6语法使用fetch的polyfill或改用XMLHttpRequest文件夹上传利用webkitdirectory属性Chrome等IE中使用并手动解析路径加密处理前端使用SM4国密算法加密后端使用AES二次加密提供加密开关配置开发建议分阶段开发第一阶段实现基本文件上传下载第二阶段添加文件夹支持第三阶段实现断点续传第四阶段添加加密功能测试要点大文件上传5GB网络中断后恢复上传文件夹层级结构验证跨浏览器兼容性测试性能优化分片大小调整5MB-10MB比较合适并发上传数控制3-5个并发使用Web Worker处理加密计算完整项目结构large-file-upload/ ├── frontend/ # 前端Vue3项目 │ ├── src/ │ │ ├── components/ │ │ │ └── FileUploader.vue │ │ ├── App.vue │ │ └── main.js │ ├── public/ │ └── package.json ├── backend/ # 后端SpringBoot项目 │ ├── src/ │ │ ├── main/ │ │ │ ├── java/com/example/upload/ │ │ │ │ ├── controller/ │ │ │ │ ├── service/ │ │ │ │ ├── model/ │ │ │ │ └── Application.java │ │ │ └── resources/ │ │ │ ├── application.yml │ │ │ └── application-dev.yml │ └── pom.xml ├── docs/ # 开发文档 │ ├── api.md │ ├── deployment.md │ └── compatibility.md └── README.md最后的话兄弟这个项目确实有点挑战性特别是100元预算还要兼容IE9。不过咱们程序员不就是喜欢挑战吗我建议先实现核心功能文件上传下载再逐步添加高级功能重点测试断点续传和文件夹结构保留加密功能可以先用简化版后续再完善我已经提供了核心代码框架你可以基于这个继续开发。如果遇到具体问题欢迎加入我们的QQ群374992201交流群里大佬众多说不定能找到帮你调试的兄弟。记住咱们虽然预算有限但志气不能限用爱发电照亮代码之路导入项目导入到Eclipse点南查看教程导入到IDEA点击查看教程springboot统一配置点击查看教程工程NOSQLNOSQL示例不需要任何配置可以直接访问测试创建数据表选择对应的数据表脚本这里以SQL为例修改数据库连接信息访问页面进行测试文件存储路径up6/upload/年/月/日/guid/filename效果预览文件上传文件刷新续传支持离线保存文件进度在关闭浏览器刷新浏览器后进行不丢失仍然能够继续上传文件夹上传支持上传文件夹并保留层级结构同样支持进度信息离线保存刷新页面关闭页面重启系统不丢失上传进度。下载示例点击下载完整示例