java超大文件上传

前端代码

java 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>分片上传前端</title>
    <style>
        .container { max-width: 800px; margin: 50px auto; padding: 20px; }
        .drop-zone { border: 2px dashed #ddd; border-radius: 8px; padding: 50px; text-align: center; cursor: pointer; }
        .progress-bar { height: 30px; background: #f0f0f0; border-radius: 15px; margin: 10px 0; }
        .progress-fill { height: 100%; background: #4CAF50; border-radius: 10px; transition: width 0.3s ease; }
        .status { margin-top: 10px; color: #666; }
        .button-group { margin: 20px 0; }
        button { padding: 8px 16px; margin-right: 10px; cursor: pointer; }
    </style>
</head>
<body>
    <div class="container">
        <h2>大文件分片上传</h2>
        
        <!-- 拖放区域 -->
        <div id="dropZone" class="drop-zone">
            拖放文件到此处或
            <input type="file" id="fileInput" style="display: none">
            <button onclick="document.getElementById('fileInput').click()">选择文件</button>
        </div>

        <!-- 上传状态 -->
        <div id="uploadStatus" class="status"></div>
        
        <!-- 进度条 -->
        <div class="progress-bar">
            <div id="progressFill" class="progress-fill" style="width: 0%"></div>
        </div>
        
        <!-- 操作按钮 -->
        <div class="button-group">
            <button id="pauseBtn" onclick="pauseUpload()">暂停</button>
            <button id="resumeBtn" onclick="resumeUpload()" style="display: none">继续</button>
            <button id="cancelBtn" onclick="cancelUpload()">取消</button>
        </div>
    </div>

    <script>
        // 配置项
        const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB 分片大小
        const UPLOAD_URL = 'http://localhost:89/api/common/config/uploadShardingFile'; // 后端上传接口
        let uploadTask = null;
        let isPaused = false;
        let isCanceled = false;
        let uploadedChunks = []; // 已上传的分片索引(从1开始)

        // 初始化拖放事件
        document.getElementById('dropZone').addEventListener('dragover', (e) => {
            e.preventDefault();
            e.target.style.backgroundColor = '#f0f0f0';
        });

        document.getElementById('dropZone').addEventListener('dragleave', (e) => {
            e.preventDefault();
            e.target.style.backgroundColor = '';
        });

        document.getElementById('dropZone').addEventListener('drop', (e) => {
            e.preventDefault();
            e.target.style.backgroundColor = '';
            const file = e.dataTransfer.files[0];
            if (file) startUpload(file);
        });

        document.getElementById('fileInput').addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (file) startUpload(file);
        });

        // 开始上传
        function startUpload(file) {
            resetState();
            uploadTask = {
                file,
                taskId: generateTaskId(), // 生成唯一任务ID
                totalChunks: Math.ceil(file.size / CHUNK_SIZE),
                currentChunk: 1 // 分片索引从1开始
            };

            // 检查本地是否有已上传记录(断点续传)
            const savedChunks = JSON.parse(localStorage.getItem(`upload_${uploadTask.taskId}`)) || [];
            uploadedChunks = savedChunks.length ? savedChunks : [];

            updateStatus();
            uploadNextChunk();
        }

        // 上传下一个分片
        async function uploadNextChunk() {
            if (isPaused || isCanceled || uploadTask.currentChunk > uploadTask.totalChunks) return;

            // 跳过已上传的分片
            if (uploadedChunks.includes(uploadTask.currentChunk)) {
                uploadTask.currentChunk++;
                return uploadNextChunk();
            }

            try {
                const chunk = createChunk(uploadTask.file, uploadTask.currentChunk);
                const formData = new FormData();
                
                formData.append('file', chunk.file);
                formData.append('taskId', uploadTask.taskId);
                formData.append('sliceNo', uploadTask.currentChunk);
                formData.append('fileSlicesNum', uploadTask.totalChunks);
                formData.append('fileName', uploadTask.file.name);

                const response = await fetch(UPLOAD_URL, {
                    method: 'POST',
                    body: formData,
                    signal: new AbortController().signal // 支持中断请求
                });

                if (!response.ok) throw new Error('上传失败');
                
                // 记录已上传分片
                uploadedChunks.push(uploadTask.currentChunk);
                localStorage.setItem(`upload_${uploadTask.taskId}`, JSON.stringify(uploadedChunks));
                
                uploadTask.currentChunk++;
                updateProgress();
                uploadNextChunk();
            } catch (error) {
                if (error.name !== 'AbortError') {
                    showStatus('上传失败,请重试', 'red');
                    isCanceled = true;
                }
            }
        }

        // 创建文件分片
        function createChunk(file, chunkNumber) {
            const start = (chunkNumber - 1) * CHUNK_SIZE;
            const end = Math.min(start + CHUNK_SIZE, file.size);
            return {
                index: chunkNumber,
                file: file.slice(start, end),
                size: end - start
            };
        }

        // 暂停上传
        function pauseUpload() {
            isPaused = true;
            document.getElementById('pauseBtn').style.display = 'none';
            document.getElementById('resumeBtn').style.display = 'inline';
            showStatus('上传已暂停');
        }

        // 恢复上传
        function resumeUpload() {
            isPaused = false;
            document.getElementById('pauseBtn').style.display = 'inline';
            document.getElementById('resumeBtn').style.display = 'none';
            uploadNextChunk();
            showStatus('继续上传...');
        }

        // 取消上传
        function cancelUpload() {
            isCanceled = true;
            localStorage.removeItem(`upload_${uploadTask?.taskId}`);
            resetState();
            showStatus('上传已取消');
        }

        // 更新状态显示
        function updateStatus() {
            showStatus(`准备上传:${uploadTask.file.name} (${formatSize(uploadTask.file.size)})`, 'green');
            document.getElementById('progressFill').style.width = '0%';
        }

        // 更新进度条
        function updateProgress() {
            const progress = (uploadedChunks.length / uploadTask.totalChunks) * 100;
            document.getElementById('progressFill').style.width = `${progress}%`;
            showStatus(`已上传 ${uploadedChunks.length}/${uploadTask.totalChunks} 分片`, 'blue');
            
            if (progress === 100) {
                // 上传完成,清理本地记录
                localStorage.removeItem(`upload_${uploadTask.taskId}`);
                showStatus('上传完成!', 'green');
            }
        }

        // 重置状态
        function resetState() {
            uploadTask = null;
            isPaused = false;
            isCanceled = false;
            uploadedChunks = [];
            document.getElementById('progressFill').style.width = '0%';
            document.getElementById('resumeBtn').style.display = 'none';
        }

        // 显示状态信息
        function showStatus(text, color = '#333') {
            document.getElementById('uploadStatus').innerHTML = text;
            document.getElementById('uploadStatus').style.color = color;
        }

        // 生成唯一任务ID
        function generateTaskId() {
            return Date.now() + Math.random().toString(36).substr(2, 5);
        }

        // 格式化文件大小
        function formatSize(bytes) {
            if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(2)} GB`;
            if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(2)} MB`;
            return `${(bytes / 1e3).toFixed(2)} KB`;
        }
    </script>
</body>
</html>

后端代码

java 复制代码
package com.talents.application.controller;

import cloud.tianai.captcha.common.response.ApiResponse;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.talents.application.config.CommentConfig;
import com.talents.application.entity.dto.Account;
import com.talents.application.entity.vo.request.CaptchaVo;
import com.talents.application.utils.AESUtils;
import com.talents.application.utils.RedisUtils;
import com.talents.application.utils.UserUtils;
import com.talents.application.utils.file.CaculateMd5Utils;
import com.talents.application.utils.file.TchunkInfo;
import com.talents.application.entity.dto.SystemFile;
import com.talents.application.utils.file.FileInfoUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.*;
import com.talents.application.entity.RestBean;
import com.talents.application.service.SystemFileService;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * <p>
 * 项目申报表 前端控制器
 * </p>
 *
 * @author zhangpu
 * @since 2024-05-10
 */
@Slf4j
@RestController
@RequestMapping("/api/applicant")
public class ApplicantController extends BaseController {
    @Autowired
    private SystemFileService systemFileService;
    @Value("${spring.profiles.active}")
    private String pofile;
    @Value("${spring.servlet.multipart.location}")
    private String path;

    @Value("${pi.domain}")
    private String domain;
    @Value("${pi.domainProfile}")
    private String domainProfile;
    @Autowired
    private RedisUtils redisUtils;

  

    @PostMapping("/getUploadFileLastNum")
    public RestBean getUploadFileLastNum(TchunkInfo chunk){
        Account account = this.currentUser();
        String taskIdAndUserName = chunk.getTaskId()+":" + account.getJobNumber() + chunk.getFileName();
        Long uploadFileTotalSize = redisUtils.getListSize(taskIdAndUserName);
        return  RestBean.success(uploadFileTotalSize);
    }
    
    @PostMapping("/uploadShardingFileSyncPlus")
    public RestBean uploadShardingFileSyncPlus(TchunkInfo chunk, MultipartFile file) throws Exception
    {
        Account account = this.currentUser();
        //分片上传到本地
        chunk.setSliceNo(chunk.getSliceNo());
        log.info("file originName: {}, chunkNumber: {}", file.getOriginalFilename(), chunk.getSliceNo());
        String filePath = CommentConfig.getProfile()+account.getId()+"/";
        try
        {
            if (chunk.getFileSlicesNum() == 1)
            {
                String md5Hex = DigestUtils.md5Hex(file.getInputStream());
                String taskId = chunk.getTaskId();
                Path path = Paths.get(FileInfoUtils.generatePathNotNum(filePath, chunk));
                byte[] bytes = file.getBytes();
                //文件写入指定路径
                Files.write(path, bytes);
                File file1 = new File(FileInfoUtils.generatePathNotNum(filePath, chunk));
                long length = file1.length();
                SystemFile systemFile = new SystemFile(null, account.getJobNumber(), chunk.getFileName(), this.domainProfile + account.getId() + "/" + taskId + "/" + chunk.getFileName(), new Date(), new Date(), length, md5Hex, taskId);
                systemFileService.save(systemFile);
                //直接保存
                return RestBean.success(systemFile);
            }
            String taskIdTm = chunk.getTaskId();
            String taskIdAndUserName = chunk.getTaskId() +":"+ account.getJobNumber() + chunk.getFileName();
            //获取当前上传分片是否存在
            Long exists = redisUtils.exists(taskIdAndUserName, chunk.getMd5());
            if (exists != null && exists != -1)
            {
                if (chunk.getSliceNo().equals(chunk.getFileSlicesNum()))
                {
                    String localFile = filePath + taskIdTm + "/" + chunk.getFileName();
                    String md5Hex = CaculateMd5Utils.calculateMD5(localFile);
                    return RestBean.success(systemFileService.getFileIdByMd5(md5Hex,account.getJobNumber()));
                }
                return RestBean.success();
            }
            byte[] bytes = file.getBytes();
            Path path = Paths.get(FileInfoUtils.generatePath(filePath, chunk));
            //文件写入指定路径
            Files.write(path, bytes);
            //将上传完的文件分片放入队列中
            redisUtils.leftPushList(taskIdAndUserName,chunk.getMd5());
            //判断如果当前块数等于总块数 合并
            if (chunk.getSliceNo().equals(chunk.getFileSlicesNum()))
            {
                String taskId = chunk.getTaskId();
                //保存到数据库
                SystemFile systemFile = new SystemFile(null,account.getJobNumber(), chunk.getFileName(), this.domainProfile + account.getId()+"/"+taskId + "/" + chunk.getFileName(), new Date(), new Date(), chunk.getFileTotalSize(), null,taskId);
                systemFileService.save(systemFile);
                String id = systemFile.getId();
                long l = System.currentTimeMillis();
               //文件地址
               String localFile = filePath + taskId + "/" + chunk.getFileName();
               String folder = filePath + taskId;
               //合并文件
               FileInfoUtils.merge(localFile, folder, chunk.getFileName());
               File fileCache = new File(localFile);
               long length = fileCache.length();
               String md5Hex = CaculateMd5Utils.calculateMD5(localFile);
               SystemFile systemFile = new SystemFile(id, account.getJobNumber(), chunk.getFileName(), null, new Date(), new Date(), length, md5Hex,null);
               systemFileService.updateById(systemFile);
               //redisUtils.del(taskId);
               log.info("文件和并与计算MD5时常------------{}毫秒",System.currentTimeMillis()-l);
                return RestBean.success(systemFile);
            }
        } catch (IOException e)
        {
            e.printStackTrace();
            return RestBean.failure(400, "上传失败!");
        }
        return RestBean.success();
    }
}

工具类

java 复制代码
package com.talents.application.utils.file;

import com.talents.application.entity.RestBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.*;

public class FileInfoUtils {

    private final static Logger logger = LoggerFactory.getLogger(FileInfoUtils.class);

    /**
     * 功能描述: 生成路径
     *
     * @author zhang pu
     * @date 13:43 2023/7/31
     */
    public static String generatePath(String uploadFolder, TchunkInfo chunk)
    {
        StringBuilder sb = new StringBuilder();
        sb.append(uploadFolder).append("/").append(chunk.getTaskId());
        //判断uploadFolder/taskId 路径是否存在,不存在则创建
        if (!Files.isWritable(Paths.get(sb.toString())))
        {
            logger.info("path not exist,create path: {}", sb.toString());
            try
            {
                Files.createDirectories(Paths.get(sb.toString()));
            } catch (IOException e)
            {
                logger.error(e.getMessage(), e);
            }
        }
        return sb.append("/").append(chunk.getFileName()).append("-").append(chunk.getSliceNo()).toString();
    }

    /**
     * 功能描述: 文件合并
     *
     * @param file
     * @param folder
     * @param filename
     * @author zhang pu
     * @date 17:06 2023/8/1
     */
    public static RestBean merge(String file, String folder, String filename)
    {
        try
        {
            //先判断文件是否存在
            if (fileExists(file))
            {
                return RestBean.failure(400, "文件已存在!");
            } else
            {
                //不存在的话,进行合并
                Files.createFile(Paths.get(file));
                Files.list(Paths.get(folder)).filter(path -> !path.getFileName().toString().equals(filename)).sorted((o1, o2) -> {
                    String p1 = o1.getFileName().toString();
                    String p2 = o2.getFileName().toString();
                    int i1 = p1.lastIndexOf("-");
                    int i2 = p2.lastIndexOf("-");
                    return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
                }).forEach(path -> {
                    try
                    {
                        //以追加的形式写入文件
                        Files.write(Paths.get(file), Files.readAllBytes(path), StandardOpenOption.APPEND);
                        //合并后删除该块
                        Files.delete(path);
                    } catch (IOException e)
                    {
                        logger.error(e.getMessage(), e);
                    }
                });
            }
        } catch (IOException e)
        {
            logger.error(e.getMessage(), e);
            //合并失败
            return RestBean.failure(400, "合并失败!");
        }
        return RestBean.success();
    }

    /**
     * 根据文件的全路径名判断文件是否存在
     *
     * @param file
     * @return
     */
    public static boolean fileExists(String file)
    {
        boolean fileExists = false;
        Path path = Paths.get(file);
        fileExists = Files.exists(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS});
        return fileExists;
    }
}