Spring Boot 项目实现邮件推送功能 (以QQ邮箱为例)

目录

[一. 前置准备(邮箱授权码的获取)](#一. 前置准备(邮箱授权码的获取))

[二. pom 依赖导入和 yml配置](#二. pom 依赖导入和 yml配置)

[2.1 pom 依赖导入](#2.1 pom 依赖导入)

[2.2 yml 配置文件配置](#2.2 yml 配置文件配置)

[三. 代码实现](#三. 代码实现)

[3.1 基本实现](#3.1 基本实现)

[3.2 邮件模板落表](#3.2 邮件模板落表)

[3.2 方法进一步优化](#3.2 方法进一步优化)

[3.2.1 学生用户表及实体类](#3.2.1 学生用户表及实体类)

[3.2.2 会员信息表及实体类](#3.2.2 会员信息表及实体类)

[3.2.3 查询SQL响应DTO](#3.2.3 查询SQL响应DTO)

[3.2.4 查询接口及SQL](#3.2.4 查询接口及SQL)

[3.2.5 邮件发送代码编写](#3.2.5 邮件发送代码编写)

[3.4 总结概括](#3.4 总结概括)


一. 前置准备(邮箱授权码的获取)

我们要想在项目中实现邮件推送功能,首先要获取邮箱密钥,因为我们平时发送邮件,都是直接登录邮箱书写,在实际项目中,可以类似的理解为需要配置我们的邮箱账号,因为系统需要知道用户是谁(也可以类似地理解为邮件发送人)。

用户账号:就是我们的邮箱,QQ邮箱原始位 QQ账号+qq.com,如果各位小伙伴进行了修改即为对应的用户账号;

授权码:类似于邮箱密码,授权码需要到个人邮箱设置中去生成,下面跟着小编一起看看怎么生成吧!

第一步:进入邮箱主页,点击设置

第二步:找到"账号与安全",点击进入

第三步:进入"账号与安全"页,找到"安全设置"页,往下滑就可以看到"生成授权码"啦

第四步:点击"生成授权码",进行校验,微信或手Q或短信验证都可以,验证通过就会生成个人邮箱授权码,复制授权码就可以使用到我们的实际项目中了,通常会配置在 yml 配置文件中。

(注:授权码不是唯一的,可以多次生成,所以忘记无需担心,重新生成一个就可以了)

OK,通过上述步骤,就可以获取到个人邮箱授权码了,下面就可以去做项目配置和代码编写了。

二. pom 依赖导入和 yml配置

项目的基本搭建,小编这里就不做过多说明了,随便创建一个 Spring 项目,编写一个 XXXApplication 启动类跑一下;

2.1 pom 依赖导入

想要在 Spring Boot 项目中集成邮件发送功能,需要引入邮箱依赖,如下,版本根据Spring项目会自适应,也可指定某个版本

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
2.2 yml 配置文件配置

邮箱发送相关配置只需要看 "mail" 下的即可。除了 username 和 password 更改为自己的之外,其他的可以都不动;

java 复制代码
server:
  port: 8090
  servlet:
    context-path: /wms-server
spring:
  mail:
    host: smtp.qq.com            # QQ邮箱服务器,固定填写smtp.qq.com即可
    port: 465                    # 使用SSL(端口465),使用TLS(端口587)
    username: 1872708361@qq.com  # 邮箱账号,这里配置为自己的账号
    password: vzqbwowypkvbdieh   # 邮箱密码,这里配置为授权码,就是刚才第一步生成的授权码
    default-encoding: utf-8      # 默认编码utf-8,防止乱码
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
          ssl:
            enable: true
  application:
    name: wms-server
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/wms?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&useOldAliasMetadataBehavior=true
    username: root
    password: 123456
  redis:
    host: 1.94.33.249
    port: 6379
    password: 123456
    lettuce:
      pool:
        max-active: 8   # 连接池最大连接数
        max-idle: 4     # 最大空闲连接
        min-idle: 1     # 最小空闲连接
        max-wait: 2000  # 获取连接的最大等待时间(毫秒)
logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
  level:
    root: info
    org.springframework: warn

三. 代码实现

3.1 基本实现

只是实现邮件发送,比较简单,我们直接写一个 Controller 和 Service 即可;

如下代码

java 复制代码
@RestController
@RequestMapping("/sendEmailApi")
public class SendEmailApi {
    @Autowired
    private SendEmailService sendEmailService;
    @PostMapping("/sendEmail")
    public Integer sendEmail() {
        Integer count = sendEmailService.sendEmail();
        return count;
    }
}
java 复制代码
@Slf4j
@Service
public class SendEmailService {
    // 邮件主机配置
    @Value("${spring.mail.host}")
    private String emailHost;
    // 邮件用户名,也可以理解为邮件发送人
    @Value("${spring.mail.username}")
    private String emailUser;
    // 邮件密码,这里的密码不是QQ账号密码,而且生成的邮件授权码
    @Value("${spring.mail.password}")
    private String emailPassword;

    @Autowired
    private JavaMailSender javaMailSender;

    /**
     * 发送邮件方法
     * */
    public Integer sendEmail() {
        try {
            MimeMessage mimeMessage = javaMailSender.createMimeMessage();
            // 第二个参数true表示需要上传附件/multipart消息
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage);
            // 邮件发送人
            helper.setFrom(emailUser);
            // 邮件接收人
            helper.setTo("1872708361@qq.com");
            // 邮件主题
            helper.setSubject("个人测试邮件标题");
            // 邮件内容
            helper.setText("个人测试邮件内容-----------------------------------");
            javaMailSender.send(mimeMessage);
            // 只发送一封邮件,返回1
            return 1;
        } catch (MessagingException e) {
            log.error("发送邮件失败:{}", e.getMessage());
            return 0;
        }
    }
}

编写完毕后,重新启动项目,打开 本地 Postman 或 Apifox 调用接口;

如下图,什么参数都不用传,点击发送,响应结果 "1",说明方法执行成功

然后我们再到收件箱去看一下,因为是自己发给自己嘛,所以应该能收到,如下图,说明邮件发送成功,这样就基本实现了邮件的发送功能;

OK,到这里,我们就算是实现了代码发送邮件这一基本功能了,下面我们来演示一下,类似正规项目中,我们实际推送邮件又是如何实现的。

且往下接着看!

3.2 邮件模板落表

经过上面的代码编写,我们已经实现了可以发送邮件这个基本功能,下面我们来对他做进一步的优化。

不难看出,现在我们都是采用硬编码的办法去进行赋值,但实际在项目中,我们很少这样做,基本都会存储到数据库或者配置在 application.yml 文件中去查询或读取;下面我们就以数据库为例,将邮件相关属性值全部存储到数据库中;

因为是配置嘛,所以我们就放到配置表,或者有些项目会有各种码表;

DDL,DML如下所示,邮件模板中的"username","nextYear","month","day","questionContact" 等都会在邮件发送前替换为实际参数值,并且将"问题联系人","业务邮件抄送人"也作为一个单独的配置,将来如果相关客服人员发生变动,可以随时进行修改。

实际上,这些配置,正常来讲在项目中,是需要在前端的网站页面单独开辟一个菜单页面,然后在菜单页面进行配置,这里我们主要说明后端逻辑,所以前端省略。

DDL

sql 复制代码
CREATE TABLE SYS_CODE
(
    id               INT AUTO_INCREMENT COMMENT '主键物理ID' PRIMARY KEY,
    code_type        VARCHAR(50)                             NOT NULL COMMENT '代码类型',
    code_type_name   VARCHAR(100)                            NULL     COMMENT '类型名称(中文)',
    code_type_ename  VARCHAR(100)                            NULL     COMMENT '类型描述(英文)',
    code_code        VARCHAR(2000)                           NOT NULL COMMENT '码值Code值',
    code_cname       VARCHAR(200)                            NOT NULL COMMENT '码值Value值(中文值)',
    code_ename       VARCHAR(200)                            NULL     COMMENT '码值Value值(英文值)',
    valid_status     CHAR(1)       DEFAULT '1'               NOT NULL COMMENT '是否有效(1-有效,0-失效)',
    created_by       VARCHAR(50)   DEFAULT 'system'          NOT NULL COMMENT '创建人',
    created_time     DATETIME      DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
    updated_by       VARCHAR(50)   DEFAULT 'system'          NOT NULL COMMENT '修改人',
    updated_time     DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL COMMENT '修改时间',
    parent_code_type VARCHAR(50)                             NULL     COMMENT '父级类型'
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci
  COMMENT = '系统代码表';

DML

sql 复制代码
-- 一. 会员续费邮件相关配置
-- 1.1 会员续费邮件标题
INSERT INTO SYS_CODE (code_type, code_type_name, code_type_ename, code_code, code_cname, code_ename, parent_code_type) VALUES
('vipRenewalSubject', '会员续费成功通知', 'Membership renewal email notice', 'currentYear年会员续费成功通知', 'currentYear年会员续费成功通知', 'currentYear year membership renewal success notice', null);
-- 1.2 会员续费邮件正文
INSERT INTO SYS_CODE (code_type, code_type_name, code_type_ename, code_code, code_cname, code_ename, parent_code_type) VALUES
('vipRenewalText', '会员续费邮件正文', 'Body of the membership renewal email',
 '尊敬的username,

    感谢您选择继续信任我们!您的会员已于今日成功自动续费。

    新的有效期至:expiryTime日

    在接下来的日子里,我们将继续为您提供专属权益与服务。

    立即体验:[访问网站/App的链接]

    如有任何问题,欢迎随时联系questionContact。

    感谢您的支持!',
 '', '', null);

-- 二. 会员到期提醒
-- 2.1 会员到期提醒标题
INSERT INTO SYS_CODE (code_type, code_type_name, code_type_ename, code_code, code_cname, code_ename, parent_code_type) VALUES
('vipExpireSubject', '会员到期提醒', 'Membership expiration reminder', '会员到期提醒', '会员到期提醒', 'Membership expiration reminder', null);
-- 2.2 会员到期提醒正文
INSERT INTO SYS_CODE (code_type, code_type_name, code_type_ename, code_code, code_cname, code_ename, parent_code_type) VALUES
('vipExpireText', '会员到期提醒正文', 'Member expiration reminder text',
 '尊敬的username,

    您的会员将于expiryTime到期。

    请注意及时续费,否则将失去如下会员权益
    (1)权益xxxxxxx;
    (2)权益xxxxxxx;
    (3)权益xxxxxxx;

    感谢您的支持!',
 '', '', null);

-- 三. 问题联系人配置
INSERT INTO SYS_CODE (code_type, code_type_name, code_type_ename, code_code, code_cname, code_ename, parent_code_type) VALUES
('questionContact', '问题联系人', 'Problem contact', '客服张三,电话号码135xxxxxxxx', '客服张三,电话号码135xxxxxxxx', 'Customer service Zhang San, phone number 135xxxxxxxx', null);

-- 四. 邮件固定抄送人配置
INSERT INTO SYS_CODE (code_type, code_type_name, code_type_ename, code_code, code_cname, code_ename, parent_code_type) VALUES
('emailCc', '公司业务邮件公共抄送人', 'Public CC of corporate business emails', 'zhangsan@qq.com', 'zhangsan@qq.com', 'zhangsan@qq.com', null),
('emailCc', '公司业务邮件公共抄送人', 'Public CC of corporate business emails', 'lisi@qq.com', 'lisi@qq.com', 'lisi@qq.com', null);
3.2 方法进一步优化

经过上面的各种配置升级,我们来做一个相对正式的发送邮件的功能。

需求目标:

编写一个方法,查询当前教学系统学生用户的会员是否有即将到期的(这里可以选择当天到期,也可以选择三日后到期,均可);若有,判断当前用户是否已开启自动续费功能,若开启,则自动续费,续费成功后,向用户维护的邮箱发送续费成功的消息通知;若没有开启自动续费功能,则向用户维护的邮箱发送会员即将到期的提醒邮件;

3.2.1 学生用户表及实体类
sql 复制代码
CREATE TABLE student_user (
    -- 物理主键
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '物理主键ID',
    
    -- 学生基本信息
    student_id VARCHAR(32) NOT NULL UNIQUE COMMENT '学生ID,唯一标识',
    student_name VARCHAR(64) NOT NULL COMMENT '学生姓名',
    student_age TINYINT UNSIGNED COMMENT '学生年龄',
    
    -- 联系信息
    phone VARCHAR(20) COMMENT '联系电话',
    email VARCHAR(100) COMMENT '联系邮箱',
    
    -- 身份信息
    id_card VARCHAR(18) COMMENT '身份证号',
    gender TINYINT DEFAULT 0 COMMENT '性别:0-未知,1-男,2-女',
    
    -- 学校信息
    school_name VARCHAR(100) COMMENT '学校名称',
    grade VARCHAR(20) COMMENT '年级',
    class_name VARCHAR(50) COMMENT '班级',
    
    -- 账户状态
    user_status TINYINT DEFAULT 1 COMMENT '用户状态:1-正常,2-冻结,3-注销',
    is_valid TINYINT DEFAULT 1 COMMENT '是否有效:1-有效,0-失效',
    
    -- 系统字段
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    last_login_time DATETIME COMMENT '最后登录时间',
    creator VARCHAR(32) COMMENT '创建人',
    updater VARCHAR(32) COMMENT '更新人'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生用户表';

-- 学生用户表测试数据
INSERT INTO student_user (student_id, student_name, student_age, phone, email, id_card, gender, school_name, grade, class_name, user_status, last_login_time, creator) VALUES
('STU2024001001', '张三', 12, '13812345678', 'zhangsan@student.com', '110101201201011234', 1, '北京市第一实验小学', '六年级', '六(1)班', 1, '2024-01-15 09:30:00', 'system'),
('STU2024001002', '李小红', 11, '13987654321', 'lixiaohong@student.com', '110101201301022345', 2, '北京市第一实验小学', '五年级', '五(2)班', 1, '2024-01-16 10:20:00', 'admin'),
('STU2024001003', '王明', 13, '13711112222', 'wangming@student.com', '110101201101033456', 1, '北京市第二实验小学', '初一', '七(1)班', 1, '2024-01-14 14:15:00', 'teacher_li'),
('STU2024001004', '赵雪', 12, '13633334444', 'zhaoxue@student.com', '110101201202044567', 2, '北京市第一实验小学', '六年级', '六(3)班', 2, '2024-01-10 16:45:00', 'system'),
('STU2024001005', '钱多多', 11, '13555556666', 'qianduoduo@student.com', '110101201303055678', 1, '北京市第三实验小学', '五年级', '五(1)班', 1, '2024-01-17 08:30:00', 'admin'),
('STU2024001006', '孙丽', 14, '13477778888', 'sunli@student.com', '110101201012066789', 2, '北京市第一中学', '初二', '八(2)班', 1, '2024-01-13 11:25:00', 'system'),
('STU2024001007', '周涛', 15, '13399990000', 'zhoutao@student.com', '110101200911077890', 1, '北京市第一中学', '初三', '九(1)班', 1, '2024-01-16 15:40:00', 'teacher_wang'),
('STU2024001008', '吴静', 13, '13211113333', 'wujing@student.com', '110101201108088901', 2, '北京市第二中学', '初一', '七(3)班', 3, '2024-01-09 13:20:00', 'system'),
('STU2024001009', '郑阳', 14, '13144445555', 'zhengyang@student.com', '110101201009099012', 1, '北京市第一中学', '初二', '八(1)班', 1, '2024-01-18 10:10:00', 'admin'),
('STU2024001010', '王雪梅', 15, '13066667777', 'wangxuemei@student.com', '110101200810101123', 2, '北京市第三中学', '初三', '九(2)班', 1, '2024-01-12 09:55:00', 'system');
3.2.2 会员信息表及实体类
sql 复制代码
CREATE TABLE student_member (
    -- 物理主键
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '物理主键ID',
    
    -- 学生关联信息
    student_id VARCHAR(32) NOT NULL COMMENT '学生ID',
    
    -- 会员状态信息
    member_level TINYINT DEFAULT 1 COMMENT '会员等级:1-普通会员,2-白银会员,3-黄金会员,4-钻石会员',
    member_status TINYINT DEFAULT 1 COMMENT '会员状态:1-有效,2-已过期,3-已冻结',
    
    -- 时间信息
    start_time DATETIME COMMENT '会员开始时间',
    expiry_time DATETIME NOT NULL COMMENT '会员到期时间',
    auto_renew TINYINT DEFAULT 0 COMMENT '是否自动续费:1-是,0-否',
    
    -- 续费相关信息
    renewal_count INT DEFAULT 0 COMMENT '续费次数',
    last_renewal_time DATETIME COMMENT '最后一次续费时间',
    
    -- 系统字段
    is_valid TINYINT DEFAULT 1 COMMENT '是否有效:1-有效,0-失效',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    creator VARCHAR(32) COMMENT '创建人',
    updater VARCHAR(32) COMMENT '更新人'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生会员表';

-- 学生会员表测试数据(关联上面的学生用户)
INSERT INTO student_member (student_id, member_level, member_status, start_time, expiry_time, auto_renew, renewal_count, last_renewal_time, creator) VALUES
('STU2024001001', 1, 1, '2024-01-01 00:00:00', '2025-10-29 23:59:59', 1, 2, '2024-06-01 10:30:00', 'system'),
('STU2024001002', 1, 1, '2024-02-15 00:00:00', '2024-08-14 23:59:59', 0, 0, NULL, 'admin'),
('STU2024001003', 2, 1, '2024-03-01 00:00:00', '2024-09-30 23:59:59', 1, 1, '2024-03-01 14:20:00', 'teacher_li'),
('STU2024001004', 1, 2, '2024-01-20 00:00:00', '2024-04-19 23:59:59', 0, 0, NULL, 'system'),
('STU2024001005', 3, 1, '2024-04-10 00:00:00', '2025-10-09 23:59:59', 1, 1, '2024-04-10 16:45:00', 'admin'),
('STU2024001006', 2, 1, '2024-03-15 00:00:00', '2024-09-14 23:59:59', 1, 2, '2024-06-20 11:30:00', 'system'),
('STU2024001007', 4, 1, '2024-01-05 00:00:00', '2025-12-31 23:59:59', 1, 3, '2024-06-10 10:00:00', 'teacher_wang'),
('STU2024001008', 1, 3, '2024-02-01 00:00:00', '2024-05-31 23:59:59', 0, 0, NULL, 'system'),
('STU2024001009', 2, 1, '2024-05-01 00:00:00', '2024-11-30 23:59:59', 1, 0, NULL, 'admin'),
('STU2024001010', 3, 1, '2024-04-20 00:00:00', '2025-10-19 23:59:59', 1, 1, '2024-04-20 14:30:00', 'system');
3.2.3 查询SQL响应DTO

查询当前教学系统学生用户的会员是否有即将到期的(这里可以选择当天到期,也可以选择三日后到期,均可),这里我们就使用当天。

java 复制代码
@Data
public class StudentMemberDTO {
    /** 学生ID */
    private String studentId;
    /** 学生姓名 */
    private String studentName;
    /** 学生邮箱 */
    private String email;
    /** 学生电话 */
    private String phone;
    /** 会员等级:1-普通会员,2-白银会员,3-黄金会员,4-钻石会员 */
    private Integer memberLevel;
    /** 会员状态:1-有效,2-已过期,3-已冻结 */
    private Integer memberStatus;
    /** 是否自动续费:1-是,0-否 */
    private Integer autoRenew;
    /** 会员到期时间 */
    private LocalDateTime expiryTime;
    /** 续费后的到期时间 */
    private LocalDateTime renewExpiryTime;
}
3.2.4 查询接口及SQL
sql 复制代码
public interface StudentMemberMapper extends BaseMapper<StudentMember> {
    List<StudentMemberDTO> selectExpireStudentMember();
}

-- 查询过期时间小于当晚24点的学生会员账户相关信息
    <select id="selectExpireStudentMember" resultType="com.wms.email.StudentMemberDTO">
        select
            u.student_id,u.student_name,u.email,u.phone,
            m.member_level,m.member_status,m.auto_renew,m.expiry_time
        from student_user u
            left join student_member m on u.student_id = m.student_id
        where
            u.is_valid = '1'
            and u.user_status = '1'
            and m.is_valid = '1'
            and m.member_status = '1'
            and m.expiry_time <![CDATA[ < ]]> (SELECT TIMESTAMP(DATE_ADD(CURDATE(), INTERVAL 1 DAY)) AS tomorrow_zero)
    </select>
sql 复制代码
public interface SysCodeMapper extends BaseMapper<SysCode> {
    List<SysCode> selectByCodeType(@Param("codeType") String codeType);
}
-- 根据类型查询相关码表值
    <select id="selectByCodeType" resultType="com.wms.email.SysCode">
        select * from sys_code where code_type = #{codeType}
    </select>
3.2.5 邮件发送代码编写

简单来讲,下面的方法可以分为如下思路

(1)封装邮件标题构建方法,以便于实时获取,这里其实还可以进一步优化代码,将构建邮件标题统一为一个方法,有传递的参数确认生成什么邮件标题;

(2)封装邮件正文构建方法,以便于实时获取,这里也可以进一步优化为一个方法,由传参决定生成什么邮件正文;

(3)封装邮件抄送人员生成方法;

(4)封装邮件请求参数的生成,使其独立为一个方法;

(5)封装发送邮件功能,独立为一个方法,方法参数就使用封装参数方法的响应结果;

(6)将上述各个方法功然后汇总到核心业务方法,查询续费或邮件提醒方法,由此就完成了一个相对完整的业务逻辑;

java 复制代码
@Slf4j
@Service
public class SendEmailService {
    // 邮件主机配置
    @Value("${spring.mail.host}")
    private String emailHost;
    // 邮件用户名,也可以理解为邮件发送人
    @Value("${spring.mail.username}")
    private String emailUser;
    // 邮件密码,这里的密码不是QQ账号密码,而且生成的邮件授权码
    @Value("${spring.mail.password}")
    private String emailPassword;

    @Autowired
    private JavaMailSender javaMailSender;
    @Autowired
    private StudentMemberMapper studentMemberMapper;
    @Autowired
    private SysCodeMapper sysCodeMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    // 会员续费邮件标题
    private static final String VIP_RENEWAL_SUBJECT = "vipRenewalSubject";
    // 会员续费邮件内容
    private static final String VIP_REMIND_TEXT = "vipRenewalText";
    // 会员到期邮件标题
    private static final String VIP_EXPIRE_SUBJECT = "vipExpireSubject";
    // 邮件到期邮件内容
    private static final String VIP_EXPIRE_TEXT = "vipExpireText";
    // 邮件问题联系人
    private static final String QUESTION_CONTACT = "questionContact";
    // 邮件公共抄送人
    private static final String EMAIL_CC = "emailCc";
    /**
     * 实际业务方法,查询即将到期会员并续费或提醒
     * */
    public Map<String, String> sendStudentMemberEmails() {
        Map<String, String> noticeMap = new HashMap<>();
        // 查询所有即将到期会员
        List<StudentMemberDTO> studentMemberDTOS = studentMemberMapper.selectExpireStudentMember();
        // 即将到期并开启自动续费的会员
        List<StudentMemberDTO> shouldRenewStudentMembers = new ArrayList<>();
        // 即将到期并未开启自动续费的会员
        List<StudentMemberDTO> shouldExpireStudentMembers = new ArrayList<>();
        // 区分是否自动续费,分到两个集合单独处理
        for (StudentMemberDTO studentMemberDTO : studentMemberDTOS) {
            if (studentMemberDTO.getAutoRenew() == 1){
                shouldRenewStudentMembers.add(studentMemberDTO);
            } else {
                shouldExpireStudentMembers.add(studentMemberDTO);
            }
        }
        // 对自动续费的会员进行续费,这里就涉及到额外的业务逻辑,我们姑且认为调用会员自动续费方法
        // 方法至少需要传递 学生ID,学生姓名,会员到期时间,会员等级,通过这些条件判断并进行相应续费
        if (shouldRenewStudentMembers.size() > 0){
            int successRenewNumber = 0;
            int failRenewNumber = 0;
            int successSendRenewNumber = 0;
            int failSendRenewNumber = 0;
            for (StudentMemberDTO studentMemberDTO : shouldRenewStudentMembers) {
                boolean isSuccess = StudentMemberService.autoRenewStudentMember(studentMemberDTO);
                if (isSuccess){
                    successRenewNumber++;
                    boolean sendRenewFlag = sendEmail(buildEmailRequestBody(studentMemberDTO, true));
                    if (sendRenewFlag){
                        successSendRenewNumber++;
                    } else {
                        failSendRenewNumber++;
                    }
                } else {
                    failRenewNumber++;
                }
            }
            noticeMap.put("successRenewNumber", String.valueOf(successRenewNumber));
            noticeMap.put("failRenewNumber", String.valueOf(failRenewNumber));
            noticeMap.put("successSendRenewNumber", String.valueOf(successSendRenewNumber));
            noticeMap.put("failSendRenewNumber", String.valueOf(failSendRenewNumber));
        }
        // 对未自动续费的会员进行提醒
        if (shouldExpireStudentMembers.size() > 0){
            int successExpireReminderNumber = 0;
            int failExpireReminderNumber = 0;
            for (StudentMemberDTO studentMemberDTO : shouldExpireStudentMembers) {
                boolean sendExpireFlag = sendEmail(buildEmailRequestBody(studentMemberDTO, false));
                if (sendExpireFlag){
                    successExpireReminderNumber++;
                } else {
                    failExpireReminderNumber++;
                }
            }
            noticeMap.put("successExpireReminderNumber", String.valueOf(successExpireReminderNumber));
            noticeMap.put("failExpireReminderNumber", String.valueOf(failExpireReminderNumber));
        }
        return noticeMap;
    }
    /**
     * 查询即将到期会员并续费或提醒
     * */
    public boolean sendEmail(EmailRequestBody emailRequestBody) {
        try {
            MimeMessage mimeMessage = javaMailSender.createMimeMessage();
            // 第二个参数true表示需要上传附件/multipart消息
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage);
            // 邮件标题
            helper.setSubject(emailRequestBody.getSubject());
            // 邮件内容
            helper.setText(emailRequestBody.getText());
            // 邮件发送人
            helper.setFrom(emailUser);
            // 邮件收件人,String 集合转化为字符数组
            String[] toArray = emailRequestBody.getTo().toArray(new String[0]);
            helper.setTo(toArray);
            // 邮件抄送人
            String[] ccArray = emailRequestBody.getCc().toArray(new String[0]);
            helper.setCc(ccArray);
            log.info("发送邮件请求报文:{}", mimeMessage);
            javaMailSender.send(mimeMessage);
            return true;
        } catch (MessagingException e) {
            log.error("发送邮件失败:{}", e.getMessage());
            return false;
        }
    }
    /**
     * 构建续费成功邮件标题
     * */
    private String buildRenewEmailSubject() {
        List<SysCode> sysCodes = sysCodeMapper.selectByCodeType(VIP_RENEWAL_SUBJECT);
        if (sysCodes != null && sysCodes.size() > 0){
            return sysCodes.get(0).getCodeCode();
        }
        return "";
    }
    /**
     * 构建续费成功提醒邮件内容
     * */
    private String buildRenewEmailText(String username, String renewExpiryTime) {
        List<SysCode> sysCodes = sysCodeMapper.selectByCodeType(VIP_REMIND_TEXT);
        List<SysCode> questionContacts = sysCodeMapper.selectByCodeType(QUESTION_CONTACT);
        String questionContact = "";
        if (questionContacts != null && questionContacts.size() > 0){
            questionContact = questionContacts.get(0).getCodeCode();
        }
        if (sysCodes != null && sysCodes.size() > 0){
            String renewText = sysCodes.get(0).getCodeCode();
            // 替换模板中的动态字段值
            return renewText.replace("username", username)
                            .replace("expiryTime", renewExpiryTime)
                            .replace("questionContact", questionContact);

        }
        return "";
    }
    /**
     * 构建会员到期提醒邮件标题
     * */
    private String buildExpireEmailSubject() {
        List<SysCode> sysCodes = sysCodeMapper.selectByCodeType(VIP_EXPIRE_SUBJECT);
        if (sysCodes != null && sysCodes.size() > 0){
            return sysCodes.get(0).getCodeCode();
        }
        return "";
    }
    /**
     * 构建会员到期提醒邮件内容
     * */
    private String buildExpireEmailText(String username, String expireTime) {
        // 传递一个过期时间,现在需要切割获获取小时
        String[] split = expireTime.split(" ");
        List<SysCode> sysCodes = sysCodeMapper.selectByCodeType(VIP_EXPIRE_TEXT);
        List<SysCode> questionContacts = sysCodeMapper.selectByCodeType(QUESTION_CONTACT);
        String questionContact = "";
        if (questionContacts != null && questionContacts.size() > 0){
            questionContact = questionContacts.get(0).getCodeCode();
        }
        if (sysCodes != null && sysCodes.size() > 0){
            String expireText = sysCodes.get(0).getCodeCode();
            // 替换为当年年份
            return expireText.replace("username", username)
                             .replace("expireTime", expireTime);
        }
        return "";
    }
    /**
     * 构建业务邮件公共抄送人员
     * */
    private List<String> buildEmailCCList(){
        List<SysCode> sysCodes = sysCodeMapper.selectByCodeType(EMAIL_CC);
        List<String> ccList = new ArrayList<>();
        if (sysCodes != null && sysCodes.size() > 0){
            for (SysCode sysCode : sysCodes) {
                ccList.add(sysCode.getCodeCode());
            }
        }
        return ccList;
    }
    /**
     * 构建邮件请求发送
     * */
    private EmailRequestBody buildEmailRequestBody(StudentMemberDTO studentMemberDTO, boolean isRenew) {
        EmailRequestBody emailRequestBody = new EmailRequestBody();
        if (isRenew){
            emailRequestBody.setSubject(buildRenewEmailSubject());
            emailRequestBody.setText(buildRenewEmailText(studentMemberDTO.getStudentName(), studentMemberDTO.getRenewExpiryTime().toString()));
        } else {
            emailRequestBody.setSubject(buildExpireEmailSubject());
            emailRequestBody.setText(buildExpireEmailText(studentMemberDTO.getStudentName(), studentMemberDTO.getExpiryTime().toString()));
        }
        emailRequestBody.setEmailFrom(emailUser);
        List<String> toList = new ArrayList<>();
        toList.add(studentMemberDTO.getEmail());
        emailRequestBody.setTo(toList);
        emailRequestBody.setCc(buildEmailCCList());
        // 秘密抄送和抄送本质一样,所以就不额外设置了
        // emailRequestBody.setBcc();
        return emailRequestBody;
    }
}
3.4 总结概括

总的来说,上面的业务逻辑是相对比较完整的,当然也有设计不足指出。

(1)关于邮件标题、正文等生成方法还可以进一步优化,综合成一个统一的方法,根据不同的参数 return 不同的结果;

(2)细心的小伙伴可以发现,Sys_Code 码表我们都是直接进行查询,实际生产项目,码表通常会提前读取到 Redis 缓存 ,所以在查询这一步,我们其实可以添加缓存这一步操作,进一步提高查询效率,优化运行时间,但是为了不让代码过于复杂,我们就不那么设计了,毕竟此次我们主要是为了学习如何发送邮件,简单书写一个 demo 进行个人练习;

(2)此设计样例未涉及到附件发送,例如 .docx、.xlsx、.pdf 等常见的邮件沟通时会出现的附件文件。其实也不复杂,举一反三的思考,我们也可以将附件的生成也单独定义为一个独立方法,传递学生会员相关参数,根据传参动态生成不同的附件内容 ,或每个人都需要收到的公共附件,然后对邮件参数构建方法和邮件发送方法略作调整,将附件生成和赋值相关操作添加进去即可;

(4)此次设计样例我们只涉及到后端,没有涉及前端,其实关于我们做的一些邮件相关配置,通常在前端会有一个单独的菜单页,可以在菜单页进行页面化配置,不会使用SQL去进行修改;

(5)另外一点,实际生产项目中,这种业务方法通常不是在页面去人工客服进行调用的,邮件发送功能同学们可以类比为短信发送,是一种向用户传递信息的工具。"短信发送"、"邮件推送"通常都是绑定在 XXLJOB 的定时任务上,我们会在定时任务页面进行配置,例如每天的中午12:00,每天晚上24:00;亦或者每周一,每月首个工作日进行发送,而不是通过 Controller 控制器的请求路径去调用的

(6)本篇文章主要围绕 QQ邮箱 为例进行设计,市面上还有常见的 网易邮箱、阿里邮箱 等。但其实他们没有什么太大区别,无非是 yml 文件的 host 主机,邮箱用户 username,授权码 authCode 这些东西不同,其实只要了解了一种,其它的也都是一样的,一看就会。

相关推荐
只会写代码20 小时前
Spring 项目别再乱注入 Service 了!用 Lambda 封装个统一调用组件,爽到飞起
spring boot
扣丁梦想家20 小时前
PostgreSQL 入门到精通 + Java & Spring Boot 实战教程
数据库·spring boot·postgresql
海奥华220 小时前
分库分表技术详解:从入门到实践
数据库·后端·mysql·golang
切糕师学AI20 小时前
Spring 中的 @Service 注解
java·spring
10km20 小时前
java:Apache Commons Configuration2 占位符使用详解
java·apache·占位符·configuration2·commons·interpolator
qq_4798754321 小时前
X-Macros(3)
java·开发语言
想不明白的过度思考者21 小时前
Spring Web MVC从入门到实战
java·前端·spring·mvc
Andy21 小时前
Docker 初识
java·docker·容器
SunnyDays101121 小时前
Java 高效实现 PPT 转 PDF
java·ppt转pdf
IUGEI21 小时前
【后端开发笔记】JVM底层原理-内存结构篇
java·jvm·笔记·后端