必看!Java 大文件上传项目实战:基于 Spring Boot + Thymeleaf 架构,深挖 ...

锦通  金牌会员 | 2025-3-17 01:46:21 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 991|帖子 991|积分 2973

??“只为成功找方法,不为失败找理由”??
  源代码:Java大文件项目实战代码
  文章目次
一、引言
二、项目演示
2.1 功能演示视频
三、项目背景与目标
四、技能选型决议
为什么选择 Spring Boot
1. 核心优势
1.1 快速开发
1.2 内嵌容器
1.3 统一的配置管理
2. 生态整合优势
2.1 文件处置惩罚组件
2.2 非常处置惩罚
2.3 性能优化
3. 开发效率提升
3.1 注解驱动开发
3.2 测试支持
4. 部署和维护
4.1?打包部署
4.2?监控管理
5.?安全性保障
5.1?文件校验
5.2?并发控制
前端为何选择 Thymeleaf
1. 服务端渲染优势
1.1 原生HTML模板
1.2 自然集成
2. 上传功能实现优势
2.1 动态数据绑定
2.2 安全性保障
3. 开发效率提升
3.1 模板复用
4. 性能考虑
4.1 缓存机制
4.2 按需加载
5. 实际应用优势
5.1 上传交互
5.2 用户体验
五、分块上传核心原理
1.分块策略
分块巨细设计
1.1 分块巨细的选择
1.2 分块计算逻辑
分块处置惩罚流程
1.1 文件分片
1.2 分块上传队列
分块巨细的动态调整策略
1.1 网络自适应
优化考虑
1.1 性能优化
1.2 用户体验
2. 实现效果
3. 上传流程
整体流程图
具体步骤阐明
2.1 前置准备
计算文件特征
验证已上传分块
2.2 分块上传阶段
分片处置惩罚
服务端吸收
2.3 归并阶段
触发归并
归并处置惩罚
关键技能点
3.1 并发控制
3.2 错误处置惩罚
3.3 进度监控
六、断点续传的实现要点
1. 断点记录机制
1.1 客户端存储
1.2 服务端记录
2. 续传实现
2.1 上传前验证
2.2 分片验证接口
3. 智能续传策略
3.1 分片有效期查抄
3.2 网络状态检测
4. 非常处置惩罚
4.1 上传非通例复
4.2 文件完整性校验
6. 续传逻辑实现
6.1 断点规复流程
6.2 进度归并与验证
6.3 智能上传策略
6.4 网络自适应
6.5 失败重试机制
七、接口设计
1. 分块上传接口
2. 分片验证接口
3. 归并状态查询接口
八、前端交互设计
1.界面布局
1.1? 拖拽上传地区
1.2?进度表现组件
1.?上传进度条
2. 归并进度条
1.3 文件列表展示
1.4?操纵按钮设计
1.5?响应式布局适配
1.6?交互效果
2. 交互逻辑
1. 文件选择与拖拽
2. 上传进度更新
3. 暂停/继续控制
4. 归并状态展示

一、引言

在当今数字化时代,文件传输是各类应用中不可或缺的一环,尤其是大文件上传,面临诸多挑战。本文将带大家深入相识我所完成的一个大文件上传项目,分享此中接纳的关键技能:分块上传与断点续传,以及基于 Spring Boot 一体化构建的项目架构,还有前端利用 spring-boot-starter-thymeleaf 的实践经验。
二、项目演示

2.1 功能演示视频



  • 底子上传功能演示
底子上传功能演示


  • 断点续传演示
断点续传演示


  • 大文件上传演示
大文件上传演示
  1. file-upload/
  2. ├── src/
  3. │   ├── main/
  4. │   │   ├── java/
  5. │   │   │   └── com/
  6. │   │   │       └── fileupload/
  7. │   │   │           ├── FileUploadApplication.java
  8. │   │   │           ├── controller/
  9. │   │   │           │   └── FileUploadController.java        # 处理分块上传、验证、合并请求
  10. │   │   │           ├── model/
  11. │   │   │           │   ├── ChunkInfo.java                   # 分片信息实体
  12. │   │   │           │   └── MergeStatus.java                 # 合并状态实体
  13. │   │   │           └── config/
  14. │   │   │               └── UploadConfig.java                # 上传相关配置
  15. │   │   └── resources/
  16. │   │       ├── application.yml                              # 应用配置文件
  17. │   │       ├── static/
  18. │   │       │   └── js/
  19. │   │       │       └── spark-md5.min.js                     # 分片计算文件的md5值
  20. │   │       └── templates/
  21. │   │           └── upload.html                              # 上传页面模板
  22. │   └── test/
  23. │       └── java/
  24. │           └── com/
  25. │               └── fileupload/
  26. │                   └── controller/
  27. │                       └── FileUploadControllerTest.java    # 控制器测试
  28. ├── pom.xml                                                 # Maven配置文件
  29. └── README.md                                               # 项目说明文档
复制代码
三、项目背景与目标

阐述项目启动的缘由,例如业务场景中对大文件(如高清视频、海量数据文件等)传输的需求,明确项目盼望告竣的目标,像进步上传效率、增强稳定性、优化用户体验,确保大文件能在复杂网络环境下可靠上传。
四、技能选型决议

