From e6b4dbf55e51a8625b69526bef16dfdc22c6592f Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Thu, 11 Dec 2025 14:54:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(ruoyi-common):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=80=9A=E7=94=A8=20Word=20=E6=A8=A1=E6=9D=BF=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E7=BB=84=E4=BB=B6ruoyi-common-word?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ruoyi-common-word 模块,封装 poi-tl 组件以支持 Word 模板导出 - 在 ruoyi-common-bom 中添加 ruoyi-common-word 依赖管理 - 在 ruoyi-common 模块中引入 ruoyi-common-word 子模块 - 在顶层 pom.xml 中添加 poi-tl 版本属性 - ruoyi-common-word 模块依赖 poi-tl、ruoyi-common-core、Spring Boot 及 Servlet API - 提供 WordTemplateUtil 工具类,支持模板渲染和多种输出方式 - 编写详细文档说明模块设计、数据结构和使用示例 - 规范模板存放路径,便于业务模块复用模板导出能力 --- pom.xml | 2 + ruoyi-common/pom.xml | 1 + ruoyi-common/ruoyi-common-bom/pom.xml | 7 + ruoyi-common/ruoyi-common-word/pom.xml | 48 ++++ .../common/word/util/WordTemplateUtil.java | 242 ++++++++++++++++++ ruoyi-common/ruoyi-common-word/word.md | 219 ++++++++++++++++ 6 files changed, 519 insertions(+) create mode 100644 ruoyi-common/ruoyi-common-word/pom.xml create mode 100644 ruoyi-common/ruoyi-common-word/src/main/java/org/dromara/common/word/util/WordTemplateUtil.java create mode 100644 ruoyi-common/ruoyi-common-word/word.md diff --git a/pom.xml b/pom.xml index 975291fe..dc304e2d 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,8 @@ 2.3.4 1.5.4 + + 1.12.1 3.14.0 diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index 91d833b9..5e9d4ce6 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -45,6 +45,7 @@ ruoyi-common-bus ruoyi-common-sse hwbm-common-workflow + ruoyi-common-word ruoyi-common diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml index 2b8a06b9..c212d55a 100644 --- a/ruoyi-common/ruoyi-common-bom/pom.xml +++ b/ruoyi-common/ruoyi-common-bom/pom.xml @@ -250,6 +250,13 @@ ${revision} + + + org.dromara + ruoyi-common-word + ${revision} + + diff --git a/ruoyi-common/ruoyi-common-word/pom.xml b/ruoyi-common/ruoyi-common-word/pom.xml new file mode 100644 index 00000000..dca55a66 --- /dev/null +++ b/ruoyi-common/ruoyi-common-word/pom.xml @@ -0,0 +1,48 @@ + + + + org.dromara + ruoyi-common + ${revision} + + 4.0.0 + + ruoyi-common-word + + ruoyi-common-word + 通用 Word 模板导出组件(封装 poi-tl) + + + + + com.deepoove + poi-tl + ${poi-tl.version} + + + + + org.dromara + ruoyi-common-core + ${revision} + + + org.springframework.boot + spring-boot-starter + + + + jakarta.servlet + jakarta.servlet-api + provided + + + org.projectlombok + lombok + true + + + + diff --git a/ruoyi-common/ruoyi-common-word/src/main/java/org/dromara/common/word/util/WordTemplateUtil.java b/ruoyi-common/ruoyi-common-word/src/main/java/org/dromara/common/word/util/WordTemplateUtil.java new file mode 100644 index 00000000..81e5413b --- /dev/null +++ b/ruoyi-common/ruoyi-common-word/src/main/java/org/dromara/common/word/util/WordTemplateUtil.java @@ -0,0 +1,242 @@ +package org.dromara.common.word.util; + +import com.deepoove.poi.XWPFTemplate; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.core.utils.file.FileUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +/** + * Word 模板导出工具(基于 poi-tl) + * + * 设计要点: + * 1) 仅提供模板加载、渲染、响应输出的通用能力,不侵入具体业务字段。 + * 2) 模板文件放在各业务模块的 resources 下(建议放 templates 目录),通过 classpath 路径传入,例如 + * {@code templates/wms_shipping_bill.docx}。 + * 3) 业务方负责组装模板所需的数据 Map(包含普通字段与列表),本工具不约束字段名称。 + * - 普通字段示例:data.put("shippingCode", "X123"); → 模板里使用 {{shippingCode}} + * - 列表字段示例:data.put("details", detailList); → 模板里使用 {{#details}}...{{/details}} + * 4) 推荐由 Service 负责组装 data,Controller / 定时任务 / 消息消费端只负责调用本工具进行导出或上传, + * 降低不同模块之间的耦合度。 + * + * 简单使用示例(Web 导出): + *
+ * Map data = new HashMap<>();
+ * data.put("shippingCode", "X123");
+ * data.put("details", detailList); // 列表占位符示例
+ * WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", "发货单_X123", data, response);
+ * 
+ * + * 更多背景说明、模板字段约定及发货单完整示例可参考: + * hwbm-cloud/ruoyi-common/ruoyi-common-word/word.md + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class WordTemplateUtil { + + /** + * 渲染模板并输出到 HttpServletResponse(典型用于 Controller 直接导出下载) + * + * @param templatePath classpath 下的模板路径(含文件名),如 "templates/wms_shipping_bill.docx" + * @param fileName 下载文件名(不含后缀),如 "发货单_X123",方法内部自动补全 .docx 后缀 + * @param data 模板数据 Map,业务自行约定占位符字段(支持普通字段和列表字段) + * @param response HttpServletResponse,由 Spring MVC 自动注入 + * + * 注意: + * 1) 本方法内部会重置响应头并向 response 写入二进制流,调用前不要再向 response 写任何内容。 + * 2) templatePath 使用的是 classpath 相对路径,打包后模板应放在各模块的 resources 目录中。 + */ + public static void renderToResponse(String templatePath, String fileName, Map data, HttpServletResponse response) { + Objects.requireNonNull(response, "HttpServletResponse must not be null"); + if (StringUtils.isBlank(templatePath)) { + throw new ServiceException("Word 模板路径不能为空"); + } + + // 兜底处理:下载文件名为空时使用默认值;模板数据为空时使用空 Map,避免 NPE + String safeFileName = StringUtils.isBlank(fileName) ? "export" : fileName.trim(); + Map safeData = (data == null ? Collections.emptyMap() : data); + + try (XWPFTemplate template = buildTemplate(templatePath, safeData); + OutputStream os = resetResponse(safeFileName, response)) { + template.write(os); + os.flush(); + } catch (IOException e) { + log.error("Word 模板导出失败, templatePath={}, fileName={}", templatePath, safeFileName, e); + throw new ServiceException("Word 模板导出失败,请联系管理员").setDetailMessage(e.getMessage()); + } + } + + /** + * 从 classpath 加载模板文件 + * + * @param templatePath 模板在 classpath 下的相对路径 + * @return 模板输入流(由调用方负责关闭) + */ + private static InputStream getTemplateStream(String templatePath) throws IOException { + // 使用当前线程的 ClassLoader 查找资源,兼容不同部署环境(应用服务器 / Spring Boot 可执行 Jar 等) + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(templatePath); + if (is == null) { + throw new IOException("未找到模板文件: " + templatePath); + } + return is; + } + + /** + * 构建并渲染模板 + * + * @param templatePath 模板路径(classpath) + * @param data 模板数据 Map + * @return 已完成占位符渲染的 XWPFTemplate 对象 + */ + private static XWPFTemplate buildTemplate(String templatePath, Map data) throws IOException { + try (InputStream templateStream = getTemplateStream(templatePath)) { + return XWPFTemplate.compile(templateStream).render(data); + } + } + + /** + * 重置响应头,返回输出流 + * + * @param fileName 下载文件名(不含后缀或含 .docx 均可) + * @param response HttpServletResponse + * @return 已设置好 Content-Type 和 Content-Disposition 的输出流 + */ + private static OutputStream resetResponse(String fileName, HttpServletResponse response) throws IOException { + String realName = fileName; + if (!realName.endsWith(".docx")) { + realName = realName + ".docx"; + } + FileUtils.setAttachmentResponseHeader(response, realName); + response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=UTF-8"); + return response.getOutputStream(); + } + + /** + * 渲染模板并输出到指定 OutputStream(不负责关闭 out) + * 适用于非 HTTP 场景,例如: + *
    + *
  • 生成后上传到 OSS / MinIO 等对象存储;
  • + *
  • 作为 MQ 附件发送;
  • + *
  • 写入本地文件(例如本地调试或定时任务导出)。
  • + *
+ * + * @param templatePath classpath 下的模板路径(含文件名) + * @param data 模板数据 Map + * @param out 目标输出流,由调用方负责关闭(例如 HttpServletResponse#getOutputStream、 + * FileOutputStream、ByteArrayOutputStream 等) + */ + public static void renderToOutputStream(String templatePath, Map data, OutputStream out) { + Objects.requireNonNull(out, "OutputStream must not be null"); + if (StringUtils.isBlank(templatePath)) { + throw new ServiceException("Word 模板路径不能为空"); + } + Map safeData = (data == null ? Collections.emptyMap() : data); + try (XWPFTemplate template = buildTemplate(templatePath, safeData)) { + template.write(out); + out.flush(); + } catch (IOException e) { + log.error("Word 模板渲染失败, templatePath={}", templatePath, e); + throw new ServiceException("Word 模板渲染失败,请联系管理员").setDetailMessage(e.getMessage()); + } + } + + /** + * 渲染模板并返回字节数组,便于在非 Web 场景中复用 + *

+ * 典型使用场景: + *

    + *
  • 先生成字节数组,再统一由 OSS 客户端上传;
  • + *
  • 生成后作为 API 的二进制返回值,由前端自行触发下载;
  • + *
  • 在集成测试中验证模板渲染结果是否符合预期。
  • + *
+ * + * @param templatePath classpath 下的模板路径(含文件名) + * @param data 模板数据 Map + * @return 渲染后的 Word 文档字节数组 + */ + public static byte[] renderToBytes(String templatePath, Map data) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + renderToOutputStream(templatePath, data, bos); + return bos.toByteArray(); + } + + // ===================================================================== + // 典型调用示例(仅作为文档说明,不参与编译): + // ===================================================================== + /* + * 1. Controller 中直接导出发货单 Word(最常见场景,推荐其他模块模仿此写法) + * + * @GetMapping("/exportShippingBill/{id}") + * public void exportShippingBill(@PathVariable Long id, HttpServletResponse response) { + * // 由业务 Service 组装模板所需数据 Map(包含主表字段 + 明细列表等) + * Map data = wmsShippingBillService.buildWordExportData(id); + * String templatePath = "templates/wms_shipping_bill.docx"; // 视具体模块而定 + * String fileName = "发货单_" + id; // 或使用业务单号 + * WordTemplateUtil.renderToResponse(templatePath, fileName, data, response); + * } + * + * 2. Service 中生成字节数组并上传 OSS / MinIO(非 Web 场景) + * + * public String generateAndUploadShippingBill(Long id) { + * Map data = wmsShippingBillService.buildWordExportData(id); + * byte[] bytes = WordTemplateUtil.renderToBytes("templates/wms_shipping_bill.docx", data); + * // 伪代码:上传到对象存储,返回访问地址 + * String objectKey = "word/shippingBill/" + id + ".docx"; + * return ossClient.upload(bytes, objectKey); + * } + * + * 3. 定时任务 / 工具类中写入本地文件(本地调试场景) + * + * public void exportToLocalFile(Long id) throws IOException { + * Map data = wmsShippingBillService.buildWordExportData(id); + * try (OutputStream out = Files.newOutputStream(Paths.get("D:/temp/shipping_bill_" + id + ".docx"))) { + * WordTemplateUtil.renderToOutputStream("templates/wms_shipping_bill.docx", data, out); + * } + * } + * + * 4. 循环表格 + 合计行示例(以发货单明细为例,适用于发货单 / 合同 / 报价等带明细的单据) + * + * // 伪代码:组装发货明细行和合计字段 + * List> details = new ArrayList<>(); + * BigDecimal totalQuantity = BigDecimal.ZERO; + * BigDecimal totalAmount = BigDecimal.ZERO; + * int seq = 1; + * for (WmsShippingDetailsVo item : itemsVo) { + * Map row = new HashMap<>(); + * row.put("seq", String.valueOf(seq++)); // 行号 → {{seq}} + * row.put("materialName", item.getMaterialName()); // 物料名称 → {{materialName}} + * row.put("quantity", item.getShippingStockAmount()); // 数量 → {{quantity}} + * row.put("unit", item.getUnitName()); // 单位 → {{unit}} + * row.put("unitPrice", item.getUnitPrice()); // 单价 → {{unitPrice}} + * BigDecimal amount = item.getUnitPrice() + * .multiply(item.getShippingStockAmount()); + * row.put("amount", amount); // 金额 → {{amount}} + * details.add(row); + * + * totalQuantity = totalQuantity.add(item.getShippingStockAmount()); + * totalAmount = totalAmount.add(amount); + * } + * data.put("details", details); // 循环表格数据源 → {{#details}}...{{/details}} + * data.put("totalQuantity", totalQuantity.toPlainString()); // 合计数量 → {{totalQuantity}} + * data.put("totalAmount", totalAmount.toPlainString()); // 合计金额 → {{totalAmount}} + * + * // Word 模板表格示意(仅说明占位符,不代表真实表格语法): + * // ┌────┬────────┬──────┬────┬──────┬───────┐ + * // │ 序号 │ 物料名称 │ 数量 │ 单位 │ 单价 │ 金额 │ + * // ├────┼────────┼──────┼────┼──────┼───────┤ + * // │{{seq}}│{{materialName}}│{{quantity}}│{{unit}}│{{unitPrice}}│{{amount}}│ ← 放在 {{#details}}...{{/details}} 内部 + * // └────┴────────┴──────┴────┴──────┴───────┘ + * // 合计:数量 {{totalQuantity}},金额 {{totalAmount}} + * */ +} diff --git a/ruoyi-common/ruoyi-common-word/word.md b/ruoyi-common/ruoyi-common-word/word.md new file mode 100644 index 00000000..aa420936 --- /dev/null +++ b/ruoyi-common/ruoyi-common-word/word.md @@ -0,0 +1,219 @@ +# poi-tl 与 ruoyi-common-word 模块说明 + +## 1. 模块定位(ruoyi-common-word) + +- **模块路径**:`hwbm-cloud/ruoyi-common/ruoyi-common-word` +- **主要职责**: + - 基于 [poi-tl](https://github.com/Sayi/poi-tl) 提供统一的 **Word 模板渲染与导出能力**; + - 仅负责模板加载、渲染、输出,不侵入具体业务字段和业务规则; + - 供各业务模块(合同、报价、发货单、项目收货/验收等)统一复用。 +- **核心工具类**:`org.dromara.common.word.util.WordTemplateUtil` + - `renderToResponse(...)`:Controller 直接导出 Word; + - `renderToOutputStream(...)`:向任意 `OutputStream` 写入 Word(OSS、本地文件等); + - `renderToBytes(...)`:返回字节数组,便于后续上传或作为接口返回值。 + +> 详细 JavaDoc 和更多示例可参考 `WordTemplateUtil` 源码本身。 + +--- + +## 2. poi-tl 简介 + +poi-tl 是构建在 Apache POI 之上的 **Word 模板引擎**,通过简单的占位符语法实现: + +- **普通字段占位符**:`{{fieldName}}` +- **列表循环占位符**: + - 语法:`{{#list}} ... {{/list}}` + - `list` 对应数据 Map 中的 `List>`; + - 循环体内部使用 `{{列名}}` 访问每一行 Map 中的字段。 + +本项目只做了一层简单封装,不改变 poi-tl 原生语义,方便直接对照官方文档理解。 + +--- + +## 3. 模板文件存放规范 + +- 建议所有业务模块将 Word 模板统一放在各自模块的 `resources/templates` 目录下,例如: + - `hwbm-cloud/ruoyi-modules/ruoyi-wms/src/main/resources/templates/wms_shipping_bill.docx` + - 其他模块可类比:`templates/erp_contract.docx`、`templates/crm_quote.docx` 等。 +- `WordTemplateUtil` 接收的是 **classpath 相对路径**,例如: + +```java +WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", "发货单_X123", data, response); +``` + +部署后,只要模板文件在 Jar 的 `BOOT-INF/classes/templates` 下即可被正常加载。 + +--- + +## 4. 数据 Map 结构约定 + +`WordTemplateUtil` 不关心业务字段含义,只约定数据以 `Map` 形式传入: + +- **普通字段**: + + ```java + Map data = new HashMap<>(); + data.put("customerName", "某某公司"); // {{customerName}} + data.put("shippingDate", "2025-12-10"); // {{shippingDate}} + data.put("contractCode", "HT-2025-001"); // {{contractCode}} + ``` + +- **列表字段(循环表格)**: + + ```java + List> details = new ArrayList<>(); + + Map row1 = new HashMap<>(); + row1.put("seq", 1); // {{seq}} + row1.put("materialName", "胶辊"); // {{materialName}} + row1.put("quantity", 10); // {{quantity}} + row1.put("unit", "件"); // {{unit}} + row1.put("remark", "加急发货"); // {{remark}} + details.add(row1); + + data.put("details", details); // 对应模板 {{#details}}...{{/details}} + ``` + +- **合计字段(可选)**: + + ```java + data.put("totalQuantity", "100"); // {{totalQuantity}} + data.put("totalAmount", "12345.67"); // {{totalAmount}} + ``` + +模板与数据字段的对应关系完全由业务模块自己约定,但**应在各自模块文档中说明**,便于维护。 + +--- + +## 5. 发货单(WmsShippingBill)标准示例 + +发货单模块(`ruoyi-wms`)是 `WordTemplateUtil` 的一个完整应用示例,推荐其他单据类模块(合同、报价、项目收货/验收等)参照其设计。 + +### 5.1 Controller 入口示例 + +文件:`WmsShippingBillController.exportWord` + +```java +@GetMapping("/exportWord/{shippingBillId}") +public void exportWord(@PathVariable Long shippingBillId, HttpServletResponse response) { + // 1)由 Service 组装模板数据 Map(包含主表字段 + 明细列表) + Map data = wmsShippingBillService.buildWordExportData(shippingBillId); + + // 2)生成下载文件名(优先使用发货单号) + WmsShippingBillVo vo = wmsShippingBillService.queryById(shippingBillId); + String fileName = "发货单_" + (vo != null && vo.getShippingCode() != null + ? vo.getShippingCode() : shippingBillId); + + // 3)调用通用工具类完成渲染与输出 + WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", fileName, data, response); +} +``` + +### 5.2 Service 组装数据示例 + +文件:`WmsShippingBillServiceImpl.buildWordExportData` + +核心思路: + +1. 查询发货单主表 + 明细列表 VO; +2. 组装主表字段(顶部信息、落款信息); +3. 组装明细列表 `details`(循环表格数据源)。 + +部分代码示意: + +```java +public Map buildWordExportData(Long shippingBillId) { + WmsShippingBillVo vo = queryById(shippingBillId); + if (vo == null) { + throw new ServiceException("发货单不存在,ID:" + shippingBillId); + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Map data = new HashMap<>(); + + // === 主表字段 === + data.put("customerName", StringUtils.blankToDefault(vo.getCustomerName(), "")); // {{customerName}} + data.put("receiverName", StringUtils.blankToDefault(vo.getReceiverName(), "")); // {{receiverName}} + data.put("receiverPhone", StringUtils.blankToDefault(vo.getReceiverPhone(), "")); // {{receiverPhone}} + data.put("shippingDate", vo.getShippingTime() != null ? sdf.format(vo.getShippingTime()) : ""); // {{shippingDate}} + data.put("contractCode", StringUtils.blankToDefault(vo.getContractCode(), "")); // {{contractCode}} + data.put("receivedDate", ""); // 收货日期留空,由客户签收时手工填写 → {{receivedDate}} + + // 落款信息(供应商 / 发货方) + data.put("shippingUnit", StringUtils.blankToDefault(vo.getSupplier(), "")); // {{shippingUnit}} + data.put("shipper", StringUtils.blankToDefault(vo.getContactUser(), "")); // {{shipper}} + data.put("contactNumber", StringUtils.blankToDefault(vo.getContactNumber(), "")); // {{contactNumber}} + + // === 明细列表,对应 {{#details}}...{{/details}} === + List> details = new ArrayList<>(); + if (CollUtil.isNotEmpty(vo.getItemsVo())) { + int seq = 1; + for (WmsShippingDetailsVo item : vo.getItemsVo()) { + Map row = new HashMap<>(); + row.put("seq", String.valueOf(seq++)); // {{seq}} + row.put("materialName", StringUtils.blankToDefault(item.getMaterialName(), "")); // {{materialName}} + row.put("quantity", item.getShippingStockAmount() != null + ? item.getShippingStockAmount().toPlainString() : ""); // {{quantity}} + row.put("unit", StringUtils.blankToDefault(item.getUnitName(), "")); // {{unit}} + row.put("remark", StringUtils.blankToDefault(item.getRemark(), "")); // {{remark}} + details.add(row); + } + } + data.put("details", details); + + return data; +} +``` + +### 5.3 发货单模板占位符示意 + +> 下面仅用伪代码示意占位符位置,实际模板在 Word 中通过表格 / 段落排版完成。 + +```text +客户名称:{{customerName}} +联系人:{{receiverName}} 联系电话:{{receiverPhone}} +发货日期:{{shippingDate}} 合同编号:{{contractCode}} + +{{#details}} +序号:{{seq}} 物料:{{materialName}} 数量:{{quantity}} 单位:{{unit}} 备注:{{remark}} +{{/details}} + +收货日期:{{receivedDate}} + +发货单位:{{shippingUnit}} +发货人:{{shipper}} 联系电话:{{contactNumber}} +``` + +> 其它单据(如报价、合同、项目收货/验收)可以直接复用上述模式: +> - 主表字段:顶部信息 + 落款信息; +> - 明细字段:`details` 循环表格; +> - 可扩展合计字段:`totalQuantity`、`totalAmount` 等。 + +--- + +## 6. 常见使用场景总结 + +1. **Web 端直接下载 Word**: + - Controller 调用 `WordTemplateUtil.renderToResponse(...)`; + - 浏览器收到 `Content-Disposition: attachment` 头,触发下载。 + +2. **上传到 OSS / MinIO 等对象存储**: + - 使用 `renderToBytes(...)` 生成字节数组; + - 由 OSS 客户端统一上传并返回访问 URL。 + +3. **定时任务或后台服务批量生成 Word**: + - 使用 `renderToOutputStream(...)` 写入本地文件或网络流; + - 后续可再做压缩、归档、推送等操作。 + +4. **集成测试 / 模板验收**: + - 使用 `renderToBytes(...)` 生成文档; + - 对生成内容进行人工或自动化检查(例如比对关键字段是否渲染正确)。 + +--- + +## 7. 推荐阅读顺序 + +1. 阅读本 `word.md` 文档,理解整体设计与发货单示例; +2. 打开 `WordTemplateUtil` 源码,查看 JavaDoc 中的详细注释和示例; +3. 在业务模块中设计自己的模板(`.docx`)和 `buildXXXExportData` 方法,与本示例保持同样的模式; +4. 将新模块的模板字段 ↔ 数据字段映射关系同步更新到各自模块的业务文档中(例如 `hwbm.md`)。 \ No newline at end of file