报告查询,出库检验报告样式优化以及改为PDF

This commit is contained in:
hayu 2025-11-07 16:40:54 +08:00
parent a1ab7cda31
commit 6635f25c6f
4 changed files with 307 additions and 184 deletions

View File

@ -118,6 +118,17 @@
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.27</version>
</dependency>
</dependencies>
<build>

View File

@ -635,7 +635,7 @@ public class BmReportController extends BaseController {
// 1. 生成合并的出库检验报告
byte[] mergedReport = DocxUtil.generateReportByList(items);
addToZip(zipOut, baseDatePath + "出库检验报告.docx", mergedReport);
addToZip(zipOut, baseDatePath + "出库检验报告.pdf", mergedReport);
// 2. 遍历每个类型-规格创建文件夹并添加附加文件
Map<String, List<DownloadRequest.ItemInfo>> typeMap = items.stream()
@ -688,9 +688,9 @@ public class BmReportController extends BaseController {
String matFolder = sanitize(item.getTypeName() + "-" + item.getTypeModelName());
String basePath = projectFolder + "/" + matFolder + "/";
// 1. 生成出库检验报告.docx
byte[] docBytes = DocxUtil.generateReport(item);
addToZip(zipOut, basePath + "出库检验报告.docx", docBytes);
// 1. 生成出库检验报告.pdf
byte[] pdfBytes = DocxUtil.generateReport(item);
addToZip(zipOut, basePath + "出库检验报告.pdf", pdfBytes);
// 2. 附加各类报告文件
addFileIfExists(zipOut, basePath, "合格证", item.getQualifiedUrl());

View File

@ -1,12 +1,17 @@
package com.bonus.material.common.utils;
import com.bonus.common.core.utils.poi.PdfUtil;
import com.bonus.material.basic.domain.report.DownloadRequest;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.util.Units;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import java.io.*;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
@ -16,96 +21,97 @@ import java.util.List;
public class DocxUtil {
public static byte[] generateReport(DownloadRequest.ItemInfo item) {
try (XWPFDocument doc = new XWPFDocument(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
try (PDDocument document = new PDDocument();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
// ===== 页面方向设为横向 =====
CTSectPr sectPr = doc.getDocument().getBody().addNewSectPr();
CTPageSz pageSize = sectPr.addNewPgSz();
pageSize.setOrient(STPageOrientation.LANDSCAPE);
pageSize.setW(BigInteger.valueOf(16840)); // A4横向宽
pageSize.setH(BigInteger.valueOf(11900)); // A4横向高
PDType0Font font = PDType0Font.load(document,
PdfUtil.class.getClassLoader().getResourceAsStream("fonts/syht.ttf"));
// ===== 标题 =====
XWPFParagraph title = doc.createParagraph();
title.setAlignment(ParagraphAlignment.CENTER);
XWPFRun runTitle = title.createRun();
runTitle.setText("施工机具设备出库检验记录表");
runTitle.setBold(true);
runTitle.setFontFamily("宋体");
runTitle.setFontSize(16);
PDRectangle pageSize = new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth());
float margin = 50;
float yStart = pageSize.getHeight() - margin;
float yPosition = yStart;
// ===== 工程单位信息 =====
XWPFParagraph info = doc.createParagraph();
info.setAlignment(ParagraphAlignment.LEFT);
XWPFRun runInfo = info.createRun();
runInfo.setFontFamily("宋体");
runInfo.setFontSize(12);
runInfo.setText("领用工程:" + safe(item.getProName()) + " 使用单位:" + safe(item.getDepartName()));
runInfo.addBreak();
// ===== 表格1标题行 + 数据行 =====
int colNum = 12;
XWPFTable table = doc.createTable(1, colNum);
table.setWidth("100%");
// 表头文字
float[] colWidths = {80, 80, 40, 40, 80, 70, 70, 75, 65, 80, 60, 40};
String[] headers = {
"机具名称", "规格型号", "单位", "数量", "设备编码",
"额定载荷KN", "试验载荷KN", "持荷时间min",
"试验日期", "下次试验日期", "检验结论", "备注"
};
// 设置表头样式
XWPFTableRow headerRow = table.getRow(0);
for (int i = 0; i < headers.length; i++) {
XWPFTableCell cell = headerRow.getCell(i);
setCellText(cell, headers[i], true);
String[][] rows = {
{
safe(item.getTypeName()), safe(item.getTypeModelName()), safe(item.getUnit()),
safe(String.valueOf(item.getNum())), safe(item.getMaCode()),
safe(item.getRatedLoad()), safe(item.getTestLoad()), safe(item.getHoldingTime()),
safe(item.getTestTime()), safe(item.getNextTestTime()),
safe(item.getCheckResult()), safe(item.getRemark())
}
// 数据行
XWPFTableRow dataRow = table.createRow();
String[] values = {
safe(item.getTypeName()),
safe(item.getTypeModelName()),
safe(item.getUnit()),
safe(String.valueOf(item.getNum())),
safe(item.getMaCode()),
safe(item.getRatedLoad()),
safe(item.getTestLoad()),
safe(item.getHoldingTime()),
safe(item.getTestTime()),
safe(item.getNextTestTime()),
safe(item.getCheckResult()),
safe(item.getRemark())
};
for (int i = 0; i < colNum; i++) {
XWPFTableCell cell = dataRow.getCell(i);
setCellText(cell, values[i], false);
PDPage page = new PDPage(pageSize);
document.addPage(page);
PDPageContentStream content = new PDPageContentStream(document, page);
// ===== 标题 =====
String titleText = "施工机具设备出库检验记录表";
content.beginText();
content.setFont(font, 16);
content.newLineAtOffset((pageSize.getWidth() - getStringWidth(titleText, font, 16)) / 2, yPosition);
content.showText(titleText);
content.endText();
yPosition -= 50;
// ===== 工程与单位 =====
content.beginText();
content.setFont(font, 12);
content.newLineAtOffset(margin, yPosition);
content.showText("领用工程:" + safe(item.getProName()));
content.newLineAtOffset(0, -20);
content.showText("使用单位:" + safe(item.getDepartName()));
content.endText();
yPosition -= 40;
// ===== 表格绘制方法 =====
float tableWidth = 0;
for (float w : colWidths) {
tableWidth += w;
}
float tableBottomMargin = 100;
float rowMargin = 5;
float fontSize = 10;
// ===== 检验单位 =====
XWPFParagraph footer = doc.createParagraph();
footer.setAlignment(ParagraphAlignment.LEFT);
footer.setSpacingBefore(400);
XWPFRun runFooter = footer.createRun();
runFooter.setFontFamily("宋体");
runFooter.setFontSize(12);
runFooter.setText("检验单位:");
// 绘制表头 + 数据
yPosition = drawTable(document, page, content, font, margin, yPosition, tableWidth, colWidths,
headers, rows, fontSize, rowMargin, pageSize, yStart, tableBottomMargin);
// ===== 插入印章图片 =====
// 使用类路径读取图片
InputStream is = DocxUtil.class.getClassLoader().getResourceAsStream("template/gaizhang.png");
if (is == null) {
throw new FileNotFoundException("找不到资源template/gaizhang.png");
// ===== 检验单位与印章 =====
yPosition -= 60;
content.beginText();
content.setFont(font, 12);
content.newLineAtOffset(margin, yPosition);
content.showText("检验单位:");
content.endText();
InputStream is = PdfUtil.class.getClassLoader().getResourceAsStream("template/gaizhang.png");
if (is != null) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int nRead;
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
byte[] imageBytes = buffer.toByteArray();
// 在同一个 Run 后面插入图片
runFooter.addPicture(is, XWPFDocument.PICTURE_TYPE_PNG, "gaizhang.png", Units.toEMU(100), Units.toEMU(100));
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "gaizhang");
content.drawImage(image, margin + 70, yPosition - 60, 100, 100);
is.close();
}
doc.write(bos);
content.close();
document.save(bos);
return bos.toByteArray();
} catch (Exception e) {
@ -114,62 +120,142 @@ public class DocxUtil {
}
}
/** 单元格通用设置 */
private static void setCellText(XWPFTableCell cell, String text, boolean isHeader) {
XWPFParagraph p = cell.getParagraphs().get(0);
p.setAlignment(ParagraphAlignment.CENTER);
XWPFRun run = p.createRun();
run.setFontFamily("宋体");
run.setFontSize(10);
run.setText(text == null ? "" : text);
if (isHeader) {
run.setBold(true);
}
// 设置单元格宽度
cell.getCTTc().addNewTcPr().addNewTcW().setW(BigInteger.valueOf(1200));
private static String safe(String v) {
return v == null ? "" : v;
}
private static String safe(String s) {
return s == null ? "" : s;
private static float getStringWidth(String text, PDFont font, float fontSize) throws IOException {
return font.getStringWidth(text) / 1000 * fontSize;
}
/**
* 读取图片文件为 byte 数组
* @param file 图片文件
* @return 图片的字节数据
* 自动换行表格绘制
*/
private static byte[] getImageBytes(File file) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (FileInputStream fileInputStream = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int length;
while ((length = fileInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
private static float drawTable(PDDocument doc, PDPage page, PDPageContentStream content,
PDFont font, float startX, float startY, float tableWidth,
float[] colWidths, String[] headers, String[][] rows,
float fontSize, float cellMargin, PDRectangle pageSize,
float yStart, float bottomMargin) throws IOException {
float y = startY;
float pageHeight = pageSize.getHeight();
float usableHeight = pageHeight - bottomMargin;
// 绘制表头
y = drawRow(doc, page, content, font, startX, y, colWidths, headers, fontSize, cellMargin, true);
for (String[] row : rows) {
float rowHeight = getRowHeight(row, font, fontSize, colWidths, cellMargin);
if (y - rowHeight < bottomMargin) {
content.close();
PDPage newPage = new PDPage(pageSize);
doc.addPage(newPage);
content = new PDPageContentStream(doc, newPage);
y = yStart;
// 重绘表头
y = drawRow(doc, newPage, content, font, startX, y, colWidths, headers, fontSize, cellMargin, true);
}
y = drawRow(doc, page, content, font, startX, y, colWidths, row, fontSize, cellMargin, false);
}
return byteArrayOutputStream.toByteArray();
return y;
}
/**
* 根据文件扩展名获取图片类型
* @param fileName 文件名
* @return 图片类型
* 绘制一行支持自动换行 + 表头加粗变黑
*/
private static int getPictureType(String fileName) {
String ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
switch (ext) {
case "png":
return XWPFDocument.PICTURE_TYPE_PNG;
case "jpeg":
case "jpg":
return XWPFDocument.PICTURE_TYPE_JPEG;
case "gif":
return XWPFDocument.PICTURE_TYPE_GIF;
default:
throw new IllegalArgumentException("Unsupported image type: " + ext);
}
private static float drawRow(PDDocument doc, PDPage page, PDPageContentStream content,
PDFont font, float startX, float startY, float[] colWidths,
String[] texts, float fontSize, float cellMargin, boolean header) throws IOException {
float y = startY;
float maxHeight = getRowHeight(texts, font, fontSize, colWidths, cellMargin);
float x = startX;
for (int i = 0; i < texts.length; i++) {
float cellWidth = colWidths[i];
// ===== 表头灰底 =====
if (header) {
// 灰色
content.setNonStrokingColor(220, 220, 220);
content.addRect(x, y - maxHeight, cellWidth, maxHeight);
content.fill();
}
// 绘制单元格边框
// 黑色
content.setNonStrokingColor(0, 0, 0);
content.addRect(x, y - maxHeight, cellWidth, maxHeight);
content.stroke();
List<String> lines = wrapText(texts[i], font, fontSize, cellWidth - 2 * cellMargin);
float textY = y - cellMargin - fontSize;
for (String line : lines) {
content.beginText();
// 黑色字体
content.setNonStrokingColor(0, 0, 0);
content.setFont(font, header ? fontSize + 1.5f : fontSize);
// ===== 居中计算 =====
float textWidth = font.getStringWidth(line) / 1000 * (header ? fontSize + 1.5f : fontSize);
float textX = x + (cellWidth - textWidth) / 2;
content.newLineAtOffset(textX, textY);
content.showText(line);
content.endText();
textY -= fontSize + 2;
}
x += cellWidth;
}
return y - maxHeight;
}
/**
* 计算行高取最长文本行数
*/
private static float getRowHeight(String[] texts, PDFont font, float fontSize, float[] colWidths, float cellMargin) throws IOException {
float max = 0;
for (int i = 0; i < texts.length; i++) {
List<String> lines = wrapText(texts[i], font, fontSize, colWidths[i] - 2 * cellMargin);
max = Math.max(max, lines.size());
}
return (fontSize + 2) * max + 2 * cellMargin;
}
/**
* 文本自动换行
*/
private static List<String> wrapText(String text, PDFont font, float fontSize, float width) throws IOException {
List<String> lines = new ArrayList<>();
if (text == null || text.isEmpty()) {
lines.add("");
return lines;
}
StringBuilder line = new StringBuilder();
for (char c : text.toCharArray()) {
String temp = line.toString() + c;
float textWidth = font.getStringWidth(temp) / 1000 * fontSize;
if (textWidth > width) {
lines.add(line.toString());
line = new StringBuilder(String.valueOf(c));
} else {
line.append(c);
}
}
lines.add(line.toString());
return lines;
}
public static byte[] generateReportByList(List<DownloadRequest.ItemInfo> items) {
if (items == null || items.isEmpty()) {
return new byte[0];
@ -177,74 +263,99 @@ public class DocxUtil {
DownloadRequest.ItemInfo first = items.get(0);
try (XWPFDocument doc = new XWPFDocument(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
// 页面横向
CTSectPr sectPr = doc.getDocument().getBody().addNewSectPr();
CTPageSz pageSize = sectPr.addNewPgSz();
pageSize.setOrient(STPageOrientation.LANDSCAPE);
pageSize.setW(BigInteger.valueOf(16840));
pageSize.setH(BigInteger.valueOf(11900));
try (PDDocument document = new PDDocument();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
// 标题
XWPFParagraph title = doc.createParagraph();
title.setAlignment(ParagraphAlignment.CENTER);
XWPFRun runTitle = title.createRun();
runTitle.setText("施工机具设备出库检验记录表");
runTitle.setBold(true);
runTitle.setFontFamily("宋体");
runTitle.setFontSize(16);
PDType0Font font = PDType0Font.load(document,
PdfUtil.class.getClassLoader().getResourceAsStream("fonts/syht.ttf"));
// 工程单位
XWPFParagraph info = doc.createParagraph();
info.setAlignment(ParagraphAlignment.LEFT);
XWPFRun runInfo = info.createRun();
runInfo.setFontFamily("宋体");
runInfo.setFontSize(12);
runInfo.setText("领用工程:" + safe(first.getProName()) + " 使用单位:" + safe(first.getDepartName()));
runInfo.addBreak();
// 表格
int colNum = 12;
XWPFTable table = doc.createTable(1, colNum);
table.setWidth("100%");
PDRectangle pageSize = new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth());
float margin = 50;
float yStart = pageSize.getHeight() - margin;
float yPosition = yStart;
float[] colWidths = {80, 80, 40, 40, 80, 70, 70, 75, 65, 80, 60, 40};
String[] headers = {
"机具名称", "规格型号", "单位", "数量", "设备编码",
"额定载荷KN", "试验载荷KN", "持荷时间min",
"试验日期", "下次试验日期", "检验结论", "备注"
};
XWPFTableRow headerRow = table.getRow(0);
for (int i = 0; i < headers.length; i++) {
setCellText(headerRow.getCell(i), headers[i], true);
}
for (DownloadRequest.ItemInfo item : items) {
XWPFTableRow row = table.createRow();
String[] values = {
safe(item.getTypeName()), safe(item.getTypeModelName()), safe(item.getUnit()), safe(String.valueOf(item.getNum())),
safe(item.getMaCode()), safe(item.getRatedLoad()), safe(item.getTestLoad()), safe(item.getHoldingTime()),
safe(item.getTestTime()), safe(item.getNextTestTime()), safe(item.getCheckResult()), safe(item.getRemark())
// 数据转为二维数组
String[][] rows = new String[items.size()][headers.length];
for (int i = 0; i < items.size(); i++) {
DownloadRequest.ItemInfo item = items.get(i);
rows[i] = new String[]{
safe(item.getTypeName()), safe(item.getTypeModelName()), safe(item.getUnit()),
safe(String.valueOf(item.getNum())), safe(item.getMaCode()),
safe(item.getRatedLoad()), safe(item.getTestLoad()), safe(item.getHoldingTime()),
safe(item.getTestTime()), safe(item.getNextTestTime()),
safe(item.getCheckResult()), safe(item.getRemark())
};
for (int i = 0; i < colNum; i++) {
setCellText(row.getCell(i), values[i], false);
}
}
// 检验单位 + 印章
XWPFParagraph footer = doc.createParagraph();
footer.setAlignment(ParagraphAlignment.LEFT);
footer.setSpacingBefore(400);
XWPFRun runFooter = footer.createRun();
runFooter.setFontFamily("宋体");
runFooter.setFontSize(12);
runFooter.setText("检验单位:");
PDPage page = new PDPage(pageSize);
document.addPage(page);
PDPageContentStream content = new PDPageContentStream(document, page);
InputStream is = DocxUtil.class.getClassLoader().getResourceAsStream("template/gaizhang.png");
// ===== 标题 =====
String titleText = "施工机具设备出库检验记录表";
content.beginText();
content.setFont(font, 16);
content.newLineAtOffset((pageSize.getWidth() - getStringWidth(titleText, font, 16)) / 2, yPosition);
content.showText(titleText);
content.endText();
yPosition -= 50;
// ===== 工程与单位 =====
content.beginText();
content.setFont(font, 12);
content.newLineAtOffset(margin, yPosition);
content.showText("领用工程:" + safe(first.getProName()));
content.newLineAtOffset(0, -20);
content.showText("使用单位:" + safe(first.getDepartName()));
content.endText();
yPosition -= 40;
// ===== 表格绘制多行 =====
float tableWidth = 0;
for (float w : colWidths) {
tableWidth += w;
}
float bottomMargin = 100;
float rowMargin = 5;
float fontSize = 10;
yPosition = drawTable(document, page, content, font, margin, yPosition, tableWidth,
colWidths, headers, rows, fontSize, rowMargin, pageSize, yStart, bottomMargin);
// ===== 检验单位与印章 =====
yPosition -= 60;
content.beginText();
content.setFont(font, 12);
content.newLineAtOffset(margin, yPosition);
content.showText("检验单位:");
content.endText();
InputStream is = PdfUtil.class.getClassLoader().getResourceAsStream("template/gaizhang.png");
if (is != null) {
runFooter.addPicture(is, XWPFDocument.PICTURE_TYPE_PNG, "gaizhang.png", Units.toEMU(100), Units.toEMU(100));
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int nRead;
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
byte[] imageBytes = buffer.toByteArray();
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "gaizhang");
content.drawImage(image, margin + 70, yPosition - 60, 100, 100);
is.close();
}
doc.write(bos);
content.close();
document.save(bos);
return bos.toByteArray();
} catch (Exception e) {
@ -253,4 +364,5 @@ public class DocxUtil {
}
}
}