这是一个基于 Spring Boot 实现的大文件分片上传项目,支持断点续传、文件秒传、实时进度表现等功能。项目接纳前后端分离架构,利用 Thymeleaf 作为模板引擎,实现了高效可靠的文件上传功能。
Maven堆栈利用的是3.8.8版本,JDK17版本。
为什么选择 Spring Boot

1. 核心优势

1.1 快速开发



  • Spring Boot 的自动配置功能极大简化了文件上传相干的配置
  • 自动配置 MultipartResolver 处置惩罚文件上传
  • 自动配置文件巨细限制
  • 自动配置临时文件存储位置
1.2 内嵌容器



  • 开箱即用:内置 Tomcat 服务器,无需额外部署
  • 配置灵活:可以轻松调整服务器参数
    server:
    tomcat:
    max-http-form-post-size: 10GB
    max-swallow-size: 10GB
1.3 统一的配置管理



  • 外部化配置:通过 application.yml 统一管理配置
    spring:
    servlet:
    multipart:
    max-file-size: 10GB
    max-request-size: 10GB
    file:
    upload:
    path: F:/uploadfiletest
2. 生态整合优势

2.1 文件处置惩罚组件



  • MultipartFile 接口简化文件操纵
  • 文件上传进度监控
  • 异步处置惩罚本领
2.2 非常处置惩罚

统一非常处置惩罚:@ControllerAdvice 处置惩罚上传非常


  • 文件巨细超限非常
  • IO非常
  • 并发处置惩罚非常
2.3 性能优化



  • 线程池管理:通过 ThreadPoolTaskExecutor 管理并发上传
  • 异步处置惩罚:@Async 注解实现异步文件处置惩罚
  • 缓存整合:可轻松集成 Spring Cache 缓存上传状态
3. 开发效率提升

3.1 注解驱动开发



  • @Value:注入配置值
  • @Slf4j:简化日记处置惩罚
  • @Data:自动生成 getter/setter
3.2 测试支持



  • Spring Test:提供完整的测试框架
  • MockMvc:方便测试文件上传接口
4. 部署和维护

4.1打包部署



  • 一键打包:mvn package
  • 独立运行:java -jar xxx.jar
  • 支持 Docker 容器化
4.2监控管理



  • 可集成 Spring Boot Actuator
  • 支持性能监控
  • 运行状态实时查看
5.安全性保障

5.1文件校验



  • MD5 完整性校验
  • 文件范例验证
  • 巨细限制控制
5.2并发控制



  • 线程安全的文件处置惩罚
  • 分块归并的原子性
  • 临时文件管理
前端为何选择 Thymeleaf

1. 服务端渲染优势

1.1 原生HTML模板

  1. <div class="progress-text" th:text="${uploadStatus}">
  2.     上传进度:0%
  3. </div>
复制代码
1.2 自然集成

Spring Boot 原生支持:无需额外配置
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4. </dependency>
复制代码
2. 上传功能实现优势

2.1 动态数据绑定



  • 进度展示:实时更新上传状态
2.2 安全性保障



  • CSRF 防护:自动集成 Spring Security
  • XSS 防护:默认转义特殊字符
  • 文件范例验证:服务端控制允许的文件范例
3. 开发效率提升

3.1 模板复用



  • 片断机制:复用公共组件
  • 布局继承:统一页面布局
4. 性能考虑

4.1 缓存机制



  • 模板缓存:生产环境自动启用
  • 资源缓存:静态资源优化
4.2 按需加载



  • 分片加载:大文件分块上传
  • 延迟加载:按需初始化组件
5. 实际应用优势

5.1 上传交互



  • 实时反馈:进度条动态更新
  • 状态展示:上传、暂停、完成状态
  • 错误处置惩罚:友爱的错误提示
5.2 用户体验



  • 响应式设计:适配不同设备
  • 优雅降级:兼容旧版浏览器
  • 交互友爱:拖拽上传支持
五、分块上传核心原理

1.分块策略

分块巨细设计

1.1 分块巨细的选择

  1. // 在前端定义分块大小常量
  2. const chunkSize = 2 * 1024 * 1024; // 2MB per chunk
复制代码
选择2MB作为分块巨细的考虑因素:


  • 网络因素

    • 单个哀求的数据量适中,不会因过大导致上传超时
    • 支持断点续传时,重传资本可控
    • 适应大多数网络环境,包括普通宽带和4G网络

  • 服务器因素

    • 内存占用公道,不会因分块过大导致服务器压力过大
    • 文件系统写入效率较好,避免过多的小文件IO
    • 临时存储空间管理方便

1.2 分块计算逻辑

  1. // 计算文件分块信息
  2. function calculateChunks(file) {
  3.     const chunks = Math.ceil(file.size / chunkSize);
  4.     const chunksArray = [];
  5.    
  6.     for (let i = 0; i < chunks; i++) {
  7.         const start = i * chunkSize;
  8.         const end = Math.min(start + chunkSize, file.size);
  9.         chunksArray.push({
  10.             start,
  11.             end,
  12.             index: i + 1,
  13.             size: end - start
  14.         });
  15.     }
  16.    
  17.     return {
  18.         totalChunks: chunks,
  19.         chunks: chunksArray
  20.     };
  21. }
