feat: 提取通用 Word/PDF 模板渲染方法,支持 DOC、DOCX 和循环表格

This commit is contained in:
2026-10-30 10:16:46 +08:00
parent d8e7adfe9a
commit 29260423d5
3 changed files with 111 additions and 62 deletions
@@ -6,6 +6,8 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.models.Sys_office_template;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.ByteArrayPictureRenderData;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
@@ -18,6 +20,12 @@ import org.nutz.ioc.loader.annotation.IocBean;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -66,6 +74,88 @@ public class SysOfficeTemplateUtil {
return new ByteArrayInputStream(fileBytes);
}
/**
* 统一渲染 DOC/DOCX 模板,保留调用方的循环表格等策略;模板输入流由调用方关闭。
*/
public byte[] renderTemplate(InputStream input, Object data, Configure config, boolean pdf, String context) throws Exception {
if (input == null) throw new BaseException("模板文件不存在");
byte[] templateBytes = input.readAllBytes();
try (ByteArrayInputStream formatInput = new ByteArrayInputStream(templateBytes)) {
if (com.aspose.words.FileFormatUtil.detectFileFormat(formatInput).getLoadFormat() != com.aspose.words.LoadFormat.DOCX) {
try (ByteArrayInputStream source = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream converted = new ByteArrayOutputStream()) {
new com.aspose.words.Document(source).save(converted, com.aspose.words.SaveFormat.DOCX);
templateBytes = converted.toByteArray();
}
}
}
byte[] wordBytes;
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream();
XWPFTemplate template = XWPFTemplate.compile(templateInput, config == null ? Configure.builder().build() : config)) {
template.render(data).write(output);
wordBytes = output.toByteArray();
} catch (Exception e) {
log.error("[Office模板渲染] Word模板渲染失败,context={}", context, e);
throw e;
}
log.info("[Office模板渲染] Word渲染完成,context={}, templateBytes={}, outputBytes={}", context, templateBytes.length, wordBytes.length);
if (!pdf) return wordBytes;
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(wordBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
com.aspose.words.Document document = new com.aspose.words.Document(wordInput);
configureDocumentFonts(document, context);
com.aspose.words.PdfSaveOptions options = new com.aspose.words.PdfSaveOptions();
options.setFontEmbeddingMode(com.aspose.words.PdfFontEmbeddingMode.EMBED_ALL);
document.save(output, options);
log.info("[Office模板渲染] PDF转换完成,context={}, outputBytes={}", context, output.size());
return output.toByteArray();
} catch (Exception e) {
log.error("[Office模板渲染] PDF转换失败,context={}", context, e);
throw e;
}
}
private void configureDocumentFonts(com.aspose.words.Document document, String context) throws Exception {
com.aspose.words.FontSettings settings = new com.aspose.words.FontSettings();
List<com.aspose.words.FontSourceBase> sources = new ArrayList<>(List.of(settings.getFontsSources()));
Set<String> folders = new LinkedHashSet<>();
String windowsDir = System.getenv("WINDIR");
if (StrUtil.isNotBlank(windowsDir)) folders.add(new File(windowsDir, "Fonts").getPath());
String localAppData = System.getenv("LOCALAPPDATA");
if (StrUtil.isNotBlank(localAppData)) folders.add(new File(localAppData, "Microsoft/Windows/Fonts").getPath());
folders.add(new File(System.getProperty("user.dir"), "fonts").getPath());
for (String folder : folders) {
File directory = new File(folder);
log.info("[Office模板渲染] 字体目录,context={}, path={}, readable={}", context, folder, directory.isDirectory() && directory.canRead());
if (directory.isDirectory() && directory.canRead()) sources.add(new com.aspose.words.FolderFontSource(folder, true));
}
settings.setFontsSources(sources.toArray(new com.aspose.words.FontSourceBase[0]));
Set<String> availableFonts = new LinkedHashSet<>();
for (com.aspose.words.FontSourceBase source : sources) {
for (com.aspose.words.PhysicalFontInfo font : source.getAvailableFonts()) availableFonts.add(font.getFontFamilyName());
}
String chineseFont = null;
for (String candidate : List.of("SimSun", "宋体", "Microsoft YaHei", "微软雅黑", "DengXian", "等线", "SimHei", "黑体", "FangSong", "仿宋", "Noto Serif CJK SC", "Noto Sans CJK SC")) {
if (availableFonts.contains(candidate)) {
chineseFont = candidate;
break;
}
}
log.info("[Office模板渲染] 中文字体选择,context={}, fontCount={}, chineseFont={}", context, availableFonts.size(), chineseFont);
if (chineseFont != null) {
settings.getSubstitutionSettings().getDefaultFontSubstitution().setDefaultFontName(chineseFont);
for (String name : List.of("宋体", "SimSun", "新宋体", "NSimSun", "黑体", "SimHei", "仿宋", "FangSong", "楷体", "KaiTi", "微软雅黑", "Microsoft YaHei")) {
if (!availableFonts.contains(name)) settings.getSubstitutionSettings().getTableSubstitution().setSubstitutes(name, chineseFont);
}
} else {
log.warn("[Office模板渲染] 未发现常用中文字体,context={}, availableFonts={}", context, availableFonts);
}
settings.getFallbackSettings().loadMsOfficeFallbackSettings();
document.setFontSettings(settings);
document.setWarningCallback(warning -> log.warn("[Office模板渲染] 字体或排版警告,context={}, type={}, detail={}", context, warning.getWarningType(), warning.getDescription()));
}
/**
* 签字url转换为poi的图片对象
*
@@ -28,7 +28,6 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureRenderData;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -48,8 +47,6 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
@@ -187,27 +184,12 @@ public class CondolenceMineController {
log.info("[职工慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
}
String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc");
ByteArrayOutputStream templateOutput = new ByteArrayOutputStream();
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
if (input == null) throw new BaseException("职工慰问申请表模板不存在");
com.aspose.words.Document document = new com.aspose.words.Document(input);
document.save(templateOutput, com.aspose.words.SaveFormat.DOCX);
log.info("[职工慰问导出] 模板转换完成,applyId={}, templateBytes={}", id, templateOutput.size());
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateOutput.toByteArray())) {
XWPFTemplate.compile(templateInput).render(docData).writeAndClose(output);
}
log.info("[职工慰问导出] Word渲染完成,applyId={}, outputBytes={}", id, output.size());
if (Boolean.TRUE.equals(print)) {
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(output.toByteArray());
ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream()) {
new com.aspose.words.Document(wordInput).save(pdfOutput, com.aspose.words.SaveFormat.PDF);
log.info("[职工慰问导出] PDF转换完成,applyId={}, outputBytes={}", id, pdfOutput.size());
CommonDownloadUtil.download(fileName + ".pdf", pdfOutput.toByteArray(), response);
}
} else {
CommonDownloadUtil.download(fileName + ".docx", output.toByteArray(), response);
}
boolean pdf = Boolean.TRUE.equals(print);
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, null, pdf, "condolence:" + id);
log.info("[职工慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
} catch (Exception e) {
log.error("[职工慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
throw e;
@@ -9,7 +9,6 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
@@ -18,7 +17,6 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.MoneyUtil;
import com.budwk.app.base.utils.OfficePlusUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
@@ -39,7 +37,6 @@ import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommon
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo;
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
@@ -74,8 +71,6 @@ import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@@ -319,9 +314,9 @@ public class DifficultHelpReadingController {
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder().bind("familyList", policy).build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("difficulty_apply"), config)
.render(docData)
.write(zipOutputStream);
try (InputStream input = sysOfficeTemplateUtil.getTemplate("difficulty_apply")) {
zipOutputStream.write(sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:batch:" + record.getString("id")));
}
zipOutputStream.closeEntry();
}
zipOutputStream.flush();
@@ -379,17 +374,18 @@ public class DifficultHelpReadingController {
String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"};
String[] fields = {"firstApproval", "secondApproval", "thirdApproval"};
for (String field : fields) {
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", businessId).desc("id"));
if (instance == null) return;
List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
// 重新申请后,仅导出本轮审批,避免混入上一次退回或撤销前的意见。
for (String field : fields) {
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
continue;
}
@@ -453,22 +449,9 @@ public class DifficultHelpReadingController {
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
XWPFTemplate.compile(getApplyTemplate(templateName),config)
.render(docData)
.writeAndClose(byteArrayOutputStream);
byte[] bytes = byteArrayOutputStream.toByteArray();
File sourceFile = File.createTempFile("file_convert_origin", ".docx");
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
File pdfFile = File.createTempFile("file_convert", ".pdf");
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
CommonDownloadUtil.download(fileName, IoUtil.readBytes(FileUtil.getInputStream(pdfFile)), response);
// 删除临时文件
FileUtil.del(sourceFile);
FileUtil.del(pdfFile.toPath());
} catch (IOException e) {
log.error("困难慰问申请表导出PDF失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
try (InputStream input = getApplyTemplate(templateName)) {
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, true, "difficulthelp:" + difficultHelp.getId());
CommonDownloadUtil.download(fileName, bytes, response);
}
}
@@ -478,13 +461,9 @@ public class DifficultHelpReadingController {
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
XWPFTemplate.compile(getApplyTemplate(templateName),config)
.render(docData)
.writeAndClose(byteArrayOutputStream);
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
} catch (IOException e) {
log.error("困难慰问申请表导出Word失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
try (InputStream input = getApplyTemplate(templateName)) {
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:" + difficultHelp.getId());
CommonDownloadUtil.download(fileName, bytes, response);
}
}
@@ -562,15 +541,13 @@ public class DifficultHelpReadingController {
throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage());
}
} else if ("docx".equals(flag)) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
try (InputStream input = sysOfficeTemplateUtil.getTemplate(resource)) {
Configure config = Configure.builder()
.bind("da", new LoopRowTableRenderPolicy())
.build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(resource), config)
.render(result)
.writeAndClose(out);
CommonDownloadUtil.download(fileName + ".docx", out.toByteArray(), response);
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, result, config, false, "difficulthelp:summary:" + resource);
CommonDownloadUtil.download(fileName + ".docx", bytes, response);
}
}
}