|
|
|
|
|
# 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<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` 等。
|
|
|
|
|
|
- `WordTemplateUtil` 接收的是 **classpath 相对路径**,例如:
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", "发货单_X123", data, response);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
部署后,只要模板文件在 Jar 的 `BOOT-INF/classes/templates` 下即可被正常加载。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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}}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **合计字段(可选)**:
|
|
|
|
|
|
|
|
|
|
|
|
```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)调用通用工具类完成渲染与输出
|
|
|
|
|
|
WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", fileName, data, response);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 Service 组装数据示例
|
|
|
|
|
|
|
|
|
|
|
|
文件:`WmsShippingBillServiceImpl.buildWordExportData`
|
|
|
|
|
|
|
|
|
|
|
|
核心思路:
|
|
|
|
|
|
|
|
|
|
|
|
1. 查询发货单主表 + 明细列表 VO;
|
|
|
|
|
|
2. 组装主表字段(顶部信息、落款信息);
|
|
|
|
|
|
3. 组装明细列表 `details`(循环表格数据源)。
|
|
|
|
|
|
|
|
|
|
|
|
部分代码示意:
|
|
|
|
|
|
|
|
|
|
|
|
```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}}
|
|
|
|
|
|
|
|
|
|
|
|
// === 明细列表,对应 {{#details}}...{{/details}} ===
|
|
|
|
|
|
List<Map<String, Object>> details = new ArrayList<>();
|
|
|
|
|
|
if (CollUtil.isNotEmpty(vo.getItemsVo())) {
|
|
|
|
|
|
int seq = 1;
|
|
|
|
|
|
for (WmsShippingDetailsVo item : vo.getItemsVo()) {
|
|
|
|
|
|
Map<String, Object> 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`)。
|