复制代码
分块处置惩罚流程

1.1 文件分片

  1. async function sliceFile(file, chunkInfo) {
  2.     const { start, end } = chunkInfo;
  3.     const chunk = file.slice(start, end);
  4.    
  5.     // 构建分片上传的表单数据
  6.     const formData = new FormData();
  7.     formData.append('file', chunk);
  8.     formData.append('fileMd5', await calculateMD5(file));
  9.     formData.append('chunkNumber', chunkInfo.index);
  10.     formData.append('chunkSize', chunkInfo.size);
  11.     formData.append('currentChunkSize', chunk.size);
  12.     formData.append('totalSize', file.size);
  13.     formData.append('filename', file.name);
  14.     formData.append('totalChunks', Math.ceil(file.size / chunkSize));
  15.    
  16.     return formData;
  17. }
复制代码
1.2 分块上传队列

  1. async function uploadChunks(file) {
  2.     const chunkInfo = calculateChunks(file);
  3.     const uploadQueue = [];
  4.    
  5.     // 创建上传队列
  6.     for (const chunk of chunkInfo.chunks) {
  7.         uploadQueue.push(async () => {
  8.             const formData = await sliceFile(file, chunk);
  9.             await uploadChunk(formData);
  10.         });
  11.     }
  12.    
  13.     // 控制并发上传数量
  14.     const concurrency = 3;
  15.     while (uploadQueue.length > 0) {
  16.         const batch = uploadQueue.splice(0, concurrency);
  17.         await Promise.all(batch.map(task => task()));
  18.     }
  19. }
复制代码
分块巨细的动态调整策略

1.1 网络自适应

  1. function adjustChunkSize(uploadSpeed, currentChunkSize) {
  2.     const MIN_CHUNK_SIZE = 1 * 1024 * 1024;  // 1MB
  3.     const MAX_CHUNK_SIZE = 5 * 1024 * 1024;  // 5MB
  4.    
  5.     // 根据上传速度动态调整分块大小
  6.     if (uploadSpeed > 5 * 1024 * 1024) {  // 速度大于5MB/s
  7.         return Math.min(currentChunkSize * 1.5, MAX_CHUNK_SIZE);
  8.     } else if (uploadSpeed < 1 * 1024 * 1024) {  // 速度小于1MB/s
  9.         return Math.max(currentChunkSize * 0.5, MIN_CHUNK_SIZE);
  10.     }
  11.    
  12.     return currentChunkSize;
  13. }
复制代码
优化考虑

1.1 性能优化



  • 并发控制:根据网络状况和服务器负载调整并发数
  • 内存管理:通过流式处置惩罚避免一次性加载大文件
  • 哀求优化:公道设置哀求超时和重试机制
1.2 用户体验



  • 进度计算:精确计算每个分块的上传进度
  • 断点续传:记录已上传分块,支持断点续传
  • 失败重试:单个分块上传失败时支持重试
2. 实现效果



  • 支持超大文件上传(>10GB)
  • 上传过程稳定可靠
  • 断点续传功能完善
  • 服务器资源占用公道
  • 用户体验流畅

3. 上传流程

整体流程图


上述流程图中,先从客户端计算文件 MD5 开始,接着验证已上传分块,根据是否有未上传分块决定是否进行文件分片和上传操纵,当所有分块上传完成后哀求归并文件。服务端在每个阶段进行相应的处置惩罚,如查抄已存在分块、存储分片、归并分片和清理临时文件等操纵,末了向客户端返回完成响应。临时存储则配合服务端完身分块的存储、归并和删除操纵。
具体步骤阐明

2.1 前置准备



  • 计算文件特征

    // 客户端计算文件MD5
    const fileMd5 = await calculateMD5(file);
  • 验证已上传分块

    // 客户端哀求验证
    const uploadedChunks = await verifyChunks(fileMd5);
2.2 分块上传阶段



  • 分片处置惩罚

    // 客户端分片逻辑
    const chunk = file.slice(start, end);
    const formData = new FormData();
    formData.append(‘file’, chunk);
    formData.append(‘fileMd5’, fileMd5);
    formData.append(‘chunkNumber’, index);
  • 服务端吸收

    @PostMapping(“/upload/chunk”)
    public ResponseEntity uploadChunk(
    @RequestParam(“file”) MultipartFile file,
    ChunkInfo chunkInfo) {
    // 创建临时目次
    String chunkDirPath = uploadPath + File.separator + chunkInfo.getFileMd5();
    File chunkDir = new File(chunkDirPath);
    if (!chunkDir.exists()) {
    chunkDir.mkdirs();
    }
    1. // 存储分片
    2. File chunkFile = new File(chunkDirPath + File.separator + chunkInfo.getChunkNumber());
    3. file.transferTo(chunkFile);
    复制代码
    }
