# 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` 等。 - 也支持直接放在 `resources` 根目录(classpath 根路径),例如: - `hwbm-cloud/ruoyi-modules/ruoyi-wms/src/main/resources/发货单模板.docx` - `WordTemplateUtil` 接收的是 **classpath 相对路径**,例如: ```java WordTemplateUtil.renderToResponse("发货单模板.docx", "发货单_X123", data, response); ``` 部署后,只要模板文件在 Jar 的 classpath 下即可被正常加载(Spring Boot 可执行 Jar 通常位于 `BOOT-INF/classes/` 下; 若放在 `resources/templates`,则路径通常为 `BOOT-INF/classes/templates/`;若放在 `resources` 根目录,则路径通常为 `BOOT-INF/classes/`)。 --- ## 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}} ``` - **动态表格字段(渲染策略 RenderPolicy,推荐明细表格场景)**: 适用场景: - Word 模板中需要用“表格”展示明细,且需要**动态插入 N 行**(常见于发货单/报价/合同明细); - 需要在渲染时做一些格式控制(比如边框、合并单元格、空数据时删除模板行等)。 约定: - 模板中使用一个占位符(例如 `{{detailsTable}}`),由 `DynamicTableRenderPolicy` 在渲染时接管; - 数据侧使用 `List`(而不是 `List>`)。 **模板制作规范(非常关键)**: 1. 在 Word 中画一个表格(例如 5 列:序号/名称/数量/单位/备注); 2. 至少包含两行: - 表头行(固定不动) - 模板行(数据行模板) 3. 在“模板行”的任意一个单元格中放置占位符 `{{detailsTable}}`。 - 渲染时会删除该模板行,并插入真实数据行; - 不建议在模板行做复杂的单元格合并,否则可能导致插入行结构与预期不一致。 **后端绑定方式(Controller 侧)**: ```java // 1) 绑定占位符 detailsTable 到渲染策略 Configure config = Configure.builder() .bind("detailsTable", new WmsShippingDetailTablePolicy()) .build(); // 2) 输出(使用带 Configure 的重载方法) WordTemplateUtil.renderToResponse("发货单模板.docx", fileName, data, config, response); ``` **后端数据结构(Service 侧)**: ```java List rows = new ArrayList<>(); rows.add(Rows.of("1", "胶辊", "10", "件", "加急").create()); data.put("detailsTable", rows); // 对应模板 {{detailsTable}} ``` **常见问题与排查**: - **表格没渲染/仍显示 {{detailsTable}}**: - `Configure.bind("detailsTable", ...)` 的 key 必须与模板占位符、`data.put("detailsTable", ...)` 完全一致; - 确认 Controller 调用的是带 `Configure` 的 `renderToResponse(...)` 重载方法。 - **报类型转换异常(ClassCastException)**: - 策略里会把 data 强转为 `List`,因此 `detailsTable` 必须放 `List`; 不要误传 `List>`。 - **表头在,明细行全没了**: - 当前策略在 rows 为空时会删除模板行并保留表头,这是预期行为; 请检查 `buildWordExportData` 是否确实组装了 rows。 - **合计字段(可选)**: ```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)配置表格动态渲染策略:将模板中的 {{detailsTable}} 占位符绑定到自定义 Policy Configure config = Configure.builder() .bind("detailsTable", new WmsShippingDetailTablePolicy()) .build(); // 4)调用通用工具类完成渲染与输出(使用自定义 Configure) WordTemplateUtil.renderToResponse("发货单模板.docx", fileName, data, config, response); } ``` ### 5.2 Service 组装数据示例 文件:`WmsShippingBillServiceImpl.buildWordExportData` 核心思路: 1. 查询发货单主表 + 明细列表 VO; 2. 组装主表字段(顶部信息、落款信息); 3. 组装明细表格 `detailsTable`(动态表格数据源)。 部分代码示意: ```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}} // === 明细表格(DynamicTableRenderPolicy 动态表格渲染)=== // 对应 Word 模板中的 {{detailsTable}} 占位符,由 WmsShippingDetailTablePolicy 策略处理 List detailRows = new ArrayList<>(); if (CollUtil.isNotEmpty(vo.getItemsVo())) { int seq = 1; for (WmsShippingDetailsVo item : vo.getItemsVo()) { RowRenderData row = Rows.of( String.valueOf(seq++), StringUtils.blankToDefault(item.getMaterialName(), ""), item.getShippingStockAmount() != null ? item.getShippingStockAmount().toPlainString() : "", StringUtils.blankToDefault(item.getUnitName(), ""), StringUtils.blankToDefault(item.getRemark(), "") ).create(); detailRows.add(row); } } data.put("detailsTable", detailRows); return data; } ``` ### 5.3 发货单模板占位符示意 > 下面仅用伪代码示意占位符位置,实际模板在 Word 中通过表格 / 段落排版完成。 ```text 客户名称:{{customerName}} 联系人:{{receiverName}} 联系电话:{{receiverPhone}} 发货日期:{{shippingDate}} 合同编号:{{contractCode}} 明细表格占位符:{{detailsTable}} 收货日期:{{receivedDate}} 发货单位:{{shippingUnit}} 发货人:{{shipper}} 联系电话:{{contactNumber}} ``` 补充说明: - **主表字段**使用 `{{fieldName}}` 直接渲染; - **明细表格**使用 `DynamicTableRenderPolicy`:模板里只需在“明细表格的模板行”放置 `{{detailsTable}}`, 渲染时由 `WmsShippingDetailTablePolicy` 删除模板行并插入真实的 `List`。 > 其它单据(如报价、合同、项目收货/验收)可以直接复用上述模式: > - 主表字段:顶部信息 + 落款信息; > - 明细字段:优先采用 `DynamicTableRenderPolicy`(如 `detailsTable`);简单场景也可使用 `{{#list}}...{{/list}}` 循环; > - 可扩展合计字段:`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`)。