使用 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技术来解决实际业务问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/4225.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JS基础(5):运算符和语句

一.运算符 1.赋值运算符 加减乘除都是一样的&#xff0c;&#xff0c;-&#xff0c;*&#xff0c;/ 2.一元运算符&#xff1a;经常用来计数 自增&#xff1a; 每次只能加一 自减&#xff1a;-- 前置自增 后置自增 结…

以租赁合同的例子讲清楚 开源协议原理和区别

开源协议通俗易懂的方式介绍清楚原理和区别 开源协议其实就是软件的“使用规则”&#xff0c;决定了别人可以如何使用、修改、分享你的代码。通俗一点说&#xff0c;如果你写了一段代码&#xff0c;开源协议就是告诉别人在什么条件下他们可以使用你的代码&#xff0c;以及他们可…

Flowable 管理各业务流程:流程设计器 (获取流程模型 XML)、流程部署、启动流程、流程审批、流程挂起和激活、任务分配

文章目录 引言I 表结构主要表前缀及其用途核心表II 流程设计器(Flowable BPMN模型编辑器插件)Flowable-UIvue插件III 流程部署部署步骤例子:根据流程模型ID部署IV 启动流程启动步骤ACT_RE_PROCDEF:流程定义相关信息例子:根据流程 ID 启动流程V 流程审批审批步骤Flowable 审…

【C++课程学习】:C++中的IO流(istream,iostream,fstream,sstream)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;C课程学习 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 C学习笔记&#xff1a; https://blog.csdn.net/djdjiejsn/category_12682189.html 前言&#xff1a; 在C语…

四、华为交换机 STP

生成树协议&#xff08;STP&#xff09;的核心目的是在存在冗余链路的网络中&#xff0c;构建一个无环的拓扑结构&#xff0c;从而防止网络环路带来的广播风暴等问题 一、STP 原理 选举根桥&#xff1a;网络中的每台交换机都会有一个唯一的桥 ID&#xff08;BID&#xff09;&am…

数字图像处理:实验二

任务一&#xff1a; 将不同像素&#xff08;32、64和256&#xff09;的原图像放大为像素大 小为1024*1024的图像&#xff08;图像自选&#xff09; 要求&#xff1a;1&#xff09;输出一幅图&#xff0c;该图包含六幅子图&#xff0c;第一排是原图&#xff0c;第 二排是对应放大…

综述:大语言模型在机器人导航中的最新进展!

简介 机器人导航是指机器人能够在环境中自主移动和定位的能力。本文系统地回顾了基于大语言模型&#xff08;LLMs&#xff09;的机器人导航研究&#xff0c;将其分为感知、规划、控制、交互和协调等方面。具体来说&#xff0c;机器人导航通常被视为一个几何映射和规划问题&…

VIVADO FIFO (同步和异步) IP 核详细使用配置步骤

VIVADO FIFO (同步和异步) IP 核详细使用配置步骤 目录 前言 一、同步FIFO的使用 1、配置 2、仿真 二、异步FIFO的使用 1、配置 2、仿真 前言 在系统设计中&#xff0c;利用FIFO&#xff08;first in first out&#xff09;进行数据处理是再普遍不过的应用了&#xff0c…

嵌入式知识点总结 C/C++ 专题提升(一)-关键字

针对于嵌入式软件杂乱的知识点总结起来&#xff0c;提供给读者学习复习对下述内容的强化。 目录 1.C语言宏中"#“和"##"的用法 1.1.(#)字符串化操作符 1.2.(##)符号连接操作符 2.关键字volatile有什么含意?并举出三个不同的例子? 2.1.并行设备的硬件寄存…

【前端动效】HTML + CSS 实现打字机效果

目录 1. 效果展示 2. 思路分析 2.1 难点 2.2 实现思路 3. 代码实现 3.1 html部分 3.2 css部分 3.3 完整代码 4. 总结 1. 效果展示 如图所示&#xff0c;这次带来的是一个有趣的“擦除”效果&#xff0c;也可以叫做打字机效果&#xff0c;其中一段文本从左到右逐渐从…

AI守护煤矿安全生产:基于视频智能的煤矿管理系统架构全解析

前言 本文我将介绍我和我的团队自主研发设计的一款AI产品的成果展示——“基于视频AI识别技术的煤矿安全生产管理系统”。 这款产品是目前我在创业阶段和几位矿业大学的博士共同从架构设计、开发到交付的全过程中首次在博客频道发布, 我之前一直想写但没有机会来整理这套系统的…

51c嵌入式~单片机~合集6

我自己的原文哦~ https://blog.51cto.com/whaosoft/13127816 一、STM32单片机的知识点总结 本文将以STM32F10x为例&#xff0c;对标准库开发进行概览。主要分为三块内容&#xff1a; STM32系统结构寄存器通过点灯案例&#xff0c;详解如何基于标准库构建STM32工程 STM3…

【Cadence tip】噪声仿真方法

1.噪声仿真设置 以五管OTA的噪声仿真为例 2.仿真结果 2.1等效输入噪声 2.2查看噪声贡献

vue3+elementPlus之后台管理系统(从0到1)(day2)

登录页面 静态页面搭建 <template><div class"login-container"><div class"form-wrap"><h2 class"header">后台管理系统</h2><el-input v-model"adminName" placeholder"请输入管理员账号&qu…

svn tag

一般发布版本前&#xff0c;需要在svn上打个tag。步骤如下&#xff1a; 1、空白处右击&#xff0c;选择TortoiseSVN->Branch/tag; 2、填写To path&#xff0c;即tag的路基以及tag命名&#xff08;一般用版本号来命名&#xff09;&#xff1b;填写tag信息&#xff1b;勾选cr…

通信协议之数据帧常用校验方法(奇偶校验、CRC校验)

前言 学习永无止境&#xff01;本篇是通信协议之数据帧校验方法&#xff0c;包括奇偶校验、CRC等校验原理及实现。 注&#xff1a;本文章为学习笔记&#xff0c;部分图片与文字来源于网络/应用手册&#xff0c;如侵权请联系&#xff01;谢谢&#xff01; 一、奇偶校验 1.1 奇偶…

[EAI-018] π0: A Vision-Language-Action Flow Model for General Robot Control

Paper Card 论文标题&#xff1a;π0: A Vision-Language-Action Flow Model for General Robot Control 论文作者&#xff1a;Kevin Black, Noah Brown, Danny Driess, Adnan Esmail, Michael Equi, Chelsea Finn, Niccolo Fusai, Lachy Groom, Karol Hausman, Brian Ichter, …

医院挂号就诊系统设计与实现(代码+数据库+LW)

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装医院挂号就诊系统软件来发挥其高效地信息处理的作用&#…

解锁辅助驾驶新境界:基于昇腾 AI 异构计算架构 CANN 的应用探秘

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《AI大模型》 期待您的关注 目录 一、引言 二、CANN 是什么 1. 异构计算与人工智能的关系 2. CANN 的定义和作用 3. CANN 的技…

【前端】CSS学习笔记

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