2.3 归并阶段



  • 触发归并

    // 查抄是否所有分片都已上传
    if (checkIfAllChunksUploaded(chunkInfo)) {
    mergeChunks(chunkInfo);
    }
  • 归并处置惩罚

    private void mergeChunks(ChunkInfo chunkInfo) throws IOException {
    String fileMd5 = chunkInfo.getFileMd5();
    String chunkDirPath = uploadPath + File.separator + fileMd5;
    Path targetPath = Paths.get(uploadPath, chunkInfo.getFilename());
    1. // 合并文件
    2. try (FileChannel outChannel = new FileOutputStream(targetPath.toFile()).getChannel()) {
    3.     for (int i = 1; i <= chunkInfo.getTotalChunks(); i++) {
    4.         File chunk = new File(chunkDirPath + File.separator + i);
    5.         try (FileChannel inChannel = new FileInputStream(chunk).getChannel()) {
    6.             inChannel.transferTo(0, inChannel.size(), outChannel);
    7.         }
    8.     }
    9. }
    复制代码
    }
关键技能点

3.1 并发控制

  1. // 控制并发上传数量
  2. const concurrency = 3;
  3. while (uploadQueue.length > 0) {
  4.     const batch = uploadQueue.splice(0, concurrency);
  5.     await Promise.all(batch.map(task => task()));
  6. }
复制代码
3.2 错误处置惩罚

  1. try {
  2.     await uploadChunk(formData);
  3. } catch (error) {
  4.     // 记录失败的分片,支持重试
  5.     failedChunks.push(chunkInfo);
  6.     console.error(`Chunk ${chunkInfo.index} upload failed:`, error);
  7. }
复制代码
3.3 进度监控

  1. // 更新上传进度
  2. function updateProgress(uploadedChunks, totalChunks) {
  3.     const progress = (uploadedChunks / totalChunks) * 100;
  4.     progressBar.style.width = `${progress}%`;
  5.     progressText.textContent = `上传进度: ${Math.round(progress)}%`;
  6. }
复制代码
六、断点续传的实现要点

1. 断点记录机制

1.1 客户端存储

  1. // 使用 sessionStorage 存储文件MD5和已上传分块信息
  2. function saveUploadProgress(fileMd5, chunks) {
  3.     // 存储文件MD5,用于恢复上传时识别文件
  4.     sessionStorage.setItem('currentFileMd5', fileMd5);
  5.    
  6.     // 存储已上传的分块信息
  7.     const uploadedChunks = {
  8.         timestamp: Date.now(),
  9.         chunks: chunks
  10.     };
  11.     sessionStorage.setItem(`uploadProgress_${fileMd5}`, JSON.stringify(uploadedChunks));
  12. }
  13. // 获取上传进度
  14. function getUploadProgress(fileMd5) {
  15.     const progressData = sessionStorage.getItem(`uploadProgress_${fileMd5}`);
  16.     if (progressData) {
  17.         return JSON.parse(progressData);
  18.     }
  19.     return null;
  20. }
复制代码
1.2 服务端记录

  1. @Service
  2. public class UploadProgressService {
  3.     // 使用内存缓存记录上传进度
  4.     private final ConcurrentHashMap<String, Set<Integer>> uploadProgressMap = new ConcurrentHashMap<>();
  5.    
  6.     // 记录分片上传状态
  7.     public void recordChunkUpload(String fileMd5, int chunkNumber) {
  8.         uploadProgressMap.computeIfAbsent(fileMd5, k -> ConcurrentHashMap.newKeySet())
  9.                         .add(chunkNumber);
  10.     }
  11.    
  12.     // 获取已上传的分片
  13.     public Set<Integer> getUploadedChunks(String fileMd5) {
  14.         return uploadProgressMap.getOrDefault(fileMd5, new HashSet<>());
  15.     }
  16. }
复制代码
2. 续传实现

2.1 上传前验证

  1. async function resumeUpload(file) {
  2.     const fileMd5 = await calculateMD5(file);
  3.    
  4.     // 验证服务器端已上传分片
  5.     const uploadedChunks = await verifyChunks(fileMd5);
  6.    
  7.     // 计算剩余需要上传的分片
  8.     const remainingChunks = calculateRemainingChunks(file, uploadedChunks);
  9.    
  10.     // 继续上传剩余分片
  11.     await uploadRemainingChunks(file, remainingChunks);
  12. }
  13. async function verifyChunks(fileMd5) {
  14.     const response = await fetch(`/upload/chunk/verify?fileMd5=${fileMd5}`);
  15.     return await response.json();
  16. }
复制代码
2.2 分片验证接口

  1. @GetMapping("/upload/chunk/verify")
  2. public ResponseEntity<Set<Integer>> verifyChunk(@RequestParam String fileMd5) {
  3.     // 获取已上传的分片列表
  4.     String chunkDirPath = uploadPath + File.separator + fileMd5;
  5.     Set<Integer> uploadedChunks = new HashSet<>();
  6.    
  7.     File chunkDir = new File(chunkDirPath);
  8.     if (chunkDir.exists()) {
  9.         File[] files = chunkDir.listFiles();
  10.         if (files != null) {
  11.             for (File file : files) {
  12.                 uploadedChunks.add(Integer.parseInt(file.getName()));
  13.             }
  14.         }
  15.     }
  16.    
  17.     return ResponseEntity.ok(uploadedChunks);
  18. }
复制代码
3. 智能续传策略

