You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

286 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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` 写入 WordOSS、本地文件等
- `renderToBytes(...)`:返回字节数组,便于后续上传或作为接口返回值。
> 详细 JavaDoc 和更多示例可参考 `WordTemplateUtil` 源码本身。
---
## 2. poi-tl 简介
poi-tl 是构建在 Apache POI 之上的 **Word 模板引擎**,通过简单的占位符语法实现:
- **普通字段占位符**`{{fieldName}}`
- **列表循环占位符**
- 语法:`{{#list}} ... {{/list}}`
- `list` 对应数据 Map 中的 `List<Map<String, Object>>`
- 循环体内部使用 `{{列名}}` 访问每一行 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<String, Object>` 形式传入:
- **普通字段**
```java
Map<String, Object> data = new HashMap<>();
data.put("customerName", "某某公司"); // {{customerName}}
data.put("shippingDate", "2025-12-10"); // {{shippingDate}}
data.put("contractCode", "HT-2025-001"); // {{contractCode}}
```
- **列表字段(循环表格)**
```java
List<Map<String, Object>> details = new ArrayList<>();
Map<String, Object> 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<RowRenderData>`(而不是 `List<Map<String,Object>>`)。
**模板制作规范(非常关键)**
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<RowRenderData> 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<RowRenderData>`,因此 `detailsTable` 必须放 `List<RowRenderData>`
不要误传 `List<Map<String,Object>>`
- **表头在,明细行全没了**
- 当前策略在 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<String, Object> 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<String, Object> buildWordExportData(Long shippingBillId) {
WmsShippingBillVo vo = queryById(shippingBillId);
if (vo == null) {
throw new ServiceException("发货单不存在ID" + shippingBillId);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Map<String, Object> 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<RowRenderData> 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<RowRenderData>`
> 其它单据(如报价、合同、项目收货/验收)可以直接复用上述模式:
> - 主表字段:顶部信息 + 落款信息;
> - 明细字段:优先采用 `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`)。