diff --git a/pom.xml b/pom.xml index 0226635..b56b669 100644 --- a/pom.xml +++ b/pom.xml @@ -185,6 +185,12 @@ ${springdoc-openapi.version} + + + org.springframework.boot + spring-boot-starter-mail + + com.mysql mysql-connector-j diff --git a/src/main/java/io/github/xxyopen/novel/core/config/MailProperties.java b/src/main/java/io/github/xxyopen/novel/core/config/MailProperties.java new file mode 100644 index 0000000..1849903 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/config/MailProperties.java @@ -0,0 +1,14 @@ +package io.github.xxyopen.novel.core.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * mail 配置属性 + * + * @author xiongxiaoyang + * @date 2023/3/25 + */ +@ConfigurationProperties(prefix = "spring.mail") +public record MailProperties(String nickname, String username) { + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/constant/MessageSenderTypeConsts.java b/src/main/java/io/github/xxyopen/novel/core/constant/MessageSenderTypeConsts.java new file mode 100644 index 0000000..8cbc25f --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/constant/MessageSenderTypeConsts.java @@ -0,0 +1,25 @@ +package io.github.xxyopen.novel.core.constant; + +/** + * 消息发送器的类型 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +public class MessageSenderTypeConsts { + + private MessageSenderTypeConsts() { + throw new IllegalStateException("Constant class"); + } + + /** + * 注册成功的邮件发送器 + */ + public static final String REGISTER_MAIL_SENDER = "registerMailSender"; + + /** + * 秒杀活动的系统通知发送器 + */ + public static final String SECKILL_SYS_NOTICE_SENDER = "seckillSysNoticeSender"; + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMailSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMailSender.java new file mode 100644 index 0000000..10d64e6 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMailSender.java @@ -0,0 +1,50 @@ +package io.github.xxyopen.novel.manager.message; + +import io.github.xxyopen.novel.core.config.MailProperties; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; + +/** + * 抽象的邮件消息发送者 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractMailSender extends AbstractMessageSender { + + private final MailProperties mailProperties; + + private final JavaMailSender mailSender; + + @Override + protected void sendMessage(Long toUserId, String messageTitle, String messageContent) { + // TODO 根据消息接收方的用户ID查询出消息接收方的邮件地址 + String toEmail = "xxyopen@foxmail.com"; + // 开始发送邮件 + log.info("发送 HTML 邮件开始:{},{},{}", toEmail, messageTitle, messageContent); + // 使用 MimeMessage,MIME 协议 + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper; + // MimeMessageHelper 帮助我们设置更丰富的内容 + try { + helper = new MimeMessageHelper(message, true); + helper.setFrom(new InternetAddress(mailProperties.username(), mailProperties.nickname(), "UTF-8")); + helper.setTo(toEmail); + helper.setSubject(messageTitle); + // 第二个参数 true 代表支持 html + helper.setText(messageContent, true); + mailSender.send(message); + log.info("发送 HTML 邮件 to {} 成功", toEmail); + } catch (Exception e) { + // 邮件发送失败不会重试 + log.error("发送 HTML 邮件 to {} 失败", toEmail, e); + } + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMessageSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMessageSender.java new file mode 100644 index 0000000..3559779 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMessageSender.java @@ -0,0 +1,92 @@ +package io.github.xxyopen.novel.manager.message; + +/** + * 抽象的消息发送器 + *

+ * 遵循松耦合的设计原则,所有的属性都使用构造函数注入,与 Spring 框架解藕 + *

+ * 所有的消息发送器既可以注册到 Spring 容器中,作为 Spring 的一个组件使用,也可以直接通过 new 对象的方式使用 + *

+ * 每种类型的消息发送时机可能都不一样,不同类型和发送时机的消息格式可能也不一样,所以由各个子类去拓展消息的格式 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +public abstract class AbstractMessageSender implements MessageSender { + + private static final String PLACEHOLDER = "{}"; + + /** + * 定义消息发送的模版,子类不能修改此模版 + */ + @Override + public final void sendMessage(Long toUserId, Object... args) { + // 1.获取消息标题模版 + String titleTemplate = getTitleTemplate(); + // 2.获取消息内容模版 + String contentTemplate = getContentTemplate(); + // 3.解析消息模版,得到最终需要发送的消息标题 + String title = resolveTitle(titleTemplate, args); + // 4.解析消息内容,得到最终需要发送的消息内容 + String content = resolveContent(contentTemplate, args); + // 5.发送消息 + sendMessage(toUserId, title, content); + } + + /** + * 发送消息,具体发送到哪里由子类决定 + * + * @param toUserId 消息接收方的用户ID + * @param messageTitle 消息标题 + * @param messageContent 消息内容 + */ + protected abstract void sendMessage(Long toUserId, String messageTitle, String messageContent); + + /** + * 获取消息标题的模版,具体如何制定模版由子类决定 + * + * @return 消息标题 + */ + protected abstract String getTitleTemplate(); + + /** + * 获取消息内容的模版,具体如何制定模版由子类决定 + * + * @return 消息内容 + */ + protected abstract String getContentTemplate(); + + /** + * 通过给定的参数列表解析消息标题模版,默认固定标题,不需要解析,可以由子类来拓展它的功能 + * + * @param titleTemplate 消息标题模版 + * @param arguments 用来解析的参数列表 + * @return 解析后的消息标题 + */ + protected String resolveTitle(String titleTemplate, Object... arguments) { + return titleTemplate; + } + + /** + * 通过给定的参数列表解析消息内容模版,默认实现是使用参数列表来替换消息内容模版中的占位符,可以由子类来拓展它的功能 + *

+ * 子类可以根据第一个/前几个参数去数据库中查询动态内容,然后重组参数列表 + * + * @param contentTemplate 消息内容模版 + * @param args 用来解析的参数列表 + * @return 解析后的消息内容 + */ + protected String resolveContent(String contentTemplate, Object... args) { + if (args.length > 0) { + StringBuilder formattedContent = new StringBuilder(contentTemplate); + for (Object arg : args) { + int start = formattedContent.indexOf(PLACEHOLDER); + formattedContent.replace(start, start + PLACEHOLDER.length(), + String.valueOf(arg)); + } + return formattedContent.toString(); + } + return contentTemplate; + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/AbstractSysNoticeSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractSysNoticeSender.java new file mode 100644 index 0000000..c457f25 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractSysNoticeSender.java @@ -0,0 +1,26 @@ +package io.github.xxyopen.novel.manager.message; + +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 抽象的系统通知发送者 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +@Slf4j +public abstract class AbstractSysNoticeSender extends AbstractMessageSender { + + @Override + protected void sendMessage(Long toUserId, String messageTitle, String messageContent) { + // 生成消息的发送时间 + LocalDateTime messageDateTime = LocalDateTime.now(); + // TODO 在数据库系统通知表中插入一条记录 + log.info("系统通知发送成功,{},{},{},{}", toUserId, messageDateTime.format(DateTimeFormatter.ISO_DATE_TIME), + messageTitle, messageContent); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/MessageSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/MessageSender.java new file mode 100644 index 0000000..ca109fa --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/MessageSender.java @@ -0,0 +1,21 @@ +package io.github.xxyopen.novel.manager.message; + +/** + * 消息发送器接口,用来发送各种消息 + *

+ * 消息按类型分系统通知、邮件、短信、小程序通知等,按发送时机分注册成功消息、充值成功消息、活动通知消息、账户封禁消息、小说下架消息等 + * + * @author xiongxiaoyang + * @date 2023/3/25 + */ +public interface MessageSender { + + /** + * 发送消息,支持动态消息标题和动态消息内容 + * + * @param toUserId 消息接收方的用户ID + * @param args 用来动态生成消息标题和消息内容的参数列表 + */ + void sendMessage(Long toUserId, Object... args); + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/RegisterMailSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/RegisterMailSender.java new file mode 100644 index 0000000..712ff85 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/RegisterMailSender.java @@ -0,0 +1,60 @@ +package io.github.xxyopen.novel.manager.message; + +import io.github.xxyopen.novel.core.config.MailProperties; +import io.github.xxyopen.novel.core.constant.MessageSenderTypeConsts; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * 注册成功的邮件发送器 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +@Component(value = MessageSenderTypeConsts.REGISTER_MAIL_SENDER) +@EnableConfigurationProperties(MailProperties.class) +public class RegisterMailSender extends AbstractMailSender { + + public RegisterMailSender(MailProperties mailProperties, JavaMailSender mailSender) { + super(mailProperties, mailSender); + } + + @Override + protected String getTitleTemplate() { + return "欢迎来到小说精品屋"; + } + + @Override + protected String getContentTemplate() { + return """ +

+ 感谢你注册小说精品屋!你的账户现在处于活动状态。 +
+ +
+ + 登录我们的网站 + +
+ + 如果你有任何问题,请通过 {} 与我们联系。 + """; + } + + @Override + protected String resolveContent(String content, Object... args) { + // TODO 去数据库/配置文件中查询网站配置 + String websiteLink = "https://www.xxyopen.com"; + String websiteEmail = "xxyopen@foxmail.com"; + return super.resolveContent(content, + Stream.of(args, new Object[]{websiteLink, websiteEmail}).flatMap(Arrays::stream).toArray()); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/SeckillSystemNoticeSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/SeckillSystemNoticeSender.java new file mode 100644 index 0000000..5a7fb4a --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/SeckillSystemNoticeSender.java @@ -0,0 +1,25 @@ +package io.github.xxyopen.novel.manager.message; + +import io.github.xxyopen.novel.core.constant.MessageSenderTypeConsts; +import org.springframework.stereotype.Component; + +/** + * 秒杀活动的系统通知发送器 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +@Component(value = MessageSenderTypeConsts.SECKILL_SYS_NOTICE_SENDER) +public class SeckillSystemNoticeSender extends AbstractSysNoticeSender { + + @Override + protected String getTitleTemplate() { + return "秒杀即将开始"; + } + + @Override + protected String getContentTemplate() { + return "{}秒杀,{}即将开始,不要错过哦!点击 {} 前往。"; + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 69d9e2c..15788be 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -189,6 +189,32 @@ springdoc: api-docs: enabled: false +--- #----------------------邮箱配置----------------------------- +#邮箱服务器 +spring: + mail: + host: smtp.163.com + #发件人昵称 + nickname: xxyopen + #邮箱账户 + username: xxx@163.com + #邮箱第三方授权码 + password: xxx + #编码类型 + default-encoding: UTF-8 + port: 465 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: rue + socketFactory: + port: 465 + class: javax.net.ssl.SSLSocketFactory + fallback: false + --- #---------------------自定义配置---------------------------- novel: # 跨域配置