From 5808505dabaf91546069a1d876c4d49556533b4e Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Mon, 19 Oct 2026 09:56:21 +0800 Subject: [PATCH] 20261019 --- .../SampleProofTrackingController.java | 20 ++ .../service/SampleProofTrackingService.java | 4 + .../impl/SampleProofTrackingServiceImpl.java | 324 +++++++++++++++--- 3 files changed, 301 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/xujie/sys/modules/sampletracking/controller/SampleProofTrackingController.java b/src/main/java/com/xujie/sys/modules/sampletracking/controller/SampleProofTrackingController.java index 86bddff9..3d7dd3af 100644 --- a/src/main/java/com/xujie/sys/modules/sampletracking/controller/SampleProofTrackingController.java +++ b/src/main/java/com/xujie/sys/modules/sampletracking/controller/SampleProofTrackingController.java @@ -67,6 +67,26 @@ public class SampleProofTrackingController { } } + @PostMapping("/exportPdf") + public void exportProofTrackingPdf(@RequestBody ProofTrackingData data, HttpServletResponse response) { + try { + sampleProofTrackingService.exportProofTrackingPdf(data, response); + } catch (Exception e) { + log.error("导出产品打样记录跟踪PDF失败: {}", e.getMessage(), e); + throw new RuntimeException("导出产品打样记录跟踪PDF失败: " + e.getMessage()); + } + } + + @PostMapping("/exportImage") + public void exportProofTrackingImage(@RequestBody ProofTrackingData data, HttpServletResponse response) { + try { + sampleProofTrackingService.exportProofTrackingImage(data, response); + } catch (Exception e) { + log.error("导出产品打样记录跟踪图片失败: {}", e.getMessage(), e); + throw new RuntimeException("导出产品打样记录跟踪图片失败: " + e.getMessage()); + } + } + @PostMapping("/oneKeyCreate") public R oneKeyCreate(@RequestBody ProofTrackingData data) { try { diff --git a/src/main/java/com/xujie/sys/modules/sampletracking/service/SampleProofTrackingService.java b/src/main/java/com/xujie/sys/modules/sampletracking/service/SampleProofTrackingService.java index 9c2d4116..099c36aa 100644 --- a/src/main/java/com/xujie/sys/modules/sampletracking/service/SampleProofTrackingService.java +++ b/src/main/java/com/xujie/sys/modules/sampletracking/service/SampleProofTrackingService.java @@ -44,6 +44,10 @@ public interface SampleProofTrackingService { void exportProofTracking(ProofTrackingData data, HttpServletResponse response); + void exportProofTrackingPdf(ProofTrackingData data, HttpServletResponse response); + + void exportProofTrackingImage(ProofTrackingData data, HttpServletResponse response); + List getProcessHistory(ProofTrackingData data); List queryBuProcessConfig(ProofTrackingData data); diff --git a/src/main/java/com/xujie/sys/modules/sampletracking/service/impl/SampleProofTrackingServiceImpl.java b/src/main/java/com/xujie/sys/modules/sampletracking/service/impl/SampleProofTrackingServiceImpl.java index 4a5e8fe8..54302162 100644 --- a/src/main/java/com/xujie/sys/modules/sampletracking/service/impl/SampleProofTrackingServiceImpl.java +++ b/src/main/java/com/xujie/sys/modules/sampletracking/service/impl/SampleProofTrackingServiceImpl.java @@ -37,6 +37,14 @@ import com.xujie.sys.modules.sampletracking.service.SampleProofTrackingService; import com.xujie.sys.modules.sampletracking.service.SampleTrackingService; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.OutputStream; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; @@ -87,6 +95,11 @@ public class SampleProofTrackingServiceImpl implements SampleProofTrackingServic private static final String PROOFING_STATUS_SCRAPPED = "报废"; private static final String NPI_PROOFING_STATUS_FINISHED = "完成"; private static final int NPI_ID_THRESHOLD = 100000; + private static final String PROOF_TRACKING_PDF_FONT = "Microsoft YaHei"; + /** 列按内容放宽,超过这个宽度就换行,Comments 与物料描述使用同一规则。 */ + private static final double PDF_MAX_COLUMN_WIDTH = 36D; + private static final double PDF_COLUMN_WIDTH_PADDING = 2D; + private static volatile boolean proofTrackingPdfFontsRegistered; /** * 工序定义兜底:当配置表未初始化或查询异常时,仍保持页面可用。 @@ -131,6 +144,51 @@ public class SampleProofTrackingServiceImpl implements SampleProofTrackingServic @Override public void exportProofTracking(ProofTrackingData data, HttpServletResponse response) { + ProofTrackingExportSheet sheet = buildProofTrackingExportSheet(data); + String fileName = buildProofTrackingExportFileName(); + try { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); + writeProofTrackingWorkbook(response.getOutputStream(), sheet, false); + response.flushBuffer(); + } catch (Exception e) { + log.error("导出产品打样记录跟踪失败: {}", e.getMessage(), e); + throw new RuntimeException("导出产品打样记录跟踪失败: " + e.getMessage()); + } + } + + @Override + public void exportProofTrackingPdf(ProofTrackingData data, HttpServletResponse response) { + // PDF 直接转换同一份工作簿,列、单元格文字和颜色与 Excel 导出保持一致。 + ProofTrackingExportSheet sheet = buildProofTrackingExportSheet(data); + String fileName = buildProofTrackingExportFileName(); + try { + ByteArrayOutputStream excelBuffer = new ByteArrayOutputStream(); + writeProofTrackingWorkbook(excelBuffer, sheet, true); + writeProofTrackingPdfResponse(response, excelBuffer.toByteArray(), fileName); + } catch (Exception e) { + log.error("导出产品打样记录跟踪PDF失败: {}", e.getMessage(), e); + throw new RuntimeException("导出产品打样记录跟踪PDF失败: " + e.getMessage()); + } + } + + @Override + public void exportProofTrackingImage(ProofTrackingData data, HttpServletResponse response) { + // 图片使用和 PDF 相同的工作簿与列宽,避免两套导出内容不一致。 + ProofTrackingExportSheet sheet = buildProofTrackingExportSheet(data); + String fileName = buildProofTrackingExportFileName(); + try { + ByteArrayOutputStream excelBuffer = new ByteArrayOutputStream(); + writeProofTrackingWorkbook(excelBuffer, sheet, true); + writeProofTrackingImageResponse(response, excelBuffer.toByteArray(), fileName); + } catch (Exception e) { + log.error("导出产品打样记录跟踪图片失败: {}", e.getMessage(), e); + throw new RuntimeException("导出产品打样记录跟踪图片失败: " + e.getMessage()); + } + } + + private ProofTrackingExportSheet buildProofTrackingExportSheet(ProofTrackingData data) { if (data == null) { throw new RuntimeException("导出参数不能为空"); } @@ -173,53 +231,225 @@ public class SampleProofTrackingServiceImpl implements SampleProofTrackingServic ); Map cellAppearances = collectExportCellAppearances( exportRows, trackingColumns, bodyRows, completedProcessCellKeySet, exportHeaderRowCount); - try { - String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); - String fileName = URLEncoder.encode("项目打样跟踪_" + timestamp, StandardCharsets.UTF_8.name()) - .replaceAll("\\+", "%20"); - response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); - response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); - // 业务要求:表头不使用底色,避免默认灰底影响业务二次标注。 - WriteCellStyle headCellStyle = new WriteCellStyle(); - - // 表头字体大小固定为 12,确保与业务模板一致。 - WriteFont headWriteFont = new WriteFont(); - headWriteFont.setFontHeightInPoints((short) 12); - headCellStyle.setWriteFont(headWriteFont); - -// 表头背景色:无填充 - headCellStyle.setFillPatternType(FillPatternType.NO_FILL); - -// 边框:全部取消 - headCellStyle.setBorderLeft(BorderStyle.NONE); - headCellStyle.setBorderRight(BorderStyle.NONE); - headCellStyle.setBorderTop(BorderStyle.NONE); - headCellStyle.setBorderBottom(BorderStyle.NONE); - - WriteCellStyle contentCellStyle = new WriteCellStyle(); - contentCellStyle.setBorderLeft(BorderStyle.NONE); - contentCellStyle.setBorderRight(BorderStyle.NONE); - contentCellStyle.setBorderTop(BorderStyle.NONE); - contentCellStyle.setBorderBottom(BorderStyle.NONE); - - HorizontalCellStyleStrategy noHeaderBackgroundStyle = - new HorizontalCellStyleStrategy(headCellStyle, contentCellStyle); - ExcelWriterBuilder writerBuilder = EasyExcel.write(response.getOutputStream()) - .autoCloseStream(false) - .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) - .registerWriteHandler(noHeaderBackgroundStyle); - if (!cellAppearances.isEmpty()) { - writerBuilder.registerWriteHandler(new ProofTrackingCellStyleHandler(cellAppearances)); - } - writerBuilder - .head(headerRows) - .sheet("项目打样跟踪") - .doWrite(bodyRows); - response.flushBuffer(); - } catch (Exception e) { - log.error("导出产品打样记录跟踪失败: {}", e.getMessage(), e); - throw new RuntimeException("导出产品打样记录跟踪失败: " + e.getMessage()); + return new ProofTrackingExportSheet(headerRows, bodyRows, cellAppearances); + } + + private String buildProofTrackingExportFileName() { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + return URLEncoder.encode("项目打样跟踪_" + timestamp, StandardCharsets.UTF_8) + .replaceAll("\\+", "%20"); + } + + private void writeProofTrackingWorkbook(OutputStream outputStream, + ProofTrackingExportSheet sheet, + boolean cjkFont) { + // 业务要求:表头不使用底色,避免默认灰底影响业务二次标注。 + WriteCellStyle headCellStyle = new WriteCellStyle(); + + // 表头字体大小固定为 12,确保与业务模板一致。 + WriteFont headWriteFont = new WriteFont(); + headWriteFont.setFontHeightInPoints((short) 12); + if (cjkFont) { + headWriteFont.setFontName(PROOF_TRACKING_PDF_FONT); + } + headCellStyle.setWriteFont(headWriteFont); + + // 表头背景色:无填充 + headCellStyle.setFillPatternType(FillPatternType.NO_FILL); + + // 边框:全部取消 + headCellStyle.setBorderLeft(BorderStyle.NONE); + headCellStyle.setBorderRight(BorderStyle.NONE); + headCellStyle.setBorderTop(BorderStyle.NONE); + headCellStyle.setBorderBottom(BorderStyle.NONE); + + WriteCellStyle contentCellStyle = new WriteCellStyle(); + contentCellStyle.setBorderLeft(BorderStyle.NONE); + contentCellStyle.setBorderRight(BorderStyle.NONE); + contentCellStyle.setBorderTop(BorderStyle.NONE); + contentCellStyle.setBorderBottom(BorderStyle.NONE); + if (cjkFont) { + // 默认 Calibri 没有中文字形,PDF 转换会变成方框;只在 PDF 这条链路指定中文字体。 + WriteFont contentWriteFont = new WriteFont(); + contentWriteFont.setFontName(PROOF_TRACKING_PDF_FONT); + contentCellStyle.setWriteFont(contentWriteFont); + } + + HorizontalCellStyleStrategy noHeaderBackgroundStyle = + new HorizontalCellStyleStrategy(headCellStyle, contentCellStyle); + ExcelWriterBuilder writerBuilder = EasyExcel.write(outputStream) + .autoCloseStream(false) + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .registerWriteHandler(noHeaderBackgroundStyle); + if (sheet.cellAppearances != null && !sheet.cellAppearances.isEmpty()) { + writerBuilder.registerWriteHandler(new ProofTrackingCellStyleHandler(sheet.cellAppearances)); + } + writerBuilder + .head(sheet.headerRows) + .sheet("项目打样跟踪") + .doWrite(sheet.bodyRows); + } + + private void writeProofTrackingPdfResponse(HttpServletResponse response, byte[] excelBytes, String fileName) throws Exception { + registerProofTrackingPdfFonts(); + com.aspose.cells.Workbook workbook = new com.aspose.cells.Workbook(new ByteArrayInputStream(excelBytes)); + com.aspose.cells.Worksheet worksheet = workbook.getWorksheets().get(0); + layoutProofTrackingPdfColumns(worksheet); + com.aspose.cells.PageSetup pageSetup = worksheet.getPageSetup(); + pageSetup.setOrientation(com.aspose.cells.PageOrientationType.LANDSCAPE); + pageSetup.setPaperSize(com.aspose.cells.PaperSizeType.PAPER_A_3); + // 高度仍按 A3 横向分页;列宽不在这里等比缩小,避免文字被裁切。 + pageSetup.setFitToPages(0, 0); + pageSetup.setLeftMargin(0.3); + pageSetup.setRightMargin(0.3); + pageSetup.setTopMargin(0.4); + pageSetup.setBottomMargin(0.4); + com.aspose.cells.PdfSaveOptions saveOptions = new com.aspose.cells.PdfSaveOptions(); + saveOptions.setDefaultFont(PROOF_TRACKING_PDF_FONT); + // 所有列放进同一页宽,页宽随列展开;行数超出页高时再向下分页。 + saveOptions.setOnePagePerSheet(false); + saveOptions.setAllColumnsInOnePagePerSheet(true); + ByteArrayOutputStream pdfBuffer = new ByteArrayOutputStream(); + workbook.save(pdfBuffer, saveOptions); + response.setContentType("application/pdf"); + response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".pdf"); + response.getOutputStream().write(pdfBuffer.toByteArray()); + response.flushBuffer(); + } + + private void writeProofTrackingImageResponse(HttpServletResponse response, byte[] excelBytes, String fileName) throws Exception { + registerProofTrackingPdfFonts(); + com.aspose.cells.Workbook workbook = new com.aspose.cells.Workbook(new ByteArrayInputStream(excelBytes)); + com.aspose.cells.Worksheet worksheet = workbook.getWorksheets().get(0); + layoutProofTrackingPdfColumns(worksheet); + com.aspose.cells.ImageOrPrintOptions options = new com.aspose.cells.ImageOrPrintOptions(); + options.setImageType(com.aspose.cells.ImageType.PNG); + options.setDefaultFont(PROOF_TRACKING_PDF_FONT); + options.setOnlyArea(true); + // 96 DPI 放大后发虚;按 2 倍分辨率绘制,文字边缘更清楚。 + options.setHorizontalResolution(192); + options.setVerticalResolution(192); + options.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING, java.awt.RenderingHints.VALUE_RENDER_QUALITY); + options.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON); + options.setRenderingHint(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING, java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + options.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC); + // 整表画成一张图,列和换行规则与 PDF 相同,不再按页拆开。 + options.setOnePagePerSheet(true); + options.setAllColumnsInOnePagePerSheet(true); + byte[] imageBytes = renderProofTrackingSheetPng(worksheet, options); + response.setContentType("image/png"); + response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".png"); + response.getOutputStream().write(imageBytes); + response.flushBuffer(); + } + + private byte[] renderProofTrackingSheetPng(com.aspose.cells.Worksheet worksheet, + com.aspose.cells.ImageOrPrintOptions options) throws Exception { + com.aspose.cells.SheetRender render = new com.aspose.cells.SheetRender(worksheet, options); + int pageCount = render.getPageCount(); + if (pageCount <= 0) { + throw new RuntimeException("没有可导出的图片内容"); + } + if (pageCount == 1) { + ByteArrayOutputStream imageBuffer = new ByteArrayOutputStream(); + render.toImage(0, imageBuffer); + return imageBuffer.toByteArray(); + } + // 行数过多被拆成多页时,按原顺序纵向拼成一张图,内容和 PDF 保持完整。 + List pageImages = new ArrayList<>(); + int imageWidth = 0; + int imageHeight = 0; + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) { + ByteArrayOutputStream pageBuffer = new ByteArrayOutputStream(); + render.toImage(pageIndex, pageBuffer); + BufferedImage pageImage = ImageIO.read(new ByteArrayInputStream(pageBuffer.toByteArray())); + if (pageImage == null) { + throw new RuntimeException("导出图片失败"); + } + pageImages.add(pageImage); + imageWidth = Math.max(imageWidth, pageImage.getWidth()); + imageHeight += pageImage.getHeight(); + } + BufferedImage combinedImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = combinedImage.createGraphics(); + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, imageWidth, imageHeight); + int offsetY = 0; + for (BufferedImage pageImage : pageImages) { + graphics.drawImage(pageImage, 0, offsetY, null); + offsetY += pageImage.getHeight(); + } + graphics.dispose(); + ByteArrayOutputStream imageBuffer = new ByteArrayOutputStream(); + if (!ImageIO.write(combinedImage, "png", imageBuffer)) { + throw new RuntimeException("导出图片失败"); + } + return imageBuffer.toByteArray(); + } + + private void layoutProofTrackingPdfColumns(com.aspose.cells.Worksheet worksheet) throws Exception { + com.aspose.cells.Cells cells = worksheet.getCells(); + int maxRow = cells.getMaxDataRow(); + int maxCol = cells.getMaxDataColumn(); + if (maxRow < 0 || maxCol < 0) { + return; + } + com.aspose.cells.Style wrapStyle = worksheet.getWorkbook().createStyle(); + wrapStyle.setTextWrapped(true); + wrapStyle.setVerticalAlignment(com.aspose.cells.TextAlignmentType.TOP); + com.aspose.cells.StyleFlag wrapFlag = new com.aspose.cells.StyleFlag(); + wrapFlag.setWrapText(true); + wrapFlag.setVerticalAlignment(true); + cells.createRange(0, 0, maxRow + 1, maxCol + 1).applyStyle(wrapStyle, wrapFlag); + + worksheet.autoFitColumns(); + for (int columnIndex = 0; columnIndex <= maxCol; columnIndex++) { + double fittedWidth = cells.getColumnWidth(columnIndex) + PDF_COLUMN_WIDTH_PADDING; + if (fittedWidth > PDF_MAX_COLUMN_WIDTH) { + fittedWidth = PDF_MAX_COLUMN_WIDTH; + } + cells.setColumnWidth(columnIndex, fittedWidth); + } + // 列宽封顶后必须重算行高,换行后的文字才会完整落在单元格里。 + worksheet.autoFitRows(); + } + + private void registerProofTrackingPdfFonts() { + if (proofTrackingPdfFontsRegistered) { + return; + } + synchronized (SampleProofTrackingServiceImpl.class) { + if (proofTrackingPdfFontsRegistered) { + return; + } + String windir = System.getenv("WINDIR"); + if (windir != null) { + File windowsFonts = new File(windir, "Fonts"); + if (windowsFonts.isDirectory()) { + com.aspose.cells.FontConfigs.setFontFolder(windowsFonts.getAbsolutePath(), true); + } + } + File linuxFonts = new File("/usr/share/fonts"); + if (linuxFonts.isDirectory()) { + com.aspose.cells.FontConfigs.setFontFolder(linuxFonts.getAbsolutePath(), true); + } + com.aspose.cells.FontConfigs.setFontSubstitutes(PROOF_TRACKING_PDF_FONT, + new String[]{"微软雅黑", "SimSun", "Noto Sans CJK SC", "WenQuanYi Micro Hei"}); + proofTrackingPdfFontsRegistered = true; + } + } + + private static final class ProofTrackingExportSheet { + private final List> headerRows; + private final List> bodyRows; + private final Map cellAppearances; + + private ProofTrackingExportSheet(List> headerRows, + List> bodyRows, + Map cellAppearances) { + this.headerRows = headerRows; + this.bodyRows = bodyRows; + this.cellAppearances = cellAppearances; } }