From e8ceffa98dace31a5878086fd587578531dc73b0 Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 24 Aug 2024 22:17:05 +0800 Subject: [PATCH] add template support --- .../hutool/poi/excel/cell/CellUtil.java | 40 ++- .../hutool/poi/excel/cell/VirtualCell.java | 269 ++++++++++++++++++ .../excel/cell/values/FormulaCellValue.java | 24 ++ .../hutool/poi/excel/writer/ExcelWriter.java | 58 ++-- .../poi/excel/writer/TemplateContext.java | 48 +++- .../poi/excel/writer/ExcelWriteTest.java | 4 +- .../poi/excel/writer/TemplateContextTest.java | 2 +- .../poi/excel/writer/TemplateWriterTest.java | 34 +++ hutool-poi/src/test/resources/template.xlsx | Bin 8905 -> 8975 bytes 9 files changed, 440 insertions(+), 39 deletions(-) create mode 100644 hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/VirtualCell.java create mode 100644 hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateWriterTest.java diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/CellUtil.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/CellUtil.java index aebb6a086..feac78ff6 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/CellUtil.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/CellUtil.java @@ -39,6 +39,7 @@ import org.dromara.hutool.poi.excel.style.StyleSet; public class CellUtil { // region ----- getCellValue + /** * 获取单元格值 * @@ -103,6 +104,7 @@ public class CellUtil { // endregion // region ----- setCellValue + /** * 设置单元格值
* 根据传入的styleSet自动匹配样式
@@ -169,8 +171,8 @@ public class CellUtil { * 根据传入的styleSet自动匹配样式
* 当为头部样式时默认赋值头部样式,但是头部中如果有数字、日期等类型,将按照数字、日期样式设置 * - * @param cell 单元格 - * @param value 值或{@link CellSetter} + * @param cell 单元格 + * @param value 值或{@link CellSetter} * @since 5.6.4 */ public static void setCellValue(final Cell cell, final Object value) { @@ -191,10 +193,11 @@ public class CellUtil { // endregion // region ----- getCell + /** * 获取指定坐标单元格,如果isCreateIfNotExist为false,则在单元格不存在时返回{@code null} * - * @param sheet {@link Sheet} + * @param sheet {@link Sheet} * @param x X坐标,从0计数,即列号 * @param y Y坐标,从0计数,即行号 * @param isCreateIfNotExist 单元格不存在时是否创建 @@ -249,6 +252,7 @@ public class CellUtil { // endregion // region ----- merging 合并单元格 + /** * 判断指定的单元格是否是合并单元格 * @@ -287,7 +291,7 @@ public class CellUtil { for (int i = 0; i < sheetMergeCount; i++) { ca = sheet.getMergedRegion(i); if (y >= ca.getFirstRow() && y <= ca.getLastRow() - && x >= ca.getFirstColumn() && x <= ca.getLastColumn()) { + && x >= ca.getFirstColumn() && x <= ca.getLastColumn()) { return true; } } @@ -297,7 +301,7 @@ public class CellUtil { /** * 合并单元格,可以根据设置的值来合并行和列 * - * @param sheet 表对象 + * @param sheet 表对象 * @param cellRangeAddress 合并单元格范围,定义了起始行列和结束行列 * @return 合并后的单元格号 */ @@ -308,9 +312,9 @@ public class CellUtil { /** * 合并单元格,可以根据设置的值来合并行和列 * - * @param sheet 表对象 + * @param sheet 表对象 * @param cellRangeAddress 合并单元格范围,定义了起始行列和结束行列 - * @param cellStyle 单元格样式,只提取边框样式,null表示无样式 + * @param cellStyle 单元格样式,只提取边框样式,null表示无样式 * @return 合并后的单元格号 */ public static int mergingCells(final Sheet sheet, final CellRangeAddress cellRangeAddress, final CellStyle cellStyle) { @@ -360,8 +364,8 @@ public class CellUtil { return null; } return ObjUtil.defaultIfNull( - getCellIfMergedRegion(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex()), - cell); + getCellIfMergedRegion(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex()), + cell); } /** @@ -376,8 +380,8 @@ public class CellUtil { */ public static Cell getMergedRegionCell(final Sheet sheet, final int x, final int y) { return ObjUtil.defaultIfNull( - getCellIfMergedRegion(sheet, x, y), - () -> SheetUtil.getCell(sheet, y, x)); + getCellIfMergedRegion(sheet, x, y), + () -> SheetUtil.getCell(sheet, y, x)); } // endregion @@ -420,13 +424,25 @@ public class CellUtil { // 修正在XSSFCell中未设置地址导致错位问题 comment.setAddress(cell.getAddress()); comment.setString(factory.createRichTextString(commentText)); - if(null != commentAuthor){ + if (null != commentAuthor) { comment.setAuthor(commentAuthor); } cell.setCellComment(comment); } + /** + * 移除指定单元格 + * + * @param cell 单元格 + */ + public static void remove(final Cell cell) { + if (null != cell) { + cell.getRow().removeCell(cell); + } + } + // -------------------------------------------------------------------------------------------------------------- Private method start + /** * 获取合并单元格,非合并单元格返回{@code null}
* 传入的x,y坐标(列行数)可以是合并单元格范围内的任意一个单元格 diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/VirtualCell.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/VirtualCell.java new file mode 100644 index 000000000..0d5c570c1 --- /dev/null +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/VirtualCell.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.poi.excel.cell; + +import org.apache.poi.ss.SpreadsheetVersion; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.dromara.hutool.poi.excel.cell.values.FormulaCellValue; + +import java.time.LocalDateTime; +import java.util.Calendar; +import java.util.Date; + +/** + * 虚拟单元格,表示一个单元格的位置、值和样式,但是并非实际创建的单元格
+ * 注意:虚拟单元格设置值和样式均不会在实际工作簿中生效 + * + * @author Looly + * @since 6.0.0 + */ +public class VirtualCell extends CellBase { + + private final Row row; + private final int columnIndex; + private final int rowIndex; + + private CellType cellType; + private Object value; + private CellStyle style; + + /** + * 构造 + * + * @param cell 参照单元格 + * @param x 新的列号,从0开始 + * @param y 新的行号,从0开始 + */ + public VirtualCell(final Cell cell, final int x, final int y) { + this(cell.getRow(), x, y); + this.cellType = cell.getCellType(); + this.value = CellUtil.getCellValue(cell); + this.style = cell.getCellStyle(); + this.comment = cell.getCellComment(); + } + + private Comment comment; + + /** + * 构造 + * + * @param row 行 + * @param y 行号,从0开始 + * @param x 列号,从0开始 + */ + public VirtualCell(final Row row, final int x, final int y) { + this.row = row; + this.rowIndex = y; + this.columnIndex = x; + } + + @Override + protected void setCellTypeImpl(final CellType cellType) { + this.cellType = cellType; + } + + @Override + protected void setCellFormulaImpl(final String formula) { + this.value = new FormulaCellValue(formula); + } + + @Override + protected void removeFormulaImpl() { + if (this.value instanceof FormulaCellValue) { + this.value = null; + } + } + + @Override + protected void setCellValueImpl(final double value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final Date value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final LocalDateTime value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final Calendar value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final String value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final RichTextString value) { + this.value = value; + } + + @Override + protected SpreadsheetVersion getSpreadsheetVersion() { + return SpreadsheetVersion.EXCEL2007; + } + + @Override + public int getColumnIndex() { + return this.columnIndex; + } + + @Override + public int getRowIndex() { + return this.rowIndex; + } + + @Override + public Sheet getSheet() { + return this.row.getSheet(); + } + + @Override + public Row getRow() { + return this.row; + } + + @Override + public CellType getCellType() { + return this.cellType; + } + + @Override + public CellType getCachedFormulaResultType() { + if (this.value instanceof FormulaCellValue) { + return ((FormulaCellValue) this.value).getResultType(); + } + return null; + } + + @Override + public String getCellFormula() { + if (this.value instanceof FormulaCellValue) { + return ((FormulaCellValue) this.value).getValue(); + } + return null; + } + + @Override + public double getNumericCellValue() { + return (double) this.value; + } + + @Override + public Date getDateCellValue() { + return (Date) this.value; + } + + @Override + public LocalDateTime getLocalDateTimeCellValue() { + return (LocalDateTime) this.value; + } + + @Override + public RichTextString getRichStringCellValue() { + return (RichTextString) this.value; + } + + @Override + public String getStringCellValue() { + return (String) this.value; + } + + @Override + public void setCellValue(final boolean value) { + this.value = value; + } + + @Override + public void setCellErrorValue(final byte value) { + this.value = value; + } + + @Override + public boolean getBooleanCellValue() { + return (boolean) this.value; + } + + @Override + public byte getErrorCellValue() { + return (byte) this.value; + } + + @Override + public void setCellStyle(final CellStyle style) { + this.style = style; + } + + @Override + public CellStyle getCellStyle() { + return this.style; + } + + @Override + public void setAsActiveCell() { + throw new UnsupportedOperationException("Virtual cell cannot be set as active cell"); + } + + @Override + public void setCellComment(final Comment comment) { + this.comment = comment; + } + + @Override + public Comment getCellComment() { + return this.comment; + } + + @Override + public void removeCellComment() { + this.comment = null; + } + + @Override + public Hyperlink getHyperlink() { + return (Hyperlink) this.value; + } + + @Override + public void setHyperlink(final Hyperlink link) { + this.value = link; + } + + @Override + public void removeHyperlink() { + if (this.value instanceof Hyperlink) { + this.value = null; + } + } + + @Override + public CellRangeAddress getArrayFormulaRange() { + return null; + } + + @Override + public boolean isPartOfArrayFormulaGroup() { + return false; + } +} diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/values/FormulaCellValue.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/values/FormulaCellValue.java index 64d1d6e12..66d1a582f 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/values/FormulaCellValue.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/values/FormulaCellValue.java @@ -16,6 +16,7 @@ package org.dromara.hutool.poi.excel.cell.values; +import org.apache.poi.ss.usermodel.CellType; import org.dromara.hutool.poi.excel.cell.setters.CellSetter; import org.apache.poi.ss.usermodel.Cell; @@ -40,6 +41,7 @@ public class FormulaCellValue implements CellValue, CellSetter { * 结果,使用ExcelWriter时可以不用 */ private final Object result; + private final CellType resultType; /** * 构造 @@ -57,8 +59,20 @@ public class FormulaCellValue implements CellValue, CellSetter { * @param result 结果 */ public FormulaCellValue(final String formula, final Object result) { + this(formula, result, null); + } + + /** + * 构造 + * + * @param formula 公式 + * @param result 结果 + * @param resultType 结果类型 + */ + public FormulaCellValue(final String formula, final Object result, final CellType resultType) { this.formula = formula; this.result = result; + this.resultType = resultType; } @Override @@ -73,12 +87,22 @@ public class FormulaCellValue implements CellValue, CellSetter { /** * 获取结果 + * * @return 结果 */ public Object getResult() { return this.result; } + /** + * 获取结果类型 + * + * @return 结果类型,{@code null}表示未明确 + */ + public CellType getResultType() { + return this.resultType; + } + @Override public String toString() { return getResult().toString(); diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java index c37c342e2..e86b4d1cc 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java @@ -91,7 +91,7 @@ public class ExcelWriter extends ExcelBase { /** * 构造
* 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
- * 若写出到文件,需要调用{@link #flush(File)} 写出到文件 + * 若写出到文件,需要调用{@link #flush(File, boolean)} 写出到文件 * * @param isXlsx 是否为xlsx格式 * @since 3.2.1 @@ -112,7 +112,7 @@ public class ExcelWriter extends ExcelBase { /** * 构造
* 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
- * 若写出到文件,需要调用{@link #flush(File)} 写出到文件 + * 若写出到文件,需要调用{@link #flush(File, boolean)} 写出到文件 * * @param isXlsx 是否为xlsx格式 * @param sheetName sheet名,第一个sheet名并写出到此sheet,例如sheet1 @@ -152,7 +152,7 @@ public class ExcelWriter extends ExcelBase { if (!FileUtil.exists(targetFile)) { this.targetFile = targetFile; - } else{ + } else { // 如果是已经存在的文件,则作为模板加载,此时不能写出到模板文件 // 初始化模板 this.templateContext = new TemplateContext(this.sheet); @@ -549,6 +549,7 @@ public class ExcelWriter extends ExcelBase { } // region ----- merge + /** * 合并当前行的单元格 * @@ -640,6 +641,7 @@ public class ExcelWriter extends ExcelBase { // endregion // region ----- write + /** * 写出数据,本方法只是将数据写入Workbook中的Sheet,并不写出到文件
* 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动增加 @@ -1010,21 +1012,16 @@ public class ExcelWriter extends ExcelBase { // region ----- fill - public ExcelWriter fillRow(final Map rowMap){ - rowMap.forEach((key, value)->{ - - }); + /** + * 填充模板行 + * + * @param rowMap 行数据 + * @return this + */ + public ExcelWriter fillRow(final Map rowMap) { + rowMap.forEach((key, value) -> this.templateContext.fillAndPointToNext(StrUtil.toStringOrNull(key), rowMap)); return this; } - - public ExcelWriter fillCell(final String name, final Object value){ - final Cell cell = this.templateContext.getCell(name); - if(null != cell){ - CellUtil.setCellValue(cell, value, this.config.getCellEditor()); - } - return this; - } - // endregion // region ----- writeCol @@ -1282,21 +1279,39 @@ public class ExcelWriter extends ExcelBase { * @throws IORuntimeException IO异常 */ public ExcelWriter flush() throws IORuntimeException { - return flush(this.targetFile); + return flush(false); + } + + /** + * 将Excel Workbook刷出到预定义的文件
+ * 如果用户未自定义输出的文件,将抛出{@link NullPointerException}
+ * 预定义文件可以通过{@link #setTargetFile(File)} 方法预定义,或者通过构造定义 + * + * @param override 是否覆盖已有文件 + * @return this + * @throws IORuntimeException IO异常 + */ + public ExcelWriter flush(final boolean override) throws IORuntimeException { + Assert.notNull(this.targetFile, "[targetFile] is null, and you must call setTargetFile(File) first."); + return flush(this.targetFile, override); } /** * 将Excel Workbook刷出到文件
* 如果用户未自定义输出的文件,将抛出{@link NullPointerException} * - * @param destFile 写出到的文件 + * @param targetFile 写出到的文件 + * @param override 是否覆盖已有文件 * @return this * @throws IORuntimeException IO异常 * @since 4.0.6 */ - public ExcelWriter flush(final File destFile) throws IORuntimeException { - Assert.notNull(destFile, "[destFile] is null, and you must call setDestFile(File) first or call flush(OutputStream)."); - return flush(FileUtil.getOutputStream(destFile), true); + public ExcelWriter flush(final File targetFile, final boolean override) throws IORuntimeException { + Assert.notNull(targetFile, "targetFile is null!"); + if (FileUtil.exists(targetFile) && !override) { + throw new IORuntimeException("File to write exist: " + targetFile); + } + return flush(FileUtil.getOutputStream(targetFile), true); } /** @@ -1321,6 +1336,7 @@ public class ExcelWriter extends ExcelBase { */ public ExcelWriter flush(final OutputStream out, final boolean isCloseOut) throws IORuntimeException { Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + try { this.workbook.write(out); out.flush(); diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/TemplateContext.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/TemplateContext.java index 066d022cf..676d78f98 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/TemplateContext.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/TemplateContext.java @@ -20,11 +20,14 @@ import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Sheet; import org.dromara.hutool.core.collection.CollUtil; +import org.dromara.hutool.core.lang.Assert; import org.dromara.hutool.core.regex.ReUtil; import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.poi.excel.SheetUtil; +import org.dromara.hutool.poi.excel.cell.CellUtil; +import org.dromara.hutool.poi.excel.cell.VirtualCell; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -50,7 +53,7 @@ public class TemplateContext { private static final Pattern ESCAPE_VAR_PATTERN = Pattern.compile("\\\\\\{([.$_a-zA-Z]+\\d*[.$_a-zA-Z]*)\\\\}"); // 存储变量对应单元格的映射 - private final Map varMap = new HashMap<>(); + private final Map varMap = new LinkedHashMap<>(); /** * 构造 @@ -62,7 +65,7 @@ public class TemplateContext { } /** - * 获取变量对应的单元格,列表变量以.开头 + * 获取变量对应的当前单元格,列表变量以开头 * * @param varName 变量名 * @return 单元格 @@ -71,6 +74,45 @@ public class TemplateContext { return varMap.get(varName); } + /** + * 填充变量名name指向的单元格,并将变量指向下一列 + * + * @param name 变量名 + * @param rowData 一行数据的键值对 + * @since 6.0.0 + */ + public void fillAndPointToNext(final String name, final Map rowData) { + Cell cell = varMap.get(name); + if (null == cell) { + // 没有对应变量占位 + return; + } + + final String templateStr = cell.getStringCellValue(); + + // 指向下一列的单元格 + final Cell next = new VirtualCell(cell, cell.getColumnIndex(), cell.getRowIndex() + 1); + next.setCellValue(templateStr); + varMap.put(name, next); + + if(cell instanceof VirtualCell){ + // 虚拟单元格,转换为实际单元格 + final Cell newCell = CellUtil.getCell(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex(), true); + Assert.notNull(newCell, "Can not get or create cell at {},{}", cell.getColumnIndex(), cell.getRowIndex()); + newCell.setCellStyle(cell.getCellStyle()); + cell = newCell; + } + + // 模板替换 + if(StrUtil.equals(name, StrUtil.unWrap(templateStr, "{", "}"))){ + // 一个单元格只有一个变量 + CellUtil.setCellValue(cell, rowData.get(name)); + } else { + // 模板中存在多个变量或模板填充 + CellUtil.setCellValue(cell, StrUtil.format(templateStr, rowData)); + } + } + /** * 初始化,提取变量及位置,并将转义的变量回填 * diff --git a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java index 61d0e9f7f..3667c6ade 100644 --- a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java +++ b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java @@ -143,7 +143,7 @@ public class ExcelWriteTest { writer.writeRow(row2); // 生成文件或导出Excel - writer.flush(FileUtil.file("d:/test/writeWithSheetTest.xlsx")); + writer.flush(FileUtil.file("d:/test/writeWithSheetTest.xlsx"), true); writer.close(); } @@ -868,7 +868,7 @@ public class ExcelWriteTest { writer.writeImg(file, 0, 0, 5, 10); - writer.flush(new File("C:\\Users\\zsz\\Desktop\\2.xlsx")); + writer.flush(new File("C:\\Users\\zsz\\Desktop\\2.xlsx"), true); writer.close(); } diff --git a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateContextTest.java b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateContextTest.java index def486b64..95c45e216 100644 --- a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateContextTest.java +++ b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateContextTest.java @@ -22,6 +22,6 @@ public class TemplateContextTest { final ExcelWriter writer = ExcelUtil.getWriter("template.xlsx"); final TemplateContext templateContext = new TemplateContext(writer.getSheet()); Assertions.assertNotNull(templateContext.getCell("date")); - Assertions.assertNotNull(templateContext.getCell(".month")); + Assertions.assertNotNull(templateContext.getCell("month")); } } diff --git a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateWriterTest.java b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateWriterTest.java new file mode 100644 index 000000000..fe653e2fc --- /dev/null +++ b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateWriterTest.java @@ -0,0 +1,34 @@ +package org.dromara.hutool.poi.excel.writer; + +import org.dromara.hutool.core.io.file.FileUtil; +import org.dromara.hutool.core.map.MapUtil; +import org.dromara.hutool.poi.excel.ExcelUtil; +import org.junit.jupiter.api.Test; + +public class TemplateWriterTest { + @Test + void writeRowTest() { + final ExcelWriter writer = ExcelUtil.getWriter("d:/test/template.xlsx"); + + // 单个替换的变量 + writer.fillRow(MapUtil + .builder("date", (Object)"2024-01-01") + .build()); + + // 列表替换 + for (int i = 0; i < 10; i++) { + writer.fillRow(MapUtil + .builder("user.name", (Object)"张三") + .put("user.age", 18) + .put("year", 2024) + .put("month", 8) + .put("day", 24) + .put("day", 24) + .put("user.area123", "某某市") + .put("invalid", "不替换") + .build()); + } + + writer.flush(FileUtil.file("d:/test/templateResult.xlsx"), true); + } +} diff --git a/hutool-poi/src/test/resources/template.xlsx b/hutool-poi/src/test/resources/template.xlsx index b78abfddc6d6ae89cd6d97ec3694994173e9d805..357afbdd1d0f74077067949066f6ae9ea093ee4d 100644 GIT binary patch delta 3228 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 delta 3128 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 C#LvP2