当前位置: 首页 > news >正文

使用 Java 和 FreeMarker 实现自动生成供货清单,动态生成 Word 文档,简化文档处理流程。

在上一篇博客中主要是使用SpringBoot+Apache POI实现了BOM物料清单Excel表格导出,详见以下博客:

Spring Boot + Apache POI 实现 Exc()el 导出:BOM物料清单生成器(支持中文文件名、样式美化、数据合并)


目录

引言

项目结构

源代码展示

1.WordController

2.WordUtil工具类

3.FreeMarker模版

4.POM依赖

WordController类深度解析

1.类结构

2.main方法

3.generateWordFile方法

4.addTestData方法

WordUtil类深度解析

1.类结构和静态成员

2.静态初始化块

3.私有构造函数

4.exportMillCertificateWord方法

5.createDoc方法

6.WordUtil类总结

FreeMarker模板深度解析

1.文档结构和样式

2.表格结构和动态数据插入

总结


引言

在电缆行业,生成供货清单是一项常见但繁琐的任务。本教程将介绍如何使用现代Java技术栈自动化这一过程,大幅提高工作效率和准确性。我们将使用SpringBoot作为框架,Apache POI处理Word文档,以及FreeMarker作为模板引擎来实现这一功能!

让我们先了解一下这个问题的背景:

  1. 在电缆行业,手动创建供货清单是一个复杂且重复的过程。
  2. 这个过程不仅耗时,还容易出错,影响工作效率和数据准确性。

为了解决这个问题,我们提出了一个技术方案,结合了以下几个关键技术:

  1. SpringBoot: 作为我们的主要开发框架
  2. Apache POI: 用于生成和操作Word文档
  3. FreeMarker模板引擎: 用于生成Word文件的内容

这个方案的主要优势包括:

  1. 灵活性: 使用FreeMarker模板可以轻松调整文档格式,而无需修改程序代码。
  2. 效率: 自动化生成过程大大减少了人工操作,提高了办公效率。
  3. 准确性: 自动化处理确保了数据的准确性和一致性。
  4. 适用性: 特别适合电缆行业的业务需求,生成符合要求的.doc文件。

通过阅读这篇博客,您将学习如何实现这个解决方案,从而帮助您或您的团队简化工作流程,提高生产效率。

效果图:

项目结构

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── pw/
│   │           ├── WordController.java  #负责生成测试数据并调用WordUtil工具类来生成Word文档
│   │           └── utils/
│   │               └── WordUtil.java  #这个工具类封装了使用FreeMarker生成Word文档的核心功能
│   └── resources/
│       └── templates/
│           └── template.ftl #模版定义了Word文档的结构和样式,使用HTML和CSS来格式化内容

1.WordController类:这个类是我们应用的入口点,负责生成测试数据并调用WordUtil来生成Word文档。

2.WordUtil类:这个工具类封装了使用FreeMarker生成Word文档的核心逻辑。

3.FreeMarker模版(template.ftl):这个模版定义了Word文档的结构和样式,使用HTML和CSS来格式化内容。

源代码展示

1.WordController

