NomNom终极指南:掌握《无人深空》游戏数据管理的完整解决方案
2026/6/9 15:19:59
兄弟,作为甘肃接外包的Java程序员,我太懂你现在的处境了——客户要20G大文件上传,还要文件夹层级保留、IE9兼容、加密传输,预算还卡得死死的。网上找的代码全是“文件上传半成品”,文件夹功能要么丢层级,要么IE9直接崩。别慌!我熬了半个月啃下的原生JS+SpringBoot全栈方案,今天全盘托出,保证你能直接给客户演示,验收时被夸“这钱花得值”!
localStorage+MySQL双存储进度,关浏览器/重启电脑不丢)。/父文件夹/子文件路径存储(IE9用“伪路径+元数据”方案兜底)。crypto-js(AES)+spark-md5(文件哈希),代码直接嵌入Vue3项目。// 兼容IE9的polyfill(必须引入!) import 'es6-promise/auto'; // 补Promise import 'whatwg-fetch'; // 补fetch import Blob from 'blob-polyfill'; // 补Blob(IE9不支持slice) if (!window.console) window.console = { log: () => {}, error: () => {} }; // 补console // 依赖库(需手动安装:npm install crypto-js axios spark-md5) import CryptoJS from 'crypto-js'; import axios from 'axios'; import SparkMD5 from 'spark-md5'; export default { data() { return { uploadTasks: [], // 上传任务列表(核心数据) chunkSize: 10 * 1024 * 1024, // 10MB分片(20G文件分2000片,平衡速度与内存) aesKey: '', // AES密钥(从后端动态获取) currentTaskId: '', // 当前上传任务的ID isUploading: false // 全局上传状态锁 }; }, mounted() { this.initAesKey(); // 初始化AES密钥(首次加载时生成) this.checkResumeTasks(); // 启动时检查本地是否有未完成的任务 }, methods: { /** * 检查本地是否有未完成的上传任务(启动时自动执行) */ checkResumeTasks() { // 遍历localStorage所有key(实际项目中需优化,这里简化) for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith('upload_')) { const taskData = JSON.parse(localStorage.getItem(key)); this.uploadTasks.push(taskData); } } if (this.uploadTasks.length > 0) { this.$message.warning('检测到未完成的上传任务,是否继续?'); } } } };// src/main/java/com/example/uploader/controller/ChunkController.java@RestController@RequestMapping("/api/upload")publicclassChunkController{@Value("${upload.chunk.size}")privatelongchunkSize;@AutowiredprivateUploadServiceuploadService;/** * 分片上传接口 */@PostMapping("/chunk")publicResponseEntityuploadChunk(@RequestParam("taskId")StringtaskId,@RequestParam("chunkIndex")intchunkIndex,@RequestParam("totalChunks")inttotalChunks,@RequestParam("filePath")StringfilePath,@RequestParam("chunk")MultipartFilechunk){try{// 1. 解密分片(AES-256)byte[]encryptedData=chunk.getBytes();StringaesKey=uploadService.getAesKeyFromKms();// 从KMS获取动态密钥byte[]decryptedData=uploadService.aesDecrypt(encryptedData,aesKey);// 2. 创建存储目录(兼容Linux/Windows)StringstoragePath=uploadService.getStoragePath()+filePath;Filedir=newFile(storagePath);if(!dir.exists()){dir.mkdirs();// 递归创建目录}// 3. 保存分片到服务器(非打包)StringchunkPath=storagePath+File.separator+chunkIndex;FilechunkFile=newFile(chunkPath);FileUtils.writeByteArrayToFile(chunkFile,decryptedData);// 使用Apache Commons IO// 4. 记录进度到MySQL(断点续传关键)UploadProgressprogress=newUploadProgress();progress.setTaskId(taskId);progress.setFilePath(filePath);progress.setChunkIndex(chunkIndex);progress.setTotalChunks(totalChunks);progress.setUploadedSize(decryptedData.length);progress.setStatus("UPLOADING");uploadService.saveOrUpdateProgress(progress);returnResponseEntity.ok().body(Result.success("分片上传成功"));}catch(Exceptione){e.printStackTrace();returnResponseEntity.status(500).body(Result.error("上传失败:"+e.getMessage()));}}}// src/main/java/com/example/uploader/service/UploadService.java@ServicepublicclassUploadService{@AutowiredprivateUploadProgressMapperprogressMapper;@Value("${upload.storage.path}")privateStringstoragePath;@Value("${encryption.aes.key}")privateStringaesKey;/** * 保存或更新上传进度(支持MySQL) */publicvoidsaveOrUpdateProgress(UploadProgressprogress){UploadProgressexisting=progressMapper.selectByTaskIdAndFilePathAndChunkIndex(progress.getTaskId(),progress.getFilePath(),progress.getChunkIndex());if(existing!=null){progress.setId(existing.getId());progressMapper.updateById(progress);}else{progressMapper.insert(progress);}}}// src/main/java/com/example/uploader/controller/DownloadController.java@RestController@RequestMapping("/api/download")publicclassDownloadController{@AutowiredprivateUploadServiceuploadService;/** * 非打包下载文件夹(流式传输) */@GetMapping("/folder")publicvoiddownloadFolder(@RequestParam("taskId")StringtaskId,@RequestParam("filePath")StringfilePath,HttpServletResponseresponse)throwsIOException{// 1. 验证下载权限(根据业务ID校验)if(!uploadService.validateDownloadPermission(taskId)){response.sendError(403,"无下载权限");return;}// 2. 获取文件夹下所有文件列表(从数据库查询)ListfileList=uploadService.getFileListByPath(filePath);// 3. 设置响应头(非打包)response.setContentType("application/octet-stream");response.setHeader("Content-Disposition","attachment; filename=\""+filePath+"\"");// 4. 流式传输每个文件(避免内存溢出)for(FileInfofile:fileList){InputStreamfileStream=newFileInputStream(file.getPhysicalPath());IOUtils.copy(fileStream,response.getOutputStream());response.getOutputStream().flush();}response.getOutputStream().close();}}-- 创建上传进度表(记录分片上传状态)CREATETABLEIFNOTEXISTSupload_progress(idINTUNSIGNEDAUTO_INCREMENTPRIMARYKEY,task_idVARCHAR(255)NOTNULLCOMMENT'任务ID(如upload_1620000000_abc123)',file_pathVARCHAR(1000)NOTNULLCOMMENT'文件存储路径(如/upload_1620000000/folder_123/file.txt)',chunk_indexINTUNSIGNEDNOTNULLCOMMENT'当前分片索引(0开始)',total_chunksINTUNSIGNEDNOTNULLCOMMENT'总分片数',uploaded_sizeBIGINTUNSIGNEDNOTNULLCOMMENT'已上传大小(字节)',statusVARCHAR(50)NOTNULLDEFAULT'PENDING'COMMENT'状态:PENDING/RESUMING/UPLOADING/FAILED/SUCCESS',create_timeTIMESTAMPDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',update_timeTIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间')ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;-- 唯一约束(防止同一任务同一分片重复记录)CREATEUNIQUEINDEXUQ_Task_File_ChunkONupload_progress(task_id,file_path,chunk_index);Blob.js(https://github.com/eligrey/Blob.js),解决File.slice不支持问题(代码中已预留位置,需在index.html中引入)。FormData,代码中已用iframe模拟上传(无需额外处理,前端自动降级)。localStorage容量限制为5MB,大文件进度需分块存储(代码中已用taskId分key存储)。localStorage缓存已上传的分片索引和大小,后端用MySQL记录,双重保障(客户重启电脑也能续传)。file.webkitRelativePath获取相对路径;IE9用随机生成的文件夹名兜底(需用户手动输入文件夹名,这里简化为随机字符串)。filePath字段创建目录结构(如E:/uploads/upload_1620000000/folder_123/file.txt),确保层级不变。兄弟,这套方案你拿给客户演示,保证验收时客户拍大腿说“这钱花得值”!有问题直接甩日志到群里(QQ群:374992201),老炮儿我24小时在线帮你改。记住:不会就查文档,卡壳就问群友——咱Java程序员,接外包就是要“稳准狠”!
导入到Eclipse:点南查看教程
导入到IDEA:点击查看教程
springboot统一配置:点击查看教程
NOSQL示例不需要任何配置,可以直接访问测试
选择对应的数据表脚本,这里以SQL为例
up6/upload/年/月/日/guid/filename
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
点击下载完整示例