java使用poi-tl模版+vform自定义表单生成word,使用LibreOffice导出为pdf,批量下载为压缩文件。
java
@Override
public void exportMeetingAll(Long id, HttpServletResponse response) {
TbProjectStartHold tbProjectStartHold = selectTbProjectStartHoldById(id, null);
TbProjectStart tbProjectStart = tbProjectStartMapper.selectTbProjectStartById(id);
//查询伦理的项目名称
ProjectInitiation initiation = projectInitiationService.findById(tbProjectStartHold.getProjectInitiationId(), null);
String fileStr=initiation.getAcceptanceNumber().replace(" ", "")+ "_启动会文件_" + DateUtils.dateTimeNow()+ ".zip";
// 设置响应头
response.setContentType("application/zip");
String fileName = URLEncoder.encode(fileStr, StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 创建ZIP输出流
try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(response.getOutputStream());
BufferedOutputStream bos = new BufferedOutputStream(zipOut)) {
zipOut.setEncoding("UTF-8"); // 设置编码
// 导出启动会预约确认单
projectStartService.exportAppointmentForZip(tbProjectStart,initiation,zipOut);
//导出会议议程
exportMeetingAgendaPdfForZip(tbProjectStartHold,initiation,zipOut);
//导出启动承诺函
exportMeetingStartForZip(tbProjectStartHold,initiation,zipOut);
//导出会议记录
exportMeetingRecordForZip(tbProjectStartHold,initiation,zipOut);
//导出会议照片
List<SysFileData> meetiongPhoto=tbProjectStartHold.getMeetingPhoto();
if(meetiongPhoto!=null){
Map<String,String> map=new HashMap<>();
meetiongPhoto.forEach(e->{
map.put(e.getFileUrl(),"会议照片/"+e.getFileName());
});
ImageExport.batchAddImagesToZip(zipOut,map,resourceLoader);
}
// 刷新流,确保所有数据写入
bos.flush();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("打包下载失败:" + e.getMessage());
}
}
java
@Override
public void exportMeetingStartForZip(TbProjectStartHold tbProjectStartHold,ProjectInitiation initiation, ZipArchiveOutputStream zipOut) {
Map<String, Object> data=new HashMap<>();
data.put("projectName",Optional.ofNullable(tbProjectStartHold.getProjectName()).orElse(""));
data.put("meetingDate","");
if(tbProjectStartHold.getMeetingDate()!=null){
data.put("meetingDate",DateUtils.parseDateToStr("yyyy-MM-dd",tbProjectStartHold.getMeetingDate()));
}
//模板选择
String url = "classpath:templates/start_chengnuohan.docx";
String fileName=initiation.getAcceptanceNumber().replace(" ", "")+ "_项目启动承诺函_" + DateUtils.dateTimeNow();
//响应返回 pdf 文件
WordToPdf.generatePdfForZip(zipOut,url,data,resourceLoader,libreoffice,fileName + ".pdf");
}
java
/**
* 生成pdf 在压缩包内
* @param zipOut
* @param url
* @param data
* @param resourceLoader
* @param libreoffice
* @param pdfFileName
*/
public static void generatePdfForZip(ZipArchiveOutputStream zipOut, String url, Map<String, Object> data,
ResourceLoader resourceLoader, String libreoffice, String pdfFileName) {
Configure config = Configure.builder().build();
generatePdfForZip(config, zipOut, url, data, resourceLoader, libreoffice, pdfFileName);
}
/**
* 生成pdf 在压缩包内
* @param zipOut
* @param url
* @param data
* @param resourceLoader
* @param libreoffice
* @param pdfFileName
*/
public static void generatePdfForZip(Configure config,ZipArchiveOutputStream zipOut, String url, Map<String, Object> data,
ResourceLoader resourceLoader, String libreoffice, String pdfFileName) {
// ========== 2. 生成临时 Word 文件(关键修改:不再直接写入响应) ==========
// 临时文件目录(跨平台兼容:Windows是C:\Users\XXX\AppData\Local\Temp,Linux是/tmp)
System.out.println("****************");;
System.out.println(System.getProperty("java.io.tmpdir"));
File tempDir = new File(System.getProperty("java.io.tmpdir"));
// 生成唯一文件名(避免并发冲突)
String uniqueName = UUID.randomUUID().toString().replace("-", "");
File tempWordFile = new File(tempDir, uniqueName + ".docx");
File tempPdfFile = new File(tempDir, uniqueName + ".pdf");
// 标记是否生成成功(用于最终清理临时文件)
boolean generateSuccess = false;
try {
// 2.1 用 poi-tl 生成 Word 到临时文件
Resource resource = resourceLoader.getResource(url);
try (InputStream inputStream = resource.getInputStream();
OutputStream wordOut = new FileOutputStream(tempWordFile)) {
XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data);
template.write(wordOut);
PoitlIOUtils.closeQuietly(template); // 关闭模板资源
}
// ========== 3. 调用 LibreOffice 转换 Word → PDF(核心步骤) ==========
boolean convertSuccess = convertWordToPdfByLibreOffice(libreoffice,tempWordFile, tempPdfFile);
if (!convertSuccess || !tempPdfFile.exists() || tempPdfFile.length() == 0) {
throw new RuntimeException("Word 转 PDF 失败,生成的 PDF 文件为空");
}
// 3. 创建ZIP条目(指定PDF在压缩包中的名称/路径)
// 支持子目录格式,例如:"2024年报表/销售数据.pdf"
ZipArchiveEntry pdfEntry = new ZipArchiveEntry(pdfFileName);
// 设置文件大小(优化ZIP压缩效率)
pdfEntry.setSize(tempPdfFile.length());
zipOut.putArchiveEntry(pdfEntry);
// 4. 将PDF文件写入ZIP条目
try (InputStream pdfIn = new FileInputStream(tempPdfFile);
BufferedInputStream bis = new BufferedInputStream(pdfIn)) {
byte[] buffer = new byte[1024 * 8];
int len;
while ((len = bis.read(buffer)) != -1) {
zipOut.write(buffer, 0, len);
}
}
// 5. 关闭当前ZIP条目(必须调用,否则后续条目无法添加)
zipOut.closeArchiveEntry();
// 刷新ZIP流,确保数据写入(外部最终需调用zipOut.close())
zipOut.flush();
generateSuccess = true;
System.out.println("PDF文件[" + pdfFileName + "]已成功添加到ZIP包");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("导出 PDF 失败:" + e.getMessage(), e);
} finally {
// ========== 5. 清理临时文件(关键:避免磁盘残留) ==========
// 6. 清理临时文件(异步延迟删除,确保ZIP写入完成)
boolean finalGenerateSuccess = generateSuccess;
new Thread(() -> {
try {
// 延迟3秒删除(如果生成失败,缩短延迟)
TimeUnit.SECONDS.sleep(finalGenerateSuccess ? 3 : 1);
// 静默删除,失败不抛异常(避免影响主流程)
FileUtils.deleteQuietly(tempWordFile);
FileUtils.deleteQuietly(tempPdfFile);
System.out.println("临时文件清理完成:" + tempWordFile.getName() + "、" + tempPdfFile.getName());
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}).start();
}
}
图片处理
java
public class ImageExport {
/**
* 单个图片写入ZIP流(支持本地文件、classpath资源、网络图片)
*
* @param zipOut ZIP输出流
* @param imageSource 图片来源(支持3种格式:1.本地路径如D:/img/1.jpg 2.classpath路径如classpath:/images/2.png 3.网络URL如https://xxx.com/3.jpg)
* @param zipImageName ZIP内图片文件名(支持子目录:如images/产品图.png)
* @param resourceLoader Spring资源加载器(用于读取classpath图片)
* @throws Exception 图片读取或写入异常
*/
public static void addImageToZip(ZipArchiveOutputStream zipOut, String imageSource, String zipImageName,
ResourceLoader resourceLoader) throws Exception {
InputStream imageIn = null;
try {
// 适配不同图片来源
if (imageSource.startsWith("classpath:")) {
// 读取classpath下的图片
Resource resource = resourceLoader.getResource(imageSource);
imageIn = resource.getInputStream();
} else if (imageSource.startsWith("http://") || imageSource.startsWith("https://")) {
// 读取网络图片
imageIn = new java.net.URL(imageSource).openStream();
} else {
// 读取本地文件图片
String localPath = RuoYiConfig.getProfile();
String downloadPath = localPath + StringUtils.substringAfter(imageSource, Constants.RESOURCE_PREFIX);
File imageFile = new File(downloadPath);
if (!imageFile.exists()) {
throw new FileNotFoundException("本地图片不存在:" + imageSource);
}
imageIn = new FileInputStream(imageFile);
}
// 图片写入ZIP
addStreamToZip(zipOut, imageIn, zipImageName);
System.out.println("图片已添加到ZIP:" + zipImageName);
} finally {
// 关闭图片输入流
if (imageIn != null) {
try {
imageIn.close();
} catch (IOException ignored) {
}
}
}
}
/**
* 多张图片批量写入ZIP流
*
* @param zipOut ZIP输出流
* @param imageMap 图片映射:key=图片来源(本地/classpath/URL),value=ZIP内文件名(含路径)
* @param resourceLoader Spring资源加载器
* @throws Exception 批量处理异常
*/
public static void batchAddImagesToZip(ZipArchiveOutputStream zipOut, Map<String, String> imageMap,
ResourceLoader resourceLoader) throws Exception {
for (Map.Entry<String, String> entry : imageMap.entrySet()) {
addImageToZip(zipOut, entry.getKey(), entry.getValue(), resourceLoader);
}
}
/**
* 工具方法:将输入流写入ZIP流(用于图片流场景)
*
* @param zipOut ZIP输出流
* @param inputStream 输入流(图片/其他文件流)
* @param zipFileName ZIP内的文件名(含路径)
* @throws Exception 写入异常
*/
private static void addStreamToZip(ZipArchiveOutputStream zipOut, InputStream inputStream, String zipFileName) throws Exception {
try (BufferedInputStream bis = new BufferedInputStream(inputStream)) {
ZipArchiveEntry entry = new ZipArchiveEntry(zipFileName);
zipOut.putArchiveEntry(entry); // 流无法提前获取大小,不设置size
byte[] buffer = new byte[1024 * 8];
int len;
while ((len = bis.read(buffer)) != -1) {
zipOut.write(buffer, 0, len);
}
zipOut.closeArchiveEntry();
zipOut.flush();
}
}