金融保险行业,JAVA如何编写网页大文件上传的开源示例?

大文件上传系统开发指南(基于原生JS+SpringBoot)

项目概述

大家好,我是一个在浙江奋斗的Java程序员,最近接了个"刺激"的外包项目 - 开发一个支持20G大文件上传下载的系统,还要兼容IE9这种上古浏览器。客户要求使用原生JS实现(不能用jQuery等库),支持文件夹上传(保留层级结构),还要加密传输和断点续传。预算只有100元,但需要提供完整技术支持和源代码。

这活儿听着就刺激,但咱是谁?是打不死的小强!下面我就分享一下我的解决方案和部分代码实现。

技术选型

  • 前端:Vue3 CLI + 原生JavaScript(兼容IE9)
  • 后端:SpringBoot + Tomcat
  • 数据库:MySQL(主要存用户信息和文件元数据)
  • 文件存储:服务器本地文件系统
  • 加密:SM4(国密)和AES

系统架构

浏览器(IE9+) ←HTTP→ SpringBoot后端 ←本地IO→ 文件存储 ↑ MySQL(元数据)

前端实现(关键代码)

1. 文件夹上传组件(兼容IE9)

export default { 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('/'); // 添加文件信息 const fileName = parts[parts.length - 1]; current[fileName] = { file: file, relativePath: path, size: file.size, chunks: Math.ceil(file.size / this.chunkSize), uploadedChunks: 0, progress: 0, status: '等待上传' }; } return tree; }, // 准备上传队列 prepareUpload(tree) { const flattenFiles = []; const traverse = (node, path = '') => { for (const key in node) { if (key === '__files__' || key === '__dirs__') continue; const newPath = path ? `${path}/${key}` : key; if (node[key].file) { // 是文件 flattenFiles.push({ ...node[key], relativePath: newPath }); } else { // 是目录,继续遍历 traverse(node[key].__dirs__, newPath); } } }; traverse(tree); this.fileList = flattenFiles; this.startUpload(); }, // 开始上传 startUpload() { const activeUploads = 0; const uploadNext = () => { if (activeUploads >= this.concurrent) return; const file = this.fileList.find(f => f.status === '等待上传'); if (!file) { if (this.fileList.every(f => f.status === '上传完成')) { this.$emit('upload-complete'); } return; } }; uploadNext(); }, // 分片上传文件 async uploadFile(fileEntry) { const file = fileEntry.file; const fileId = this.generateFileId(file); // 检查断点续传信息 const resumeInfo = this.getResumeInfo(fileId); let startChunk = resumeInfo ? resumeInfo.uploadedChunks : 0; for (let i = startChunk; i < fileEntry.chunks; 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); // 加密处理(这里简化,实际应该用Web Crypto API或polyfill) const encryptedData = this.encryptData(chunkData, 'AES'); // 实际应该用SM4 const formData = new FormData(); formData.append('fileId', fileId); formData.append('chunkIndex', i); formData.append('totalChunks', fileEntry.chunks); formData.append('relativePath', fileEntry.relativePath); formData.append('fileSize', file.size); formData.append('chunk', new Blob([encryptedData])); formData.append('fileName', file.name); } // 所有分片上传完成,通知服务器合并 await this.mergeFile(fileId, fileEntry.relativePath, file.size, file.name); fileEntry.status = '上传完成'; this.clearResumeInfo(fileId); }, // 以下是兼容IE9的工具方法 readFileAsArrayBuffer(file) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.readAsArrayBuffer(file); }); }, encryptData(data, algorithm) { // 实际项目中应该使用Web Crypto API或polyfill // 这里简化处理,实际加密代码会更复杂 if (typeof data === 'string') { return btoa(data); // 简单base64模拟加密 } else { const bytes = new Uint8Array(data); let result = ''; for (let i = 0; i < bytes.length; i++) { result += String.fromCharCode(bytes[i]); } return btoa(result); } }, async uploadChunk(formData) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload/chunk', true); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error('上传失败')); } }; xhr.onerror = () => reject(new Error('网络错误')); xhr.send(formData); }); }, async mergeFile(fileId, relativePath, fileSize, fileName) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload/merge', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error('合并失败')); } }; xhr.onerror = () => reject(new Error('网络错误')); xhr.send(JSON.stringify({ fileId, relativePath, fileSize, fileName })); }); }, // 断点续传相关方法(使用localStorage存储) generateFileId(file) { // 简单生成文件ID,实际应该更可靠 return `${file.name}-${file.size}-${file.lastModified}`; }, getResumeInfo(fileId) { const info = localStorage.getItem(`upload_resume_${fileId}`); return info ? JSON.parse(info) : null; }, saveResumeInfo(fileId, fileEntry) { localStorage.setItem(`upload_resume_${fileId}`, JSON.stringify({ uploadedChunks: fileEntry.uploadedChunks, relativePath: fileEntry.relativePath, fileSize: fileEntry.size, fileName: fileEntry.file.name })); }, clearResumeInfo(fileId) { localStorage.removeItem(`upload_resume_${fileId}`); }, formatSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } } }

后端实现(SpringBoot关键代码)

1. 文件上传控制器

@RestController@RequestMapping("/api/upload")publicclassFileUploadController{privatestaticfinalStringUPLOAD_DIR="/var/bigfileupload/";// 实际应该从配置读取privatestaticfinalintCHUNK_SIZE=5*1024*1024;// 5MB@PostMapping("/chunk")publicResponseEntityuploadChunk(@RequestParam("fileId")StringfileId,@RequestParam("chunkIndex")intchunkIndex,@RequestParam("totalChunks")inttotalChunks,@RequestParam("relativePath")StringrelativePath,@RequestParam("fileSize")longfileSize,@RequestParam("fileName")StringfileName,@RequestParam("chunk")MultipartFilechunk)throwsIOException{// 创建临时目录StringtempDir=UPLOAD_DIR+"temp/"+fileId+"/";FiletempDirFile=newFile(tempDir);if(!tempDirFile.exists()){tempDirFile.mkdirs();}// 保存分片(实际应该先解密)StringchunkPath=tempDir+chunkIndex;chunk.transferTo(newFile(chunkPath));// 记录上传进度(实际应该用数据库)UploadProgressprogress=newUploadProgress();progress.setFileId(fileId);progress.setUploadedChunks(chunkIndex+1);progress.setTotalChunks(totalChunks);progress.setRelativePath(relativePath);progress.setFileSize(fileSize);progress.setFileName(fileName);// saveToDatabase(progress); // 实际应该存数据库returnResponseEntity.ok().body(Map.of("status","success","chunkIndex",chunkIndex,"fileId",fileId));}@PostMapping("/merge")publicResponseEntitymergeFile(@RequestBodyMergeRequestrequest)throwsIOException,NoSuchAlgorithmException{StringfileId=request.getFileId();StringtempDir=UPLOAD_DIR+"temp/"+fileId+"/";FiletempDirFile=newFile(tempDir);if(!tempDirFile.exists()){returnResponseEntity.badRequest().body(Map.of("error","临时文件不存在"));}// 创建目标目录结构StringrelativePath=request.getRelativePath();StringtargetPath=UPLOAD_DIR+relativePath;FiletargetFile=newFile(targetPath);}// 下载文件接口(非打包方式)@GetMapping("/download")publicResponseEntitydownloadFile(@RequestParamStringfilePath,HttpServletResponseresponse)throwsIOException{// 设置响应头HttpHeadersheaders=newHttpHeaders();headers.add(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=\""+file.getName()+"\"");headers.add(HttpHeaders.CONTENT_TYPE,Files.probeContentType(file.toPath()));headers.add(HttpHeaders.CONTENT_LENGTH,String.valueOf(file.length()));returnResponseEntity.ok().headers(headers).body(resource);}// 文件夹下载(递归下载)@GetMapping("/download/folder")publicvoiddownloadFolder(@RequestParamStringfolderPath,HttpServletResponseresponse)throwsIOException{// 实际实现应该递归遍历文件夹,生成zip或逐个文件下载// 这里简化处理,实际项目中需要更复杂的实现response.setContentType("application/zip");response.setHeader("Content-Disposition","attachment; filename=\""+newFile(folderPath).getName()+".zip\"");// 实际应该使用ZipOutputStream打包// 这里只是示例,实际不会这样实现try(ServletOutputStreamout=response.getOutputStream()){out.write("这不是真正的zip文件,实际应该递归打包文件夹".getBytes());}}}

2. 数据库实体类

@Entity@Table(name="file_metadata")publicclassFileMetadata{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)privateLongid;privateStringfileId;privateStringfilePath;privateStringfileName;privatelongfileSize;privateDateuploadTime;// getters and setters}

兼容IE9的注意事项

  1. XMLHttpRequest:IE9不支持FormData,但支持XMLHttpRequest上传文件
  2. FileReader:IE10+才完全支持,IE9需要polyfill
  3. Blob:IE10+支持,IE9需要使用BlobBuilder
  4. 加密:Web Crypto API在IE9不可用,需要使用第三方库如CryptoJS

IE9兼容的加密方案示例

// 在index.html中引入CryptoJS//// 修改encryptData方法encryptData(data,algorithm){if(algorithm==='AES'){// 使用CryptoJS进行AES加密constkey=CryptoJS.enc.Utf8.parse('1234567890123456');// 实际应该从安全配置读取constiv=CryptoJS.enc.Utf8.parse('1234567890123456');letdataToEncrypt;if(typeofdata==='string'){dataToEncrypt=data;}else{// 如果是ArrayBuffer,转换为字符串constbytes=newUint8Array(data);letstr='';for(leti=0;i<bytes.length;i++){str+=String.fromCharCode(bytes[i]);}dataToEncrypt=str;}constencrypted=CryptoJS.AES.encrypt(dataToEncrypt,key,{iv:iv,mode:CryptoJS.mode.CBC,padding:CryptoJS.pad.Pkcs7});returnencrypted.toString();}// 其他算法...returndata;// 未加密}

部署说明

  1. 前端构建

    npminstallnpmrun build

    将生成的dist目录内容部署到Tomcat的webapps/ROOT目录

  2. 后端配置

    • 修改application.properties
      server.port=8080 spring.servlet.multipart.max-file-size=21GB spring.servlet.multipart.max-request-size=21GB file.upload-dir=/var/bigfileupload/
    • 确保上传目录存在且有写入权限:
      mkdir-p /var/bigfileupload/tempchmod777/var/bigfileupload
  3. 数据库初始化

    • 创建MySQL数据库并执行SQL脚本:
      CREATETABLEfile_metadata(idBIGINTAUTO_INCREMENTPRIMARYKEY,file_idVARCHAR(255)NOTNULL,file_pathTEXTNOTNULL,file_nameVARCHAR(255)NOTNULL,file_sizeBIGINTNOTNULL,upload_timeDATETIMENOTNULL);

开发文档要点

  1. 系统功能

    • 大文件分片上传(支持20GB+)
    • 文件夹上传(保留层级结构)
    • 断点续传(基于localStorage)
    • 加密传输和存储(AES/SM4)
    • 兼容IE9+等主流浏览器
  2. API文档

    • POST /api/upload/chunk- 上传文件分片
    • POST /api/upload/merge- 合并文件分片
    • GET /api/upload/download- 下载单个文件
    • GET /api/upload/download/folder- 下载整个文件夹
  3. 部署文档

    • 环境要求:JDK 8+, Node.js, MySQL, Tomcat 8+
    • 配置文件说明
    • 初始化脚本

总结

这个项目确实挑战不小,但通过合理的分片上传、断点续传机制和兼容性处理,我们还是能够实现客户的需求。关键点在于:

  1. 前端使用原生JS实现文件夹结构解析和上传队列管理
  2. 后端提供分片上传和合并接口
  3. 使用localStorage存储上传进度实现断点续传
  4. 通过CryptoJS等库实现兼容IE9的加密

由于预算有限,我省略了一些高级功能如:

  • 完整的SM4加密实现(实际需要引入Bouncy Castle等库)
  • 分布式存储支持
  • 详细的权限控制
  • 完善的错误处理和日志

如果需要更完整的实现,建议考虑:

  1. 使用WebUploader等成熟库(但需要处理兼容性问题)
  2. 增加预算购买商业组件
  3. 分阶段开发,先实现核心功能

最后,欢迎加入我们的QQ群374992201,一起交流技术,合作接单!群里经常有红包和技术分享,还有项目合作机会哦!

导入项目

导入到Eclipse:点南查看教程
导入到IDEA:点击查看教程
springboot统一配置:点击查看教程

工程

NOSQL

NOSQL示例不需要任何配置,可以直接访问测试

创建数据表

选择对应的数据表脚本,这里以SQL为例

修改数据库连接信息

访问页面进行测试

文件存储路径

up6/upload/年/月/日/guid/filename

效果预览

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。

下载示例

点击下载完整示例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1163991.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

国产化数据库如何存储XHEDITOR编辑器中的动态公式内容?

.NET程序员接单记&#xff1a;那个让我月入百万的CMS插件需求 各位老铁们&#xff0c;我是来自山西的.NET码农小王&#xff0c;最近接了个企业官网外包项目&#xff0c;客户提了个"简单"需求——给xhEditor编辑器加个Word文档导入功能。听起来是不是平平无奇&#x…

四川哪家医药批发商中医药供应链管理最完善?

在四川的医药批发商中&#xff0c;四川合纵药易购医药股份有限公司的中医药供应链管理极具完善性&#xff0c;同时重药控股&#xff08;四川&#xff09;有限公司、国药集团西南医药有限公司也表现突出。 其中&#xff0c;合纵药易购作为国内首家 A 股上市的医药产业互联网企业…

机械CAD如何与XHEDITOR实现公式参数实时同步?

CMS企业官网Word一键粘贴解决方案 项目背景与需求分析 兄弟们&#xff0c;最近接了个CMS企业官网的外包项目&#xff0c;客户爸爸提出了个新需求&#xff1a;要在后台编辑器里加个Word一键粘贴功能&#xff01;这需求太真实了&#xff0c;特别是对高龄用户特别友好——你懂的…

互联网医疗怎样实现电子处方公式跨平台转存至XHEDITOR?

打造Word内容一键转存的CMS新闻管理系统升级方案 大家好&#xff01;我是山西某校软件工程专业的一名大三学生&#xff0c;最近正在给我的CMS新闻管理系统添加一个酷炫的功能——Word内容一键转存&#xff01;&#x1f389; 需求分析 我需要给后台编辑器增加一个按钮&#x…

当使用Mac的“LookUp”意味着两种未来:一个在照亮知识,另一个在凝视关系

两款同名应用&#xff0c;一个斩获苹果年度大奖&#xff0c;致力于在方寸表盘间传递语言之美&#xff1b;另一个在隐秘角落下载量超10万次&#xff0c;将亲密关系置于实时监控之下。这不是名字的巧合&#xff0c;而是科技岔路口的鲜明路标。清晨&#xff0c;你的智能手表轻轻一…

年终总结PPT怎么做?我用ChatExcel AIPPT一键生成,被领导夸疯了!

年末了&#xff01;又到年终汇报季&#xff0c;但是&#xff0c;很多同学写年终总结时&#xff0c;没思路、没灵感、没精力……现在&#xff0c;【ChatExcel AIPPT】让你“一句话生成PPT”&#xff0c;把一年的辛苦&#xff0c;一键升级为老板眼中的高分成绩单&#x1f4c4;~01…

沉浸式Mac写作利器 Ulysses 深度体验

在信息过载、注意力碎片化的时代&#xff0c;写作者面临的最大挑战或许并非灵感枯竭&#xff0c;而是难以进入并维持一种深度专注的“心流”状态。频繁的界面切换、繁琐的格式调整、多设备间的同步断层……这些技术性干扰不断消耗着我们宝贵的认知资源。今天&#xff0c;我们将…

信创云文档如何解决Word公式粘贴到XHEDITOR的兼容性?

我是如何用99元预算给CMS系统加了个"Word全家桶"功能的 各位学弟学妹们好&#xff0c;我是你们山西老学长&#xff0c;最近不是在搞毕业设计嘛&#xff0c;做了个CMS新闻管理系统。导师看了说&#xff1a;“你这系统发新闻还得手动排版&#xff1f;太土了吧&#xf…

Mellel for Mac 深度体验与解析

如果你曾为论文格式崩溃&#xff0c;或因书稿结构调整而彻夜难眠&#xff0c;这篇文章或许能改变你的写作人生。一、写作之痛&#xff1a;当通用工具遇上长文档打开电脑&#xff0c;面对一个即将动笔的庞大项目——可能是十万字的学术论文&#xff0c;也可能是一本技术书籍的初…

我使用MAC WiFi Explorer Pro完成了一次家庭网络“大扫除”

摘要&#xff1a;你是否也受困于卧室信号弱、游戏高延迟、视频频繁缓冲&#xff1f;这些问题&#xff0c;盲目升级宽带或路由器可能治标不治本。本文将通过一款专业工具 WiFi Explorer Pro&#xff0c;带你透视复杂的家庭无线环境&#xff0c;从信道冲突、信号干扰到设备配置&a…

2026年一站式论文通关指南:7款免费AI神器手把手教学,知网查重AIGC痕迹全搞定

还在为论文从零到一的“鸿沟”发愁吗&#xff1f;面对导师的修改意见无从下手&#xff1f;深夜还在为查重率和AI检测报告焦虑不安&#xff1f;如果你对以上任何一个问题点头&#xff0c;那么恭喜你&#xff0c;这篇指南就是为你量身定制的“通关秘籍”。 告别过去低效、盲目的…

CAN通信速率和传输距离关系

来自《汇川 IS620P系列伺服应用手册 CANopen通信篇》1M通信速率&#xff0c;通信距离要小于25m。 500K速率能到100m。 对于CAN通信&#xff0c;线缆线径对于传输距离影响不大。

AI编程新范式:从自动化代码生成到算法优化的全栈实践

AI正深刻重构编程的生产方式——当大语言模型能理解复杂需求并生成高质量代码&#xff0c;当低代码平台让非专业开发者构建企业级应用&#xff0c;当自动化工具将算法性能提升数倍&#xff0c;编程不再仅是工程师的专属技能&#xff0c;而成为一种普惠的创造力工具。本文系统拆…

超越 Grok,阶跃星辰拿下全球权威语音模型榜第一

1 月 15 日消息&#xff0c;全球知名权威大模型评测榜单 Artificial Analysis Speech Reasoning 更新&#xff0c;大模型创业公司阶跃星辰原生音频推理模型 Step-Audio-R1.1 登顶榜首&#xff0c;以 96.4% 准确率&#xff0c;超越 Grok、Gemini、GPT-Realtime 等主流一线模型&a…

脑机接口安全危机:测试工程师的防线重构

——2026年Q1事故技术白皮书 一、事故全景&#xff1a;数据背后的测试盲区 1.1 灾难性数据剖面&#xff08;2026年Q1&#xff09; 事故类型 占比 典型失效场景 测试覆盖缺失环节 记忆数据覆写 42% 睡眠模式下的异常数据流注入 边缘场景压力测试 神经协议劫持 31% 第…

AI编程范式革命:从自动化代码生成到算法深度优化的全景实践

引言&#xff1a;软件工程的“奇点”时刻我们正处于软件工程史上一个前所未有的转折点。过去的编程范式——从面向过程的汇编指令到面向对象的抽象设计——都是关于“如何告诉机器做什么”。然而&#xff0c;随着大语言模型&#xff08;LLM&#xff09;的爆发&#xff0c;编程范…

vxe-table 修改为紧凑型样式单元格编辑和虚拟滚动

vxe-table 修改为紧凑型样式单元格编辑和虚拟滚动&#xff0c;由于公司业务需求&#xff0c;需要一屏看非常多的数据&#xff0c;所以需要将行高调低&#xff0c;这样就能显示更多的数据&#xff1b;需要注意的是当行高越小&#xff0c;渲染的单元格数量越多&#xff0c;可能会…

网站标签SEO实战指南:小白也能让搜索引擎爱上你

网站标签SEO实战指南&#xff1a;小白也能让搜索引擎爱上你网站标签SEO实战指南&#xff1a;小白也能让搜索引擎爱上你我当年乱打标签&#xff0c;被百度当成垃圾站按在地上摩擦标签到底是个啥&#xff1f;别把情书写成请假条现在还用 keywords&#xff1f;你要这么写就等着被嫌…

我在『尽量裸奔编码』(从不拒绝“穿衣服”,但更喜欢『只穿“内裤”』)(Python)

#裸奔编码#编码哲学#个人思想#Python 微风过体裸舒爽&#xff0c;轻薄着装亲内裤。 笔记模板由python脚本于2026-01-12 12:35:54创建&#xff0c;本篇笔记适合喜欢经营学习时间的coder翻阅。 学习的细节是欢悦的历程 博客的核心价值&#xff1a;在于输出思考与经验&#xff0c…

磁盘罢工?教你一招让“无法访问”的数据起死回生!

在数字化时代&#xff0c;数据已成为我们生活和工作中不可或缺的一部分。然而&#xff0c;当遇到“磁盘位置不可用无法访问”的提示时&#xff0c;许多用户会感到手足无措&#xff0c;担心重要数据就此丢失。本文将深入解析这一问题的本质&#xff0c;并提供一套切实可行的数据…