3.1 分片有效期查抄

  1. function isChunkValid(uploadProgress) {
  2.     const MAX_CHUNK_AGE = 24 * 60 * 60 * 1000; // 24小时
  3.     const now = Date.now();
  4.    
  5.     // 检查分片是否过期
  6.     return (now - uploadProgress.timestamp) < MAX_CHUNK_AGE;
  7. }
复制代码
3.2 网络状态检测

  1. async function checkNetworkAndResume() {
  2.     // 监听网络状态
  3.     window.addEventListener('online', async () => {
  4.         if (currentUpload) {
  5.             // 网络恢复时继续上传
  6.             await resumeUpload(currentUpload.file);
  7.         }
  8.     });
  9. }
复制代码
4. 非常处置惩罚

4.1 上传非通例复

  1. async function handleUploadError(error, file, chunkInfo) {
  2.     // 记录失败信息
  3.     const failedChunk = {
  4.         chunkNumber: chunkInfo.index,
  5.         retryCount: 0,
  6.         lastError: error.message
  7.     };
  8.    
  9.     // 添加到重试队列
  10.     await addToRetryQueue(failedChunk);
  11.    
  12.     // 定时重试
  13.     await retryFailedChunks(file);
  14. }
复制代码
4.2 文件完整性校验

  1. private boolean validateMergedFile(String filePath, String expectedMd5) {
  2.     try {
  3.         String actualMd5 = calculateFileMd5(new File(filePath));
  4.         return expectedMd5.equals(actualMd5);
  5.     } catch (IOException e) {
  6.         log.error("Failed to validate merged file", e);
  7.         return false;
  8.     }
  9. }
复制代码
6. 续传逻辑实现

6.1 断点规复流程

  1. async function resumeUploadFromBreakpoint(file) {
  2.     try {
  3.         // 1. 获取文件标识
  4.         const fileMd5 = await calculateMD5(file);
  5.         
  6.         // 2. 获取本地进度记录
  7.         const localProgress = getUploadProgress(fileMd5);
  8.         
  9.         // 3. 验证服务器端进度
  10.         const serverChunks = await verifyChunks(fileMd5);
  11.         
  12.         // 4. 合并本地和服务器进度
  13.         const validChunks = await validateAndMergeProgress(localProgress, serverChunks);
  14.         
  15.         // 5. 计算待上传分块
  16.         const remainingChunks = calculateRemainingChunks(file, validChunks);
  17.         
  18.         // 6. 继续上传
  19.         return uploadRemainingChunks(file, remainingChunks);
  20.     } catch (error) {
  21.         console.error('Resume upload failed:', error);
  22.         throw error;
  23.     }
  24. }
复制代码
6.2 进度归并与验证

  1. async function validateAndMergeProgress(localProgress, serverChunks) {
  2.     const validChunks = new Set();
  3.    
  4.     // 验证本地记录的有效性
  5.     if (localProgress && isChunkValid(localProgress)) {
  6.         localProgress.chunks.forEach(chunk => {
  7.             // 只记录同时存在于本地和服务器的分块
  8.             if (serverChunks.includes(chunk)) {
  9.                 validChunks.add(chunk);
  10.             }
  11.         });
  12.     }
  13.    
  14.     // 添加仅在服务器存在的分块
  15.     serverChunks.forEach(chunk => validChunks.add(chunk));
  16.    
  17.     return Array.from(validChunks);
  18. }
复制代码
6.3 智能上传策略

  1. async function uploadRemainingChunks(file, remainingChunks) {
  2.     // 1. 计算上传队列
  3.     const uploadQueue = remainingChunks.map(chunkNumber => {
  4.         return async () => {
  5.             const chunkInfo = calculateChunkInfo(file, chunkNumber);
  6.             const formData = await prepareChunkFormData(file, chunkInfo);
  7.             return uploadChunk(formData);
  8.         };
  9.     });
  10.     // 2. 动态调整并发数
  11.     const concurrency = calculateOptimalConcurrency();
  12.    
  13.     // 3. 分批上传
  14.     while (uploadQueue.length > 0) {
  15.         const batch = uploadQueue.splice(0, concurrency);
  16.         await Promise.all(batch.map(task => task()));
  17.         
  18.         // 4. 保存上传进度
  19.         saveUploadProgress(fileMd5, uploadedChunks);
  20.     }
  21. }
复制代码
6.4 网络自适应

  1. function calculateOptimalConcurrency() {
  2.     // 基于网络状况动态调整并发数
  3.     const connection = navigator.connection;
  4.     if (connection) {
  5.         if (connection.effectiveType === '4g') {
  6.             return 3;  // 4G网络使用3个并发
  7.         } else if (connection.effectiveType === '3g') {
  8.             return 2;  // 3G网络使用2个并发
  9.         }
  10.     }
  11.     return 1;  // 默认使用1个并发
  12. }
