Spring Boot 集成邮件推送功能 (以QQ邮箱为例)

目录

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

二. pom 依赖导入和 yml配置

2.1 pom 依赖导入

2.2 yml 配置文件配置

三. 代码实现

3.1 基本实现

3.2 邮件模板落表

3.2 方法进一步优化

3.2.1 学生用户表及实体类

3.2.2 会员信息表及实体类

3.2.3 查询SQL响应DTO

3.2.4 查询接口及SQL

3.2.5 邮件发送代码编写

3.4 总结概括


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

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

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

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

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

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

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

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

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

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

二. pom 依赖导入和 yml配置

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

2.1 pom 依赖导入

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

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>

2.2 yml 配置文件配置

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

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 即可;

如下代码

@RestController @RequestMapping("/sendEmailApi") public class SendEmailApi { @Autowired private SendEmailService sendEmailService; @PostMapping("/sendEmail") public Integer sendEmail() { Integer count = sendEmailService.sendEmail(); return count; } }
@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

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

-- 一. 会员续费邮件相关配置 -- 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 学生用户表及实体类
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 会员信息表及实体类
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

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

@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
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>
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)将上述各个方法功然后汇总到核心业务方法,查询续费或邮件提醒方法,由此就完成了一个相对完整的业务逻辑;

@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 这些东西不同,其实只要了解了一种,其它的也都是一样的,一看就会。