diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/PatrolTaskResultMainController.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/PatrolTaskResultMainController.java index b743aca..8992633 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/PatrolTaskResultMainController.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/PatrolTaskResultMainController.java @@ -45,13 +45,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.http.ResponseEntity; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.web.bind.annotation.*; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -448,42 +450,34 @@ public class PatrolTaskResultMainController extends BaseController { } } } - Map streamHashMap = new HashMap<>(); -// for (String algorithmBaseImagePath : images) { -// try { -// sftpClient.downLoad(algorithmBaseImagePath, (inputStream) -> { -// byte[] bytes = streamHashMap.get(algorithmBaseImagePath); -// byte[] byInputStream = getStringByInputStream(inputStream); -// if (bytes == null) { -// streamHashMap.put(algorithmBaseImagePath, byInputStream); -// } -// -// }); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } - images.parallelStream().forEach(algorithmBaseImagePath -> { + // 将图片下载到临时文件,避免大量图片字节数据全部驻留内存 + Map tempFileMap = new LinkedHashMap<>(); + List tempFiles = new ArrayList<>(); + for (String imgSrc : images) { try { - sftpClient.downLoad(algorithmBaseImagePath, (inputStream) -> { - byte[] bytes = streamHashMap.get(algorithmBaseImagePath); - byte[] byInputStream = getStringByInputStream(inputStream); - if (bytes == null) { - streamHashMap.put(algorithmBaseImagePath, byInputStream); - } - - }); + Path tempFile = downloadImageToTemp(imgSrc); + if (tempFile != null) { + tempFileMap.put(imgSrc, tempFile); + tempFiles.add(tempFile); + } } catch (Exception e) { - logger.info("报告导出下载图片失败: {}", algorithmBaseImagePath, e); - e.printStackTrace(); + logger.warn("报告归档下载图片失败: {}", imgSrc, e); } - }); - try (SXSSFWorkbook sxssfWorkbook = getSXSSFWorkbookShaoxing(inspectionReport, streamHashMap); + } + + try (SXSSFWorkbook sxssfWorkbook = getSXSSFWorkbookShaoxing(inspectionReport, tempFileMap); ServletOutputStream outputStream = response.getOutputStream()) { sxssfWorkbook.write(outputStream); } + for (Path tempFile : tempFiles) { + try { + Files.deleteIfExists(tempFile); + } catch (IOException e) { + logger.warn("删除临时文件失败: {}", tempFile, e); + } + } return ResponseEntity.ok("报告导出成功!"); } @@ -565,7 +559,7 @@ public class PatrolTaskResultMainController extends BaseController { return doc; } - private XWPFDocument getXWPFDocumentLingzhou(InspectionReport report, Map images) { + private XWPFDocument getXWPFDocumentLingzhou(InspectionReport report, Map tempFileMap) { XWPFDocument doc = new XWPFDocument(); // 标题 @@ -586,7 +580,7 @@ public class PatrolTaskResultMainController extends BaseController { List statusList = Arrays.asList(MessageUtils.get("缺陷"), MessageUtils.get("异常"), MessageUtils.get("正常")); // List statusList = Collections.singletonList(MessageUtils.get("缺陷")); for (String status : statusList) { - addStatusSectionLingzhou(doc, report, status, images); + addStatusSectionLingzhou(doc, report, status, tempFileMap); } // addQXStatusSection(doc, report, MessageUtils.get("缺陷"), images); // addNormalStatusSection(doc, report, MessageUtils.get("正常"), images); @@ -800,7 +794,7 @@ public class PatrolTaskResultMainController extends BaseController { } private void addStatusSectionLingzhou(XWPFDocument doc, InspectionReport report, - String status, Map images) { + String status, Map tempFileMap) { // addSectionHeader(doc, status + MessageUtils.get("点位汇总")); addSectionHeader(doc, status + MessageUtils.get("汇总")); @@ -829,7 +823,7 @@ public class PatrolTaskResultMainController extends BaseController { for (InspectionReportData data : dataList) { XWPFTableRow row = table.createRow(); - fillDataRowLingzhou(row, data, images); + fillDataRowLingzhou(row, data, tempFileMap); } } addEmptyParagraph(doc); @@ -994,7 +988,7 @@ public class PatrolTaskResultMainController extends BaseController { } private void fillDataRowLingzhou(XWPFTableRow row, InspectionReportData data, - Map images) { + Map tempFileMap) { String[] values = { String.valueOf(data.getCode()), data.getArea(), @@ -1020,8 +1014,14 @@ public class PatrolTaskResultMainController extends BaseController { if (data.getReportImgList() != null) { data.getReportImgList().stream() .filter(img -> "0".equals(img.getImgType())) - .forEach(img -> insertImageToCell(row.getCell(9), - images.get(img.getImgSrc()))); + .forEach(img -> { + try { + insertImageToCell(row.getCell(9), + Files.readAllBytes(tempFileMap.get(img.getImgSrc()))); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); } } @@ -1351,6 +1351,7 @@ public class PatrolTaskResultMainController extends BaseController { int imgIndex = 1; // 每个报告数据项下的图片索引 for (InspectionReportImg reportImg : reportImgList) { if (StringUtils.isNotEmpty(reportImg.getImgSrc()) && reportImg.getImgType().equals("0")) { + // /2/2026/06/21/1670/CCD/1670_20260621140000_2023923_02160558348953680127.jpg String originalImgSrc = reportImg.getImgSrc(); images.add(originalImgSrc); String pointName = reportData.getPointName(); @@ -1363,9 +1364,11 @@ public class PatrolTaskResultMainController extends BaseController { } // 生成唯一文件名(包含 lineId 防止重复) + // 2023923_极Ⅱ低Y/Y-A相换流变油面温度1_3212_100097_1.jpg String uniqueFileName = String.format("%s_%s_%s_%d%s", originalImgSrc.split("_")[2], - pointName, + pointName.replaceAll("[\\\\/:*?\"<>|]", "") + .replaceAll("\\s+", ""), // 空格, reportData.getLineId(), imgIndex, extension); @@ -1376,34 +1379,58 @@ public class PatrolTaskResultMainController extends BaseController { } } - Map streamHashMap = new HashMap<>(); -// for (String algorithmBaseImagePath : images) { -// try { -// sftpClient.downLoad(algorithmBaseImagePath, (inputStream) -> { -// byte[] bytes = streamHashMap.get(algorithmBaseImagePath); -// byte[] byInputStream = getStringByInputStream(inputStream); -// if (bytes == null) { -// streamHashMap.put(algorithmBaseImagePath, byInputStream); -// } -// -// }); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } - images.parallelStream().forEach(imagePath -> { - byte[] shm = streamHashMap.get(imagePath); - byte[] bytes = downloadAndCompressImage(imagePath); - if (shm == null) { - streamHashMap.put(imagePath, bytes); + // 将图片下载到临时文件,避免大量图片字节数据全部驻留内存 + Map tempFileMap = new LinkedHashMap<>(); + List tempFiles = new ArrayList<>(); + for (String imgSrc : images) { + try { + Path tempFile = downloadImageToTemp(imgSrc); + if (tempFile != null) { + tempFileMap.put(imgSrc, tempFile); + tempFiles.add(tempFile); + } + } catch (Exception e) { + logger.warn("报告归档下载图片失败: {}", imgSrc, e); + } + } + + // 懒加载适配:按需从临时文件读取图片字节,读取后调用方使用完毕即被GC回收 + Map streamHashMap = new AbstractMap() { + @Override + public byte[] get(Object key) { + Path path = tempFileMap.get(key); + if (path == null) return null; + try { + return Files.readAllBytes(path); + } catch (IOException e) { + logger.warn("读取临时图片文件失败: {}", path, e); + return null; + } } - }); + @Override + public boolean containsKey(Object key) { + return tempFileMap.containsKey(key); + } + + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return tempFileMap.size(); + } + }; + logMemoryUsage("AFTER_IMAGE_DOWNLOAD"); + + SXSSFWorkbook sxssfWorkbook = null; + XWPFDocument wordDocument = null; - SXSSFWorkbook sxssfWorkbook = getSXSSFWorkbookLingzhou(inspectionReport, streamHashMap); - XWPFDocument wordDocument = getXWPFDocumentLingzhou(inspectionReport, streamHashMap); - logger.info("[ARCHIVE] hssfWorkbook: {}", sxssfWorkbook); try { + sxssfWorkbook = getSXSSFWorkbookLingzhou(inspectionReport, streamHashMap); + wordDocument = getXWPFDocumentLingzhou(inspectionReport, tempFileMap); String basePath = this.basePath + stationCode + "/Model/"; Date startTime = inspectionReport.getInspectionStartTime() != null ? inspectionReport.getInspectionStartTime() : new Date(); @@ -1415,18 +1442,17 @@ public class PatrolTaskResultMainController extends BaseController { namePrefix = MessageUtils.get("大模型") + "_"; } String reportName = namePrefix + timestamp; - // 上传媒体包 + // 上传媒体包(直接从临时文件流式写入ZIP,不加载到内存) CompletableFuture zipUploadFuture = CompletableFuture.runAsync(() -> { uploadFile(basePath, reportName, ".zip", outputStream -> { try (ZipOutputStream zipOut = new ZipOutputStream(outputStream)) { - for (Map.Entry entry : streamHashMap.entrySet()) { + for (Map.Entry entry : tempFileMap.entrySet()) { String imagePath = entry.getKey(); String pointFileName = imgSrcToFileNameMap.getOrDefault(imagePath, "unnamed"); - byte[] imageData = entry.getValue(); -// String fileName = new File(imagePath).getName(); + Path tempFile = entry.getValue(); ZipEntry zipEntry = new ZipEntry(pointFileName); zipOut.putNextEntry(zipEntry); - zipOut.write(imageData); + Files.copy(tempFile, zipOut); zipOut.closeEntry(); } } catch (IOException e) { @@ -1434,24 +1460,39 @@ public class PatrolTaskResultMainController extends BaseController { } }); }, executor); + logMemoryUsage("AFTER_ZIP_UPLOAD"); + // 上传Excel + SXSSFWorkbook finalSxssfWorkbook = sxssfWorkbook; CompletableFuture excelUploadFuture = CompletableFuture.runAsync(() -> { - uploadFile(basePath, reportName, ".xls", sxssfWorkbook::write); + uploadFile(basePath, reportName, ".xls", finalSxssfWorkbook::write); }, executor); + // 上传Word + XWPFDocument finalWordDocument = wordDocument; CompletableFuture wordUploadFuture = CompletableFuture.runAsync(() -> { - uploadFile(basePath, reportName, ".docx", wordDocument::write); + uploadFile(basePath, reportName, ".docx", finalWordDocument::write); }, executor); + logMemoryUsage("AFTER_WORD_UPLOAD"); + CompletableFuture.allOf(zipUploadFuture, excelUploadFuture, wordUploadFuture).whenComplete((v, t) -> { - inspectionReport.setFilePath(basePath + reportName + ".xls"); - inspectionReportService.updateInspectionReport(inspectionReport); + try { + inspectionReport.setFilePath(basePath + reportName + ".xls"); + inspectionReportService.updateInspectionReport(inspectionReport); + } finally { + // 清理资源 + closeResources(finalSxssfWorkbook, finalWordDocument, tempFiles); + } }); } catch (Exception e) { + // 清理资源 + closeResources(sxssfWorkbook, wordDocument, tempFiles); e.printStackTrace(); } } public void exportExcelWordAndZipShaoxing(String lineId) { + logMemoryUsage("START exportExcelWordAndZipShaoxing"); List images = new ArrayList<>(); Map imgSrcToFileNameMap = new HashMap<>(); InspectionReport inspectionReport = @@ -1477,6 +1518,7 @@ public class PatrolTaskResultMainController extends BaseController { int imgIndex = 1; // 每个报告数据项下的图片索引 for (InspectionReportImg reportImg : reportImgList) { if (StringUtils.isNotEmpty(reportImg.getImgSrc()) && reportImg.getImgType().equals("0")) { + // /2/2026/06/21/1670/CCD/1670_20260621140000_2023923_02160558348953680127.jpg String originalImgSrc = reportImg.getImgSrc(); images.add(originalImgSrc); String pointName = reportData.getPointName(); @@ -1489,9 +1531,11 @@ public class PatrolTaskResultMainController extends BaseController { } // 生成唯一文件名(包含 lineId 防止重复) + // 2023923_极Ⅱ低Y/Y-A相换流变油面温度1_3212_100097_1.jpg String uniqueFileName = String.format("%s_%s_%s_%d%s", originalImgSrc.split("_")[2], - pointName, + pointName.replaceAll("[\\\\/:*?\"<>|]", "") + .replaceAll("\\s+", ""), // 空格, reportData.getLineId(), imgIndex, extension); @@ -1502,48 +1546,34 @@ public class PatrolTaskResultMainController extends BaseController { } } - Map streamHashMap = new HashMap<>(); - images.parallelStream().forEach(algorithmBaseImagePath -> { + // 将图片下载到临时文件,避免大量图片字节数据全部驻留内存 + Map tempFileMap = new LinkedHashMap<>(); + List tempFiles = new ArrayList<>(); + for (String imgSrc : images) { try { - sftpClient.downLoad(algorithmBaseImagePath, (inputStream) -> { - byte[] bytes = streamHashMap.get(algorithmBaseImagePath); - byte[] byInputStream = getStringByInputStream(inputStream); - if (bytes == null) { - streamHashMap.put(algorithmBaseImagePath, byInputStream); - } - }); + Path tempFile = downloadImageToTemp(imgSrc); + if (tempFile != null) { + tempFileMap.put(imgSrc, tempFile); + tempFiles.add(tempFile); + } } catch (Exception e) { - logger.info("报告归档下载图片失败: {}", algorithmBaseImagePath, e); - e.printStackTrace(); + logger.warn("报告归档下载图片失败: {}", imgSrc, e); } - }); -// for (String algorithmBaseImagePath : images) { -// try { -// sftpClient.downLoad(algorithmBaseImagePath, (inputStream) -> { -// byte[] bytes = streamHashMap.get(algorithmBaseImagePath); -// byte[] byInputStream = getStringByInputStream(inputStream); -// if (bytes == null) { -// streamHashMap.put(algorithmBaseImagePath, byInputStream); -// } -// -// }); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } -// images.parallelStream().forEach(imagePath -> { -// byte[] shm = streamHashMap.get(imagePath); -// byte[] bytes = downloadAndCompressImage(imagePath); -// if (shm == null) { -// streamHashMap.put(imagePath, bytes); -// } -// }); + } + logMemoryUsage("AFTER_IMAGE_DOWNLOAD"); - SXSSFWorkbook sxssfWorkbook = getSXSSFWorkbookShaoxing(inspectionReport, streamHashMap); - XWPFDocument wordDocument = getXWPFDocumentLingzhou(inspectionReport, streamHashMap); + SXSSFWorkbook sxssfWorkbook = null; + XWPFDocument wordDocument = null; logger.info("[ARCHIVE] hssfWorkbook: {}", sxssfWorkbook); + try { + sxssfWorkbook = getSXSSFWorkbookShaoxing(inspectionReport, tempFileMap); + logMemoryUsage("AFTER_EXCEL_CREATE"); + + wordDocument = getXWPFDocumentLingzhou(inspectionReport, tempFileMap); + logMemoryUsage("AFTER_WORD_CREATE"); + String basePath = this.basePath + stationCode + "/Model/"; Date startTime = inspectionReport.getInspectionStartTime() != null ? inspectionReport.getInspectionStartTime() : new Date(); @@ -1554,19 +1584,20 @@ public class PatrolTaskResultMainController extends BaseController { } else if ("0".equals(inspectionReport.getFilter())) { namePrefix = MessageUtils.get("大模型") + "_"; } + String reportName = inspectionReport.getInspectionTaskName() + "_" + timestamp; - // 上传媒体包 + + // 上传媒体包(直接从临时文件流式写入ZIP,不加载到内存) CompletableFuture zipUploadFuture = CompletableFuture.runAsync(() -> { uploadFile(basePath, reportName, ".zip", outputStream -> { try (ZipOutputStream zipOut = new ZipOutputStream(outputStream)) { - for (Map.Entry entry : streamHashMap.entrySet()) { + for (Map.Entry entry : tempFileMap.entrySet()) { String imagePath = entry.getKey(); String pointFileName = imgSrcToFileNameMap.getOrDefault(imagePath, "unnamed"); - byte[] imageData = entry.getValue(); -// String fileName = new File(imagePath).getName(); + Path tempFile = entry.getValue(); ZipEntry zipEntry = new ZipEntry(pointFileName); zipOut.putNextEntry(zipEntry); - zipOut.write(imageData); + Files.copy(tempFile, zipOut); zipOut.closeEntry(); } } catch (IOException e) { @@ -1574,23 +1605,97 @@ public class PatrolTaskResultMainController extends BaseController { } }); }, executor); + logMemoryUsage("AFTER_ZIP_UPLOAD"); + // 上传Excel + SXSSFWorkbook finalSxssfWorkbook = sxssfWorkbook; CompletableFuture excelUploadFuture = CompletableFuture.runAsync(() -> { - uploadFile(basePath, reportName, ".xls", sxssfWorkbook::write); + uploadFile(basePath, reportName, ".xls", finalSxssfWorkbook::write); }, executor); + logMemoryUsage("AFTER_EXCEL_UPLOAD"); + // 上传Word + XWPFDocument finalWordDocument = wordDocument; CompletableFuture wordUploadFuture = CompletableFuture.runAsync(() -> { - uploadFile(basePath, reportName, ".docx", wordDocument::write); + uploadFile(basePath, reportName, ".docx", finalWordDocument::write); }, executor); + logMemoryUsage("AFTER_WORD_UPLOAD"); + CompletableFuture.allOf(zipUploadFuture, excelUploadFuture, wordUploadFuture).whenComplete((v, t) -> { - inspectionReport.setFilePath(basePath + reportName + ".xls"); - inspectionReportService.updateInspectionReport(inspectionReport); + try { + inspectionReport.setFilePath(basePath + reportName + ".xls"); + inspectionReportService.updateInspectionReport(inspectionReport); + } finally { + closeResources(finalSxssfWorkbook, finalWordDocument, tempFiles); + } }); + } catch (Exception e) { logger.error("生成巡视文件异常:{}", e); + closeResources(sxssfWorkbook, wordDocument, tempFiles); + } + logMemoryUsage("END exportExcelWordAndZipShaoxing"); + } + + + + + private void closeResources(SXSSFWorkbook sxssfWorkbook, XWPFDocument wordDocument, List tempFiles) { + // 清理资源 + // 关闭 Excel + if (sxssfWorkbook != null) { + try { + sxssfWorkbook.close(); + logger.info("SXSSFWorkbook 已关闭"); + } catch (IOException e) { + logger.error("关闭SXSSFWorkbook失败", e); + } + } + if (wordDocument != null) { + try { + wordDocument.close(); + logger.info("XWPFDocument 已关闭"); + } catch (IOException e) { + logger.error("关闭XWPFDocument失败", e); + } + } + + for (Path tempFile : tempFiles) { + try { + Files.deleteIfExists(tempFile); + } catch (IOException e) { + logger.warn("删除临时文件失败: {}", tempFile, e); + } + } + } + + /** + * 下载图片并保存到临时文件,避免内存中持有大量图片字节数据 + */ + private Path downloadImageToTemp(String imagePath) { + try { + Path tempFile = Files.createTempFile("inspect_img_", ".jpg"); + sftpClient.downLoad(imagePath, inputStream -> { + Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING); + }); + return tempFile; + } catch (Exception e) { + logger.warn("图片下载到临时文件失败: {}", imagePath, e); + return null; } } + private void logMemoryUsage(String stage) { + Runtime runtime = Runtime.getRuntime(); + long maxMemory = runtime.maxMemory() / (1024 * 1024); + long totalMemory = runtime.totalMemory() / (1024 * 1024); + long freeMemory = runtime.freeMemory() / (1024 * 1024); + long usedMemory = totalMemory - freeMemory; + + logger.info("[MEMORY] Stage: {}, Max: {}MB, Total: {}MB, Used: {}MB, Free: {}MB", + stage, maxMemory, totalMemory, usedMemory, freeMemory); + } + private byte[] downloadAndCompressImage(String imagePath) { try { // 直接下载并压缩图片 @@ -3112,7 +3217,7 @@ public class PatrolTaskResultMainController extends BaseController { return wb; } - public SXSSFWorkbook getSXSSFWorkbookShaoxing(InspectionReport inspectionReport, Map inputStreamMap) { + public SXSSFWorkbook getSXSSFWorkbookShaoxing(InspectionReport inspectionReport, Map tempFileMap) { // 使用SXSSFWorkbook并设置行访问窗口大小(1000行) SXSSFWorkbook wb = new SXSSFWorkbook(1000); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); @@ -3518,7 +3623,8 @@ public class PatrolTaskResultMainController extends BaseController { cella1 = rowb1.createCell(12); if (crr.getImgType().equals("0")) { try { - insertImage(wb, patriarch, this.images(crr.getImg(), inputStreamMap), 8 + total, 0, 2); + byte[] imgData = Files.readAllBytes(tempFileMap.get(crr.getImg())); + insertImage(wb, patriarch, imgData, 8 + total, 0, 2); } catch (Exception var97) { var97.printStackTrace(); }