Spring Boot 利用Word当模板,实现自动生成Word合同的功能。

前言

想像一下,我们做了一个Sass模式下的人事合同管理系统,每公司的合同都不是统一的,所以每家公司都有一个合同模板;要求实现下载员工合同的功能。

基于以上需求有两个解决方案:1.我博客中提到过《Spring Boot 手把手实现PDF功能

2.就是通过poi-tl实现word内容填充,而本文就重点介绍它的基本实现。

一、目标

两个表:一个是用户表、一个用户识别表,通过用户ID,输入word文件中包含用户的基本信息、用户的识别记录。

二、数据库表

三、POM依赖

html 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>org.example</groupId>
	<artifactId>word</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>word</name>
	<description>word</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>17</java.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <poi-tl.version>1.12.1</poi-tl.version>
        <springdoc.version>1.6.15</springdoc.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
        <!-- OpenAPI/Swagger UI -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <!-- POI-TL for Word template -->
        <dependency>
            <groupId>com.deepoove</groupId>
            <artifactId>poi-tl</artifactId>
            <version>${poi-tl.version}</version>
        </dependency>

    </dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
							<version>${lombok.version}</version>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

四、配置

4.1 YML

html 复制代码
server:
  port: 2025
spring:
  application:
      name: word
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 密码
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0
  mapper-locations: classpath*:/mapper/**/*.xml

springdoc:
  api-docs:
    enabled: true
    path: /v3/api-docs
  swagger-ui:
    enabled: true
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  packages-to-scan: org.example.word.controller
  default-consumes-media-type: application/json
  default-produces-media-type: application/json

4.2 配置类

MyBatisPlusConfig

java 复制代码
@Configuration
public class MyBatisPlusConfig {

    /**
     * MyBatis Plus Interceptor for pagination
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

OpenApiConfig

java 复制代码
@Configuration
public class OpenApiConfig {

    /**
     * 配置 OpenAPI 信息
     */
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Word 模板导入系统 API")
                        .description("基于 Spring Boot + POI-TL 的 Word 报告生成系统")
                        .version("1.0.0")
                        .contact(new Contact()
                                .name("开发团队")
                                .email("developer@example.com")
                                .url("https://example.com"))
                        .license(new License()
                                .name("Apache 2.0")
                                .url("https://www.apache.org/licenses/LICENSE-2.0.html")));
    }
}

五、实现

5.1 模板文件内容

5.2 主程序

java 复制代码
@MapperScan("org.example.word.mapper")
@SpringBootApplication
public class WordApplication {

	public static void main(String[] args) {
		SpringApplication.run(WordApplication.class, args);
	}

}

5.3 实体类

java 复制代码
**
 * User Entity
 */
@Data
@TableName("user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String username;

    private String email;

    private String phone;

    private Integer age;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    private Integer deleted;

}
java 复制代码
@Data
@TableName("user_reg")
public class UserReg implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private Long userId;

    private LocalDateTime regTime;

}

5.4 mapper

java 复制代码
@Mapper
public interface UserMapper extends BaseMapper<User> {

}
java 复制代码
@Mapper
public interface UserRegMapper extends BaseMapper<UserReg> {

}

5.5 service

