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

news2025/1/19 23:44:48

在上一篇博客中主要是使用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);  // 序号是当前列表的大小 + 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 列表中
    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.coloradmin.cn/o/2279112.html

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

相关文章

二进制/源码编译安装mysql 8.0

二进制方式&#xff1a; 1.下载或上传安装包至设备&#xff1a; 2.创建组与用户&#xff1a; [rootopenEuler-1 ~]# groupadd mysql [rootopenEuler-1 ~]# useradd -r -g mysql -s /bin/false mysql 3.解压安装包&#xff1a; tar xf mysql-8.0.36-linux-glibc2.12-x86_64.ta…

VB.net实战(VSTO):解决WPS Ribbon图标灰色背景

问题&#xff1a;用VSTO制作插件&#xff0c;在MS Office中图标显示正常&#xff0c;但在WPS Office中图标显示为灰色背景 原因&#xff1a;使用的图标是纯透明背景的&#xff0c;这样的图标在WPS中会变为灰色背景。 以下这个解决办法是我自己摸索出来的&#xff0c;对您有用的…

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

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

金融项目实战 06|Python实现接口自动化——日志、实名认证和开户接口

目录 一、日志封装及应用&#xff08;理解&#xff09; 二、认证开户接口脚本编写 1、代码编写 1️⃣api目录 2️⃣script目录 2、BeautifulSoup库 1️⃣简介及例子 2️⃣提取html数据工具封装 3、认证开户参数化 一、日志封装及应用&#xff08;理解&#xff09; &…

Sqlmap入门

原理 在owasp发布的top10 漏洞里面&#xff0c;注入漏洞一直是危害排名第一&#xff0c;其中数据库注入漏洞是危害的。 当攻击者发送的sql语句被sql解释器执行&#xff0c;通过执行这些恶意语句欺骗数据库执行&#xff0c;导致数据库信息泄漏 分类 按注入类型 常见的sql注入…

Maven在Win10上的安装教程

诸神缄默不语-个人CSDN博文目录 这个文件可以跟我要&#xff0c;也可以从官网下载&#xff1a; 第一步&#xff1a;解压文件 第二步&#xff1a;设置环境变量 在系统变量处点击新建&#xff0c;输入变量名MAVEN_HOME&#xff0c;变量值为解压路径&#xff1a; 在系统变…

C# OpenCvSharp 部署3D人脸重建3DDFA-V3

目录 说明 效果 模型信息 landmark.onnx net_recon.onnx net_recon_mbnet.onnx retinaface_resnet50.onnx 项目 代码 下载 参考 C# OpenCvSharp 部署3D人脸重建3DDFA-V3 说明 地址&#xff1a;https://github.com/wang-zidu/3DDFA-V3 3DDFA_V3 uses the geometri…

SpringMVC (2)

目录 1. RequestMapping 注解介绍 2. RequestMapping 使用 3. RequestMapping与请求方式 3.1 RequestMapping 支持Get和Post类型的请求 3.2 RequestMapping 指定接收某种请求 3.3 GetMapping和PostMapping 4. 传参 4.1 通过查询字符串传参 4.2 在 Body 中传参 4.2.1 …

Python爬虫学习前传 —— Python从安装到学会一站式服务

早上好啊&#xff0c;大佬们。我们的python基础内容的这一篇终于写好了&#xff0c;啪唧啪唧啪唧…… 说实话&#xff0c;这一篇确实写了很久&#xff0c;一方面是在忙其他几个专栏的内容&#xff0c;再加上生活学业上的事儿&#xff0c;确实精力有限&#xff0c;另一方面&…

力扣 打家劫舍

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

Banana Pi BPI-RV2 RISC-V路由开发板采用矽昌通信SF2H8898芯片

Banana Pi BPI-RV2 开源网关是⼀款基于矽昌SF2H8898 SoC的设备&#xff0c;1 2.5 G WAN⽹络接⼝、5 个千兆LAN ⽹络接⼝、板载 512MB DDR3 内存 、128 MiB NAND、16 MiB NOR、M.2接⼝&#xff0c;MINI PCIE和USB 2.0接⼝等。 Banana Pi BPI-RV2 开源网关是矽昌和⾹蕉派开源社…

