报告一键下载功能优化

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,24 +621,56 @@ 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");
// 初始化进度
DownloadProgress progress = new DownloadProgress();
progress.setTotalFiles(calculateTotalFiles(request.getItems()));
progress.setStatus("processing");
progress.setStartTime(System.currentTimeMillis());
downloadProgressMap.put(taskId, progress);
try {
String encoded = java.net.URLEncoder.encode(zipName + ".zip", "UTF-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
// 设置流式传输相关头部
response.setHeader("Transfer-Encoding", "chunked");
response.setHeader("X-Content-Type-Options", "nosniff");
// 使用低压缩级别提高速度
try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(response.getOutputStream())) { try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(response.getOutputStream())) {
zipOut.setLevel(Deflater.BEST_SPEED);
zipOut.setEncoding("UTF-8");
zipOut.setUseZip64(Zip64Mode.AsNeeded);
// 按工程 + 领用日期分组 // 按工程 + 领用日期分组
Map<String, Map<String, List<DownloadRequest.ItemInfo>>> grouped = request.getItems().stream() Map<String, Map<String, List<DownloadRequest.ItemInfo>>> grouped =
request.getItems().stream()
.collect(Collectors.groupingBy( .collect(Collectors.groupingBy(
item -> sanitize(item.getProName()), // 工程 item -> sanitize(item.getProName()),
LinkedHashMap::new, LinkedHashMap::new,
Collectors.groupingBy(item -> sanitize(item.getTestTime()), LinkedHashMap::new, Collectors.toList()) // 日期 Collectors.groupingBy(
item -> sanitize(item.getTestTime()),
LinkedHashMap::new,
Collectors.toList()
)
)); ));
AtomicInteger fileCount = new AtomicInteger(0);
for (Map.Entry<String, Map<String, List<DownloadRequest.ItemInfo>>> projectEntry : grouped.entrySet()) { for (Map.Entry<String, Map<String, List<DownloadRequest.ItemInfo>>> projectEntry : grouped.entrySet()) {
String projectFolder = projectEntry.getKey(); String projectFolder = projectEntry.getKey();
Map<String, List<DownloadRequest.ItemInfo>> dateMap = projectEntry.getValue(); Map<String, List<DownloadRequest.ItemInfo>> dateMap = projectEntry.getValue();
@ -635,40 +680,85 @@ public class BmReportController extends BaseController {
List<DownloadRequest.ItemInfo> items = dateEntry.getValue(); List<DownloadRequest.ItemInfo> items = dateEntry.getValue();
String baseDatePath = projectFolder + "/" + dateFolder + "/"; String baseDatePath = projectFolder + "/" + dateFolder + "/";
// 1. 生成合并的出库检验报告 // 1. 生成合并的出库检验报告使用新的分页方法
byte[] mergedReport = DocxUtil.generateReportByList(items); progress.setCurrentFileName("正在生成出库检验报告...");
addToZip(zipOut, baseDatePath + "出库检验报告.pdf", mergedReport); progress.setCurrentFile(fileCount.incrementAndGet());
updateProgress(taskId, progress);
// 2. 遍历每个类型-规格创建文件夹并添加附加文件 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() Map<String, List<DownloadRequest.ItemInfo>> typeMap = items.stream()
.collect(Collectors.groupingBy(item -> sanitize(item.getTypeName() + "-" + item.getTypeModelName()))); .collect(Collectors.groupingBy(
item -> sanitize(item.getTypeName() + "-" + item.getTypeModelName()),
LinkedHashMap::new,
Collectors.toList()
));
for (Map.Entry<String, List<DownloadRequest.ItemInfo>> typeEntry : typeMap.entrySet()) { for (Map.Entry<String, List<DownloadRequest.ItemInfo>> typeEntry : typeMap.entrySet()) {
String typeFolder = typeEntry.getKey(); String typeFolder = typeEntry.getKey();
String typePath = baseDatePath + typeFolder + "/"; String typePath = baseDatePath + typeFolder + "/";
// **创建文件夹** // 创建文件夹
ZipArchiveEntry folderEntry = new ZipArchiveEntry(typePath); ZipArchiveEntry folderEntry = new ZipArchiveEntry(typePath);
zipOut.putArchiveEntry(folderEntry); zipOut.putArchiveEntry(folderEntry);
zipOut.closeArchiveEntry(); zipOut.closeArchiveEntry();
// 再添加附加文件如果存在 // 处理每个项目的附加文件
for (DownloadRequest.ItemInfo item : typeEntry.getValue()) { for (DownloadRequest.ItemInfo item : typeEntry.getValue()) {
String maCode=""; String maCode = StringHelper.isEmpty(item.getMaCode()) ? "" : "_" + item.getMaCode();
if (!StringHelper.isEmpty(item.getMaCode())){
maCode="_"+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(); zipOut.finish();
} catch (IOException e) {
progress.setStatus("error");
progress.setErrorMessage(e.getMessage());
updateProgress(taskId, progress);
throw e;
}
} 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 "";
} }
int dotIndex = url.lastIndexOf('.');
/** if (dotIndex > 0 && dotIndex < url.length() - 1) {
* 资产占有月度报表查询 return url.substring(dotIndex);
* @param bean
* @return
*/
@ApiOperation(value = "资产占有月度报表查询")
@GetMapping("/getAssetReportList")
public AjaxResult getAssetReportList(AssetReportInfo bean) {
startPage();
List<AssetReportInfo> pageList = bmReportService.getAssetReportList(bean);
return AjaxResult.success(getDataTable(pageList));
} }
return "";
/**
* 导出资产占有月度报表
* @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);
// 获取当前年月日时分秒导出时间用括号拼接在后面
String title = "资产占有月度报表" + "" + "导出时间:" + DateUtils.getTime() + "";
util.exportExcel(response, list, fileName, title);
} }
} }

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,17 +22,93 @@ 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());
// 数据转为二维数组
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<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 margin = 50;
float yStart = pageSize.getHeight() - margin; float yStart = pageSize.getHeight() - margin;
float yPosition = yStart; float bottomMargin = 120; // 底部留出足够空间给印章
float[] colWidths = {80, 80, 40, 40, 80, 70, 70, 75, 65, 80, 60, 40}; float[] colWidths = {80, 80, 40, 40, 80, 70, 70, 75, 65, 80, 60, 40};
String[] headers = { String[] headers = {
@ -38,20 +116,20 @@ public class DocxUtil {
"额定载荷KN", "试验载荷KN", "持荷时间min", "额定载荷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())
}
};
// 分页处理
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,31 +140,43 @@ public class DocxUtil {
yPosition -= 50; yPosition -= 50;
// ===== 工程与单位 ===== // ===== 工程与单位第一页显示后续页面只显示页码 =====
if (pageNum == 0) {
content.beginText(); content.beginText();
content.setFont(font, 12); content.setFont(font, 12);
content.newLineAtOffset(margin, yPosition); content.newLineAtOffset(margin, yPosition);
content.showText("领用工程:" + safe(item.getProName())); content.showText("领用工程:" + safe(firstItem.getProName()));
content.newLineAtOffset(0, -20); content.newLineAtOffset(0, -20);
content.showText("使用单位:" + safe(item.getDepartName())); content.showText("使用单位:" + safe(firstItem.getDepartName()));
content.endText(); content.endText();
yPosition -= 40; yPosition -= 40;
} else {
// 后续页面显示页码
content.beginText();
content.setFont(font, 10);
content.newLineAtOffset(pageSize.getWidth() - margin - 50, yStart - 30);
content.showText("" + (pageNum + 1) + "");
content.endText();
}
// ===== 表格绘制方法 ===== // ===== 计算本页的行数 =====
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);
// ===== 检验单位与印章 ===== // ===== 检验单位与印章最后一页显示 =====
if (currentRow + rowsThisPage >= totalRows) {
yPosition -= 60; yPosition -= 60;
content.beginText(); content.beginText();
content.setFont(font, 12); content.setFont(font, 12);
@ -94,45 +184,51 @@ 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); currentRow += rowsThisPage;
return bos.toByteArray(); pageNum++;
} catch (Exception e) {
e.printStackTrace();
return new byte[0];
} }
} }
private static String safe(String v) {
return v == null ? "" : v;
}
private static float getStringWidth(String text, PDFont font, float fontSize) throws IOException {
return font.getStringWidth(text) / 1000 * fontSize;
}
/** /**
* 自动换行表格绘制 * 获取当前页的行数据
*/ */
private static float drawTable(PDDocument doc, PDPage page, PDPageContentStream content, 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;
// 绘制表头所需高度
float headerHeight = getRowHeight(headers, font, fontSize, colWidths, cellMargin);
currentY -= headerHeight;
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 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,
@ -140,25 +236,13 @@ public class DocxUtil {
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];
} }
} }
} }