使用 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.mzph.cn/pingmian/67553.shtml

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

相关文章

探索与创作:2024年CSDN平台上的成长与突破

文章目录 我与CSDN的初次邂逅初学阶段的阅读CSDN&#xff1a;编程新手的避风港初学者的福音&#xff1a;细致入微的知识讲解考试复习神器&#xff1a;技术总结的“救命指南”曾经的自己&#xff1a;为何迟迟不迈出写博客的第一步兴趣萌芽&#xff1a;从“读”到“想写”的初体验…

SSM课设-学生管理系统

【课设者】SSM课设-学生管理系统 技术栈: 后端: SpringSpringMVCMybatisMySQLJSP 前端: HtmlCssJavaScriptEasyUIAjax 功能: 学生端: 登陆 学生信息管理 个人信息管理 老师端: 多了教师信息管理 管理员端: 多了班级信息管理 多了年级信息管理 多了系统用户管理

力扣 打家劫舍

动态规划&#xff0c;当前状态由前两个状态获得&#xff0c;滚动数组。 题目 从题可以看出要达到最高金额时&#xff0c;要从相邻的房屋拿。因此是当前房屋的金额隔一个做累加&#xff0c;当然还需要跟前一个相邻的房屋做比较&#xff0c;便于取到哪边金额更高&#xff0c;因此…

【Django开发】django美多商城项目完整开发4.0第12篇:商品部分,表结构【附代码文档】

本教程的知识点为&#xff1a; 项目准备 项目准备 配置 1. 修改settings/dev.py 文件中的路径信息 2. INSTALLED_APPS 3. 数据库 用户部分 图片 1. 后端接口设计&#xff1a; 视图原型 2. 具体视图实现 用户部分 使用Celery完成发送 判断帐号是否存在 1. 判断用户名是否存在 后…

Redis的安装和使用--Windows系统

Redis下载地址&#xff1a; windows版本readis下载&#xff08;GitHub&#xff09;&#xff1a; https://github.com/tporadowski/redis/releases &#xff08;推荐使用&#xff09; https://github.com/MicrosoftArchive/redis/releases 官网下载&#xff08;无Windows版本…

Linux操作命令之云计算基础命令

一、图形化界面/文本模式 ctrlaltF2-6 图形切换到文本 ctrlalt 鼠标跳出虚拟机 ctrlaltF1 文本切换到图形 shift ctrl "" 扩大 ctrl "-" 缩小 shift ctrl "n" 新终端 shift ctrl "t" 新标签 alt 1,…

LabVIEW桥接传感器配置与数据采集

该LabVIEW程序主要用于配置桥接传感器并进行数据采集&#xff0c;涉及电压激励、桥接电阻、采样设置及错误处理。第一个VI&#xff08;"Auto Cleanup"&#xff09;用于自动清理资源&#xff0c;建议保留以确保系统稳定运行。 以下是对图像中各个组件的详细解释&#…

网络编程 | UDP广播通信

1、什么是广播 在上一篇博客文章中已经对UDP进行了详细的说明介绍及如何编程实现。本文将接着上一文的内容&#xff0c;在其基础上&#xff0c;对UDP的知识体系进一步深入的讲解。 网络编程 | UDP套接字通信及编程实现经验教程-CSDN博客 例子&#xff1a;在一些中小学的操场中&…

Count Sketch--计数草图

背景 Count Sketch 是一种空间高效的概率型数据结构&#xff0c;由 Moses Charikar、Kevin Chen 和 Martin Farach-Colton 在 2002 年提出&#xff0c;用于估计数据流中元素的频率&#xff0c;也可用于解决重击者问题。 原理 算法结构 参数设定&#xff1a;Count Sketch算法…

2025.1.17——三、SQLi regexp正则表达式|

题目来源&#xff1a;buuctf [NCTF2019]SQLi1 目录 一、打开靶机&#xff0c;整理信息 二、解题思路 step 1&#xff1a;正常注入 step 2&#xff1a;弄清关键字黑名单 1.目录扫描 2.bp爆破 step 3&#xff1a;根据过滤名单构造payload step 4&#xff1a;regexp正则注…

