使用 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作为模板引擎来实现这一功能!
让我们先了解一下这个问题的背景:
- 在电缆行业,手动创建供货清单是一个复杂且重复的过程。
- 这个过程不仅耗时,还容易出错,影响工作效率和数据准确性。
为了解决这个问题,我们提出了一个技术方案,结合了以下几个关键技术:
- SpringBoot: 作为我们的主要开发框架
- Apache POI: 用于生成和操作Word文档
- FreeMarker模板引擎: 用于生成Word文件的内容
这个方案的主要优势包括:
- 灵活性: 使用FreeMarker模板可以轻松调整文档格式,而无需修改程序代码。
- 效率: 自动化生成过程大大减少了人工操作,提高了办公效率。
- 准确性: 自动化处理确保了数据的准确性和一致性。
- 适用性: 特别适合电缆行业的业务需求,生成符合要求的.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>
定义变量如totalQuantity
和subtotalQuantity
,在循环中累加数量。 - 使用
<#if>
判断条件,插入小计行,并在循环结束后插入总计行。
数据处理:
- 使用
sortedList = qdList?sort_by("model")
按型号对清单数据进行排序。 - 处理数量
itemNum = item.num?replace(",", "")?number
,移除逗号并转换为数字,确保计算正确。
格式化输出:
- 小计和总计行使用
colspan
属性合并单元格,确保表格显示整洁。 - 使用 CSS 类
subtotal
和total
为小计和总计行应用加粗和突出显示的样式。
总结:此表格通过 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表格导出,详见以下博客: Spring Boot Apache POI 实现 Exc()el 导出:BOM物料清单生成器(支持中文文件名、样式美化、数据合并&#…...
R语言的并发编程
R语言的并发编程 引言 在现代计算中,如何有效地利用计算资源进行数据处理和分析已成为一个重要的研究方向。尤其在大数据时代,数据量的急剧增加让单线程处理方式显得力不从心。为了解决这一问题,各种编程语言都开展了并发编程的研究和应用。…...
音乐播放器实现:前端HTML,CSS,JavaScript综合大项目
音乐播放器实现:前端HTML,CSS,JavaScript综合大项目 项目概述项目视图效果一、侧边栏相关代码(一)HTML代码(二)css代码 二、登录页面(一)HTML代码(二)css代码…...
PixArt--alpha笔记
PixArt-α 是华为发布的文生图模型。 训练策略分解:设计三个不同训练步骤,分别优化像素依赖、文本图像对齐和图像审美质量。高效T2I transformer:将 cross-attention融入 Diffusion Transformer (DiT)注入文本条件,简化计算密集的…...
内网渗透测试工具及渗透测试安全审计方法总结
1. 内网安全检查/渗透介绍 1.1 攻击思路 有2种思路: 攻击外网服务器,获取外网服务器的权限,接着利用入侵成功的外网服务器作为跳板,攻击内网其他服务器,最后获得敏感数据,并将数据传递到攻击者࿰…...
java工程学习步骤
1、安装idea,安装maven,mysql数据库 2、创建一个多maven的springboot的项目,可以正常启动以及可以访问web页面 3、引入日志使用slflogbck进行日志打印,同时封装统一的日志打印工具,idea安装maven analyzer工具&#…...
使用Flask和Pydantic实现参数验证
使用Flask和Pydantic实现参数验证 1 简介 Pydantic是一个用于数据验证和解析的 Python 库,版本2的性能有较大提升,很多框架使用Pydantic做数据校验。 # 官方参考文档 https://docs.pydantic.dev/latest/# Github地址 https://github.com/pydantic/pyd…...
LabVIEW 蔬菜精密播种监测系统
在当前蔬菜播种工作中,存在着诸多问题。一方面,播种精度难以达到现代农业的高标准要求,导致种子分布不均,影响作物的生长发育和最终产量;另一方面,对于小粒径种子,传统的监测手段难以实现有效监…...
统信UOS系统安装redis
1、yum引入redis yum install redis2、创建日志文件夹 mkdir -p /var/log/redis mkdir -p /var/lib/redis3、添加用户 useradd redis -s /usr/sbin/nologin4、文件夹赋权限 chown -R redis:root /var/log/redis chown redis:root /etc/redis.conf chown -R redis:root /var…...
课程如何抵御脑腐的冲击
课程需要引导学生逐步形成深度思考的能力,才有可能抵御‘Brain Rot’,否则都不会取得任何改善。 一步步失去的思维力,需要一步步引导恢复,类似康复训练。这是一个非常长期而艰辛但又十分有意义和有价值的工作。 这是一篇类似工作转…...
2.4 如何学习表示学习(Representation Learning)
如何学习表示学习(Representation Learning) 学习表示学习(Representation Learning)是理解和掌握机器学习、深度学习以及人工智能的关键一步。表示学习主要关注从原始数据中提取有效的特征或表示,使得模型能够高效处理和理解复杂的数据。以下是一些系统的方法,帮助你一…...
介绍下常用的前端框架及时优缺点
以下是一些常用的前端框架及其优缺点介绍: React • 优点 • 组件化架构:可构建可复用的UI组件,提高开发效率和组件可维护性。 • 虚拟DOM:高效更新页面,减少直接操作DOM的性能开销。 • 灵活性和可扩展性…...
Rust 猜数字游戏:从 0 到 1 的完整实现与深入解析
一、项目概述 1.1 为什么选择“猜数字”? “猜数字”是编程入门中非常经典的一个项目。它看似简单,却能很好地展示: 输入输出 (I/O):提示用户输入并读取内容。随机数:每次运行生成一个随机值,保证游戏的…...
使用C语言实现栈的插入、删除和排序操作
栈是一种后进先出(LIFO, Last In First Out)的数据结构,这意味着最后插入的元素最先被删除。在C语言中,我们可以通过数组或链表来实现栈。本文将使用数组来实现一个简单的栈,并提供插入(push)、删除(pop)以及排序(这里采用一种简单的排序方法,例如冒泡排序)的操作示…...
职场的三个阶段及其应对规划:以前端开发工程师为例
作为职场人士,特别是 IT 开发工程师,职业生涯的发展路径往往伴随着不同的挑战与机遇。一般而言,职场生涯可以划分为三个阶段:找工作阶段、有工作阶段以及职业转型阶段。每个阶段都有其特定的特征和应对策略,下面我将从…...
【人工智能】:搭建本地AI服务——Ollama、LobeChat和Go语言的全方位实践指南
前言 随着自然语言处理(NLP)技术的快速发展,越来越多的企业和个人开发者寻求在本地环境中运行大型语言模型(LLM),以确保数据隐私和提高响应速度。Ollama 作为一个强大的本地运行框架,支持多种先…...
蓝桥杯训练—斐波那契数列
文章目录 一、题目二、解析三、代码 一、题目 求100以内的斐波那契数列 斐波那契数列(Fibonacci sequence),又称黄金分割数列 ,因数学家莱昂纳多斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入&…...
TensorFlow DAY3: 高阶 API(Keras,Estimator)(完)
TensorFlow 作为深度学习框架,当然是为了帮助我们更便捷地构建神经网络。所以,本次实验将会了解如何使用 TensorFlow 来构建神经网络,并学会 TensorFlow 构建神经网络的重要函数和方法。 知识点 Keras 顺序模型Keras 函数模型Keras 模型存储…...
复健第二天之[MoeCTF 2022]baby_file
打开题目在线环境可以看到: 感觉要用伪协议去求,但是我们并不知道flag的位置,这里我选择用dirsearch去扫一下: 最像的应该就是flag.php了 于是就构建payload: **?filephp://filter/convert.base64-encode/resource…...
【QT用户登录与界面跳转】
【QT用户登录与界面跳转】 1.前言2. 项目设置3.设计登录界面3.1 login.pro参数3.2 界面设置3.2.1 登录界面3.2.2 串口主界面 4. 实现登录逻辑5.串口界面6.测试功能7.总结 1.前言 在Qt应用程序开发中,实现用户登录及界面跳转功能是构建交互式应用的重要步骤之一。下…...
【docker踩坑记录】
docker踩坑记录 踩坑记录(持续更新中.......)docker images 权限问题 踩坑记录(持续更新中…) docker images 权限问题 permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Head "http://%2Fvar%2Frun%2Fdocker.s…...
【Azure 架构师学习笔记】- Azure Function (2) --实操1
本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Function 】系列。 接上文【Azure 架构师学习笔记】- Azure Function (1) --环境搭建和背景介绍 前言 上一文介绍了环境搭建,接下来就在本地环境下使用一下。 环境准备 这里我下载了最新的VS studio&…...
豆包MarsCode:构造特定数组的逆序拼接
问题描述 思路分析 1. 数组的组成: 我们要根据 i 的不同值拼接出不同长度的子数组。对于每个 i 从 1 到 n,我们要把数字从 n 逆序到 i 拼接成一个子数组。 例如,当 i 1 时,拼接 [n, n-1, ..., 1]。当 i 2 时,拼接 …...
14-美妆数据分析
前言 美妆数据分析可以帮助企业更好地理解市场趋势、客户偏好和产品表现 import pandas as pd import numpy as np 一、数据清洗 data pd.read_csv(rC:\Users\B\Desktop\美妆数据.csv,encodinggbk) data.head()data.info()data data.drop_duplicates(inplaceFalse) data.r…...
新阿里云买服务器配置需手动配置80端口
新买阿里云服务器需手动配置80,端口才可以访问nginx CentOS系统 安装nginx 1. 安装 Nginx yum install nginx 2. 启动 Nginx 服务 systemctl start nginx 3. 修改默认网页 cd /usr/share/nginx/ echo "666" >index.html cat index.html 访问ngin最后…...
Mysql--实战篇--连接泄漏问题(什么是连接泄漏,未关闭SqlSession,长事务处理,连接池管理等)
1、什么是连接泄漏(Connection Leak)? 连接泄漏是指应用程序未能正确关闭数据库连接,导致连接池中的可用连接逐渐减少,最终耗尽所有连接。连接泄漏可能会导致新的请求无法获得连接,进而引发服务中断。 连…...
【ESP32】ESP-IDF开发 | WiFi开发 | AP模式 + 基站连接例程
1. 简介 前面一篇讲了WiFi的基站模式,演示了怎么编程连接AP,所以这一篇讲一讲AP模式,ESP32作AP,让其他的设备连接自己。 1.1 DHCP 这里需要补充一个知识点——DHCP服务器。当基站连接一个AP时,会被分配一个IP…...
Ubuntu升级Linux内核教程
本文作者CVE-柠檬i: CVE-柠檬i-CSDN博客 本文使用的方法是dpkg安装,目前版本为5.4.0-204,要升级成5.8.5版本 下载 下载网站:https://kernel.ubuntu.com/mainline/ 在该网站下载deb包,选择自己想要升级的版本,这里是5…...
关于AWS网络架构的思考
目录: AWS概述 EMR Serverless AWS VPC及其网络 关于AWS网络架构的思考 在AWS K8S中部署的业务,有不同的流量路径。 流量进入 客户端请求 普通的客户端流量流向从前到后是: 客户端公司网关(endpoint)业务的Endpoint ServiceLoad Balancers(监听80和…...
Pandas库的常用内容归纳
Pandas 是一个强大的 Python 数据分析库,提供了大量用于数据处理和分析的功能。以下是一些 Pandas 库中常用的功能: 数据创建和操作 Series 和 DataFrame:创建一维的 Series 和二维的 DataFrame 对象。数据导入:从 CSV、Excel、…...
【错误解决方案记录】spine3.8.75导出的数据使用unity-spine3.8插件解析失败报错的解决方案
报错信息 Exception: Unsupported skeleton data, please export with a newer version of Spine. Spine.SkeletonBinary.ReadSkeletonData (System.IO.Stream file) (at Assets/Spine/Runtime/spine-csharp/SkeletonBinary.cs:132) Spine.Unity.Editor.AssetUtility.AddRequi…...
Python与PyTorch的浅拷贝与深拷贝
1.Python赋值操作的原理 在python中,x something, 这样的赋值操作,准确的理解是:给存储something建立一个索引x (即存储地址), x通过访问something的存储内容,获得something的值。 在下面代码中ÿ…...
【unity进阶篇】弧度、角度和三角函数(Mathf),并实现类似蛇的运动
考虑到每个人基础可能不一样,且并不是所有人都有同时做2D、3D开发的需求,所以我把 【零基础入门unity游戏开发】 分为成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】:主要讲解C#的基础语法,包括变量、数据类型、运算符、…...
【分类】【损失函数】处理类别不平衡:CEFL 和 CEFL2 损失函数的实现与应用
引言 在深度学习中的分类问题中,类别不平衡问题是常见的挑战之一。尤其在面部表情分类任务中,不同表情类别的样本数量可能差异较大,比如“开心”表情的样本远远多于“生气”表情。面对这种情况,普通的交叉熵损失函数容易导致模型…...
支持selenium的chrome driver更新到132.0.6834.83
最近chrome释放新版本:132.0.6834.83 如果运行selenium自动化测试出现以下问题,是需要升级chromedriver才可以解决的。 selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only su…...
IO模型与NIO基础二
抽象基类之二 FilterInputStream FilterInputStream 的作用是用来“封装其它的输入流,并为它们提供额外的功能”。 它的常用的子类有BufferedInputStream和DataInputStream。 (1) BufferedInputStream的作用就是为“输入流提供缓冲功能,以及mark()和res…...
什么是FPGA开发?
FPGA(Field-Programmable Gate Array),即现场可编程门阵列,是一种通过编程方式实现特定功能的集成电路。与传统的ASIC(专用集成电路)相比,FPGA具有灵活性高、开发周期短、成本相对较低等优势&am…...
寻找川味:成都九尺板鸭
寻找川味:成都九尺板鸭 在四川这片美食的天堂里,隐藏着无数令人垂涎的传统名吃。其中,成都九尺板鸭以其悠久的历史、独特的制作工艺和令人难以抗拒的美味,成为了许多食客心中不可错过的地道川味。 历史渊源 九尺板鸭,顾名思义,因主产于四川省成都市彭州市九尺镇而得名。早在5…...
Object常用的方法及开发中的使用场景
在前端开发中,Object 对象提供了许多常用的方法,这些方法帮助我们操作对象的属性和结构。以下是常用的 Object 方法及其功能简要说明: 对象常用的方法 1. 创建对象 Object.create(proto[, propertiesObject]) 创建一个具有指定原型对象和属性…...
ElasticSearch上
安装ElasticSearch Lucene:Java语言的搜索引擎类库,易扩展;高性能(基于倒排索引)Elasticsearch基于Lucene,支持分布式,可水平扩展;提供Restful接口,可被任何语言调用Ela…...
基于SpringCloud的广告系统设计与实现(二)
一、common公共模块 1.common的作用 通用的代码、配置不应该散落在各个业务模块中,不利于维护与更新 一个大的系统,响应对象需要统一外层格式 各种业务设计与实现,可能会抛出各种各样的异常,异常信息的收集也应该做到统一 2.统…...
2011年西部数学奥林匹克的几何题
2011G1 证明: 显然, O O O, I I I, F F F 共线, E I / / M O EI//MO EI//MO, △ E I F ∼ △ M O F \triangle EIF \sim \triangle MOF △EIF∼△MOF. 进而 E F / M F r 1 / r 2 EF/MFr_1/r_2 EF/MFr1/r2. ( r 1 (r_1 (r1 为圆 I I I 的半径, r 2 r_2 r2 为…...
(一)afsim第三方库编译
注意:防止奇怪的问题,源码编译的路径最好不要有中文,请先检查各文件夹名 AFSIM版本 Version: 2.9 Plugin API Version: 11 软件环境 操作系统: Kylin V10 SP1 项目构建工具: cmake-3.26.0-linux-aarch6…...
boss直聘 __zp_stoken__ 分析
声明: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 逆向过程 py代码 import execjs imp…...
python matplotlib绘图,显示和保存没有标题栏和菜单栏的图像
目录 1. 使用plt.savefig保存无边框图形 2. 显示在屏幕上,并且去掉窗口的标题栏和工具栏 3. 通过配置 matplotlib 的 backend 和使用 Tkinter(或其他图形库) 方法 1:使用 TkAgg 后端,并禁用窗口的工具栏和标题栏 …...
AI-Talk开发板之替换唤醒词
一、说明 聆思提供的Demo唤醒词默认为"小美,小美",V2.4及之后的SDK支持替换唤醒词,聆思提供了在线工具生成唤醒词固件。 工具:tool.listenai.com/audio-custom/products/ 官方博客教程:实操给桌面AI语音助理ÿ…...
C#实现字符串反转的4种方法
见过不少人、经过不少事、也吃过不少苦,感悟世事无常、人心多变,靠着回忆将往事串珠成链,聊聊感情、谈谈发展,我慢慢写、你一点一点看...... 1、string.Reverse 方法 string content "Hello World";string reverseStri…...
【MySQL索引:B+树与页的深度解析】
文章目录 MySQL索引:B树与页的深度解析1. 索引使用的数据结构——B树1.1 B树介绍1.2 B树的特点1.3 B树和B树的对比 2. MySQL中的页2.1 页的介绍2.2 页主体2.3 页目录2.4 B树在MySQL索引中的应用 MySQL索引:B树与页的深度解析 在MySQL数据库中࿰…...
在 macOS 上,用命令行连接 MySQL(/usr/local/mysql/bin/mysql -u root -p)
根据你提供的文件内容,MySQL 的安装路径是 /usr/local/mysql。要直接使用 mysql 命令,你需要找到 mysql 可执行文件的路径。 在 macOS 上,mysql 客户端通常位于 MySQL 安装目录的 bin 子目录中。因此,完整的路径应该是࿱…...
深入HDFS——数据上传源码
引入 就如RPC篇章里提到的观点一样,任何一种能广为传播的技术,都是通过抽象和封装的思想,屏蔽底层底层复杂实现,提供简单且强大的工具,来降低使用门槛的。 HDFS的风靡自然也是如此。 通过前面深入了NameNode和DataN…...