报告一键下载功能优化
This commit is contained in:
parent
59d22c923a
commit
a065b8ecde
|
|
@ -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<String, DownloadProgress> downloadProgressMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 新购入库报表查询
|
||||
* @param bean
|
||||
|
|
@ -608,24 +621,56 @@ 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");
|
||||
|
||||
// 初始化进度
|
||||
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.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())) {
|
||||
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(
|
||||
item -> sanitize(item.getProName()), // 工程
|
||||
item -> sanitize(item.getProName()),
|
||||
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()) {
|
||||
String projectFolder = projectEntry.getKey();
|
||||
Map<String, List<DownloadRequest.ItemInfo>> dateMap = projectEntry.getValue();
|
||||
|
|
@ -635,40 +680,85 @@ public class BmReportController extends BaseController {
|
|||
List<DownloadRequest.ItemInfo> items = dateEntry.getValue();
|
||||
String baseDatePath = projectFolder + "/" + dateFolder + "/";
|
||||
|
||||
// 1. 生成合并的出库检验报告
|
||||
byte[] mergedReport = DocxUtil.generateReportByList(items);
|
||||
addToZip(zipOut, baseDatePath + "出库检验报告.pdf", mergedReport);
|
||||
// 1. 生成合并的出库检验报告(使用新的分页方法)
|
||||
progress.setCurrentFileName("正在生成出库检验报告...");
|
||||
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()
|
||||
.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()) {
|
||||
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="";
|
||||
if (!StringHelper.isEmpty(item.getMaCode())){
|
||||
maCode="_"+item.getMaCode();
|
||||
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;
|
||||
}
|
||||
|
||||
} 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<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) {
|
||||
int idx = url.lastIndexOf('.');
|
||||
return (idx > 0 && idx < url.length() - 1) ? url.substring(idx) : "";
|
||||
if (url == null || url.isEmpty()) {
|
||||
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));
|
||||
int dotIndex = url.lastIndexOf('.');
|
||||
if (dotIndex > 0 && dotIndex < url.length() - 1) {
|
||||
return url.substring(dotIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出资产占有月度报表
|
||||
* @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);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,17 +22,93 @@ import java.util.List;
|
|||
*/
|
||||
public class DocxUtil {
|
||||
|
||||
/**
|
||||
* 生成单个报告
|
||||
*/
|
||||
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();
|
||||
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());
|
||||
|
||||
// 数据转为二维数组
|
||||
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 yStart = pageSize.getHeight() - margin;
|
||||
float yPosition = yStart;
|
||||
float bottomMargin = 120; // 底部留出足够空间给印章
|
||||
|
||||
float[] colWidths = {80, 80, 40, 40, 80, 70, 70, 75, 65, 80, 60, 40};
|
||||
String[] headers = {
|
||||
|
|
@ -38,20 +116,20 @@ public class DocxUtil {
|
|||
"额定载荷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);
|
||||
document.addPage(page);
|
||||
PDPageContentStream content = new PDPageContentStream(document, page);
|
||||
|
||||
float yPosition = yStart;
|
||||
|
||||
// ===== 标题 =====
|
||||
String titleText = "施工机具设备出库检验记录表";
|
||||
content.beginText();
|
||||
|
|
@ -62,31 +140,43 @@ public class DocxUtil {
|
|||
|
||||
yPosition -= 50;
|
||||
|
||||
// ===== 工程与单位 =====
|
||||
// ===== 工程与单位(第一页显示,后续页面只显示页码) =====
|
||||
if (pageNum == 0) {
|
||||
content.beginText();
|
||||
content.setFont(font, 12);
|
||||
content.newLineAtOffset(margin, yPosition);
|
||||
content.showText("领用工程:" + safe(item.getProName()));
|
||||
content.showText("领用工程:" + safe(firstItem.getProName()));
|
||||
content.newLineAtOffset(0, -20);
|
||||
content.showText("使用单位:" + safe(item.getDepartName()));
|
||||
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();
|
||||
}
|
||||
|
||||
// ===== 表格绘制方法 =====
|
||||
// ===== 计算本页的行数 =====
|
||||
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);
|
||||
|
||||
// ===== 检验单位与印章 =====
|
||||
// ===== 检验单位与印章(最后一页显示) =====
|
||||
if (currentRow + rowsThisPage >= totalRows) {
|
||||
yPosition -= 60;
|
||||
content.beginText();
|
||||
content.setFont(font, 12);
|
||||
|
|
@ -94,45 +184,51 @@ 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);
|
||||
// 添加印章图片
|
||||
addStamp(document, content, margin, yPosition);
|
||||
}
|
||||
byte[] imageBytes = buffer.toByteArray();
|
||||
|
||||
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "gaizhang");
|
||||
content.drawImage(image, margin + 70, yPosition - 60, 100, 100);
|
||||
is.close();
|
||||
}
|
||||
|
||||
|
||||
content.close();
|
||||
document.save(bos);
|
||||
return bos.toByteArray();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return new byte[0];
|
||||
currentRow += rowsThisPage;
|
||||
pageNum++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
float[] colWidths, String[] headers, String[][] rows,
|
||||
float fontSize, float cellMargin, PDRectangle pageSize,
|
||||
|
|
@ -140,25 +236,13 @@ public class DocxUtil {
|
|||
|
||||
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<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()) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue