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