java 复制代码
public interface UserService extends IService<User> {
    void generateUserReport(Long userId, HttpServletResponse response);
}
java 复制代码
package org.example.word.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.Rows;
import com.deepoove.poi.data.Tables;
import jakarta.servlet.http.HttpServletResponse;
import org.example.word.entity.User;
import org.example.word.entity.UserReg;
import org.example.word.mapper.UserMapper;
import org.example.word.service.UserRegService;
import org.example.word.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * User Service Implementation
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Autowired
    private UserRegService userRegService;

    @Autowired
    private UserMapper userMapper;

    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void generateUserReport(Long userId, HttpServletResponse response) {
        try {
            // 查询用户信息
            User user = userMapper.selectById(userId);
            // 判断用户是否存在
            if (user == null) {
                throw new RuntimeException("用户不存在,ID: " + userId);
            }
            // 查询用户识别信息
            List<UserReg> userRegs= userRegService.list(new QueryWrapper<UserReg>().
                    eq("user_id", userId).orderByDesc("reg_time"));
            Map<String, Object> data = buildReportData(user, userRegs);

            //重点代码
            // 构建模板数据
            ClassPathResource resource = new ClassPathResource("templates/user_registration_template_simple.docx");
            // 判断模板文件是否存在
            if (!resource.exists()) {
                throw new RuntimeException("Word 模板文件不存在");
            }
            XWPFTemplate template = XWPFTemplate.compile(resource.getInputStream()).render(data);
            // 设置响应内容类型为Word文档
            response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
            // 设置字符编码为UTF-8(支持中文文件名)
            response.setCharacterEncoding("UTF-8");
            // 设置下载文件名(URL编码,防止中文乱码)
            String fileName = URLEncoder.encode("用户识别报告_" + user.getUsername() + ".docx", "UTF-8");
            // 设置Content-Disposition响应头,告诉浏览器以附件形式下载文件
            response.setHeader("Content-Disposition", "attachment; filename=" + fileName);

            // 获取HTTP响应的输出流
            OutputStream out = response.getOutputStream();
            // 将生成的Word文档写入输出流
            template.write(out);
            // 刷新输出流,确保所有数据都发送到客户端
            out.flush();
            // 关闭输出流
            out.close();
            // 关闭模板对象,释放资源
            template.close();

        } catch (Exception e) {
            throw new RuntimeException("生成用户报告失败", e);
        }
    }
    private Map<String, Object> buildReportData(User user, List<UserReg> userRegs) {
        // 创建数据映射容器
        Map<String, Object> data = new HashMap<>();

        // ==================== 填充用户基本信息 ====================
        // 这些数据对应模板中的 {{username}}、{{email}} 等占位符

        // 用户名(必填)
        data.put("username", user.getUsername());

        // 邮箱(可能为空,提供默认值)
        data.put("email", user.getEmail() != null ? user.getEmail() : "未填写");

        // 电话(可能为空,提供默认值)
        data.put("phone", user.getPhone() != null ? user.getPhone() : "未填写");

        // 年龄(可能为空,需要转换为字符串)
        data.put("age", user.getAge() != null ? user.getAge().toString() : "未填写");

        // 创建时间(格式化为字符串)
        data.put("createTime", user.getCreateTime() != null ?
                user.getCreateTime().format(DATE_FORMATTER) : "未知");

        // ==================== 构建识别记录表格 ====================
        // 使用 POI-TL 的 TableRenderData 创建真实的Word表格
        // 对应模板中的 {{#regTable}} 占位符

        // 创建行数据列表(包括表头和数据行)
        java.util.List<com.deepoove.poi.data.RowRenderData> allRows = new java.util.ArrayList<>();

        // --- 第1步:创建表头行 ---
        // Rows.of(): 创建一行,参数为各列的内容
        // .center(): 设置单元格内容居中对齐
        // .bgColor("CCCCCC"): 设置背景色为灰色(十六进制颜色代码)
        // .create(): 生成 RowRenderData 对象
        allRows.add(Rows.of("序号", "用户ID", "识别时间")
                .center()           // 表头文字居中
                .bgColor("CCCCCC")  // 表头灰色背景
                .create());

        // --- 第2步:创建数据行 ---
        // 遍历所有识别记录,每条记录生成一行
        for (int i = 0; i < userRegs.size(); i++) {
            UserReg reg = userRegs.get(i);

            // 创建一行数据,包含3列:序号、用户ID、识别时间
            allRows.add(Rows.of(
                    String.valueOf(i + 1),  // 第1列:序号(从1开始)
                    String.valueOf(reg.getUserId()),  // 第2列:用户ID
                    reg.getRegTime() != null ?  // 第3列:识别时间
                            reg.getRegTime().format(DATE_FORMATTER) : "未知"
            ).create());
        }

        // --- 第3步:创建表格对象 ---
        // Tables.of(): 将所有行数据组装成表格
        // allRows.toArray(): 将List转换为数组
        // .create(): 生成 TableRenderData 对象
        com.deepoove.poi.data.TableRenderData table = Tables.of(
                allRows.toArray(new com.deepoove.poi.data.RowRenderData[0])
        ).create();

        // 将表格对象放入数据映射,键名为 "regTable"
        // 对应模板中的 {{#regTable}} 占位符
        data.put("regTable", table);

        // ==================== 填充统计信息 ====================
        // 总识别次数(识别记录的数量)
        data.put("totalRegCount", userRegs.size());

        // 报告生成时间(当前时间)
        data.put("reportTime", LocalDateTime.now().format(DATE_FORMATTER));

        // 返回完整的数据映射
        return data;
    }
}
java 复制代码
public interface UserRegService extends IService<UserReg> {

}
java 复制代码
@Service
public class UserRegServiceImpl extends ServiceImpl<UserRegMapper, UserReg> implements UserRegService {

}

5.6 conntroller

java 复制代码
@RestController
@RequestMapping("/api/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/generate/{userId}")
    public void generateReport(@PathVariable Long userId, HttpServletResponse response) {
        userService.generateUserReport(userId, response);
    }


}

六、效果展示

七、扩展:显示图片

用户表增加字段 与数据

