From 029b96ae99219e8ddad922f016f3e3d998fafacc Mon Sep 17 00:00:00 2001 From: litianxiang Date: Mon, 13 Apr 2026 11:47:20 +0800 Subject: [PATCH] =?UTF-8?q?GlobalAward=E4=B8=8B=E8=BD=BD=E5=88=B0=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../da/controller/GlobalAwardController.java | 17 ++- .../com/ai/da/service/GlobalAwardService.java | 6 +- .../service/impl/GlobalAwardServiceImpl.java | 138 ++++++++---------- 3 files changed, 75 insertions(+), 86 deletions(-) diff --git a/src/main/java/com/ai/da/controller/GlobalAwardController.java b/src/main/java/com/ai/da/controller/GlobalAwardController.java index 9d1171cc..fb5f0001 100644 --- a/src/main/java/com/ai/da/controller/GlobalAwardController.java +++ b/src/main/java/com/ai/da/controller/GlobalAwardController.java @@ -176,10 +176,19 @@ public class GlobalAwardController { } @PostMapping("/contestants/export/files") - @ApiOperation(value = "导出参赛者文件到本地", notes = "根据参赛者编号范围导出PDF和视频文件到本地temp/uploads/contestants目录") - public Response exportContestantFiles(@ApiParam(value = "参赛者文件导出请求", required = true) @RequestBody ContestantExportRequest request) throws Exception { - int exportedCount = globalAwardService.exportContestantFiles(request.getMinContestantNumber(), request.getMaxContestantNumber()); - return Response.success(exportedCount); + @ApiOperation(value = "导出参赛者文件为ZIP", notes = "根据参赛者编号范围导出PDF、视频和信息文件为ZIP,直接响应给浏览器") + public void exportContestantFiles(@ApiParam(value = "参赛者文件导出请求", required = true) @RequestBody ContestantExportRequest request, HttpServletResponse response) throws Exception { + byte[] zipData = globalAwardService.exportContestantFilesAsZip(request.getMinContestantNumber(), request.getMaxContestantNumber()); + if (zipData.length == 0) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.getWriter().write("No contestants found in the specified range."); + return; + } + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment; filename=\"contestants.zip\""); + response.setContentLength(zipData.length); + response.getOutputStream().write(zipData); + response.getOutputStream().flush(); } @GetMapping("/contestants/count") diff --git a/src/main/java/com/ai/da/service/GlobalAwardService.java b/src/main/java/com/ai/da/service/GlobalAwardService.java index 16091a9b..a6643276 100644 --- a/src/main/java/com/ai/da/service/GlobalAwardService.java +++ b/src/main/java/com/ai/da/service/GlobalAwardService.java @@ -33,12 +33,12 @@ public interface GlobalAwardService { void saveContestantsToLocal() throws Exception; /** - * 根据参赛者编号范围导出参赛者文件到本地目录 + * 将参赛者文件打包为 ZIP 并返回字节数组(不落盘,直接响应给浏览器) * @param minContestantNumber 最小参赛者编号 * @param maxContestantNumber 最大参赛者编号 - * @return 导出的参赛者数量 + * @return ZIP 文件的字节数组 */ - int exportContestantFiles(Integer minContestantNumber, Integer maxContestantNumber) throws Exception; + byte[] exportContestantFilesAsZip(Integer minContestantNumber, Integer maxContestantNumber) throws Exception; /** * 查询参赛者总数 diff --git a/src/main/java/com/ai/da/service/impl/GlobalAwardServiceImpl.java b/src/main/java/com/ai/da/service/impl/GlobalAwardServiceImpl.java index ffb84c07..17395a0e 100644 --- a/src/main/java/com/ai/da/service/impl/GlobalAwardServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/GlobalAwardServiceImpl.java @@ -26,24 +26,22 @@ import org.springframework.transaction.annotation.Transactional; import jakarta.annotation.Resource; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; 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.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import jakarta.servlet.http.HttpServletResponse; +import java.util.zip.ZipEntry; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.io.FileOutputStream; -import java.time.format.DateTimeFormatter; @Service @Slf4j @@ -476,7 +474,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService { } @Override - public int exportContestantFiles(Integer minContestantNumber, Integer maxContestantNumber) throws Exception { + public byte[] exportContestantFilesAsZip(Integer minContestantNumber, Integer maxContestantNumber) throws Exception { if (minContestantNumber == null || maxContestantNumber == null) { throw new BusinessException("minContestantNumber and maxContestantNumber are required."); } @@ -484,7 +482,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService { throw new BusinessException("minContestantNumber cannot be greater than maxContestantNumber."); } - // 1. 根据contestantNumber范围查询参赛者 + // 1. 根据 contestantNumber 范围查询参赛者 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.lambda() .ge(Contestant::getContestantNumber, minContestantNumber) @@ -494,59 +492,37 @@ public class GlobalAwardServiceImpl implements GlobalAwardService { if (contestants.isEmpty()) { log.info("No contestants found in range [{}, {}]", minContestantNumber, maxContestantNumber); - return 0; + return new byte[0]; } - // 2. 创建基础目录 - String baseDir = uploadDir + "/contestants"; - Path basePath = Paths.get(baseDir).toAbsolutePath(); - Files.createDirectories(basePath); - log.info("Base directory created: {}", basePath); + // 2. 在内存中构建 ZIP + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(baos)) { - int exportedCount = 0; - - // 3. 遍历每个参赛者,下载文件 - for (Contestant contestant : contestants) { - Integer contestantNumber = contestant.getContestantNumber(); - if (contestantNumber == null) { - log.warn("Contestant {} has no contestantNumber, skipping", contestant.getId()); - continue; - } - - // 创建参赛者文件夹 - String contestantDir = baseDir + "/" + contestantNumber; - Path contestantPath = Paths.get(contestantDir); - Files.createDirectories(contestantPath); - - // 下载PDF文件 - String pdfPath = contestant.getPdfPath(); - if (StringUtils.isNotBlank(pdfPath)) { - try { - String fileName = pdfPath.contains("/") ? - pdfPath.substring(pdfPath.lastIndexOf("/") + 1) : "design.pdf"; - downloadFileFromMinio(pdfPath, contestantPath.toString(), "design.pdf"); - log.info("Downloaded PDF for contestant {}", fileName); - } catch (Exception e) { - log.error("Failed to download PDF for contestant {}: {}", contestantNumber, e.getMessage()); + for (Contestant contestant : contestants) { + Integer contestantNumber = contestant.getContestantNumber(); + if (contestantNumber == null) { + log.warn("Contestant {} has no contestantNumber, skipping", contestant.getId()); + continue; } - } - // 下载视频文件 - String videoPath = contestant.getVideoPath(); - if (StringUtils.isNotBlank(videoPath)) { - try { - // 根据路径判断视频格式 + String dirPrefix = contestantNumber + "/"; + + // 添加 PDF 文件 + String pdfPath = contestant.getPdfPath(); + if (StringUtils.isNotBlank(pdfPath)) { + addMinioFileToZip(zos, pdfPath, dirPrefix + "design.pdf"); + } + + // 添加视频文件 + String videoPath = contestant.getVideoPath(); + if (StringUtils.isNotBlank(videoPath)) { String fileName = videoPath.contains("/") ? videoPath.substring(videoPath.lastIndexOf("/") + 1) : "video.mp4"; - downloadFileFromMinio(videoPath, contestantPath.toString(), fileName); - log.info("Downloaded video for contestant {}", contestantNumber); - } catch (Exception e) { - log.error("Failed to download video for contestant {}: {}", contestantNumber, e.getMessage()); + addMinioFileToZip(zos, videoPath, dirPrefix + fileName); } - } - // 生成参赛者信息 txt 文件 - try { + // 添加参赛者信息 txt 文件 StringBuilder sb = new StringBuilder(); sb.append("=== Contestant Information ===\n\n"); sb.append("ID: ").append(nullSafe(contestant.getId())).append("\n"); @@ -561,54 +537,58 @@ public class GlobalAwardServiceImpl implements GlobalAwardService { sb.append("Phone Number: ").append(nullSafe(contestant.getPhoneNumber())).append("\n"); sb.append("Design Title: ").append(nullSafe(contestant.getDesignTitle())).append("\n"); sb.append("Design Description: ").append(nullSafe(contestant.getDesignDescription())).append("\n"); - sb.append("PDF Path: ").append(nullSafe(contestant.getPdfPath())).append("\n"); + sb.append("PDF Path: ").append(nullSafe(pdfPath)).append("\n"); sb.append("PDF Size (bytes): ").append(contestant.getPdfSize() != null ? contestant.getPdfSize() : "N/A").append("\n"); - sb.append("Video Path: ").append(nullSafe(contestant.getVideoPath())).append("\n"); + sb.append("Video Path: ").append(nullSafe(videoPath)).append("\n"); sb.append("Video Duration (seconds): ").append(contestant.getVideoDuration() != null ? contestant.getVideoDuration() : "N/A").append("\n"); sb.append("Video Size (bytes): ").append(contestant.getVideoSize() != null ? contestant.getVideoSize() : "N/A").append("\n"); sb.append("Created At: ").append(contestant.getCreatedAt() != null ? contestant.getCreatedAt() : "N/A").append("\n"); sb.append("Updated At: ").append(contestant.getUpdatedAt() != null ? contestant.getUpdatedAt() : "N/A").append("\n"); - String infoFilePath = contestantPath.resolve("contestant_info.txt").toString(); - Files.write(Paths.get(infoFilePath), sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)); - log.info("Generated info file for contestant {}", contestantNumber); - } catch (Exception e) { - log.error("Failed to generate info file for contestant {}: {}", contestantNumber, e.getMessage()); + ZipEntry infoEntry = new ZipEntry(dirPrefix + "contestant_info.txt"); + zos.putNextEntry(infoEntry); + zos.write(sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)); + zos.closeEntry(); + log.info("Added contestant {} info to zip", contestantNumber); } - exportedCount++; + zos.finish(); + log.info("ZIP built for {} contestants, size: {} bytes", contestants.size(), baos.size()); + return baos.toByteArray(); } - - log.info("Exported {} contestants' files to {}", exportedCount, basePath); - return exportedCount; } /** - * 从MinIO下载文件到本地 - * @param minioPath MinIO路径 (格式: bucketName/objectPath) - * @param localDir 本地目录 - * @param fileName 本地文件名 + * 将 MinIO 文件流式写入 ZIP,不落盘 + * @param zos ZIP 输出流 + * @param minioPath MinIO 路径(格式: bucketName/objectPath) + * @param entryName ZIP 条目名称 */ - private void downloadFileFromMinio(String minioPath, String localDir, String fileName) { + private void addMinioFileToZip(java.util.zip.ZipOutputStream zos, String minioPath, String entryName) { if (StringUtils.isBlank(minioPath)) { return; } - - // 从路径中提取bucket名称和对象名称 int index = minioPath.indexOf("/"); if (index == -1) { log.warn("Invalid MinIO path: {}", minioPath); return; } - String bucketName = minioPath.substring(0, index); String objectName = minioPath.substring(index + 1); - // 构建本地文件完整路径 - Path localFilePath = Paths.get(localDir, fileName); - - // 下载文件 - minioUtil.downloadMinioObjectToLocal(bucketName, objectName, localFilePath.toString()); + try (InputStream in = minioUtil.download(bucketName, objectName)) { + ZipEntry entry = new ZipEntry(entryName); + zos.putNextEntry(entry); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + zos.write(buffer, 0, bytesRead); + } + zos.closeEntry(); + log.info("Added {} to zip ({} bytes)", entryName, entry.getSize()); + } catch (Exception e) { + log.error("Failed to add {} to zip: {}", entryName, e.getMessage()); + } } @Override