报告一键下载功能优化

This commit is contained in:
hayu 2025-12-15 10:37:06 +08:00
parent 59d22c923a
commit a065b8ecde
2 changed files with 649 additions and 196 deletions

View File

@ -4,11 +4,11 @@ import cn.hutool.core.convert.Convert;
import com.alibaba.nacos.common.utils.CollectionUtils; import com.alibaba.nacos.common.utils.CollectionUtils;
import com.bonus.common.biz.config.ListPagingUtil; import com.bonus.common.biz.config.ListPagingUtil;
import com.bonus.common.biz.utils.StringHelper; import com.bonus.common.biz.utils.StringHelper;
import com.bonus.common.core.utils.DateUtils;
import com.bonus.common.core.utils.ServletUtils; import com.bonus.common.core.utils.ServletUtils;
import com.bonus.common.core.utils.poi.ExcelUtil; import com.bonus.common.core.utils.poi.ExcelUtil;
import com.bonus.common.core.web.controller.BaseController; import com.bonus.common.core.web.controller.BaseController;
import com.bonus.common.core.web.domain.AjaxResult; import com.bonus.common.core.web.domain.AjaxResult;
import com.bonus.material.basic.domain.dto.DownloadProgress;
import com.bonus.material.basic.domain.report.*; import com.bonus.material.basic.domain.report.*;
import com.bonus.material.basic.service.BmReportService; import com.bonus.material.basic.service.BmReportService;
import com.bonus.material.common.utils.DocxUtil; import com.bonus.material.common.utils.DocxUtil;
@ -17,8 +17,15 @@ import com.bonus.material.part.domain.PartTypeCheckInfo;
import com.bonus.material.part.domain.PartTypeQueryDto; import com.bonus.material.part.domain.PartTypeQueryDto;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.poi.util.IOUtils; import org.apache.poi.util.IOUtils;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -27,15 +34,18 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.List; import java.util.concurrent.atomic.AtomicInteger;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.LinkedHashMap; import java.util.zip.Deflater;
import static com.bonus.common.core.web.page.TableSupport.PAGE_NUM; import static com.bonus.common.core.web.page.TableSupport.PAGE_NUM;
import static com.bonus.common.core.web.page.TableSupport.PAGE_SIZE; import static com.bonus.common.core.web.page.TableSupport.PAGE_SIZE;
@ -52,6 +62,9 @@ public class BmReportController extends BaseController {
@Resource @Resource
private BmReportService bmReportService; private BmReportService bmReportService;
// 存储下载进度
private static final Map<String, DownloadProgress> downloadProgressMap = new ConcurrentHashMap<>();
/** /**
* 新购入库报表查询 * 新购入库报表查询
* @param bean * @param bean
@ -608,67 +621,144 @@ public class BmReportController extends BaseController {
handleDownload(request, response); handleDownload(request, response);
} }
@ApiOperation("报告一键下载")
@PostMapping("/downloadBulk")
public void downloadBulk(@RequestBody DownloadRequest request, HttpServletResponse response) throws IOException { /**
* 流式批量下载接口
*/
@PostMapping("/downloadBulkStream")
public void downloadBulkStream(@RequestBody DownloadRequest request,
HttpServletResponse response) throws IOException {
// String taskId = UUID.randomUUID().toString();
String taskId = request.getTaskId() != null ? request.getTaskId() : UUID.randomUUID().toString();
String zipName = request.getZipName() != null ? request.getZipName() : "报告下载_" + LocalDate.now(); String zipName = request.getZipName() != null ? request.getZipName() : "报告下载_" + LocalDate.now();
String encoded = URLEncoder.encode(zipName + ".zip", "UTF-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(response.getOutputStream())) { // 初始化进度
DownloadProgress progress = new DownloadProgress();
progress.setTotalFiles(calculateTotalFiles(request.getItems()));
progress.setStatus("processing");
progress.setStartTime(System.currentTimeMillis());
downloadProgressMap.put(taskId, progress);
// 按工程 + 领用日期分组 try {
Map<String, Map<String, List<DownloadRequest.ItemInfo>>> grouped = request.getItems().stream() String encoded = java.net.URLEncoder.encode(zipName + ".zip", "UTF-8");
.collect(Collectors.groupingBy( response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);
item -> sanitize(item.getProName()), // 工程 response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
LinkedHashMap::new,
Collectors.groupingBy(item -> sanitize(item.getTestTime()), LinkedHashMap::new, Collectors.toList()) // 日期
));
for (Map.Entry<String, Map<String, List<DownloadRequest.ItemInfo>>> projectEntry : grouped.entrySet()) { // 设置流式传输相关头部
String projectFolder = projectEntry.getKey(); response.setHeader("Transfer-Encoding", "chunked");
Map<String, List<DownloadRequest.ItemInfo>> dateMap = projectEntry.getValue(); response.setHeader("X-Content-Type-Options", "nosniff");
for (Map.Entry<String, List<DownloadRequest.ItemInfo>> dateEntry : dateMap.entrySet()) { // 使用低压缩级别提高速度
String dateFolder = dateEntry.getKey(); try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(response.getOutputStream())) {
List<DownloadRequest.ItemInfo> items = dateEntry.getValue(); zipOut.setLevel(Deflater.BEST_SPEED);
String baseDatePath = projectFolder + "/" + dateFolder + "/"; zipOut.setEncoding("UTF-8");
zipOut.setUseZip64(Zip64Mode.AsNeeded);
// 1. 生成合并的出库检验报告 // 按工程 + 领用日期分组
byte[] mergedReport = DocxUtil.generateReportByList(items); Map<String, Map<String, List<DownloadRequest.ItemInfo>>> grouped =
addToZip(zipOut, baseDatePath + "出库检验报告.pdf", mergedReport); request.getItems().stream()
.collect(Collectors.groupingBy(
item -> sanitize(item.getProName()),
LinkedHashMap::new,
Collectors.groupingBy(
item -> sanitize(item.getTestTime()),
LinkedHashMap::new,
Collectors.toList()
)
));
// 2. 遍历每个类型-规格创建文件夹并添加附加文件 AtomicInteger fileCount = new AtomicInteger(0);
Map<String, List<DownloadRequest.ItemInfo>> typeMap = items.stream()
.collect(Collectors.groupingBy(item -> sanitize(item.getTypeName() + "-" + item.getTypeModelName())));
for (Map.Entry<String, List<DownloadRequest.ItemInfo>> typeEntry : typeMap.entrySet()) { for (Map.Entry<String, Map<String, List<DownloadRequest.ItemInfo>>> projectEntry : grouped.entrySet()) {
String typeFolder = typeEntry.getKey(); String projectFolder = projectEntry.getKey();
String typePath = baseDatePath + typeFolder + "/"; Map<String, List<DownloadRequest.ItemInfo>> dateMap = projectEntry.getValue();
// **先创建空文件夹** for (Map.Entry<String, List<DownloadRequest.ItemInfo>> dateEntry : dateMap.entrySet()) {
ZipArchiveEntry folderEntry = new ZipArchiveEntry(typePath); String dateFolder = dateEntry.getKey();
zipOut.putArchiveEntry(folderEntry); List<DownloadRequest.ItemInfo> items = dateEntry.getValue();
zipOut.closeArchiveEntry(); String baseDatePath = projectFolder + "/" + dateFolder + "/";
// 再添加附加文件如果存在 // 1. 生成合并的出库检验报告使用新的分页方法
for (DownloadRequest.ItemInfo item : typeEntry.getValue()) { progress.setCurrentFileName("正在生成出库检验报告...");
String maCode=""; progress.setCurrentFile(fileCount.incrementAndGet());
if (!StringHelper.isEmpty(item.getMaCode())){ updateProgress(taskId, progress);
maCode="_"+item.getMaCode();
byte[] mergedReport = DocxUtil.generateReportByList(items);
if (mergedReport != null && mergedReport.length > 0) {
addToZipStream(zipOut, baseDatePath + "出库检验报告.pdf", mergedReport);
zipOut.flush();
response.flushBuffer();
}
// 2. 按类型-规格分组处理附加文件
Map<String, List<DownloadRequest.ItemInfo>> typeMap = items.stream()
.collect(Collectors.groupingBy(
item -> sanitize(item.getTypeName() + "-" + item.getTypeModelName()),
LinkedHashMap::new,
Collectors.toList()
));
for (Map.Entry<String, List<DownloadRequest.ItemInfo>> typeEntry : typeMap.entrySet()) {
String typeFolder = typeEntry.getKey();
String typePath = baseDatePath + typeFolder + "/";
// 创建文件夹
ZipArchiveEntry folderEntry = new ZipArchiveEntry(typePath);
zipOut.putArchiveEntry(folderEntry);
zipOut.closeArchiveEntry();
// 处理每个项目的附加文件
for (DownloadRequest.ItemInfo item : typeEntry.getValue()) {
String maCode = StringHelper.isEmpty(item.getMaCode()) ? "" : "_" + item.getMaCode();
// 使用流式方式下载和添加文件
processAndAddFile(zipOut, typePath, "合格证" + maCode,
item.getQualifiedUrl(), taskId, progress, fileCount);
processAndAddFile(zipOut, typePath, "型式试验报告" + maCode,
item.getTestReportUrl(), taskId, progress, fileCount);
processAndAddFile(zipOut, typePath, "第三方检测报告" + maCode,
item.getThirdReportUrl(), taskId, progress, fileCount);
processAndAddFile(zipOut, typePath, "出厂检测报告" + maCode,
item.getFactoryReportUrl(), taskId, progress, fileCount);
processAndAddFile(zipOut, typePath, "其他文件" + maCode,
item.getOtherReportUrl(), taskId, progress, fileCount);
// 定期刷新缓冲区避免内存占用过高
if (fileCount.get() % 10 == 0) {
zipOut.flush();
response.flushBuffer();
}
} }
addFileIfExists(zipOut, typePath, "合格证"+maCode, item.getQualifiedUrl());
addFileIfExists(zipOut, typePath, "型式试验报告"+maCode, item.getTestReportUrl());
addFileIfExists(zipOut, typePath, "第三方检测报告"+maCode, item.getThirdReportUrl());
addFileIfExists(zipOut, typePath, "出厂检测报告"+maCode, item.getFactoryReportUrl());
addFileIfExists(zipOut, typePath, "其他文件"+maCode, item.getOtherReportUrl());
} }
} }
} }
// 更新进度为完成
progress.setStatus("completed");
progress.setPercentage(100);
progress.setEndTime(System.currentTimeMillis());
progress.setCurrentFileName("下载完成");
updateProgress(taskId, progress);
zipOut.finish();
} catch (IOException e) {
progress.setStatus("error");
progress.setErrorMessage(e.getMessage());
updateProgress(taskId, progress);
throw e;
} }
zipOut.finish(); } finally {
// 清理进度信息延迟清理以便前端可以获取最终状态
new Timer().schedule(new TimerTask() {
@Override
public void run() {
downloadProgressMap.remove(taskId);
}
}, 300000); // 5分钟后清理
} }
} }
@ -714,7 +804,7 @@ public class BmReportController extends BaseController {
return; return;
} }
try { try {
byte[] bytes = HttpFileUtil.downloadFile(url); byte[] bytes = downloadFile(url);
if (bytes != null) { if (bytes != null) {
addToZip(zipOut, base + name + getFileExtension(url), bytes); addToZip(zipOut, base + name + getFileExtension(url), bytes);
} }
@ -723,7 +813,23 @@ public class BmReportController extends BaseController {
} }
} }
public static byte[] downloadFile(String url) throws IOException {
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpGet get = new HttpGet(url);
try (CloseableHttpResponse response = client.execute(get)) {
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toByteArray(entity) : null;
}
}
}
/**
* 辅助方法
*/
private void addToZip(ZipArchiveOutputStream zipOut, String path, byte[] bytes) throws IOException { private void addToZip(ZipArchiveOutputStream zipOut, String path, byte[] bytes) throws IOException {
if (bytes == null || bytes.length == 0) {
return;
}
ZipArchiveEntry entry = new ZipArchiveEntry(path); ZipArchiveEntry entry = new ZipArchiveEntry(path);
zipOut.putArchiveEntry(entry); zipOut.putArchiveEntry(entry);
try (ByteArrayInputStream in = new ByteArrayInputStream(bytes)) { try (ByteArrayInputStream in = new ByteArrayInputStream(bytes)) {
@ -732,49 +838,194 @@ public class BmReportController extends BaseController {
zipOut.closeArchiveEntry(); zipOut.closeArchiveEntry();
} }
private String sanitize(String name) {
if (name == null) { /**
return "未知"; * 处理并添加文件到ZIP流式方式
*/
private void processAndAddFile(ZipArchiveOutputStream zipOut, String path,
String name, String url, String taskId,
DownloadProgress progress, AtomicInteger fileCount) throws IOException {
if (url == null || url.isEmpty()) {
return;
} }
return name.replaceAll("[\\\\/:*?\"<>|]", "_");
try {
// 更新进度
progress.setCurrentFileName("正在下载: " + name);
progress.setCurrentFile(fileCount.incrementAndGet());
updateProgress(taskId, progress);
// 使用流式下载
byte[] fileData = downloadFileWithProgress(url, progress, taskId);
if (fileData != null && fileData.length > 0) {
String fileName = name + getFileExtension(url);
addToZipStream(zipOut, path + fileName, fileData);
// 更新进度
progress.setProcessedBytes(progress.getProcessedBytes() + fileData.length);
updateProgress(taskId, progress);
}
} catch (Exception e) {
System.err.println("跳过文件:" + url + " -> " + e.getMessage());
// 记录错误但不中断整个下载过程
progress.addError(url, e.getMessage());
}
}
/**
* 带进度的文件下载
*/
private byte[] downloadFileWithProgress(String url, DownloadProgress progress, String taskId) throws IOException {
HttpURLConnection connection = null;
InputStream inputStream = null;
try {
connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("GET");
// 30秒连接超时
connection.setConnectTimeout(30000);
// 120秒读取超时
connection.setReadTimeout(120000);
connection.setRequestProperty("Accept", "*/*");
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
throw new IOException("HTTP " + responseCode + " for URL: " + url);
}
int contentLength = connection.getContentLength();
progress.setCurrentFileSize(contentLength);
updateProgress(taskId, progress);
inputStream = connection.getInputStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
// 8KB缓冲区
byte[] data = new byte[8192];
int bytesRead;
long totalRead = 0;
while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, bytesRead);
totalRead += bytesRead;
// 更新下载进度
if (contentLength > 0) {
int percentage = (int) ((totalRead * 100) / contentLength);
progress.setCurrentFilePercentage(percentage);
updateProgress(taskId, progress);
}
}
return buffer.toByteArray();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
if (connection != null) {
connection.disconnect();
}
}
}
/**
* 流式添加到ZIP
*/
private void addToZipStream(ZipArchiveOutputStream zipOut, String path, byte[] bytes) throws IOException {
if (bytes == null || bytes.length == 0) {
return;
}
ZipArchiveEntry entry = new ZipArchiveEntry(path);
entry.setSize(bytes.length);
zipOut.putArchiveEntry(entry);
try (ByteArrayInputStream in = new ByteArrayInputStream(bytes)) {
IOUtils.copy(in, zipOut);
}
zipOut.closeArchiveEntry();
zipOut.flush();
}
/**
* 获取下载进度接口
*/
@PostMapping("/downloadProgress")
@ResponseBody
public DownloadProgress getDownloadProgress(@RequestBody Map<String, String> request) {
String taskId = request.get("taskId");
if (taskId == null || !downloadProgressMap.containsKey(taskId)) {
DownloadProgress progress = new DownloadProgress();
progress.setStatus("not_found");
return progress;
}
DownloadProgress progress = downloadProgressMap.get(taskId);
if (progress.getTotalFiles() > 0) {
int percentage = (int) ((progress.getCurrentFile() * 100.0) / progress.getTotalFiles());
progress.setPercentage(Math.min(percentage, 100));
}
return progress;
}
/**
* 计算总文件数
*/
private int calculateTotalFiles(List<DownloadRequest.ItemInfo> items) {
int total = 0;
for (DownloadRequest.ItemInfo item : items) {
// 每个项目至少有一个出库检验报告
total += 1;
// 加上各个附件
if (item.getQualifiedUrl() != null && !item.getQualifiedUrl().isEmpty()) {
total += 1;
}
if (item.getTestReportUrl() != null && !item.getTestReportUrl().isEmpty()) {
total += 1;
}
if (item.getThirdReportUrl() != null && !item.getThirdReportUrl().isEmpty()) {
total += 1;
}
if (item.getFactoryReportUrl() != null && !item.getFactoryReportUrl().isEmpty()) {
total += 1;
}
if (item.getOtherReportUrl() != null && !item.getOtherReportUrl().isEmpty()) {
total += 1;
}
}
return total;
}
/**
* 更新进度信息
*/
private void updateProgress(String taskId, DownloadProgress progress) {
if (taskId != null && progress != null) {
downloadProgressMap.put(taskId, progress);
}
}
// 辅助方法
private String sanitize(String str) {
return str != null ? str.replaceAll("[\\\\/:*?\"<>|]", "_") : "";
} }
private String getFileExtension(String url) { private String getFileExtension(String url) {
int idx = url.lastIndexOf('.'); if (url == null || url.isEmpty()) {
return (idx > 0 && idx < url.length() - 1) ? url.substring(idx) : ""; return "";
}
/**
* 资产占有月度报表查询
* @param bean
* @return
*/
@ApiOperation(value = "资产占有月度报表查询")
@GetMapping("/getAssetReportList")
public AjaxResult getAssetReportList(AssetReportInfo bean) {
startPage();
List<AssetReportInfo> pageList = bmReportService.getAssetReportList(bean);
return AjaxResult.success(getDataTable(pageList));
}
/**
* 导出资产占有月度报表
* @param response
* @param bean
*/
@ApiOperation("导出资产占有月度报表")
@PostMapping("/exportAssetReportList")
public void exportAssetReportList(HttpServletResponse response, AssetReportInfo bean)
{
String fileName = "资产占有月度报表";
List<AssetReportInfo> list = bmReportService.getAssetReportList(bean);
// 根据list集合数去填充序号
for (int i = 0; i < list.size(); i++) {
list.get(i).setSeq(i + 1);
} }
ExcelUtil<AssetReportInfo> util = new ExcelUtil<>(AssetReportInfo.class); int dotIndex = url.lastIndexOf('.');
// 获取当前年月日时分秒导出时间用括号拼接在后面 if (dotIndex > 0 && dotIndex < url.length() - 1) {
String title = "资产占有月度报表" + "" + "导出时间:" + DateUtils.getTime() + ""; return url.substring(dotIndex);
util.exportExcel(response, list, fileName, title); }
return "";
} }
} }

