diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java index eaf5bbf..67a96b6 100644 --- a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java @@ -3,6 +3,7 @@ package com.bonus.bmw.controller; import com.bonus.bmw.domain.vo.BmWorkerAtt; import com.bonus.bmw.service.ZipDownloadService; import com.bonus.bmw.service.impl.FileUploadUtils; +import com.bonus.common.core.web.domain.AjaxResult; import com.bonus.system.api.domain.DownloadTask; import com.bonus.system.api.domain.ZipFileMapping; import io.minio.GetObjectArgs; @@ -13,7 +14,6 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.http.ResponseEntity; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.web.bind.annotation.*; @@ -43,24 +43,24 @@ public class ZipDownloadController { private static final String TASK_PREFIX = "zip_task:"; // key 前缀 @PostMapping("/createZipTask") - public ResponseEntity createZipTask(@RequestBody BmWorkerAtt o) { + public AjaxResult createZipTask(@RequestBody BmWorkerAtt o) { //查询文件地址 List objectNames = service.getFileList(o); DownloadTask task = fileUploadUtils.zipFile(objectNames); - return ResponseEntity.accepted().body(task); + return AjaxResult.success(task); } /** * 查询任务状态 */ @GetMapping("/taskStatus/{taskId}") - public ResponseEntity getTaskStatus(@PathVariable String taskId) { + public AjaxResult getTaskStatus(@PathVariable String taskId) { String redisKey = TASK_PREFIX + taskId; DownloadTask task = redisTemplate.opsForValue().get(redisKey); if (task == null) { - return ResponseEntity.notFound().build(); + return AjaxResult.error("打包下载任务未查询"); } - return ResponseEntity.ok(task); + return AjaxResult.success(task); } /** diff --git a/bonus-modules/bonus-file/src/main/java/com/bonus/file/controller/FileUtilController.java b/bonus-modules/bonus-file/src/main/java/com/bonus/file/controller/FileUtilController.java index 4af636f..4ddec7e 100644 --- a/bonus-modules/bonus-file/src/main/java/com/bonus/file/controller/FileUtilController.java +++ b/bonus-modules/bonus-file/src/main/java/com/bonus/file/controller/FileUtilController.java @@ -236,7 +236,7 @@ public class FileUtilController { @PostMapping("/zipFile") public R zipFile(@RequestBody List objectNames) { if (objectNames == null || objectNames.isEmpty()) { - log.warn("Received null or empty zip file list"); + log.warn("提示:Received null or empty zip file list"); return R.fail("文件列表不能为空"); } return R.ok(service.zipFile(objectNames)); diff --git a/bonus-modules/bonus-file/src/main/java/com/bonus/file/service/impl/FileUtilsServiceImpl.java b/bonus-modules/bonus-file/src/main/java/com/bonus/file/service/impl/FileUtilsServiceImpl.java index 89ab00d..1740e81 100644 --- a/bonus-modules/bonus-file/src/main/java/com/bonus/file/service/impl/FileUtilsServiceImpl.java +++ b/bonus-modules/bonus-file/src/main/java/com/bonus/file/service/impl/FileUtilsServiceImpl.java @@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.BufferedOutputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -539,8 +540,15 @@ public class FileUtilsServiceImpl { /** * 使用 Apache Commons Compress 从 MinIO 打包 ZIP(支持 UTF-8 中文) + * + * @param fileMappings 要打包的文件映射列表(MinIO 对象名 → ZIP 内路径) + * @param zipPath 生成的 ZIP 文件绝对路径 + * @throws IOException 若无法创建 ZIP 文件或写入失败(非单个文件错误) */ - public void generateZipFromMinIO( List fileMappings, String zipPath) throws Exception { + public void generateZipFromMinIO(List fileMappings, String zipPath) throws IOException { + if (fileMappings == null || fileMappings.isEmpty()) { + throw new IllegalArgumentException("文件映射列表不能为空"); + } Path outputPath = Paths.get(zipPath); Files.createDirectories(outputPath.getParent()); try (FileOutputStream fos = new FileOutputStream(outputPath.toFile()); @@ -549,15 +557,25 @@ public class FileUtilsServiceImpl { zipOut.setEncoding("UTF-8"); zipOut.setUseZip64(Zip64Mode.AsNeeded); for (ZipFileMapping mapping : fileMappings) { - String objectName = mapping.getObjectName(); - String targetPath = mapping.getTargetPath(); - // 安全校验 - if (objectName == null || targetPath == null || - objectName.contains("..") || targetPath.contains("..") || - objectName.startsWith("/") || targetPath.startsWith("/")) { - continue; // 跳过非法项 + // 安全校验:防止路径穿越和空值 + if (mapping == null + || mapping.getObjectName() == null + || mapping.getTargetPath() == null + || mapping.getObjectName().contains("..") + || mapping.getTargetPath().contains("..") + || mapping.getObjectName().startsWith("/") + || mapping.getTargetPath().startsWith("/")) { + log.warn("跳过非法或空的文件映射: {}", mapping); + continue; } - // 统一使用正斜杠(兼容 Windows) + String objectName = mapping.getObjectName().trim(); + String targetPath = mapping.getTargetPath().trim(); + + if (objectName.isEmpty() || targetPath.isEmpty()) { + log.warn("跳过空路径映射: objectName='{}', targetPath='{}'", objectName, targetPath); + continue; + } + // 统一使用正斜杠(兼容 Windows 解压) String entryName = targetPath.replace('\\', '/'); try (InputStream objectStream = minioClient.getObject( GetObjectArgs.builder() @@ -565,6 +583,8 @@ public class FileUtilsServiceImpl { .object(objectName) .build())) { ZipArchiveEntry entry = new ZipArchiveEntry(entryName); + // 可选:设置时间戳为当前时间(避免 MinIO 时间暴露) + entry.setTime(System.currentTimeMillis()); zipOut.putArchiveEntry(entry); byte[] buffer = new byte[8192]; int len; @@ -572,13 +592,28 @@ public class FileUtilsServiceImpl { zipOut.write(buffer, 0, len); } zipOut.closeArchiveEntry(); + log.debug("成功添加文件到 ZIP: {} -> {}", objectName, entryName); } catch (Exception e) { - log.error(e.toString(), e); - System.err.println("Failed to process object: " + objectName + " -> " + targetPath); - // 可选择继续或中断 + // 记录具体错误,但继续处理其他文件 + log.error("从 MinIO 下载文件失败,已跳过: objectName='{}', targetPath='{}'", + objectName, entryName, e); + // 注意:不要在这里 closeArchiveEntry!因为 putArchiveEntry 后若未写完就异常, + // Apache Commons Compress 会自动清理未完成的 entry(只要不调用 closeArchiveEntry) + // 所以直接 continue 即可 } } + // 确保 ZIP 结构完整 zipOut.finish(); + log.info("ZIP 文件生成完成: {}", zipPath); + } catch (IOException e) { + log.error("写入 ZIP 文件时发生严重错误: {}", zipPath, e); + // 尝试删除可能损坏的 ZIP 文件 + try { + Files.deleteIfExists(outputPath); + } catch (IOException deleteEx) { + log.warn("无法删除损坏的 ZIP 文件: {}", outputPath, deleteEx); + } + throw e; // 向上抛出,表示任务整体失败 } }