搭建一个基于Spring Boot的书籍学习平台

搭建一个基于Spring Boot的书籍学习平台可以涵盖多个功能模块&#xff0c;例如用户管理、书籍管理、学习进度跟踪、笔记管理、评论和评分等。以下是一个简化的步骤指南&#xff0c;帮助你快速搭建一个基础的书籍学习平台。 — 1. 项目初始化 使用 Spring Initializr 生成一个…

【Linux 之一 】Linux常用命令汇总

Linux常用命令 ./catcd 命令chmodclearcphistoryhtoplnmkdirmvpwdrmtailunamewcwhoami 我从2021年4月份开始才开始真正意义上接触Linux&#xff0c;最初学习时是一脸蒙圈&#xff0c;啥也不会&#xff0c;啥也不懂&#xff0c;做了很多乱七八糟&#xff0c;没有条理的笔记。不知…

Hexo + NexT + Github搭建个人博客

文章目录 一、 安装二、配置相关项NexT config更新主题主题样式本地实时预览常用命令 三、主题设置1.侧边栏2.页脚3.帖子发布字数统计 4.自定义自定义页面Hexo 的默认页面自定义 404 页自定义样式 5.杂项搜索服务 四、第三方插件NexT 自带插件评论系统阅读和访问人数统计 五、部…

开发神器之cursor

文章目录 cursor简介主要特点 下载cursor页面的简单介绍切换大模型指定ai学习的文件指定特定的代码喂给ai创建项目框架文件 cursor简介 Cursor 是一款专为开发者设计的智能代码编辑器&#xff0c;集成了先进的 AI 技术&#xff0c;旨在提升编程效率。以下是其主要特点和功能&a…

当前目录不是一个git仓库/远程仓库已经有了一些你本地没有的更改

目录 问题1&#xff1a;问题2&#xff1a;解决1解决2 问题1&#xff1a; fatal: not a git repository (or any parent up to mount point /) Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set). # 初始化 Git 仓库 git init需要到本地目录下先添加…

差异基因富集分析(R语言——GOKEGGGSEA)

接着上次的内容&#xff0c;上篇内容给大家分享了基因表达量怎么做分组差异分析&#xff0c;从而获得差异基因集&#xff0c;想了解的可以去看一下&#xff0c;这篇主要给大家分享一下得到显著差异基因集后怎么做一下通路富集。 1.准备差异基因集 我就直接把上次分享的拿到这…

BGP边界网关协议(Border Gateway Protocol)路由引入、路由反射器

一、路由引入背景 BGP协议本身不发现路由&#xff0c;因此需要将其他协议路由&#xff08;如IGP路由等&#xff09;引入到BGP路由表中&#xff0c;从而将这些路由在AS之内和AS之间传播。 BGP协议支持通过以下两种方式引入路由&#xff1a; Import方式&#xff1a;按协议类型将…

使用FFmpeg和Python将短视频转换为GIF的使用指南

使用FFmpeg和Python将短视频转换为GIF的使用指南 在数字时代&#xff0c;GIF动图已成为表达情感和分享幽默的重要媒介。无论是社交媒体上的搞笑片段还是创意项目中的视觉效果&#xff0c;GIF都能迅速抓住观众的注意力。然而&#xff0c;很多人不知道如何将短视频转换为GIF。本…

LLM - 大模型 ScallingLaws 的迁移学习与混合训练(PLM) 教程(3)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/145212097 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 Scalin…

解决leetcode第3418题机器人可以获得的最大金币数

3418.机器人可以获得的最大金币数 难度&#xff1a;中等 问题描述&#xff1a; 给你一个mxn的网格。一个机器人从网格的左上角(0,0)出发&#xff0c;目标是到达网格的右下角(m-1,n-1)。在任意时刻&#xff0c;机器人只能向右或向下移动。 网格中的每个单元格包含一个值coin…