前端代码
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;
}
}