复制代码
6.5 失败重试机制

  1. async function retryFailedChunks(failedChunks, maxRetries = 3) {
  2.     for (const chunk of failedChunks) {
  3.         let retryCount = 0;
  4.         while (retryCount < maxRetries) {
  5.             try {
  6.                 await uploadChunk(chunk);
  7.                 break;  // 上传成功,跳出重试循环
  8.             } catch (error) {
  9.                 retryCount++;
  10.                 if (retryCount === maxRetries) {
  11.                     throw new Error(`Upload failed after ${maxRetries} retries`);
  12.                 }
  13.                 // 等待时间递增
  14.                 await new Promise(resolve =>
  15.                     setTimeout(resolve, 1000 * Math.pow(2, retryCount))
  16.                 );
  17.             }
  18.         }
  19.     }
  20. }
复制代码
七、接口设计

1. 分块上传接口

  1. @PostMapping("/upload/chunk")
  2. public ResponseEntity<String> uploadChunk(
  3.     @RequestParam("file") MultipartFile file,
  4.     ChunkInfo chunkInfo) {
  5.     try {
  6.         // 创建文件块存储目录
  7.         String chunkDirPath = uploadPath + File.separator + chunkInfo.getFileMd5();
  8.         File chunkDir = new File(chunkDirPath);
  9.         if (!chunkDir.exists()) {
  10.             chunkDir.mkdirs();
  11.         }
  12.         // 写入文件块
  13.         File chunkFile = new File(chunkDirPath + File.separator + chunkInfo.getChunkNumber());
  14.         file.transferTo(chunkFile);
  15.         // 检查是否所有块都已上传,并且当前文件不在合并过程中
  16.         if (checkIfAllChunksUploaded(chunkInfo) && !mergingFiles.contains(chunkInfo.getFileMd5())) {
  17.             try {
  18.                 // 标记文件正在合并
  19.                 mergingFiles.add(chunkInfo.getFileMd5());
  20.                 mergeChunks(chunkInfo);
  21.             } finally {
  22.                 // 合并完成后移除标记
  23.                 mergingFiles.remove(chunkInfo.getFileMd5());
  24.             }
  25.         }
  26.         return ResponseEntity.ok("Chunk uploaded successfully");
  27.     } catch (IOException e) {
  28.         log.error("Upload chunk failed", e);
  29.         return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
  30.                            .body("Upload failed: " + e.getMessage());
  31.     }
  32. }
复制代码
接口阐明:


  • 哀求方式:POST
  • 路径:/upload/chunk
  • 参数

    • file:当前上传的文件分片

  • chunkInfo:分片信息对象,包罗:
    @Data
    public class ChunkInfo {
    private String fileMd5; // 文件MD5值
    private Integer chunkNumber; // 分片序号
    private Long chunkSize; // 分片巨细
    private Long currentChunkSize; // 当前分片实际巨细
    private Long totalSize; // 文件总巨细
    private String filename; // 文件名
    private Integer totalChunks; // 总分片数
    }
2. 分片验证接口

  1. @GetMapping("/upload/chunk/verify")
  2. public ResponseEntity<Set<Integer>> verifyChunk(@RequestParam String fileMd5) {
  3.     // 获取已上传的块号
  4.     Set<Integer> uploadedChunks = getUploadedChunks(fileMd5);
  5.     return ResponseEntity.ok(uploadedChunks);
  6. }
  7. private Set<Integer> getUploadedChunks(String fileMd5) {
  8.     Set<Integer> uploadedChunks = new HashSet<>();
  9.     File chunkDir = new File(uploadPath + File.separator + fileMd5);
  10.     if (chunkDir.exists()) {
  11.         File[] files = chunkDir.listFiles();
  12.         if (files != null) {
  13.             for (File file : files) {
  14.                 uploadedChunks.add(Integer.parseInt(file.getName()));
  15.             }
  16.         }
  17.     }
  18.     return uploadedChunks;
  19. }
复制代码
接口阐明:


  • 哀求方式:GET
  • 路径:/upload/chunk/verify
  • 参数:fileMd5(文件的MD5值)
  • 返回值:已上传的分片序号集合
3. 归并状态查询接口

  1. @GetMapping("/upload/merge/status")
  2. public ResponseEntity<MergeStatus> getMergeStatus(@RequestParam String fileMd5) {
  3.     MergeStatus status = mergeStatusMap.getOrDefault(fileMd5,
  4.         new MergeStatus("waiting", 0));
  5.     return ResponseEntity.ok(status);
  6. }
复制代码
接口阐明:


  • 哀求方式:GET
  • 路径:/upload/merge/status
  • 参数:fileMd5(文件的MD5值)
  • 返回值:归并状态对象
    @Data
    @AllArgsConstructor
    public class MergeStatus {
    private String status; // waiting, merging, completed, error
    private int progress; // 0-100
    private String message; // 错误信息(可选)
    }
八、前端交互设计

1.界面布局

1.1 拖拽上传地区

  1. <!-- 上传区域容器 -->
  2. <div class="upload-container">
  3.     <!-- 拖拽区域 -->
  4.     <div id="dropZone" class="upload-area">
  5.         <div class="upload-text">
  6.             拖拽文件到此处或
  7.             <button class="btn btn-primary" onclick="selectFile()">选择文件</button>
  8.         </div>
  9.         <!-- 隐藏的文件输入框 -->
  10.         <input type="file" id="fileInput" class="upload-input">
  11.     </div>
  12. </div>
  13. <!-- 相关样式 -->
  14. <style>
  15. .upload-area {
  16.     border: 2px dashed #ccc;
  17.     padding: 20px;
  18.     text-align: center;
  19.     margin-bottom: 20px;
  20.     border-radius: 4px;
  21.     cursor: pointer;
  22.     transition: all 0.3s ease;
  23. }
  24. .upload-area:hover {
  25.     border-color: #2196F3;
  26.     background-color: #f8f9fa;
  27. }
  28. .upload-area.dragover {
  29.     border-color: #2196F3;
  30.     background-color: #e3f2fd;
  31. }
  32. </style>
