|
|
|
@ -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<Long, ExportCellAppearance> 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<BufferedImage> 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<List<String>> headerRows; |
|
|
|
private final List<List<String>> bodyRows; |
|
|
|
private final Map<Long, ExportCellAppearance> cellAppearances; |
|
|
|
|
|
|
|
private ProofTrackingExportSheet(List<List<String>> headerRows, |
|
|
|
List<List<String>> bodyRows, |
|
|
|
Map<Long, ExportCellAppearance> cellAppearances) { |
|
|
|
this.headerRows = headerRows; |
|
|
|
this.bodyRows = bodyRows; |
|
|
|
this.cellAppearances = cellAppearances; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|