View File

@ -12,6 +12,8 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import java.io.*; import java.io.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@ -20,38 +22,114 @@ import java.util.List;
*/ */
public class DocxUtil { public class DocxUtil {
/**
* 生成单个报告
*/
public static byte[] generateReport(DownloadRequest.ItemInfo item) { public static byte[] generateReport(DownloadRequest.ItemInfo item) {
return generateReportByList(Collections.singletonList(item));
}
/**
* 生成多个报告带分页
*/
public static byte[] generateReportByList(List<DownloadRequest.ItemInfo> items) {
return generateReportByListWithPagination(items);
}
/**
* 生成PDF报告带自动分页功能
*/
public static byte[] generateReportByListWithPagination(List<DownloadRequest.ItemInfo> items) {
if (items == null || items.isEmpty()) {
return new byte[0];
}
try (PDDocument document = new PDDocument(); try (PDDocument document = new PDDocument();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) { ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
PDType0Font font = PDType0Font.load(document, PDType0Font font = PDType0Font.load(document,
PdfUtil.class.getClassLoader().getResourceAsStream("fonts/syht.ttf")); PdfUtil.class.getClassLoader().getResourceAsStream("fonts/syht.ttf"));
// 页面尺寸横向A4
PDRectangle pageSize = new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()); 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 = { String[][] rows = convertItemsToRows(items);
"机具名称", "规格型号", "单位", "数量", "设备编码",
"额定载荷KN", "试验载荷KN", "持荷时间min",
"试验日期", "下次试验日期", "检验结论", "备注"
};
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())
}
};
// 生成PDF
generatePdfWithPagination(document, font, pageSize, items.get(0), rows);
document.save(bos);
return bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
// 返回一个简单的错误报告而不是空数组
return generateErrorReport(e.getMessage());
}
}
/**
* 将Item列表转为二维数组
*/
private static String[][] convertItemsToRows(List<DownloadRequest.ItemInfo> items) {
String[] headers = {
"机具名称", "规格型号", "单位", "数量", "设备编码",
"额定载荷KN", "试验载荷KN", "持荷时间min",
"试验日期", "下次试验日期", "检验结论", "备注"
};
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())
};
}
return rows;
}
/**
* 生成带分页的PDF
*/
private static void generatePdfWithPagination(PDDocument document, PDType0Font font,
PDRectangle pageSize, DownloadRequest.ItemInfo firstItem,
String[][] rows) throws IOException {
float margin = 50;
float yStart = pageSize.getHeight() - margin;
float bottomMargin = 120; // 底部留出足够空间给印章
float[] colWidths = {80, 80, 40, 40, 80, 70, 70, 75, 65, 80, 60, 40};
String[] headers = {
"机具名称", "规格型号", "单位", "数量", "设备编码",
"额定载荷KN", "试验载荷KN", "持荷时间min",
"试验日期", "下次试验日期", "检验结论", "备注"
};
// 分页处理
int totalRows = rows.length;
int currentRow = 0;
int pageNum = 0;
while (currentRow < totalRows) {
// 创建新页面
PDPage page = new PDPage(pageSize); PDPage page = new PDPage(pageSize);
document.addPage(page); document.addPage(page);
PDPageContentStream content = new PDPageContentStream(document, page); PDPageContentStream content = new PDPageContentStream(document, page);
float yPosition = yStart;
// ===== 标题 ===== // ===== 标题 =====
String titleText = "施工机具设备出库检验记录表"; String titleText = "施工机具设备出库检验记录表";
content.beginText(); content.beginText();
@ -62,103 +140,109 @@ public class DocxUtil {
yPosition -= 50; yPosition -= 50;
// ===== 工程与单位 ===== // ===== 工程与单位第一页显示后续页面只显示页码 =====
content.beginText(); if (pageNum == 0) {
content.setFont(font, 12); content.beginText();
content.newLineAtOffset(margin, yPosition); content.setFont(font, 12);
content.showText("领用工程:" + safe(item.getProName())); content.newLineAtOffset(margin, yPosition);
content.newLineAtOffset(0, -20); content.showText("领用工程:" + safe(firstItem.getProName()));
content.showText("使用单位:" + safe(item.getDepartName())); content.newLineAtOffset(0, -20);
content.endText(); content.showText("使用单位:" + safe(firstItem.getDepartName()));
content.endText();
yPosition -= 40;
} else {
// 后续页面显示页码
content.beginText();
content.setFont(font, 10);
content.newLineAtOffset(pageSize.getWidth() - margin - 50, yStart - 30);
content.showText("" + (pageNum + 1) + "");
content.endText();
}
yPosition -= 40; // ===== 计算本页的行数 =====
String[][] pageRows = getRowsForCurrentPage(rows, currentRow, margin, yPosition, bottomMargin,
colWidths, headers, font);
int rowsThisPage = pageRows.length;
// ===== 表格绘制方法 ===== // ===== 表格绘制 =====
float tableWidth = 0; float tableWidth = 0;
for (float w : colWidths) { for (float w : colWidths) {
tableWidth += w; tableWidth += w;
} }
float tableBottomMargin = 100;
float rowMargin = 5; float rowMargin = 5;
float fontSize = 10; float fontSize = 10;
// 绘制表头 + 数据 yPosition = drawTableWithPagination(document, page, content, font, margin, yPosition, tableWidth,
yPosition = drawTable(document, page, content, font, margin, yPosition, tableWidth, colWidths, colWidths, headers, pageRows, fontSize, rowMargin, pageSize, yStart, bottomMargin);
headers, rows, fontSize, rowMargin, pageSize, yStart, tableBottomMargin);
// ===== 检验单位与印章 ===== // ===== 检验单位与印章最后一页显示 =====
yPosition -= 60; if (currentRow + rowsThisPage >= totalRows) {
content.beginText(); yPosition -= 60;
content.setFont(font, 12); content.beginText();
content.newLineAtOffset(margin, yPosition); content.setFont(font, 12);
content.showText("检验单位:"); content.newLineAtOffset(margin, yPosition);
content.endText(); content.showText("检验单位:");
content.endText();
InputStream is = PdfUtil.class.getClassLoader().getResourceAsStream("template/gaizhang.png"); // 添加印章图片
if (is != null) { addStamp(document, content, margin, yPosition);
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();
} }
content.close(); content.close();
document.save(bos); currentRow += rowsThisPage;
return bos.toByteArray(); pageNum++;
} catch (Exception e) {
e.printStackTrace();
return new byte[0];
} }
} }
/**
* 获取当前页的行数据
*/
private static String[][] getRowsForCurrentPage(String[][] rows, int startRow,
float margin, float startY, float bottomMargin,
float[] colWidths, String[] headers, PDFont font) throws IOException {
List<String[]> pageRows = new ArrayList<>();
float currentY = startY;
float fontSize = 10;
float cellMargin = 5;
private static String safe(String v) { // 绘制表头所需高度
return v == null ? "" : v; float headerHeight = getRowHeight(headers, font, fontSize, colWidths, cellMargin);
} currentY -= headerHeight;
private static float getStringWidth(String text, PDFont font, float fontSize) throws IOException { for (int i = startRow; i < rows.length; i++) {
return font.getStringWidth(text) / 1000 * fontSize; float rowHeight = getRowHeight(rows[i], font, fontSize, colWidths, cellMargin);
// 检查是否有足够空间绘制这一行
if (currentY - rowHeight < bottomMargin) {
break;
}
pageRows.add(rows[i]);
currentY -= rowHeight;
}
// 转换为数组
return pageRows.toArray(new String[0][]);
} }
/** /**
* 自动换行表格绘制 * 绘制表格分页版本
*/ */
private static float drawTable(PDDocument doc, PDPage page, PDPageContentStream content, private static float drawTableWithPagination(PDDocument doc, PDPage page, PDPageContentStream content,
PDFont font, float startX, float startY, float tableWidth, PDFont font, float startX, float startY, float tableWidth,
float[] colWidths, String[] headers, String[][] rows, float[] colWidths, String[] headers, String[][] rows,
float fontSize, float cellMargin, PDRectangle pageSize, float fontSize, float cellMargin, PDRectangle pageSize,
float yStart, float bottomMargin) throws IOException { float yStart, float bottomMargin) throws IOException {
float y = startY; float y = startY;
float pageHeight = pageSize.getHeight(); float pageHeight = pageSize.getHeight();
float usableHeight = pageHeight - bottomMargin;
// 绘制表头 // 绘制表头
y = drawRow(doc, page, content, font, startX, y, colWidths, headers, fontSize, cellMargin, true); y = drawRow(doc, page, content, font, startX, y, colWidths, headers, fontSize, cellMargin, true);
// 绘制数据行
for (String[] row : rows) { for (String[] row : rows) {
float rowHeight = getRowHeight(row, font, fontSize, colWidths, cellMargin); 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); y = drawRow(doc, page, content, font, startX, y, colWidths, row, fontSize, cellMargin, false);
} }
@ -188,8 +272,8 @@ public class DocxUtil {
} }
// 绘制单元格边框 // 绘制单元格边框
// 黑色 content.setStrokingColor(0, 0, 0);
content.setNonStrokingColor(0, 0, 0); content.setLineWidth(1f);
content.addRect(x, y - maxHeight, cellWidth, maxHeight); content.addRect(x, y - maxHeight, cellWidth, maxHeight);
content.stroke(); content.stroke();
@ -218,8 +302,6 @@ public class DocxUtil {
return y - maxHeight; return y - maxHeight;
} }
/** /**
* 计算行高取最长文本行数 * 计算行高取最长文本行数
*/ */
@ -256,7 +338,141 @@ public class DocxUtil {
lines.add(line.toString()); lines.add(line.toString());
return lines; return lines;
} }
public static byte[] generateReportByList(List<DownloadRequest.ItemInfo> items) {
/**
* 添加印章
*/
private static void addStamp(PDDocument document, PDPageContentStream content,
float margin, float yPosition) throws IOException {
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();
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "gaizhang");
content.drawImage(image, margin + 70, yPosition - 60, 100, 100);
is.close();
}
}
/**
* 安全获取字符串处理null
*/
private static String safe(String str) {
return str != null ? str : "";
}
/**
* 获取字符串宽度
*/
private static float getStringWidth(String text, PDFont font, float fontSize) throws IOException {
if (text == null || text.isEmpty()) {
return 0;
}
return font.getStringWidth(text) / 1000 * fontSize;
}
/**
* 生成错误报告
*/
private static byte[] generateErrorReport(String errorMessage) {
try (PDDocument document = new PDDocument();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
PDType0Font font = PDType0Font.load(document,
PdfUtil.class.getClassLoader().getResourceAsStream("fonts/syht.ttf"));
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(document, page)) {
content.beginText();
content.setFont(font, 16);
content.newLineAtOffset(100, 700);
content.showText("出库检验报告生成失败");
content.endText();
content.beginText();
content.setFont(font, 12);
content.newLineAtOffset(100, 650);
content.showText("错误信息: " + (errorMessage != null ?
errorMessage.substring(0, Math.min(100, errorMessage.length())) : "未知错误"));
content.endText();
content.beginText();
content.setFont(font, 12);
content.newLineAtOffset(100, 600);
content.showText("生成时间: " + new java.util.Date());
content.endText();
content.beginText();
content.setFont(font, 10);
content.newLineAtOffset(100, 550);
content.showText("提示: 请减少一次下载的数据量,或联系系统管理员");
content.endText();
}
document.save(bos);
return bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
return new byte[0];
}
}
/**
* 原有的drawTable方法保持兼容
*/
public 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();
// 绘制表头
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坐标
y = yStart;
// 重绘表头
y = drawRow(doc, newPage, content, font, startX, y, colWidths, headers, fontSize, cellMargin, true);
// 绘制当前行
y = drawRow(doc, newPage, content, font, startX, y, colWidths, row, fontSize, cellMargin, false);
} else {
// 在当前页面绘制行
y = drawRow(doc, page, content, font, startX, y, colWidths, row, fontSize, cellMargin, false);
}
}
return y;
}
/**
* 旧版生成多个报告方法无分页兼容旧代码
*/
public static byte[] generateReportByListOld(List<DownloadRequest.ItemInfo> items) {
if (items == null || items.isEmpty()) { if (items == null || items.isEmpty()) {
return new byte[0]; return new byte[0];
} }
@ -339,20 +555,8 @@ public class DocxUtil {
content.showText("检验单位:"); content.showText("检验单位:");
content.endText(); content.endText();
InputStream is = PdfUtil.class.getClassLoader().getResourceAsStream("template/gaizhang.png"); // 添加印章图片
if (is != null) { addStamp(document, content, margin, yPosition);
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();
}
content.close(); content.close();
document.save(bos); document.save(bos);
@ -363,6 +567,4 @@ public class DocxUtil {
return new byte[0]; return new byte[0];
} }
} }
} }