From a065b8ecded777ddc3d5ca1f14e665d7a045b593 Mon Sep 17 00:00:00 2001 From: hayu <1604366271@qq.com> Date: Mon, 15 Dec 2025 10:37:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E5=91=8A=E4=B8=80=E9=94=AE=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../basic/controller/BmReportController.java | 433 ++++++++++++++---- .../bonus/material/common/utils/DocxUtil.java | 412 ++++++++++++----- 2 files changed, 649 insertions(+), 196 deletions(-) diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/basic/controller/BmReportController.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/basic/controller/BmReportController.java index cef9c6f5..4a0d3d13 100644 --- a/bonus-modules/bonus-material/src/main/java/com/bonus/material/basic/controller/BmReportController.java +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/basic/controller/BmReportController.java @@ -4,11 +4,11 @@ import cn.hutool.core.convert.Convert; import com.alibaba.nacos.common.utils.CollectionUtils; import com.bonus.common.biz.config.ListPagingUtil; 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.poi.ExcelUtil; import com.bonus.common.core.web.controller.BaseController; 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.service.BmReportService; 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 io.swagger.annotations.Api; 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.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.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -27,15 +34,18 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; import java.net.URLEncoder; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; 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_SIZE; @@ -52,6 +62,9 @@ public class BmReportController extends BaseController { @Resource private BmReportService bmReportService; + // 存储下载进度 + private static final Map downloadProgressMap = new ConcurrentHashMap<>(); + /** * 新购入库报表查询 * @param bean @@ -608,67 +621,144 @@ public class BmReportController extends BaseController { 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 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); - // 按工程 + 领用日期分组 - Map>> grouped = request.getItems().stream() - .collect(Collectors.groupingBy( - item -> sanitize(item.getProName()), // 工程 - LinkedHashMap::new, - Collectors.groupingBy(item -> sanitize(item.getTestTime()), LinkedHashMap::new, Collectors.toList()) // 日期 - )); + try { + String encoded = java.net.URLEncoder.encode(zipName + ".zip", "UTF-8"); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); - for (Map.Entry>> projectEntry : grouped.entrySet()) { - String projectFolder = projectEntry.getKey(); - Map> dateMap = projectEntry.getValue(); + // 设置流式传输相关头部 + response.setHeader("Transfer-Encoding", "chunked"); + response.setHeader("X-Content-Type-Options", "nosniff"); - for (Map.Entry> dateEntry : dateMap.entrySet()) { - String dateFolder = dateEntry.getKey(); - List items = dateEntry.getValue(); - String baseDatePath = projectFolder + "/" + dateFolder + "/"; + // 使用低压缩级别提高速度 + try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(response.getOutputStream())) { + zipOut.setLevel(Deflater.BEST_SPEED); + zipOut.setEncoding("UTF-8"); + zipOut.setUseZip64(Zip64Mode.AsNeeded); - // 1. 生成合并的出库检验报告 - byte[] mergedReport = DocxUtil.generateReportByList(items); - addToZip(zipOut, baseDatePath + "出库检验报告.pdf", mergedReport); + // 按工程 + 领用日期分组 + Map>> grouped = + request.getItems().stream() + .collect(Collectors.groupingBy( + item -> sanitize(item.getProName()), + LinkedHashMap::new, + Collectors.groupingBy( + item -> sanitize(item.getTestTime()), + LinkedHashMap::new, + Collectors.toList() + ) + )); - // 2. 遍历每个类型-规格,创建文件夹并添加附加文件 - Map> typeMap = items.stream() - .collect(Collectors.groupingBy(item -> sanitize(item.getTypeName() + "-" + item.getTypeModelName()))); + AtomicInteger fileCount = new AtomicInteger(0); - for (Map.Entry> typeEntry : typeMap.entrySet()) { - String typeFolder = typeEntry.getKey(); - String typePath = baseDatePath + typeFolder + "/"; + for (Map.Entry>> projectEntry : grouped.entrySet()) { + String projectFolder = projectEntry.getKey(); + Map> dateMap = projectEntry.getValue(); - // **先创建空文件夹** - ZipArchiveEntry folderEntry = new ZipArchiveEntry(typePath); - zipOut.putArchiveEntry(folderEntry); - zipOut.closeArchiveEntry(); + for (Map.Entry> dateEntry : dateMap.entrySet()) { + String dateFolder = dateEntry.getKey(); + List items = dateEntry.getValue(); + String baseDatePath = projectFolder + "/" + dateFolder + "/"; - // 再添加附加文件(如果存在) - for (DownloadRequest.ItemInfo item : typeEntry.getValue()) { - String maCode=""; - if (!StringHelper.isEmpty(item.getMaCode())){ - maCode="_"+item.getMaCode(); + // 1. 生成合并的出库检验报告(使用新的分页方法) + progress.setCurrentFileName("正在生成出库检验报告..."); + progress.setCurrentFile(fileCount.incrementAndGet()); + updateProgress(taskId, progress); + + byte[] mergedReport = DocxUtil.generateReportByList(items); + if (mergedReport != null && mergedReport.length > 0) { + addToZipStream(zipOut, baseDatePath + "出库检验报告.pdf", mergedReport); + zipOut.flush(); + response.flushBuffer(); + } + + // 2. 按类型-规格分组处理附加文件 + Map> typeMap = items.stream() + .collect(Collectors.groupingBy( + item -> sanitize(item.getTypeName() + "-" + item.getTypeModelName()), + LinkedHashMap::new, + Collectors.toList() + )); + + for (Map.Entry> 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; } try { - byte[] bytes = HttpFileUtil.downloadFile(url); + byte[] bytes = downloadFile(url); if (bytes != null) { 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 { + if (bytes == null || bytes.length == 0) { + return; + } ZipArchiveEntry entry = new ZipArchiveEntry(path); zipOut.putArchiveEntry(entry); try (ByteArrayInputStream in = new ByteArrayInputStream(bytes)) { @@ -732,49 +838,194 @@ public class BmReportController extends BaseController { 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 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 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) { - int idx = url.lastIndexOf('.'); - return (idx > 0 && idx < url.length() - 1) ? url.substring(idx) : ""; - } - - /** - * 资产占有月度报表查询 - * @param bean - * @return - */ - @ApiOperation(value = "资产占有月度报表查询") - @GetMapping("/getAssetReportList") - public AjaxResult getAssetReportList(AssetReportInfo bean) { - startPage(); - List 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 list = bmReportService.getAssetReportList(bean); - // 根据list集合数,去填充序号 - for (int i = 0; i < list.size(); i++) { - list.get(i).setSeq(i + 1); + if (url == null || url.isEmpty()) { + return ""; } - ExcelUtil util = new ExcelUtil<>(AssetReportInfo.class); - // 获取当前年月日时分秒导出时间,用括号拼接在后面 - String title = "资产占有月度报表" + "(" + "导出时间:" + DateUtils.getTime() + ")"; - util.exportExcel(response, list, fileName, title); + int dotIndex = url.lastIndexOf('.'); + if (dotIndex > 0 && dotIndex < url.length() - 1) { + return url.substring(dotIndex); + } + return ""; } } diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/common/utils/DocxUtil.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/common/utils/DocxUtil.java index d75e79a0..dfcad9a6 100644 --- a/bonus-modules/bonus-material/src/main/java/com/bonus/material/common/utils/DocxUtil.java +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/common/utils/DocxUtil.java @@ -12,6 +12,8 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import java.io.*; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -20,38 +22,114 @@ import java.util.List; */ public class DocxUtil { + /** + * 生成单个报告 + */ public static byte[] generateReport(DownloadRequest.ItemInfo item) { + return generateReportByList(Collections.singletonList(item)); + } + + /** + * 生成多个报告(带分页) + */ + public static byte[] generateReportByList(List items) { + return generateReportByListWithPagination(items); + } + + /** + * 生成PDF报告(带自动分页功能) + */ + public static byte[] generateReportByListWithPagination(List items) { + if (items == null || items.isEmpty()) { + return new byte[0]; + } + try (PDDocument document = new PDDocument(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { PDType0Font font = PDType0Font.load(document, PdfUtil.class.getClassLoader().getResourceAsStream("fonts/syht.ttf")); + // 页面尺寸(横向A4) 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", - "试验日期", "下次试验日期", "检验结论", "备注" - }; - 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()) - } - }; + // 数据转为二维数组 + String[][] rows = convertItemsToRows(items); + // 生成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 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); document.addPage(page); PDPageContentStream content = new PDPageContentStream(document, page); + float yPosition = yStart; + // ===== 标题 ===== String titleText = "施工机具设备出库检验记录表"; content.beginText(); @@ -62,103 +140,109 @@ public class DocxUtil { 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(); + // ===== 工程与单位(第一页显示,后续页面只显示页码) ===== + if (pageNum == 0) { + content.beginText(); + content.setFont(font, 12); + content.newLineAtOffset(margin, yPosition); + content.showText("领用工程:" + safe(firstItem.getProName())); + content.newLineAtOffset(0, -20); + 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; for (float w : colWidths) { tableWidth += w; } - float tableBottomMargin = 100; float rowMargin = 5; float fontSize = 10; - // 绘制表头 + 数据 - yPosition = drawTable(document, page, content, font, margin, yPosition, tableWidth, colWidths, - headers, rows, fontSize, rowMargin, pageSize, yStart, tableBottomMargin); + yPosition = drawTableWithPagination(document, page, content, font, margin, yPosition, tableWidth, + colWidths, headers, pageRows, fontSize, rowMargin, pageSize, yStart, bottomMargin); - // ===== 检验单位与印章 ===== - yPosition -= 60; - content.beginText(); - content.setFont(font, 12); - content.newLineAtOffset(margin, yPosition); - content.showText("检验单位:"); - content.endText(); + // ===== 检验单位与印章(最后一页显示) ===== + if (currentRow + rowsThisPage >= totalRows) { + 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(); - - PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "gaizhang"); - content.drawImage(image, margin + 70, yPosition - 60, 100, 100); - is.close(); + // 添加印章图片 + addStamp(document, content, margin, yPosition); } - content.close(); - document.save(bos); - return bos.toByteArray(); - - } catch (Exception e) { - e.printStackTrace(); - return new byte[0]; + currentRow += rowsThisPage; + pageNum++; } } + /** + * 获取当前页的行数据 + */ + private static String[][] getRowsForCurrentPage(String[][] rows, int startRow, + float margin, float startY, float bottomMargin, + float[] colWidths, String[] headers, PDFont font) throws IOException { + List 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 { - return font.getStringWidth(text) / 1000 * fontSize; + for (int i = startRow; i < rows.length; i++) { + 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, - 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 { + private static float drawTableWithPagination(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); } @@ -188,8 +272,8 @@ public class DocxUtil { } // 绘制单元格边框 - // 黑色 - content.setNonStrokingColor(0, 0, 0); + content.setStrokingColor(0, 0, 0); + content.setLineWidth(1f); content.addRect(x, y - maxHeight, cellWidth, maxHeight); content.stroke(); @@ -218,8 +302,6 @@ public class DocxUtil { return y - maxHeight; } - - /** * 计算行高(取最长文本行数) */ @@ -256,7 +338,141 @@ public class DocxUtil { lines.add(line.toString()); return lines; } - public static byte[] generateReportByList(List 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 items) { if (items == null || items.isEmpty()) { return new byte[0]; } @@ -339,20 +555,8 @@ public class DocxUtil { 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(); - - PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "gaizhang"); - content.drawImage(image, margin + 70, yPosition - 60, 100, 100); - is.close(); - } + // 添加印章图片 + addStamp(document, content, margin, yPosition); content.close(); document.save(bos); @@ -363,6 +567,4 @@ public class DocxUtil { return new byte[0]; } } - - -} +} \ No newline at end of file