From 29260423d5143570c204bcd2967b434c77dcd491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=8C=E5=85=88=E8=88=9C?= <2499071264@qq.com> Date: Fri, 30 Oct 2026 10:16:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E5=8F=96=E9=80=9A=E7=94=A8=20?= =?UTF-8?q?Word/PDF=20=E6=A8=A1=E6=9D=BF=E6=B8=B2=E6=9F=93=E6=96=B9?= =?UTF-8?q?=E6=B3=95=EF=BC=8C=E6=94=AF=E6=8C=81=20DOC=E3=80=81DOCX=20?= =?UTF-8?q?=E5=92=8C=E5=BE=AA=E7=8E=AF=E8=A1=A8=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/base/utils/SysOfficeTemplateUtil.java | 90 +++++++++++++++++++ .../controller/CondolenceMineController.java | 28 ++---- .../DifficultHelpReadingController.java | 55 ++++-------- 3 files changed, 111 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/budwk/app/base/utils/SysOfficeTemplateUtil.java b/src/main/java/com/budwk/app/base/utils/SysOfficeTemplateUtil.java index 165f672..d71d801 100644 --- a/src/main/java/com/budwk/app/base/utils/SysOfficeTemplateUtil.java +++ b/src/main/java/com/budwk/app/base/utils/SysOfficeTemplateUtil.java @@ -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 sources = new ArrayList<>(List.of(settings.getFontsSources())); + Set 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 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的图片对象 * diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/condolence/controller/CondolenceMineController.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/condolence/controller/CondolenceMineController.java index 2c7369a..d40f90f 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/condolence/controller/CondolenceMineController.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/condolence/controller/CondolenceMineController.java @@ -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; diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/difficulthelp/controller/DifficultHelpReadingController.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/difficulthelp/controller/DifficultHelpReadingController.java index b0fb63c..16c666a 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/difficulthelp/controller/DifficultHelpReadingController.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/difficulthelp/controller/DifficultHelpReadingController.java @@ -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 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); } } }