目录
[一. 前置准备(邮箱授权码的获取)](#一. 前置准备(邮箱授权码的获取))
[二. 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 这些东西不同,其实只要了解了一种,其它的也都是一样的,一看就会。