sql 复制代码
ALTER TABLE user ADD COLUMN photo_path VARCHAR(500) COMMENT '用户照片网络路径';


UPDATE user SET photo_path = 'https://example.com/photo.jpg';

为实体类增加成员

java 复制代码
@Data
@TableName("user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String username;

    private String email;

    private String phone;

    private Integer age;

    private String photoPath;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    private Integer deleted;

}

创建模板标签:

实现核心方法代码

java 复制代码
private Map<String, Object> buildReportData(User user, List<UserReg> userRegs) {
        // 创建数据映射容器
        Map<String, Object> data = new HashMap<>();

        // ==================== 填充用户基本信息 ====================
        // 这些数据对应模板中的 {{username}}、{{email}} 等占位符

        // 用户名(必填)
        data.put("username", user.getUsername());

        // 邮箱(可能为空,提供默认值)
        data.put("email", user.getEmail() != null ? user.getEmail() : "未填写");

        // 电话(可能为空,提供默认值)
        data.put("phone", user.getPhone() != null ? user.getPhone() : "未填写");

        // 年龄(可能为空,需要转换为字符串)
        data.put("age", user.getAge() != null ? user.getAge().toString() : "未填写");

        // 创建时间(格式化为字符串)
        data.put("createTime", user.getCreateTime() != null ?
                user.getCreateTime().format(DATE_FORMATTER) : "未知");

        // ==================== 处理用户照片 ====================
        // 如果用户有照片路径,则添加照片到报告中
        if (user.getPhotoPath() != null && !user.getPhotoPath().isEmpty()) {
            try {
                // 使用 Pictures.ofUrl() 从网络路径加载图片
                // 参数:图片URL, 宽度(像素), 高度(像素)
                PictureRenderData picture = Pictures.ofUrl(user.getPhotoPath())
                        .size(120, 160)  // 设置照片尺寸:宽120px,高160px
                        .create();
                data.put("userPhoto", picture);
            } catch (Exception e) {
                // 如果加载照片失败,使用默认文本
                data.put("userPhoto", "照片加载失败");
            }
        } else {
            // 如果没有照片路径,显示默认文本
            data.put("userPhoto", "暂无照片");
        }

        // ==================== 构建识别记录表格 ====================
        // 使用 POI-TL 的 TableRenderData 创建真实的Word表格
        // 对应模板中的 {{#regTable}} 占位符

        // 创建行数据列表(包括表头和数据行)
        java.util.List<com.deepoove.poi.data.RowRenderData> allRows = new java.util.ArrayList<>();

        // --- 第1步:创建表头行 ---
        // Rows.of(): 创建一行,参数为各列的内容
        // .center(): 设置单元格内容居中对齐
        // .bgColor("CCCCCC"): 设置背景色为灰色(十六进制颜色代码)
        // .create(): 生成 RowRenderData 对象
        allRows.add(Rows.of("序号", "用户ID", "识别时间")
                .center()           // 表头文字居中
                .bgColor("CCCCCC")  // 表头灰色背景
                .create());

        // --- 第2步:创建数据行 ---
        // 遍历所有识别记录,每条记录生成一行
        for (int i = 0; i < userRegs.size(); i++) {
            UserReg reg = userRegs.get(i);

            // 创建一行数据,包含3列:序号、用户ID、识别时间
            allRows.add(Rows.of(
                    String.valueOf(i + 1),  // 第1列:序号(从1开始)
                    String.valueOf(reg.getUserId()),  // 第2列:用户ID
                    reg.getRegTime() != null ?  // 第3列:识别时间
                            reg.getRegTime().format(DATE_FORMATTER) : "未知"
            ).create());
        }

        // --- 第3步:创建表格对象 ---
        // Tables.of(): 将所有行数据组装成表格
        // allRows.toArray(): 将List转换为数组
        // .create(): 生成 TableRenderData 对象
        com.deepoove.poi.data.TableRenderData table = Tables.of(
                allRows.toArray(new com.deepoove.poi.data.RowRenderData[0])
        ).create();

        // 将表格对象放入数据映射,键名为 "regTable"
        // 对应模板中的 {{#regTable}} 占位符
        data.put("regTable", table);

        // ==================== 填充统计信息 ====================
        // 总识别次数(识别记录的数量)
        data.put("totalRegCount", userRegs.size());

        // 报告生成时间(当前时间)
        data.put("reportTime", LocalDateTime.now().format(DATE_FORMATTER));

        // 返回完整的数据映射
        return data;
    }

实现效果

八、关于模板标签说明

  • {``{变量名}} - 用于文本
  • {``{@变量名}} - 用于图片
  • {``{#变量名}} - 用于表格
  • {``{*变量名}} - 用于列表*

(全文结束,原创不易,欢迎点赞收藏)