复制代码
1.2进度表现组件

1.上传进度条

  1. <!-- 进度条容器 -->
  2. <div class="progress-container">
  3.     <!-- 上传进度条 -->
  4.     <div class="progress-bar">
  5.         <div id="progressFill" class="progress-fill"></div>
  6.     </div>
  7.     <!-- 进度文本 -->
  8.     <div id="progressText" class="progress-text">
  9.         准备上传...
  10.     </div>
  11.     <!-- 已用时间 -->
  12.     <div id="elapsedTime" class="elapsed-time"></div>
  13. </div>
  14. <style>
  15. .progress-bar {
  16.     width: 100%;
  17.     height: 20px;
  18.     background-color: #f0f0f0;
  19.     border-radius: 10px;
  20.     overflow: hidden;
  21.     margin-bottom: 10px;
  22. }
  23. .progress-fill {
  24.     height: 100%;
  25.     background-color: #2196F3;
  26.     width: 0;
  27.     transition: width 0.3s ease;
  28. }
  29. </style>
复制代码
2. 归并进度条

  1. <!-- 合并进度显示 -->
  2. <div class="merge-progress" style="display: none;">
  3.     <div class="progress-bar">
  4.         <div id="mergeProgressFill" class="progress-fill green"></div>
  5.     </div>
  6.     <div id="mergeProgressText" class="progress-text">
  7.         准备合并文件...
  8.     </div>
  9. </div>
  10. <style>
  11. .progress-fill.green {
  12.     background-color: #4CAF50;
  13. }
  14. </style>
复制代码
1.3 文件列表展示

  1. <!-- 文件列表区域 -->
  2. <div id="fileList" class="file-list" style="display: none;">
  3.     <!-- 文件统计信息 -->
  4.     <div id="uploadStats" class="upload-stats">
  5.         <span>总文件数:<span id="totalFiles">0</span></span>
  6.         <span>总大小:<span id="totalSize">0 B</span></span>
  7.         <span>总分片数:<span id="totalChunks">0</span></span>
  8.     </div>
  9.     <!-- 文件列表项 -->
  10.     <div class="file-items">
  11.         <!-- 动态生成的文件项 -->
  12.     </div>
  13. </div>
  14. <style>
  15. .file-list {
  16.     margin: 20px 0;
  17.     max-height: 200px;
  18.     overflow-y: auto;
  19.     border: 1px solid #eee;
  20.     border-radius: 4px;
  21.     padding: 10px;
  22. }
  23. .file-item {
  24.     display: flex;
  25.     justify-content: space-between;
  26.     align-items: center;
  27.     padding: 5px;
  28.     border-bottom: 1px solid #eee;
  29. }
  30. </style>
复制代码
1.4操纵按钮设计

  1. <!-- 操作按钮组 -->
  2. <div class="button-group">
  3.     <!-- 上传按钮 -->
  4.     <button id="uploadBtn"
  5.             class="btn btn-primary"
  6.             onclick="startUpload()"
  7.             disabled>
  8.         开始上传
  9.     </button>
  10.    
  11.     <!-- 暂停/继续按钮 -->
  12.     <button id="pauseBtn"
  13.             class="btn btn-warning"
  14.             onclick="togglePause()"
  15.             disabled>
  16.         暂停
  17.     </button>
  18. </div>
  19. <style>
  20. .button-group {
  21.     display: flex;
  22.     gap: 10px;
  23.     justify-content: center;
  24.     margin-top: 20px;
  25. }
  26. .btn {
  27.     padding: 8px 16px;
  28.     border: none;
  29.     border-radius: 4px;
  30.     cursor: pointer;
  31.     font-size: 14px;
  32.     transition: all 0.3s ease;
  33. }
  34. .btn:disabled {
  35.     opacity: 0.5;
  36.     cursor: not-allowed;
  37. }
  38. </style>
复制代码
1.5响应式布局适配

  1. /* 响应式设计 */
  2. @media (max-width: 768px) {
  3.     .upload-container {
  4.         padding: 10px;
  5.     }
  6.    
  7.     .button-group {
  8.         flex-direction: column;
  9.     }
  10.    
  11.     .progress-text {
  12.         font-size: 12px;
  13.     }
  14. }
复制代码
1.6交互效果

  1. // 拖拽效果
  2. dropZone.addEventListener('dragover', (e) => {
  3.     e.preventDefault();
  4.     dropZone.classList.add('dragover');
  5. });
  6. dropZone.addEventListener('dragleave', (e) => {
  7.     e.preventDefault();
  8.     dropZone.classList.remove('dragover');
  9. });
  10. // 按钮状态管理
  11. function updateButtonStates(isUploading) {
  12.     uploadBtn.disabled = isUploading;
  13.     pauseBtn.disabled = !isUploading;
  14.     pauseBtn.textContent = isPaused ? '继续' : '暂停';
  15. }
