基于java8
引入依赖
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
<version>8.3.1</version> <!-- Java 8 兼容版本 -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version> <!-- 日志依赖 -->
</dependency>
package com.inspur.plugins.project.export;import com.inspur.plugins.bill.dto.ProjectBillOutDTO; import com.inspur.plugins.partner.entity.PartnerFile; import org.docx4j.XmlUtils; import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; import org.docx4j.wml.*;import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import java.io.*; import java.lang.reflect.Field; import java.math.BigDecimal; import java.math.BigInteger; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.docx4j.dml.wordprocessingDrawing.Inline; // 新增:图片内联对象类 import org.docx4j.wml.Drawing; // 新增:绘图对象类/*** Java 8 兼容的 Word 模板填充工具类* 支持文本内容控件和表格数据填充*/ public class WordTemplateWriter {/*** 填充Word模板并输出文件* @param templatePath 模板路径(支持类路径或绝对路径)* @param outputPath 输出文件路径* @param dataMap 填充数据(key: 内容控件标签, value: 填充值)* @throws Exception 处理异常*/public static void fillTemplate(String templatePath, String outputPath, Map<String, Object> dataMap) throws Exception {// 加载模板WordprocessingMLPackage wordMLPackage = loadTemplate(templatePath);MainDocumentPart mainDocPart = wordMLPackage.getMainDocumentPart();// 新增:判断是否为保证金模板并填充表头boolean isDepositTemplate = templatePath.contains("(保证金)");if (isDepositTemplate && dataMap.containsKey("payRate")) {fillTableHeader(mainDocPart, dataMap);}// 新增:替换文本占位符(<<tag>>格式) replaceTextPlaceholders(mainDocPart, dataMap);// 新增:替换图片占位符([[image:tag]]格式) replaceImagePlaceholders(mainDocPart, dataMap);// 填充表格数据(如果存在表格数据)if (dataMap.containsKey("tableData")) {fillTableData(mainDocPart, dataMap.get("tableData")); // 直接传递原始对象 }// 保存文件 saveDocument(wordMLPackage, outputPath);}/*** 填充表格表头数据(针对保证金模板特殊处理)*/private static void fillTableHeader(MainDocumentPart mainDocPart, Map<String, Object> headerData) throws Docx4JException, JAXBException {// Map<String, String> headerData = convertToMap(headerDataObj);if (headerData == null || headerData.isEmpty()) return;// 获取文档中第一个表格List<Object> tables = mainDocPart.getJAXBNodesViaXPath("//w:tbl", false);if (tables.isEmpty()) return;Object tableObj = tables.get(0);if (tableObj instanceof JAXBElement) {tableObj = ((JAXBElement<?>) tableObj).getValue();}Tbl table = (Tbl) tableObj;List<Tr> rows = table.getContent().stream().filter(Tr.class::isInstance).map(Tr.class::cast).collect(Collectors.toList());if (rows.isEmpty()) return; // 确保表格至少有一行// 获取表头行(第一行)Tr headerRow = rows.get(0);List<Tc> headerCells = getCellsFromRow(headerRow);// 替换表头单元格中的 {{tag}} 占位符 replacePlaceholdersInTextNodes(extractTextNodesFromContents(headerRow.getContent()), headerData);}/*** 保证金特殊处理支持表格单元格、段落等嵌套结构)保证金特殊处理*/private static List<Object> extractTextNodesFromContents(List<Object> content) {List<Object> textNodes = new ArrayList<>();for (Object obj : content) {// 处理JAXBElement包装的元素if (obj instanceof JAXBElement) {obj = ((JAXBElement<?>) obj).getValue();}// 递归处理所有内容容器(替换原有的P和R类型判断)if (obj instanceof ContentAccessor) { // ContentAccessor是docx4j中容器元素的通用接口 textNodes.addAll(extractTextNodesFromContents(((ContentAccessor) obj).getContent()));} else if (obj instanceof Text) { // 文本节点 textNodes.add(obj);}}return textNodes;}/*** 将任意对象转换为字段名-值字符串映射(支持基本类型、String、BigDecimal等)*/private static Map<String, String> convertToMap(Object obj) {if (obj == null) return null;// 若已是Map则直接转换(兼容旧逻辑)if (obj instanceof Map) {Map<?, ?> rawMap = (Map<?, ?>) obj;Map<String, String> strMap = new HashMap<>();rawMap.forEach((k, v) -> strMap.put(k.toString(), v != null ? v.toString() : ""));return strMap;}// 反射提取对象字段值(支持DTO对象)Map<String, String> fieldMap = new HashMap<>();Class<?> clazz = obj.getClass();// 获取所有字段(包括父类字段)List<Field> fields = new ArrayList<>();while (clazz != null) {fields.addAll(Arrays.asList(clazz.getDeclaredFields()));clazz = clazz.getSuperclass();}// 提取字段值for (Field field : fields) {try {field.setAccessible(true);Object value = field.get(obj);fieldMap.put(field.getName(), value != null ? value.toString() : "");} catch (Exception e) {}}return fieldMap;}/*** 加载Word模板*/private static WordprocessingMLPackage loadTemplate(String templatePath) throws Docx4JException, IOException {// 尝试从类路径加载模板try (InputStream is = WordTemplateWriter.class.getClassLoader().getResourceAsStream(templatePath)) {if (is != null) {return WordprocessingMLPackage.load(is);}}// 类路径加载失败则尝试绝对路径return WordprocessingMLPackage.load(new File(templatePath));}/*** 填充表格数据* 模板要求:表格需包含表头行和至少一行"模板行"(内容控件作为占位符)*/private static void fillTableData(MainDocumentPart mainDocPart, Object tableDataObj) throws Docx4JException, JAXBException {// 1. 校验输入是否为列表if (!(tableDataObj instanceof List)) {System.out.println("tableData 不是有效列表类型,跳过表格填充");return;}List<?> tableData = (List<?>) tableDataObj;if (tableData.isEmpty()) return;// 获取文档中第一个表格List<Object> tables = mainDocPart.getJAXBNodesViaXPath("//w:tbl", false);if (tables.isEmpty()) return;Object tableObj = tables.get(0);if (tableObj instanceof JAXBElement) {tableObj = ((JAXBElement<?>) tableObj).getValue();}Tbl table = (Tbl) tableObj;List<Tr> rows = table.getContent().stream().filter(Tr.class::isInstance).map(Tr.class::cast).collect(Collectors.toList());if (rows.size() < 2) return; // 至少需要表头行 + 1行模板行// 获取模板行(假设第二行为模板行)Tr templateRow = rows.get(1);// 移除模板行(后续会替换为实际数据行) rows.remove(templateRow);// 新增:计算表格总列数(从模板行获取)List<Tc> templateCells = getCellsFromRow(templateRow);int totalColumns = templateCells.size();// 填充数据行并添加合并行for (Object rowObj : tableData) {// 将对象转换为字段名-值映射(核心适配)Map<String, String> rowData = convertToMap(rowObj);if (rowData == null) continue;// 创建数据行Tr dataRow = (Tr) XmlUtils.deepCopy(templateRow);fillTableRow(dataRow, rowData);// 创建要合并的第二行Tr mergedRow = (Tr) XmlUtils.deepCopy(templateRow);clearTableRowContent(mergedRow); // 清空合并行内容// 添加两行到表格 rows.add(dataRow);rows.add(mergedRow);// 合并两行的第一列(垂直合并)Tc dataRowFirstCell = getCellByIndex(dataRow, 0);setVerticalMerge(dataRowFirstCell, "restart");Tc mergedRowFirstCell = getCellByIndex(mergedRow, 0);setVerticalMerge(mergedRowFirstCell, "continue");// 给第二列赋值并横向合并后续所有列Tc mergedRowSecondCell = getCellByIndex(mergedRow, 1);if (mergedRowSecondCell != null) {setCellContent(mergedRowSecondCell, rowData.get("calculationFormula"));// 新增:设置横向合并(合并剩余所有列)setCellGridSpan(mergedRowSecondCell, totalColumns - 1);// 新增:删除合并行中多余的单元格(从第3列开始)List<Tc> mergedRowCells = getCellsFromRow(mergedRow);if (mergedRowCells.size() > 2) {// 只保留前两列单元格,删除后续多余单元格mergedRow.getContent().subList(2, mergedRow.getContent().size()).clear();}}}// 更新表格内容 table.getContent().clear();table.getContent().addAll(rows);}/*** 从行中获取所有单元格*/private static List<Tc> getCellsFromRow(Tr row) {return row.getContent().stream().map(obj -> {if (obj instanceof JAXBElement) {return (Tc) ((JAXBElement<?>) obj).getValue();} else if (obj instanceof Tc) {return (Tc) obj;}return null;}).filter(Objects::nonNull).collect(Collectors.toList());}/*** 设置单元格横向合并(跨列)* @param cell 单元格* @param span 合并列数(>=2表示合并后续span-1列)*/private static void setCellGridSpan(Tc cell, int span) {if (cell == null || span <= 1) return; // 无需合并或参数无效TcPr tcPr = cell.getTcPr();if (tcPr == null) {tcPr = new TcPr();cell.setTcPr(tcPr);}// 设置横向合并属性(gridSpan表示当前单元格跨越的列数)TcPrInner.GridSpan gridSpan = new TcPrInner.GridSpan();gridSpan.setVal(BigInteger.valueOf(span));tcPr.setGridSpan(gridSpan);}/*** 填充表格行数据*/private static void fillTableRow(Tr row, Map<String, String> rowData) {List<Tc> cells = row.getContent().stream()// 新增:处理 JAXBElement 包装的单元格.map(obj -> {if (obj instanceof JAXBElement) {return ((JAXBElement<?>) obj).getValue();}return obj;}).filter(Tc.class::isInstance).map(Tc.class::cast).collect(Collectors.toList());for (Tc cell : cells) {// 提取单元格内所有文本节点List<Object> cellTextNodes = extractTextNodesFromContent(cell.getContent());// 新增:合并多个文本节点为一个(若存在)if (cellTextNodes.size() > 1) {// 1. 合并所有文本节点内容StringBuilder mergedText = new StringBuilder();for (Object node : cellTextNodes) {if (node instanceof Text) {mergedText.append(((Text) node).getValue());}}// 2. 清空单元格原有内容,添加合并后的文本节点 cell.getContent().clear();P paragraph = new P();R run = new R();Text mergedTextNode = new Text();mergedTextNode.setValue(mergedText.toString());run.getContent().add(mergedTextNode);paragraph.getContent().add(run);cell.getContent().add(paragraph);// 3. 更新文本节点列表为合并后的单个节点cellTextNodes = Collections.singletonList(mergedTextNode);}// 替换单元格内的 {{tag}} 占位符 replacePlaceholdersInTextNodes(cellTextNodes, rowData);}}/*** 通用占位符替换工具(供文本和表格共用)* 替换文本节点列表中的 {{tag}} 占位符*/private static void replacePlaceholdersInTextNodes(List<Object> textNodes, Map<String, ?> dataMap) {Pattern placeholderPattern = Pattern.compile("\\[\\[(.+?)\\]\\]");for (Object obj : textNodes) {Text textNode = null;// 处理JAXBElement包装的文本节点if (obj instanceof JAXBElement) {JAXBElement<?> jaxbElement = (JAXBElement<?>) obj;if (jaxbElement.getValue() instanceof Text) {textNode = (Text) jaxbElement.getValue();}} else if (obj instanceof Text) {textNode = (Text) obj;}if (textNode != null) {String originalText = textNode.getValue();Matcher matcher = placeholderPattern.matcher(originalText);StringBuffer replacedText = new StringBuffer();// 替换所有匹配的占位符while (matcher.find()) {String tag = matcher.group(1);Object value = dataMap.get(tag);String replacement = value != null ? value.toString() : "";matcher.appendReplacement(replacedText, Matcher.quoteReplacement(replacement));}matcher.appendTail(replacedText);textNode.setValue(replacedText.toString());}}}/*** 从内容中递归提取所有文本节点(支持表格单元格、段落等嵌套结构)*/private static List<Object> extractTextNodesFromContent(List<Object> content) {List<Object> textNodes = new ArrayList<>();for (Object obj : content) {// 处理JAXBElement包装的元素if (obj instanceof JAXBElement) {obj = ((JAXBElement<?>) obj).getValue();}// 递归处理段落(P)和文本块(R)if (obj instanceof P) { // 段落 textNodes.addAll(extractTextNodesFromContent(((P) obj).getContent()));} else if (obj instanceof R) { // 文本块 textNodes.addAll(extractTextNodesFromContent(((R) obj).getContent()));} else if (obj instanceof Text) { // 文本节点 textNodes.add(obj);}}return textNodes;}/*** 替换文档中的文本占位符(格式:<<tag>>)* 支持占位符前后有其他文本的场景*/private static void replaceTextPlaceholders(MainDocumentPart mainDocPart, Map<String, Object> dataMap) throws Docx4JException, JAXBException {// 1. 获取文档中所有文本节点(<w:t>元素)List<Object> textNodes = mainDocPart.getJAXBNodesViaXPath("//w:t", false);// 2. 定义占位符匹配模式({{tag}}格式)Pattern placeholderPattern = Pattern.compile("\\<\\<(.+?)\\>\\>");for (Object obj : textNodes) {Text textNode = null;// 处理可能的JAXBElement包装节点if (obj instanceof JAXBElement) {JAXBElement<?> jaxbElement = (JAXBElement<?>) obj;if (jaxbElement.getValue() instanceof Text) {textNode = (Text) jaxbElement.getValue();}} else if (obj instanceof Text) {textNode = (Text) obj;}if (textNode != null) {String originalText = textNode.getValue();Matcher matcher = placeholderPattern.matcher(originalText);StringBuffer replacedText = new StringBuffer();// 3. 匹配并替换所有占位符while (matcher.find()) {String tag = matcher.group(1); // 获取占位符中的tag(如{{projectName}}中的projectName)Object value = dataMap.get(tag);String replacement = value != null ? value.toString() : ""; // 无数据时替换为空字符串 matcher.appendReplacement(replacedText, Matcher.quoteReplacement(replacement));}matcher.appendTail(replacedText); // 拼接剩余文本// 4. 更新文本节点值 textNode.setValue(replacedText.toString());}}}/*** 从行中获取指定索引的单元格*/private static Tc getCellByIndex(Tr row, int index) {List<Tc> cells = row.getContent().stream().map(obj -> {if (obj instanceof JAXBElement) {return (Tc) ((JAXBElement<?>) obj).getValue();} else if (obj instanceof Tc) {return (Tc) obj;}return null;}).filter(Objects::nonNull).collect(Collectors.toList());return cells.size() > index ? cells.get(index) : null;}/*** 设置单元格垂直合并属性* @param cell 单元格* @param val "restart" 开始合并, "continue" 继续合并*/private static void setVerticalMerge(Tc cell, String val) {if (cell == null) return;TcPr tcPr = cell.getTcPr();if (tcPr == null) {tcPr = new TcPr();cell.setTcPr(tcPr);}TcPrInner.VMerge vMerge = new TcPrInner.VMerge();vMerge.setVal(val);tcPr.setVMerge(vMerge);}/*** 清空行中所有单元格的内容*/private static void clearTableRowContent(Tr row) {List<Tc> cells = row.getContent().stream().map(obj -> {if (obj instanceof JAXBElement) {return (Tc) ((JAXBElement<?>) obj).getValue();} else if (obj instanceof Tc) {return (Tc) obj;}return null;}).filter(Objects::nonNull).collect(Collectors.toList());for (Tc cell : cells) {cell.getContent().clear();}}/*** 设置单元格内容*/private static void setCellContent(Tc cell, String content) {if (cell == null) return;cell.getContent().clear();P paragraph = new P();R run = new R();Text text = new Text();text.setValue(content != null ? content : "");run.getContent().add(text);paragraph.getContent().add(run);cell.getContent().add(paragraph);}/*** 保存文档到指定路径*/private static void saveDocument(WordprocessingMLPackage wordMLPackage, String outputPath) throws Docx4JException, IOException {File outputFile = new File(outputPath);// 确保父目录存在if (outputFile.getParentFile() != null) {outputFile.getParentFile().mkdirs();}try (FileOutputStream fos = new FileOutputStream(outputFile)) {wordMLPackage.save(fos);}}// 新增:图片插入相关方法开始/*** 替换文档中的图片占位符(格式:[[image:tag]])*/private static void replaceImagePlaceholders(MainDocumentPart mainDocPart, Map<String, Object> dataMap) throws Exception {// 获取所有段落List<Object> paragraphs = mainDocPart.getJAXBNodesViaXPath("//w:p", false);for (Object paraObj : paragraphs) {P paragraph = (P) paraObj;List<Object> content = paragraph.getContent();// 遍历段落内容查找图片占位符for (int i = 0; i < content.size(); i++) {Object obj = content.get(i);if (obj instanceof JAXBElement) {obj = ((JAXBElement<?>) obj).getValue();}if (obj instanceof R) { // 文本块R run = (R) obj;List<Object> runContent = run.getContent();for (int j = 0; j < runContent.size(); j++) {Object runObj = runContent.get(j);// 新增:处理 JAXBElement 包装的文本节点(关键修复)if (runObj instanceof JAXBElement) {runObj = ((JAXBElement<?>) runObj).getValue(); // 提取包装的实际值 }if (runObj instanceof Text) { // 现在能正确识别被包装的 Text 节点Text textNode = (Text) runObj;String text = textNode.getValue();// 匹配图片占位符格式 {{image:tag}}Pattern pattern = Pattern.compile("\\[\\[image:(.+?)\\]\\]");Matcher matcher = pattern.matcher(text);if (matcher.find()) {String imageTag = matcher.group(1);Object imageObj = dataMap.get(imageTag); // 不再强制转换为String,支持Listif (imageObj == null) continue;// 1. 移除原始占位符文本节点 runContent.remove(j);// 2. 统一处理单图片(String)和多图片(List<String>)List<String> imagePaths = new ArrayList<>();if (imageObj instanceof List) {imagePaths = (List<String>) imageObj; // 多图片列表} else if (imageObj instanceof String) {imagePaths.add((String) imageObj); // 兼容单图片字符串 }// 3. 循环插入所有图片for (String imgPath : imagePaths) {Drawing drawing = createImageDrawing(mainDocPart, imgPath, 400, 300);runContent.add(j++, drawing); // 逐个添加图片,索引递增 }// 4. 处理占位符前后的剩余文本(移至所有图片之后)String remainingText = text.replace(matcher.group(0), "");if (!remainingText.isEmpty()) {Text remainingTextNode = new Text();remainingTextNode.setValue(remainingText);runContent.add(j, remainingTextNode);}}}}}}}}/*** 创建图片Drawing对象(适配 docx4j-core-8.3.1)*/private static Drawing createImageDrawing(MainDocumentPart mainDocPart, String imagePath, int width, int height) throws Exception {// 1. 读取图片文件(Java 8 兼容方式)byte[] imageBytes;try (InputStream fis = getInputStreamByPath(imagePath);ByteArrayOutputStream bos = new ByteArrayOutputStream()) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = fis.read(buffer)) != -1) {bos.write(buffer, 0, bytesRead);}imageBytes = bos.toByteArray();}// 2. 使用 docx4j 8.3.1 推荐的工具类创建图片部件BinaryPartAbstractImage imagePart = BinaryPartAbstractImage.createImagePart(mainDocPart.getPackage(), // 当前文档包mainDocPart, // 源部件(主文档)imageBytes // 图片字节数组 );// 3. 计算图片尺寸(1px = 1440 EMUs,Word 内部单位)long emuWidth = width * 1440L;long emuHeight = height * 1440L;// 4. 创建内联图片对象(适配 8.3.1 版本参数要求)Inline inline = imagePart.createImageInline("image", // 文件名提示"alt text", // 替代文本0, // 图片ID(整数)1, // 图片索引(整数)emuWidth, // 宽度(EMU)emuHeight, // 高度(EMU)false // 是否链接(false=嵌入) );// 5. 包装为Drawing对象返回Drawing drawing = new Drawing();drawing.getAnchorOrInline().add(inline);return drawing;}private static InputStream getInputStreamByPath(String path) throws IOException, MalformedURLException {if (path.startsWith("http://") || path.startsWith("https://")) {// 网络URL:使用URL.openStream()URL url = new URL(path);return url.openStream();} else {// 本地文件:使用FileInputStream(保持原逻辑)return new FileInputStream(path);}}/*** 生成Word文档* @param dataMap 数据模型* @param outPath 输出路径* @throws Exception*/public static void outPutWordFile(Map<String, Object> dataMap, String outPath) throws Exception {try {// 根据条件动态确定模板文件名(resources/template目录下的文件名)String templateFileName = "";String cooperationWay = (String) dataMap.get("cooperationWay");Integer isDeposit = (Integer) dataMap.get("isDeposit");// 拼接模板文件名(根据实际模板文件命名调整)if ("渠道合作".equals(cooperationWay)) {templateFileName = isDeposit == 1 ? "【充电渠道费】计算说明(保证金).docx" : "【充电渠道费】计算说明.docx";} else if ("设施租赁".equals(cooperationWay)) {templateFileName = "【充电设施租赁费】计算说明.docx";} else {templateFileName = "【充电场地管理服务费】计算说明.docx";}// 构建 resources/template 目录下的模板资源路径(类路径相对路径)String templateResourcePath = "template/" + templateFileName;// 校验模板是否存在于 resources/template 目录ClassLoader classLoader = WordTemplateWriter.class.getClassLoader();if (classLoader.getResource(templateResourcePath) == null) {throw new FileNotFoundException("模板文件不存在于 resources/template: " + templateResourcePath);}// 2. 填充模板(直接传入类路径下的模板资源路径)// ... 现有代码 ...// 2. 填充模板(直接传入类路径下的模板资源路径) fillTemplate(templateResourcePath, // 类路径下的模板路径:resources/template/xxx.docxoutPath + File.separator + templateFileName, // 使用跨平台路径分隔符 dataMap); // ... 现有代码 ...System.out.println("Word文档生成成功!");} catch (Exception e) {e.printStackTrace();}}// 测试方法 // public static void main(String[] args) { // try { // // 1. 准备数据 // Map<String, Object> dataMap = new HashMap<>(); // // 文本数据 // dataMap.put("cooperationWay","设施租赁"); // // isDeposit 1涉及保证金 0不涉及保证金 // dataMap.put("isDeposit",1); // // 最终支付比例,只有涉及保证金才有这个字段 // dataMap.put("payRate","95%"); // dataMap.put("contractCode","contractCode1"); // dataMap.put("calculationBaseName","测试计算基准名称"); // dataMap.put("billMonth","2025-09、2025-08、2025-07"); // dataMap.put("billingModel","用户消费金额(不含税)*考核利用率系数*考评得分系数*(1-保证金预留比例)"); // String outPath = "C:\\Users\\sombo\\Desktop\\"; // // 根据条件动态确定模板文件名(resources/template目录下的文件名) // String templateFileName = ""; // String cooperationWay = (String) dataMap.get("cooperationWay"); // Integer isDeposit = (Integer) dataMap.get("isDeposit"); // // // 拼接模板文件名(根据实际模板文件命名调整) // if ("渠道合作".equals(cooperationWay)) { // templateFileName = isDeposit == 1 ? "【充电渠道费】计算说明(保证金).docx" : "【充电渠道费】计算说明.docx"; // } else if ("设施租赁".equals(cooperationWay)) { // templateFileName = "【充电设施租赁费】计算说明.docx"; // } else { // templateFileName = "【充电场地管理服务费】计算说明.docx"; // } // // // 构建 resources/template 目录下的模板资源路径(类路径相对路径) // String templateResourcePath = "template/" + templateFileName; // // // 校验模板是否存在于 resources/template 目录 // ClassLoader classLoader = WordTemplateWriter.class.getClassLoader(); // if (classLoader.getResource(templateResourcePath) == null) { // throw new FileNotFoundException("模板文件不存在于 resources/template: " + templateResourcePath); // } // // // 表格数据(需与模板中表格控件标签对应) // List<ProjectBillOutDTO> tableData = new ArrayList<>(); // ProjectBillOutDTO row1 = new ProjectBillOutDTO(); // row1.setStaMonth("2023-10"); // row1.setCalculationBaseAmount(new BigDecimal("1000000")); // row1.setCalculationBaseName("端口"); // row1.setCalculationFormula("1000000*95%*100%*(1-0.1)"); // tableData.add(row1); // ProjectBillOutDTO row2 = new ProjectBillOutDTO(); // row2.setStaMonth("2023-11"); // row2.setCalculationBaseAmount(new BigDecimal("1000000")); // row2.setCalculationBaseName("端口"); // row2.setCalculationFormula("1000*95%*100%*(1-0.1)"); // tableData.add(row2); // dataMap.put("tableData", tableData); // // 新增:图片数据(key对应模板中的[[image:tag]]) // dataMap.put("projectLogo", Arrays.asList( // "http://10.110.141.88:8855/scap/others/2025/7/23/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20250430104053_1753232919287.png", // "http://10.110.141.88:8855/scap/others/2025/7/23/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20250430104053_1753232919287.png", // "http://10.110.141.88:8855/scap/others/2025/7/23/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20250430104053_1753232919287.png" // )); // fillTemplate( // templateResourcePath, // 类路径下的模板路径:resources/template/xxx.docx // outPath+templateFileName, // 输出路径 // dataMap // ); // System.out.println("Word文档生成成功!"); // } catch (Exception e) { // e.printStackTrace(); // } // } }