import com.pw.utils.WordUtil;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class WordController {public static void main(String[] args) throws IOException {// 指定保存Word文件的目录String filePath = "F:\\Poi2Word\\src\\main\\resources\\output"; // 更改为您希望的目录new WordController().generateWordFile(filePath);}public void generateWordFile(String directory) throws IOException {List<Map<String, Object>> listMap = new ArrayList<>();//测试数据addTestData(listMap, "4600025747", "绝缘导线", "AC10kV,JKLYJ,300", 1500, "米", "盘号:A1");addTestData(listMap, "4600025748", "绝缘导线", "AC10kV,JKLGYJ,150/30", 2500, "米", "盘号:A2");addTestData(listMap, "4600025749", "绝缘导线", "AC10kV,JKLGYJ,150/30", 3500, "米", "盘号:A3");addTestData(listMap, "4600025750", "绝缘导线", "AC10kV,JKLGYJ,150/30", 4500, "米", "盘号:A4");addTestData(listMap, "4600025751", "绝缘导线", "AC10kV,JKLGYJ,150/30", 3800, "米", "盘号:A5");addTestData(listMap, "4600025752", "绝缘导线", "AC10kV,JKLYJ,180", 2000, "米", "盘号:A6");addTestData(listMap, "4600025753", "绝缘导线", "AC10kV,JKLYJ,120", 4200, "米", "盘号:A7");addTestData(listMap, "4600025754", "绝缘导线", "AC10kV,JKLYJ,120", 3700, "米", "盘号:A8");addTestData(listMap, "4600025755", "绝缘导线", "AC10kV,JKLYJ,120", 4300, "米", "盘号:A9");addTestData(listMap, "4600025756", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2800, "米", "盘号:A10");addTestData(listMap, "4600025757", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2400, "米", "盘号:A11");addTestData(listMap, "4600025758", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2600, "米", "盘号:A12");HashMap<String, Object> map = new HashMap<>();map.put("qdList", listMap);  // 添加供货清单数据map.put("contacts", "张三");  // 联系人map.put("contactsPhone", "13988887777");  // 联系电话map.put("date", "2025年01月18日");  // 日期map.put("company", "新电缆科技有限公司");  // 公司名称map.put("customer", "国网北京市电力公司");  // 客户String wordName = "template.ftl"; // FreeMarker模板文件名String fileName = "供货清单" + System.currentTimeMillis() + ".doc"; // 带时间戳的文件名String name = "name";  // 临时文件名// 确保输出目录存在File directoryFile = new File(directory);if (!directoryFile.exists()) {directoryFile.mkdirs();  // 如果目录不存在则创建}// 生成Word文件WordUtil.exportMillCertificateWord(directory, map, wordName, fileName, name);System.out.println("文件成功生成在:" + directory + fileName);}private void addTestData(List<Map<String, Object>> listMap, String danhao, String name, String model, int num, String unit, String remark) {Map<String, Object> item = new HashMap<>();item.put("serNo", listMap.size() + 1);  // 序号item.put("danhao", danhao);  // 单号item.put("name", name);  // 产品名称item.put("model", model);  // 规格型号item.put("num", String.valueOf(num));  // 数量,转换为字符串item.put("unit", unit);  // 单位item.put("remark", remark);  // 备注listMap.add(item);  // 将数据添加到列表}
}

2.WordUtil工具类

package com.pw.utils;import freemarker.template.Configuration;
import freemarker.template.Template;import java.io.*;
import java.util.Map;public class WordUtil {private static Configuration configuration = null;// 模板文件夹路径private static final String templateFolder = WordUtil.class.getResource("/templates").getPath();static {configuration = new Configuration();configuration.setDefaultEncoding("utf-8");try {System.out.println(templateFolder);configuration.setDirectoryForTemplateLoading(new File(templateFolder));  // 设置模板加载路径} catch (IOException e) {e.printStackTrace();}}private WordUtil() {throw new AssertionError();  // 防止实例化}/*** 导出Word文档* @param map Word文档中参数* @param wordName 模板的名字,例如xxx.ftl* @param fileName Word文件的名字 格式为:"xxxx.doc"* @param outputDirectory 输出文件的目录路径* @param name 临时的文件夹名称,作为Word文件生成的标识* @throws IOException*/public static void exportMillCertificateWord(String outputDirectory, Map map, String wordName, String fileName, String name) throws IOException {Template freemarkerTemplate = configuration.getTemplate(wordName);  // 获取模板文件File file = null;try {// 调用工具类的createDoc方法生成Word文档file = createDoc(map, freemarkerTemplate, name);// 确保输出目录存在File dir = new File(outputDirectory);if (!dir.exists()) {dir.mkdirs();  // 如果目录不存在则创建}// 定义完整的文件路径File outputFile = new File(outputDirectory, fileName);// 重命名并移动文件到指定目录file.renameTo(outputFile);System.out.println("文件成功生成在: " + outputFile.getAbsolutePath());} finally {if (file != null && file.exists()) {file.delete();  // 删除临时文件}}}private static File createDoc(Map<?, ?> dataMap, Template template, String name) {File f = new File(name);try {// 使用OutputStreamWriter来指定编码,防止特殊字符出问题Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");template.process(dataMap, w);  // 使用FreeMarker处理模板w.close();} catch (Exception ex) {ex.printStackTrace();throw new RuntimeException(ex);}return f;  // 返回生成的文件}
}

3.FreeMarker模版

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>${company}送货清单</title><style>body { font-family: SimSun, serif; }  <!-- 设置字体 -->table { border-collapse: collapse; width: 100%; }  <!-- 设置表格样式 -->th, td { border: 1px solid black; padding: 5px; text-align: center; }  <!-- 设置表格的单元格样式 -->th { background-color: #f2f2f2; }  <!-- 设置表头背景色 -->.subtotal { font-weight: bold; }  <!-- 小计行加粗 -->.total { font-weight: bold; font-size: 1.1em; }  <!-- 总计行加粗并设置字体大小 --></style>
</head>
<body>
<h1 style="text-align: center;">${company}送货清单</h1>  <!-- 顶部公司名称 --><table><tr><th>序号</th>  <!-- 表头:序号 --><th>供货单号</th>  <!-- 表头:供货单号 --><th>产品名称</th>  <!-- 表头:产品名称 --><th>规格型号</th>  <!-- 表头:规格型号 --><th>数量</th>  <!-- 表头:数量 --><th>单位</th>  <!-- 表头:单位 --><th>备注</th>  <!-- 表头:备注 --></tr><#assign totalQuantity = 0>  <!-- 总数量初始化 --><#assign totalItems = 0>  <!-- 总项数初始化 --><#assign sortedList = qdList?sort_by("model")>  <!-- 按照规格型号排序 --><#assign currentModel = "">  <!-- 当前型号初始化 --><#assign subtotalQuantity = 0>  <!-- 小计数量初始化 --><#assign subtotalItems = 0>  <!-- 小计项数初始化 --><#list sortedList as item>  <!-- 遍历排序后的列表 --><#if item.model != currentModel>  <!-- 如果规格型号变了 --><#if currentModel != "">  <!-- 如果当前规格型号不是空 --><tr class="subtotal"><td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td><td>${subtotalQuantity}</td><td>${sortedList[0].unit}</td><td></td></tr></#if><#assign currentModel = item.model>  <!-- 更新当前型号 --><#assign subtotalQuantity = 0>  <!-- 重置小计数量 --><#assign subtotalItems = 0>  <!-- 重置小计项数 --></#if><tr><td>${item?counter}</td>  <!-- 序号 --><td>${item.danhao}</td>  <!-- 单号 --><td>${item.name}</td>  <!-- 产品名称 --><td>${item.model}</td>  <!-- 规格型号 --><td>${item.num}</td>  <!-- 数量 --><td>${item.unit}</td>  <!-- 单位 --><td>${item.remark}</td>  <!-- 备注 --></tr><#assign itemNum = item.num?replace(",", "")?number>  <!-- 将数量转为数字并处理逗号 --><#assign subtotalQuantity = subtotalQuantity + itemNum>  <!-- 累加小计数量 --><#assign subtotalItems = subtotalItems + 1>  <!-- 累加小计项数 --><#assign totalQuantity = totalQuantity + itemNum>  <!-- 累加总数量 --><#assign totalItems = totalItems + 1>  <!-- 累加总项数 --></#list><#if currentModel != "">  <!-- 如果当前规格型号不是空 --><tr class="subtotal"><td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td><td>${subtotalQuantity}</td><td>${sortedList[0].unit}</td><td></td></tr></#if><tr class="total"><td colspan="4">合计:${totalQuantity}${qdList[0].unit} ${totalItems}轴</td><td>${totalQuantity}</td><td>${qdList[0].unit}</td><td></td></tr>
</table><p>发货联系人:${contacts}</p>  <!-- 发货联系人 -->
<p>联系电话:${contactsPhone}</p>  <!-- 联系电话 -->
<p>日期:${date}</p>  <!-- 日期 --><p style="text-align: right;">收货人(签字):_______________</p>  <!-- 收货人签字 -->
<p style="text-align: right;">联系电话:_______________</p>  <!-- 收货人联系电话 -->
<p style="text-align: right;">${customer}</p>  <!-- 客户 -->
</body>
</html>

4.POM依赖

<!-- freemarker依赖,用于模板引擎,方便进行页面的渲染和数据的展示等操作 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- Apache POI 的核心依赖,用于操作 Microsoft Office 格式的文档,如 Excel、Word 等文件 -->
<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.0.0</version>
</dependency>
<!-- Apache POI 的 OOXML 扩展依赖,主要用于处理 Office 2007 及以后版本的 OOXML 格式的文件,例如.xlsx 等 -->
<dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.0.0</version>
</dependency>
<!-- OOXML 模式相关的依赖,提供了对 OOXML 文档结构和内容模式的支持,有助于 Apache POI 更好地操作 OOXML 格式文件 -->
<dependency><groupId>org.apache.poi</groupId><artifactId>ooxml-schemas</artifactId><version>1.4</version>
</dependency>

WordController类深度解析

WordController类是整个应用的核心控制器,负责协调数据生成和文档创建的过程。让我们逐步分析它的主要组成部分:

1.类结构

public class WordController {// 方法定义...
}

这个类没有继承任何其他类,也没有实现任何接口,是一个独立的控制器类。

2.main方法

public static void main(String[] args) throws IOException {String filePath = "F:\\Poi2Word\\src\\main\\resources\\output";new WordController().generateWordFile(filePath);
}
  • 这是应用的入口点。
  • 它设置了输出文件的路径,然后调用generateWordFile方法。
  • 请注意:在常规的 Spring Boot 实际应用场景下,我们一般不会直接在控制器类中使用 main 方法。此处之所以将 main 方法置于控制器中,纯粹是出于演示目的,旨在让相关流程更加直观易懂。

而当进入到正式开发环节时,有几个关键要点务必落实:

其一,需要引入数据库集成功能,将当前所使用的测试数据全面替换为从数据库中精准查询获取的真实数据,以此确保数据的准确性与时效性;

其二,要对控制器进行优化改造,摒弃现有的演示模式,将其转换为遵循标准规范的请求接口实现方式,进而满足实际业务需求,提升系统的稳定性与可扩展性。

3.generateWordFile方法

此方法的只要目的是生成Word文件,首先需要先收集和存储测试数据,存储表格数据是将一条数据存储在Map集合中,再将每一条数据存储到List集合中。将其他数据存储到单独的一个Map集合中。然后确保输出目录存在,最后调用WordUtil中的exportMillCertificateWord方法生成文件,并输出文件的生成位置。

// 生成 Word 文件的方法
public void generateWordFile(String directory) throws IOException {// 存储测试数据的列表,每个元素都是一个 Map,存储了具体的信息List<Map<String, Object>> listMap = new ArrayList<>();// 添加测试数据,调用 addTestData 方法添加一条记录addTestData(listMap, "4600025747", "绝缘导线", "AC10kV,JKLYJ,300", 1500, "米", "盘号:A1");//... 可以继续调用 addTestData 方法添加更多测试数据...// 存储最终要填充到 Word 模板的数据的 Map,包含各种信息HashMap<String, Object> map = new HashMap<>();// 将测试数据列表添加到 map 中,键为 "qdList"map.put("qdList", listMap);// 联系人信息map.put("contacts", "张三");// 联系人电话map.put("contactsPhone", "13988887777");// 日期信息map.put("date", "2025年01月18日");// 公司名称map.put("company", "新电缆科技有限公司");// 客户名称map.put("customer", "国网北京市电力公司");// Word 模板文件的名称String wordName = "template.ftl";// 生成的 Word 文件的名称,使用当前时间戳保证文件名的唯一性String fileName = "供货清单" + System.currentTimeMillis() + ".doc";// 名称信息,具体含义可能根据实际情况而定String name = "name";// 创建一个文件对象,用于表示输出目录File directoryFile = new File(directory);// 检查输出目录是否存在,如果不存在则创建目录if (!directoryFile.exists()) {directoryFile.mkdirs();}// 调用 WordUtil 的 exportMillCertificateWord 方法生成 Word 文件// 传入目录、数据 Map、模板名称、生成的文件名称和名称信息WordUtil.exportMillCertificateWord(directory, map, wordName, fileName, name);// 打印生成文件的成功信息System.out.println("文件成功生成在:" + directory + fileName);
}

这个方法完成以下任务:

  • 创建一个一个List<Map<String,Object>>集合来存储供货清单数据
  • 使用addTestData方法添加多条测试数据
  • 创建一个Map集合来存储企业名称,发货联系人,联系电话等信息
  • 确保输出目录存在
  • 调用WordUtil.exportMillCertificateWord方法来生成Word文档

4.addTestData方法

这个方法用于创建单个供货项目的数据

// 添加一条测试数据到 listMap 中
private void addTestData(List<Map<String, Object>> listMap, String danhao, String name, String model, int num, String unit, String remark) {// 创建一个新的 HashMap,用于存储每一条数据Map<String, Object> item = new HashMap<>();// 将数据项依次放入 HashMap 中,"serNo" 表示序号,使用 listMap 的大小+1 生成序号item.put("serNo", listMap.size() + 1);  // 序号是当前列表的大小 + 1item.put("danhao", danhao);  // 供货单号item.put("name", name);  // 产品名称item.put("model", model);  // 规格型号item.put("num", String.valueOf(num));  // 数量,将整数转为字符串item.put("unit", unit);  // 单位item.put("remark", remark);  // 备注// 将该条数据项添加到 listMap 列表中listMap.add(item);
}

这个方法完成以下任务:

  • 它接收多个参数,代表一个供货项目的各个属性。
  • 创建一个新的Map来存储这个项目的数据。
  • 自动计算序号(serNo)基于当前列表的大小。
  • 将所有数据添加到Map中。
  • 将这个Map添加到供货清单列表中。

WordUtil类深度解析

WordUtil类是整个文档生成过程的核心,它封装了FreeMarker模板引擎的配置和使用逻辑。让我们逐步分析它的主要组成部分:

1.类结构和静态成员

public class WordUtil {private static Configuration configuration = null;private static final String templateFolder = WordUtil.class.getResource("/templates").getPath();// 其他方法...
}

configuration:这是FreeMarker的核心配置对象,用于设置模版加载路径。

templateFolder:定义了模版文件的存储路径。使用getResource()方法确保在不同环境下都能正确找到模版文件。

2.静态初始化块

这段代码的作用是初始化FreeMarker的Configuration对象,设置模版加载目录以及编码格式,以便FreeMarker后续能够正确加载和处理模版文件。

// 静态初始化块,用于初始化 FreeMarker 配置
static {// 创建一个 FreeMarker 配置对象,用于后续模板处理configuration = new Configuration();// 设置 FreeMarker 配置对象的默认编码为 "utf-8"configuration.setDefaultEncoding("utf-8");try {// 输出模板文件夹路径,帮助调试System.out.println(templateFolder);// 设置模板加载目录为 templateFolder 指定的路径,模板文件会从该目录加载configuration.setDirectoryForTemplateLoading(new File(templateFolder));} catch (IOException e) {// 如果加载模板目录时出现异常,打印错误堆栈信息e.printStackTrace();}
}

这个静态初始化块在类加载时执行,主要完成以下任务:

  • 创建FreeMarker的Configuration对象
  • 设置默认编码为UTF-8,确保正确处理中文等字符
  • 设置模版加载目录,这样FreeMarker就知道从哪里查找加载模版文件了
  • 错误处理:如果执行过程中出现了IO异常,就会打印堆栈跟踪

3.私有构造函数

这个构造函数防止类被实例化,确保WordUtil只能通过其静态方法使用。

private WordUtil() {throw new AssertionError();
}

私有构造函数的好处包括:

  • 防止类被实例化

当类的构造函数被声明为private时,外部代码无法直接创建该类的实例。这就意味着该类只能公国静态方法访问,确保类的功能是全局共享的。

  • 实现单例模式的基础

在一些设计模式中,例如单例模式,类只允许有一个实例,私有构造函数确保了这一点。通过private构造函数,我们可以控制类的实例化过程,并确保只有一个实例被创建。

  • 封装类的内部实现

私有构造函数可以帮助隐藏类的具体实现细节,外部代码不需要关心如何创建类的实例,只需要使用类提供的静态方法即可。这增加了类的封装性,降低了与外部代码的耦合度。

  • 避免多余的对象创建

由于无法实例化类,每次调用静态方法时,都会使用已有的类实例,这可以避免无意义的对象创建,节省内存和资源。

4.exportMillCertificateWord方法

这个方法的主要功能是通过加载指定的 FreeMarker 模板生成一个临时的 Word 文档,确保输出目录存在后,将临时文件重命名并保存到指定的位置,同时在过程结束后清理临时文件,并打印文件生成的成功消息。

// 导出 Word 文档的方法
public static void exportMillCertificateWord(String outputDirectory, Map map, String wordName, String fileName, String name) throws IOException {// 获取 FreeMarker 模板文件Template freemarkerTemplate = configuration.getTemplate(wordName);// 初始化一个 File 对象,用于存储生成的临时文件File file = null;try {// 使用模板和数据创建 Word 文档,返回临时文件file = createDoc(map, freemarkerTemplate, name);// 创建目标目录的 File 对象File dir = new File(outputDirectory);// 如果目录不存在,则创建该目录if (!dir.exists()) {dir.mkdirs();  // 创建目录及其父目录}// 定义最终输出文件的完整路径(包括目录和文件名)File outputFile = new File(outputDirectory, fileName);// 将临时生成的文件重命名为目标文件,并将其移动到指定目录file.renameTo(outputFile);// 打印输出文件的绝对路径,a通知文件生成成功System.out.println("文件成功生成在: " + outputFile.getAbsolutePath());} finally {// 最后,无论是否成功生成文件,都确保临时文件被删除if (file != null && file.exists()) {file.delete();  // 删除临时文件}}
}

这个方法是文档导出的主要入口,主要实现了以下功能:

  • 加载指定的FreeMarker模版
  • 调用createDoc方法生成临时文档文件
  • 确保输出目录存在
  • 将临时文件重命名并移动到指定的输出位置
  • 使用finally块确保临时文件被删除,无论过程是否成功

5.createDoc方法

这个方法是创建文档的核心方法,主要是通过创建一个临时文件,使用指定的FreeMarker模版和数据模型将内容填充到文件中,并确保文件使用UTF-8编码进行写入。该方法在执行过程中捕获异常并打印堆栈信息,确保发生错误时能够正确处理。最后。方法返回生成的文件对象,以便后续操作或保存。

// 创建文档的方法,使用 FreeMarker 模板生成内容并写入文件
private static File createDoc(Map<?, ?> dataMap, Template template, String name) {// 创建一个新的 File 对象,表示生成的文档文件,文件名由参数 "name" 提供File f = new File(name);try {// 使用 OutputStreamWriter 创建一个写入文件的 Writer 对象,设置编码为 "utf-8"Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");// 使用 FreeMarker 模板将数据填充到文件中template.process(dataMap, w);// 关闭 Writer,确保所有内容写入文件w.close();} catch (Exception ex) {// 捕获异常并打印错误堆栈信息ex.printStackTrace();// 抛出 RuntimeException,确保错误被传播到调用者throw new RuntimeException(ex);}// 返回生成的文件对象return f;
}

这个方法是实际创建文档的核心,主要实现以下功能:

  • 创建一个临时文件。

  • 使用OutputStreamWriter设置UTF-8编码,确保正确处理所有字符。

  • 调用FreeMarker的template.process()方法,将数据模型(dataMap)应用到模板上。

  • 关闭写入器。

  • 如果过程中发生异常,打印堆栈跟踪并抛出RuntimeException。

  • 返回生成的文件对象。

6.WordUtil类总结

WordUtil 类通过封装 FreeMarker 模板引擎的配置和文件操作,提供了一个简洁的文档生成工具。它加载指定模板,使用数据模型填充内容,创建临时文件,并确保文件按照指定路径保存。该类通过静态方法确保全局共享功能,使用 UTF-8 编码处理字符,捕获异常并清理临时文件,确保文档生成过程的稳定性和高效性。

FreeMarker模板深度解析

FreeMarker模板是整个文档生成过程的核心,它定义了最终Word文档的结构和样式。让我们来逐步分析模板的主要组成部分

1.文档结构和样式

<!DOCTYPE html> <!-- 声明文档类型为 HTML5 -->
<html>
<head><!-- 设置文档字符编码为 UTF-8,支持中文和其他字符集 --><meta charset="UTF-8"><!-- 设置页面标题,动态插入公司名称 --><title>${company}送货清单</title><style>/* 设置页面正文的字体为 SimSun(宋体),如果没有则使用 serif */body { font-family: SimSun, serif; }/* 设置表格样式:表格边框合并,宽度100% */table { border-collapse: collapse; width: 100%; }/* 设置表格头部和单元格的边框、内边距和文本居中对齐 */th, td { border: 1px solid black; padding: 5px; text-align: center; }/* 设置表头背景色为浅灰色 */th { background-color: #f2f2f2; }/* 设置小计行字体加粗 */.subtotal { font-weight: bold; }/* 设置合计行字体加粗,字体大小稍大 */.total { font-weight: bold; font-size: 1.1em; }</style>
</head>
<body><!-- 页面标题,居中显示公司名称和送货清单 --><h1 style="text-align: center;">${company}送货清单</h1><!-- 表格内容将在这里生成,动态插入数据 -->
</body>
</html>

这段代码通过HTML和内嵌CSS定义了页面布局和样式:

动态公司名称:<title>标签使用${company}插入动态的公司名称,显示在浏览器标签中。

字体和表格样式:

  • 设置页面字体为宋体(Simsun)
  • 定义表格边框合并、100%宽度,并使单元格内容居中

小计和总计行样式:为小计行加粗字体,并为总计行加粗且增大字体,突出显示重要数据。

2.表格结构和动态数据插入

<table><!-- 表头,定义表格的列名 --><tr><th>序号</th>  <!-- 序号 --><th>供货单号</th>  <!-- 供货单号 --><th>产品名称</th>  <!-- 产品名称 --><th>规格型号</th>  <!-- 规格型号 --><th>数量</th>  <!-- 数量 --><th>单位</th>  <!-- 单位 --><th>备注</th>  <!-- 备注 --></tr><!-- 初始化总计和小计相关变量 --><#assign totalQuantity = 0>  <!-- 总数量 --><#assign totalItems = 0>  <!-- 总项数 --><#assign sortedList = qdList?sort_by("model")>  <!-- 按照规格型号对数据进行排序 --><#assign currentModel = "">  <!-- 当前规格型号 --><#assign subtotalQuantity = 0>  <!-- 小计数量 --><#assign subtotalItems = 0>  <!-- 小计项数 --><!-- 遍历排序后的列表 --><#list sortedList as item><!-- 如果当前项的规格型号与上一项不同,则输出上一项的小计 --><#if item.model != currentModel><#if currentModel != ""><!-- 输出上一规格型号的小计行 --><tr class="subtotal"><td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td><td>${subtotalQuantity}</td><td>${sortedList[0].unit}</td><td></td></tr></#if><!-- 更新当前规格型号为当前项的规格型号,并重置小计 --><#assign currentModel = item.model><#assign subtotalQuantity = 0><#assign subtotalItems = 0></#if><!-- 输出当前行数据 --><tr><td>${item?counter}</td>  <!-- 序号,使用 FreeMarker 的 counter 计数 --><td>${item.danhao}</td>  <!-- 供货单号 --><td>${item.name}</td>  <!-- 产品名称 --><td>${item.model}</td>  <!-- 规格型号 --><td>${item.num}</td>  <!-- 数量 --><td>${item.unit}</td>  <!-- 单位 --><td>${item.remark}</td>  <!-- 备注 --></tr><!-- 更新小计和总计的数量和项数 --><#assign itemNum = item.num?replace(",", "")?number>  <!-- 将数量转为数字并处理逗号 --><#assign subtotalQuantity = subtotalQuantity + itemNum>  <!-- 累加小计数量 --><#assign subtotalItems = subtotalItems + 1>  <!-- 累加小计项数 --><#assign totalQuantity = totalQuantity + itemNum>  <!-- 累加总数量 --><#assign totalItems = totalItems + 1>  <!-- 累加总项数 --></#list><!-- 如果最后一项有数据,输出最后的规格型号小计 --><#if currentModel != ""><tr class="subtotal"><td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td><td>${subtotalQuantity}</td><td>${sortedList[0].unit}</td><td></td></tr></#if><!-- 输出最终的合计行 --><tr class="total"><td colspan="4">合计:${totalQuantity}${qdList[0].unit} ${totalItems}轴</td>  <!-- 显示合计的数量和项数 --><td>${totalQuantity}</td>  <!-- 合计数量 --><td>${qdList[0].unit}</td>  <!-- 单位 --><td></td></tr>
</table>

表格结构

  • 使用 <table> 标签创建表格,并通过 <th> 定义表头,包含7列:序号、供货单号、产品名称等。

动态数据插入

  • 使用 FreeMarker <#list> 遍历排序后的清单数据,并通过 ${item.属性名} 动态插入每项数据,如 ${item.danhao} 插入供货单号。

小计和总计计算

  • 通过 <#assign> 定义变量如 totalQuantitysubtotalQuantity,在循环中累加数量。
  • 使用 <#if> 判断条件,插入小计行,并在循环结束后插入总计行。

数据处理

  • 使用 sortedList = qdList?sort_by("model") 按型号对清单数据进行排序。
  • 处理数量 itemNum = item.num?replace(",", "")?number,移除逗号并转换为数字,确保计算正确。

格式化输出

  • 小计和总计行使用 colspan 属性合并单元格,确保表格显示整洁。
  • 使用 CSS 类 subtotaltotal 为小计和总计行应用加粗和突出显示的样式。

总结:此表格通过 FreeMarker 动态插入数据、计算小计和总计,并通过合适的排序和格式化样式,确保清单展示清晰且易于阅读。

最后,模板还包括了一些额外信息:

<p>发货联系人:${contacts}</p>
<p>联系电话:${contactsPhone}</p>
<p>日期:${date}</p><p style="text-align: right;">收货人(签字):_______________</p>
<p style="text-align: right;">联系电话:_______________</p>
<p style="text-align: right;">${customer}</p>

这部分添加了额外的联系信息和签名区域,进一步完善了文档的实用性。

总的来,这个FreeMarker模板展示了如何结合HTML、CSS和FreeMarker的模板语法来创建一个复杂、动态且格式良好的文档。它不仅能够准确地呈现数据,还能执行必要的计算和格式化,从而生成一个专业的供货清单文档。

总结

通过使用SpingBoot、Apache POI和FreeMarker,我们成功自动化了电缆供货清单的生成过程。这不仅提高了效率,还减少了人为错误。本解决方案的模块化设计使其易于维护和扩展。

希望本教程能够帮助您理解如何使用Java技术来解决实际业务问题。

相关文章:

使用 Java 和 FreeMarker 实现自动生成供货清单,动态生成 Word 文档,简化文档处理流程。

在上一篇博客中主要是使用SpringBootApache POI实现了BOM物料清单Excel表格导出&#xff0c;详见以下博客&#xff1a; Spring Boot Apache POI 实现 Exc&#xff08;&#xff09;el 导出&#xff1a;BOM物料清单生成器&#xff08;支持中文文件名、样式美化、数据合并&#…...

20250118拿掉荣品pro-rk3566开发板上Android13下在uboot和kernel启动阶段的Rockchip这个LOGO标识

20250118拿掉荣品pro-rk3566开发板上Android13下在uboot和kernel启动阶段的Rockchip这个LOGO标识 2025/1/18 15:12 缘起&#xff1a;做飞凌OK3588-C开发板/核心板【Linux R4】的时候&#xff0c;测试/生产要求没有开机LOGO【飞凌/Rockchip】 要求&#xff1a;黑屏或者中性界面。…...

《Hands_On_LLM》8.3: 检索增强生成-RAG技术概论

3.检索增强生成 (Retrieval-Augmented Generation (RAG)) LLM 的大规模应用很快导致人们向它们提问&#xff0c;并期望得到符合实际的答案。虽然这些模型可以正确回答一些问题&#xff0c;但它们也自信地回答了许多错误的问题。为了纠正这种行为&#xff0c;业界转而采用的主要…...

CSS中样式继承+优先级

继承属性和非继承属性 一、定义及分类 1、继承属性是指在父元素上设置了这些属性后&#xff0c;子元素会自动继承这些属性的值&#xff0c;除非子元素显式地设置了不同的值。 常见的继承属性: 字体 font 系列文本text-align text-ident line-height letter-spacing颜色 col…...

Vue进阶之旅:核心技术与页面应用实战(路由进阶)

文章目录 一、路由模块封装二、声明式导航&#xff08;一&#xff09;导航链接与高亮&#xff08;二&#xff09;声明式导航传参1. 查询参数传参2. 动态路由传参 三、路由重定向、404 与模式设置&#xff08;一&#xff09;路由重定向&#xff08;二&#xff09;路由 404&#…...

单片机存储器和C程序编译过程

1、 单片机存储器 只读存储器不是并列关系&#xff0c;是从ROM发展到FLASH的过程 RAM ROM 随机存储器 只读存储器 CPU直接存储和访问 只读可访问不可写 临时存数据&#xff0c;存的是CPU正在使用的数据 永久存数据&#xff0c;存的是操作系统启动程序或指令 断电易失 …...

Vue.js 动态设置表格最大高度的实现

概述 在现代 Web 开发中&#xff0c;响应式设计至关重要&#xff0c;尤其是在处理复杂的布局和数据表格时。表格通常会受到多种因素的影响&#xff0c;如分页、合计行或动态内容&#xff0c;这可能导致表格高度的变化。本文将介绍一个基于 Vue.js 的方法 setMaxHeight&#xf…...

Java测试开发平台搭建(九)前端

1. 搭建前端vue环境 Vue3 安装 | 菜鸟教程 2. 创建项目 1.进入ui vue ui 2. create项目 3. 成功之后添加插件&#xff1a; cli-plugin-router vue-cli-plugin-vuetify 4. 添加依赖 axios 5. 点击任务开始运行 如果报错&#xff1a; 修改vue.config.jsconst { defineConfig }…...

MySQL多表查询练习

1.找出销售部门中年纪最大的员工的姓名 mysql> select name,age from dept a ,emp_new b where a.dept1b.dept2 and dept_name销售order by age desc limit 1; ------------ | name | age | ------------ | 荣七 | 64 | ------------ 1 row in set (0.00 sec) 2.求财务…...

低代码运维与管理服务

文章目录 前言一、服务内容二、服务范围三、服务流程四、服务交付件五、责任矩阵六、验收标准 前言 随着云计算技术的发展&#xff0c;数字化转型是企业的必然选择&#xff0c;企业需要实现广泛的连接并走向开放&#xff0c;传统集成工具无法满足当前企业面临的数字化转型诉求…...

【机器学习:三十二、强化学习:理论与应用】

1. 强化学习概述 **强化学习&#xff08;Reinforcement Learning, RL&#xff09;**是一种机器学习方法&#xff0c;旨在通过试验与反馈的交互&#xff0c;使智能体&#xff08;Agent&#xff09;在动态环境中学习决策策略&#xff0c;以最大化累积奖励&#xff08;Cumulative…...

解决wordpress媒体文件无法被搜索的问题

最近,我在wordpress上遇到了一个令人困扰的问题:我再也无法在 WordPress 的媒体库中搜索媒体文件了。之前,搜索媒体非常方便,但现在无论是图片还是其他文件,似乎都无法通过名称搜索到。对于我这样需要频繁使用图片的博主来说,这简直是个大麻烦。 问题源头 一开始,我怀…...

【2024年华为OD机试】(B卷,100分)- 增强的strstr (Java JS PythonC/C++)

一、问题描述 题目描述 C 语言有一个库函数 char *strstr(const char *haystack, const char *needle)&#xff0c;用于在字符串 haystack 中查找第一次出现字符串 needle 的位置&#xff0c;如果未找到则返回 null。 现要求实现一个 strstr 的增强函数&#xff0c;可以使用…...

【前端】CSS学习笔记

目录 CSS的简介CSS的概念语法 CSS的引入方式内联样式&#xff08;行内样式&#xff09;内部样式外部样式&#xff08;推荐&#xff09; 选择器全局选择器元素选择器类选择器ID选择器合并选择器后代选择器子选择器相邻兄弟选择器通用兄弟选择器伪类选择器:link:visited:hover:ac…...

项目架构调整,新增sunrays-combinations模块

文章目录 1.介绍2.环境搭建1.sunrays-framework下新建sunrays-combinations模块2.删除src3.pom.xml4.查看是否交给sunrays-framework管理5.删除sunrays-common中module引用的common-core-starter6.sunrays-combinations统一管理子模块7.common-all-starter的父模块修改为sunray…...

linux网络编程11——线程池

1. 线程池 1.1 池化技术原理 池化技术 当一个资源或对象的创建或者销毁的开销较大时&#xff0c;可以使用池化技术来保持一定数量的创建好的对象以供随时取用&#xff0c;于是就有了池式结构。常见的池式结构包括线程池、内存池和连接池。 池化技术应用的前提条件主要包括三…...

MySQL - 主从同步

​​​​​​1.主从同步原理&#xff1a; MySQL 主从同步是一种数据库复制技术&#xff0c;它通过将主服务器上的数据更改复制到一个或多个从服务器&#xff0c;实现数据的自动同步。 主从同步的核心原理是将主服务器上的二进制日志复制到从服务器&#xff0c;并在从服务器上执…...

基于微信小程序的安心陪诊管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…...

深入剖析iOS网络优化策略,提升App性能

一、引言 在当今移动互联网时代&#xff0c;iOS 应用的网络性能直接关系到用户体验。无论是加载速度缓慢、频繁的网络错误&#xff0c;还是高额的流量消耗&#xff0c;都可能导致用户流失。因此&#xff0c;iOS 网络优化成为开发者提升应用质量、增强用户满意度的关键环节。本文…...

游戏开发中常用的设计模式

目录 前言一、工厂模式二、单例模式三、观察者模式观察者模式的优势 四、状态模式状态模式的优势 五、策略模式策略模式的优势 六、组合模式七、命令模式八、装饰器模式 前言 本文介绍了游戏开发中常用的设计模式&#xff0c;如工厂模式用于创建对象&#xff0c;单例模式确保全…...

rknn优化教程(二)

文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK&#xff0c;开始写第二篇的内容了。这篇博客主要能写一下&#xff1a; 如何给一些三方库按照xmake方式进行封装&#xff0c;供调用如何按…...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八

现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet&#xff0c;点击确认后如下提示 最终上报fail 解决方法 内核升级导致&#xff0c;需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...

IT供电系统绝缘监测及故障定位解决方案

随着新能源的快速发展&#xff0c;光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域&#xff0c;IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选&#xff0c;但在长期运行中&#xff0c;例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...

用docker来安装部署freeswitch记录

今天刚才测试一个callcenter的项目&#xff0c;所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...

大学生职业发展与就业创业指导教学评价

这里是引用 作为软工2203/2204班的学生&#xff0c;我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要&#xff0c;而您认真负责的教学态度&#xff0c;让课程的每一部分都充满了实用价值。 尤其让我…...

Qt 事件处理中 return 的深入解析

Qt 事件处理中 return 的深入解析 在 Qt 事件处理中&#xff0c;return 语句的使用是另一个关键概念&#xff0c;它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别&#xff1a;不同层级的事件处理 方…...

tomcat指定使用的jdk版本

说明 有时候需要对tomcat配置指定的jdk版本号&#xff0c;此时&#xff0c;我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...

Axure 下拉框联动

实现选省、选完省之后选对应省份下的市区...