复制代码
2. 交互逻辑

1. 文件选择与拖拽

  1. // 文件选择处理
  2. function selectFile() {
  3.     const input = document.getElementById('fileInput');
  4.     input.click();
  5.     input.addEventListener('change', handleFileSelect, { once: true });
  6. }
  7. // 文件拖拽处理
  8. async function handleDrop(e) {
  9.     e.preventDefault();
  10.     e.stopPropagation();
  11.     dropZone.classList.remove('dragover');
  12.    
  13.     // 处理拖拽的文件
  14.     const files = Array.from(e.dataTransfer.files);
  15.     if (files.length > 0) {
  16.         selectedFiles = files;
  17.         // 更新文件列表显示
  18.         await updateFileList(selectedFiles);
  19.         // 更新上传按钮状态
  20.         updateUploadButton();
  21.         // 计算文件MD5
  22.         showLoading('正在计算文件信息...');
  23.         try {
  24.             for (const file of files) {
  25.                 await calculateMD5(file);
  26.             }
  27.         } finally {
  28.             hideLoading();
  29.         }
  30.     }
  31. }
复制代码
2. 上传进度更新

  1. // 更新上传进度
  2. function updateProgress(percent, text) {
  3.     // 更新进度条
  4.     requestAnimationFrame(() => {
  5.         const progressFill = document.getElementById('progressFill');
  6.         const progressText = document.getElementById('progressText');
  7.         
  8.         progressFill.style.width = `${percent}%`;
  9.         progressText.textContent = text;
  10.         
  11.         // 更新已用时间
  12.         if (startTime > 0) {
  13.             const elapsed = Math.floor((Date.now() - startTime) / 1000);
  14.             document.getElementById('elapsedTime').textContent =
  15.                 `已用时间: ${formatTime(elapsed)}`;
  16.         }
  17.     });
  18. }
  19. // 更新合并进度
  20. function updateMergeProgress(percent, text) {
  21.     requestAnimationFrame(() => {
  22.         const mergeProgress = document.querySelector('.merge-progress');
  23.         const mergeProgressFill = document.getElementById('mergeProgressFill');
  24.         const mergeProgressText = document.getElementById('mergeProgressText');
  25.         
  26.         mergeProgress.style.display = 'block';
  27.         mergeProgressFill.style.width = `${percent}%`;
  28.         mergeProgressText.textContent = text;
  29.     });
  30. }
复制代码
3. 暂停/继续控制

  1. let isPaused = false;
  2. let currentUpload = null;
  3. // 暂停/继续切换
  4. async function togglePause() {
  5.     const pauseBtn = document.getElementById('pauseBtn');
  6.    
  7.     if (isPaused) {
  8.         // 继续上传
  9.         isPaused = false;
  10.         pauseBtn.textContent = '暂停';
  11.         if (currentUpload && currentUpload.resume) {
  12.             currentUpload.resume();
  13.         }
  14.     } else {
  15.         // 暂停上传
  16.         isPaused = true;
  17.         pauseBtn.textContent = '继续';
  18.     }
  19. }
  20. // 上传过程中的暂停检查
  21. async function uploadWithPause(file) {
  22.     while (isPaused) {
  23.         await new Promise(resolve => {
  24.             currentUpload = { resume: resolve };
  25.         });
  26.     }
  27.     // 继续上传逻辑
  28. }
复制代码
4. 归并状态展示

  1. // 轮询合并状态
  2. async function pollMergeStatus(fileMd5) {
  3.     let lastProgress = -1;
  4.    
  5.     while (true) {
  6.         try {
  7.             const response = await fetch(`/upload/merge/status?fileMd5=${fileMd5}`);
  8.             const status = await response.json();
  9.             
  10.             // 避免重复更新相同进度
  11.             if (status.progress !== lastProgress) {
  12.                 updateMergeProgress(status.progress,
  13.                     `正在合并文件... ${status.progress}%`);
  14.                 lastProgress = status.progress;
  15.             }
  16.             
  17.             // 检查合并状态
  18.             if (status.status === 'completed') {
  19.                 updateMergeProgress(100, '文件合并完成');
  20.                 break;
  21.             } else if (status.status === 'error') {
  22.                 throw new Error(status.message || '合并失败');
  23.             }
  24.             
  25.             // 等待一段时间后继续轮询
  26.             await new Promise(resolve => setTimeout(resolve, 500));
  27.         } catch (error) {
  28.             console.error('获取合并状态失败:', error);
  29.             updateMergeProgress(0, `合并失败: ${error.message}`);
  30.             break;
  31.         }
  32.     }
  33. }
  34. // 显示上传完成状态
  35. function showUploadComplete(filename) {
  36.     disableDropZone(`${filename} 上传完成`);
  37.     updateProgress(100, '上传完成');
  38.    
  39.     // 3秒后重置上传区域
  40.     setTimeout(() => {
  41.         enableDropZone();
  42.         resetProgress();
  43.     }, 3000);
  44. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

锦通

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表