前端代码
<!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; const UPLOAD_URL = 'http://localhost:89/api/common/config/uploadShardingFile'; let uploadTask = null;let isPaused = false;let isCanceled = false;let uploadedChunks = []; 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(), totalChunks: Math.ceil(file.size / CHUNK_SIZE),currentChunk: 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;}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>
后端代码
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;
@Slf4j
@RestController
@RequestMapping("/api/applicant")
public class ApplicantController extends BaseController {@Autowiredprivate 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;@Autowiredprivate 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);log.info("文件和并与计算MD5时常------------{}毫秒",System.currentTimeMillis()-l);return RestBean.success(systemFile);}} catch (IOException e){e.printStackTrace();return RestBean.failure(400, "上传失败!");}return RestBean.success();}
}
工具类
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);public static String generatePath(String uploadFolder, TchunkInfo chunk){StringBuilder sb = new StringBuilder();sb.append(uploadFolder).append("/").append(chunk.getTaskId());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();}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();}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;}
}