ESP8266-01S、手机、STM32连接

1、ESP8266-01S的工作原理 1.1、AP和STA ESP8266-01S为WIFI的透传模块&#xff0c;主要模式如下图&#xff1a; 上节说到&#xff0c;我们需要用到AT固件进行局域网应用&#xff08;ESP8266连接的STM32和手机进行连接&#xff09;。 ESP8266为一个WiFi透传模块&#xff0c;和…

【BUUCTF】[GXYCTF2019]BabySQli

进入页面如下 尝试万能密码注入 显示这个&#xff08;qyq&#xff09; 用burp suite抓包试试 发现注释处是某种编码像是base编码格式 MMZFM422K5HDASKDN5TVU3SKOZRFGQRRMMZFM6KJJBSG6WSYJJWESSCWPJNFQSTVLFLTC3CJIQYGOSTZKJ2VSVZRNRFHOPJ5 可以使用下面这个网页在线工具很方便…

CSS基础知识 ----- 学习

介绍 CSS&#xff08;Cascading Style Sheets&#xff09;是一种用于定义网页样式和布局的样式表语言。 它与 HTML 一起用于构建 Web 页面&#xff0c;HTML 负责定义页面的结构和内容&#xff0c;而 CSS 则负责控制页面的外观和样式(各种元素的字体,颜色,大小,间距,边距)。 …

计算机网络 (50)两类密码体制

前言 计算机网络中的两类密码体制主要包括对称密钥密码体制&#xff08;也称为私钥密码体制、对称密码体制&#xff09;和公钥密码体制&#xff08;也称为非对称密码体制、公开密钥加密技术&#xff09;。 一、对称密钥密码体制 定义&#xff1a; 对称密钥密码体制是一种传…

Asp.Net Core 8.0 使用 Serilog 按日志级别写入日志文件的两种方式

1、所需的Nuget包 本文项目的版本是.NET 8.0&#xff0c;如果使用其它版本安装适配版本即可。 Serilog.AspNetCore(8.0.2) Serilog.Sinks.File(5.0.0) Serilog.Expressions(5.0.0) 2、两种配置方式 2.1 代码形式&#xff08;Program.cs&#xff09; 在Program.cs文件中&am…

华为HuaweiCloudStack(一)介绍与架构

本文简单介绍了华为HCS私有云解决方案&#xff0c;并从下至上介绍HCS的整体架构&#xff0c;部署架构、部署方式等内容。 目录 HCS简介 HCS架构 纵向结构 ?管理平台类型 HCS节点类型 FusionSphere OpenStack CPS ServiceOM SC 运营面 OC 运维面 HCS部署架构 regi…

彻底讲清楚 单体架构、集群架构、分布式架构及扩展架构

目录 什么是系统架构 单体架构 介绍 示例图 优点 缺点 集群架构 介绍 示意图 优点 缺点 分布式架构 示意图 优点 缺点 生态扩展 介绍 示意图 优点 缺点 扩展&#xff1a;分布式服务解析 纵切拆服务 全链路追踪能力 循环依赖 全链路日志&#xff08;En…

flutter 常用UI组件

文章目录 1. Toast 文本提示框oktoastbot_toast2. loading 加载窗flutter_easyloading3. 对话框gex dialog4.下拉刷新pull_to_refresh5. pop 窗custom_pop_up_menu6. pin code 密码框pinput7. 二维码qr_flutter8. swiper 滚动组件carousel_sliderflutter_swiper_view9. Badge 角…

【王树森搜索引擎技术】概要01:搜索引擎的基本概念

1. 基本名词 query&#xff1a;查询词SUG&#xff1a;搜索建议文档&#xff1a;搜索结果标签/筛选项 文档单列曝光 文档双列曝光 2. 曝光与点击 曝光&#xff1a;用户在搜索结果页上看到文档&#xff0c;就算曝光文档点击&#xff1a;在曝光后&#xff0c;用户点击文档&…