
后端处理流程设计1. 主要处理类javaRestController RequestMapping(/api/file) public class FileUploadController { Autowired private FileUploadService fileUploadService; PostMapping(/upload/chunk) public ResponseEntity? uploadChunk( RequestParam String fileId, RequestParam String fileName, RequestParam Integer chunkSize, RequestParam Integer chunkTotals, RequestParam Integer chunkIndex, RequestParam(file) MultipartFile file) { try { fileUploadService.saveChunk(fileId, fileName, chunkSize, chunkTotals, chunkIndex, file); return ResponseEntity.ok().body(Map.of(code, 200, message, 上传成功)); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of(code, 500, message, e.getMessage())); } } }2. 核心服务实现javaService Slf4j public class FileUploadService { Autowired private FileMergeService fileMergeService; // 临时分片存储目录 private static final String TEMP_CHUNK_DIR uploads/temp/chunks/; // 最终文件存储目录 private static final String FINAL_FILE_DIR uploads/files/; /** * 保存分片 */ public void saveChunk(String fileId, String fileName, Integer chunkSize, Integer chunkTotals, Integer chunkIndex, MultipartFile file) throws IOException { // 创建临时目录 String chunkDir TEMP_CHUNK_DIR fileId /; File dir new File(chunkDir); if (!dir.exists()) { dir.mkdirs(); } // 保存分片文件 String chunkFilePath chunkDir chunkIndex .part; File chunkFile new File(chunkFilePath); file.transferTo(chunkFile); log.info(保存分片成功: fileId{}, chunkIndex{}/{}, fileId, chunkIndex 1, chunkTotals); // 检查是否所有分片都已上传完成 checkAndMergeChunks(fileId, fileName, chunkTotals); } /** * 检查并合并分片 */ private void checkAndMergeChunks(String fileId, String fileName, Integer chunkTotals) { String chunkDir TEMP_CHUNK_DIR fileId /; File dir new File(chunkDir); if (!dir.exists()) { return; } // 统计已上传的分片数量 File[] chunkFiles dir.listFiles((d, name) - name.endsWith(.part)); if (chunkFiles null || chunkFiles.length chunkTotals) { log.info(分片未完整: fileId{}, 已上传{}, 总共{}, fileId, chunkFiles ! null ? chunkFiles.length : 0, chunkTotals); return; } log.info(所有分片已上传完成开始异步合并: fileId{}, fileId); // 异步执行合并任务 CompletableFuture.runAsync(() - { try { fileMergeService.mergeChunks(fileId, fileName, chunkTotals); } catch (Exception e) { log.error(合并文件失败: fileId fileId, e); } }); } }3. 文件合并服务javaService Slf4j public class FileMergeService { Autowired private FileProcessService fileProcessService; private static final String TEMP_CHUNK_DIR uploads/temp/chunks/; private static final String FINAL_FILE_DIR uploads/files/; private static final String TEMP_MERGE_DIR uploads/temp/merge/; /** * 合并分片 */ public void mergeChunks(String fileId, String fileName, Integer chunkTotals) throws IOException { String chunkDir TEMP_CHUNK_DIR fileId /; String tempMergePath TEMP_MERGE_DIR fileId _ fileName; String finalFilePath FINAL_FILE_DIR fileName; // 创建临时合并目录 File mergeDir new File(TEMP_MERGE_DIR); if (!mergeDir.exists()) { mergeDir.mkdirs(); } // 合并所有分片到临时文件 File mergedFile new File(tempMergePath); try (FileOutputStream fos new FileOutputStream(mergedFile)) { for (int i 0; i chunkTotals; i) { File chunkFile new File(chunkDir i .part); if (!chunkFile.exists()) { throw new IOException(分片文件缺失: chunkFile.getPath()); } try (FileInputStream fis new FileInputStream(chunkFile)) { byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead fis.read(buffer)) ! -1) { fos.write(buffer, 0, bytesRead); } } } } log.info(分片合并完成: fileId{}, 临时文件{}, fileId, tempMergePath); // 处理合并后的文件判断是否是压缩包 fileProcessService.processMergedFile(fileId, fileName, mergedFile, finalFilePath); // 清理临时文件 cleanupTempFiles(fileId); } /** * 清理临时文件 */ private void cleanupTempFiles(String fileId) { // 删除分片目录 String chunkDir TEMP_CHUNK_DIR fileId /; File dir new File(chunkDir); if (dir.exists()) { File[] files dir.listFiles(); if (files ! null) { for (File file : files) { file.delete(); } } dir.delete(); } } }4. 文件处理服务解压和移动javaService Slf4j public class FileProcessService { private static final String FINAL_FILE_DIR uploads/files/; private static final String TEMP_EXTRACT_DIR uploads/temp/extract/; // 支持的压缩包格式 private static final ListString ARCHIVE_EXTENSIONS Arrays.asList(.zip, .rar, .7z, .tar, .gz); /** * 处理合并后的文件 */ public void processMergedFile(String fileId, String fileName, File mergedFile, String finalFilePath) throws IOException { // 检查是否是压缩包 String extension getFileExtension(fileName).toLowerCase(); boolean isArchive ARCHIVE_EXTENSIONS.stream().anyMatch(ext - extension.equals(ext)); if (isArchive) { // 是压缩包执行解压 log.info(检测到压缩包开始解压: fileId{}, fileName{}, fileId, fileName); extractArchive(fileId, fileName, mergedFile); } else { // 不是压缩包直接移动到目标目录 log.info(非压缩包直接移动文件: fileId{}, fileName{}, fileId, fileName); moveToFinalLocation(mergedFile, finalFilePath); } } /** * 解压压缩包 */ private void extractArchive(String fileId, String fileName, File archiveFile) throws IOException { String extractDir TEMP_EXTRACT_DIR fileId /; File dir new File(extractDir); if (!dir.exists()) { dir.mkdirs(); } String extension getFileExtension(fileName).toLowerCase(); try { switch (extension) { case .zip: extractZip(archiveFile, extractDir); break; case .rar: extractRar(archiveFile, extractDir); break; case .7z: extract7z(archiveFile, extractDir); break; case .tar: case .gz: extractTarGz(archiveFile, extractDir); break; default: throw new IOException(不支持的压缩格式: extension); } log.info(解压完成: fileId{}, 解压目录{}, fileId, extractDir); // 将解压后的文件移动到目标目录 moveExtractedFiles(extractDir, fileId, fileName); } catch (Exception e) { log.error(解压失败: fileId fileId, e); throw new IOException(解压失败: e.getMessage(), e); } finally { // 删除压缩包临时文件 if (archiveFile.exists()) { archiveFile.delete(); } } } /** * 解压ZIP文件 */ private void extractZip(File zipFile, String destDir) throws IOException { try (ZipInputStream zis new ZipInputStream(new FileInputStream(zipFile))) { ZipEntry entry; while ((entry zis.getNextEntry()) ! null) { File file new File(destDir, entry.getName()); if (entry.isDirectory()) { file.mkdirs(); } else { // 确保父目录存在 file.getParentFile().mkdirs(); try (FileOutputStream fos new FileOutputStream(file)) { byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead zis.read(buffer)) ! -1) { fos.write(buffer, 0, bytesRead); } } } zis.closeEntry(); } } } /** * 解压RAR文件需要依赖rar解压工具或使用Apache Commons VFS */ private void extractRar(File rarFile, String destDir) throws IOException { // 方式1: 使用命令行工具需要系统安装unrar ProcessBuilder pb new ProcessBuilder(unrar, x, rarFile.getAbsolutePath(), destDir); Process process pb.start(); try { int exitCode process.waitFor(); if (exitCode ! 0) { throw new IOException(RAR解压失败退出码: exitCode); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(解压被中断, e); } // 方式2: 使用Apache Commons VFS需要添加依赖 // 这里使用方式1作为示例 } /** * 解压7Z文件需要依赖7z解压工具 */ private void extract7z(File sevenZipFile, String destDir) throws IOException { ProcessBuilder pb new ProcessBuilder(7z, x, sevenZipFile.getAbsolutePath(), -o destDir, -y); Process process pb.start(); try { int exitCode process.waitFor(); if (exitCode ! 0) { throw new IOException(7Z解压失败退出码: exitCode); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(解压被中断, e); } } /** * 解压TAR.GZ文件 */ private void extractTarGz(File tarGzFile, String destDir) throws IOException { try (FileInputStream fis new FileInputStream(tarGzFile); GZIPInputStream gis new GZIPInputStream(fis); TarArchiveInputStream tarIn new TarArchiveInputStream(gis)) { TarArchiveEntry entry; while ((entry (TarArchiveEntry) tarIn.getNextEntry()) ! null) { File file new File(destDir, entry.getName()); if (entry.isDirectory()) { file.mkdirs(); } else { file.getParentFile().mkdirs(); try (FileOutputStream fos new FileOutputStream(file)) { byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead tarIn.read(buffer)) ! -1) { fos.write(buffer, 0, bytesRead); } } } } } } /** * 移动解压后的文件到最终目录 */ private void moveExtractedFiles(String extractDir, String fileId, String originalFileName) throws IOException { File dir new File(extractDir); if (!dir.exists() || !dir.isDirectory()) { return; } // 获取解压后的所有文件 File[] files dir.listFiles(); if (files null || files.length 0) { log.warn(解压目录为空: fileId{}, fileId); return; } // 如果只有一个文件或目录直接移动 if (files.length 1) { File source files[0]; String destPath FINAL_FILE_DIR source.getName(); moveToFinalLocation(source, destPath); } else { // 多个文件创建同名目录存放 String baseName originalFileName.substring(0, originalFileName.lastIndexOf(.)); String destDirPath FINAL_FILE_DIR baseName /; File destDir new File(destDirPath); if (!destDir.exists()) { destDir.mkdirs(); } for (File file : files) { String destPath destDirPath file.getName(); moveToFinalLocation(file, destPath); } } // 删除解压临时目录 deleteDirectory(dir); } /** * 移动文件到最终位置 */ private void moveToFinalLocation(File sourceFile, String destPath) throws IOException { File destFile new File(destPath); File parentDir destFile.getParentFile(); if (!parentDir.exists()) { parentDir.mkdirs(); } // 如果目标文件已存在添加时间戳 if (destFile.exists()) { String timestamp String.valueOf(System.currentTimeMillis()); String name destFile.getName(); String extension ; String baseName name; int dotIndex name.lastIndexOf(.); if (dotIndex 0) { extension name.substring(dotIndex); baseName name.substring(0, dotIndex); } destPath destFile.getParent() / baseName _ timestamp extension; destFile new File(destPath); } Files.move(sourceFile.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING); log.info(文件移动完成: {} - {}, sourceFile.getPath(), destPath); } /** * 获取文件扩展名 */ private String getFileExtension(String fileName) { if (fileName null) { return ; } int dotIndex fileName.lastIndexOf(.); return dotIndex 0 ? fileName.substring(dotIndex) : ; } /** * 递归删除目录 */ private void deleteDirectory(File dir) { if (dir.isDirectory()) { File[] files dir.listFiles(); if (files ! null) { for (File file : files) { deleteDirectory(file); } } } dir.delete(); } }前端调用示例javascript// 前端分片上传示例 async function uploadFile(file) { const chunkSize 5 * 1024 * 1024; // 5MB每片 const totalChunks Math.ceil(file.size / chunkSize); const fileId generateFileId(); // 生成唯一ID for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, file.size); const chunk file.slice(start, end); const formData new FormData(); formData.append(fileId, fileId); formData.append(fileName, file.name); formData.append(chunkSize, chunkSize); formData.append(chunkTotals, totalChunks); formData.append(chunkIndex, i); formData.append(file, chunk); try { const response await fetch(/api/file/upload/chunk, { method: POST, body: formData }); if (!response.ok) { throw new Error(上传分片失败); } console.log(分片 ${i 1}/${totalChunks} 上传成功); } catch (error) { console.error(上传失败:, error); // 实现重试逻辑 } } }关键点说明异步处理合并、解压都在异步线程中执行避免阻塞主线程原子性使用临时文件处理完成后才移动到最终位置压缩包只解压一次通过文件扩展名判断解压后删除压缩包临时文件错误处理完善的异常捕获和日志记录目录结构uploads/temp/chunks/{fileId}/- 分片存储uploads/temp/merge/- 合并临时文件uploads/temp/extract/{fileId}/- 解压临时目录uploads/files/- 最终文件存储这样设计可以保证文件处理的完整性和可靠性。