han-note项目初始化完毕!

- 邮箱验证码接口完成
This commit is contained in:
Hanserwei
2025-09-30 15:36:31 +08:00
parent fe12d54c92
commit 765a1a7e4f
17 changed files with 632 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
package com.hanserwei.hannote.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

View File

@@ -0,0 +1,35 @@
package com.hanserwei.hannote.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class ThreadPoolConfig {
@Bean(name = "authTaskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数
executor.setCorePoolSize(10);
//最大线程数
executor.setMaxPoolSize(50);
//队列容量
executor.setQueueCapacity(200);
//活跃时间
executor.setKeepAliveSeconds(30);
//线程名前缀
executor.setThreadNamePrefix("AuthExecutor-");
//拒绝策略:由调用线程处理,一般为主线程
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 设置等待时间,如果超过这个时间还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是被没有完成的任务阻塞
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,18 @@
package com.hanserwei.hannote.auth.constant;
public class RedisKeyConstants {
/**
* 验证码 KEY 前缀
*/
private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";
/**
* 构建验证码 KEY
* @param email 手机号
* @return 验证码key
*/
public static String buildVerificationCodeKey(String email) {
return VERIFICATION_CODE_KEY_PREFIX + email;
}
}

View File

@@ -0,0 +1,26 @@
package com.hanserwei.hannote.auth.controller;
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.auth.model.vo.SendVerificationCodeReqVO;
import com.hanserwei.hannote.auth.service.VerificationCodeService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequiredArgsConstructor
public class VerificationCodeController {
private final VerificationCodeService verificationCodeService;
@PostMapping("/verification/code/send")
@ApiOperationLog(description = "发送邮件验证码")
public Response<?> send(@Validated @RequestBody SendVerificationCodeReqVO sendVerificationCodeReqVO) {
return verificationCodeService.send(sendVerificationCodeReqVO);
}
}

View File

@@ -13,6 +13,9 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
PARAM_NOT_VALID("AUTH-10001", "参数错误!!!"),
// ----------- 业务异常状态码 -----------
VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁请3分钟后再试"),
MAIL_SEND_ERROR("AUTH-20001", "邮件发送失败,请稍后再试"),
TEMPLATE_RENDER_ERROR("AUTH-20002", "模板渲染错误")
;
// 异常码

View File

@@ -0,0 +1,18 @@
package com.hanserwei.hannote.auth.model.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SendVerificationCodeReqVO {
@NotBlank(message = "邮箱不能为空")
private String email;
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.hannote.auth.service;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.auth.model.vo.SendVerificationCodeReqVO;
public interface VerificationCodeService {
/**
* 发送短信验证码
*
* @param sendVerificationCodeReqVO 发送验证码VO
* @return 返回响应
*/
Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO);
}

View File

@@ -0,0 +1,57 @@
package com.hanserwei.hannote.auth.service.impl;
import cn.hutool.core.util.RandomUtil;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.auth.constant.RedisKeyConstants;
import com.hanserwei.hannote.auth.enums.ResponseCodeEnum;
import com.hanserwei.hannote.auth.model.vo.SendVerificationCodeReqVO;
import com.hanserwei.hannote.auth.service.VerificationCodeService;
import com.hanserwei.hannote.auth.utils.MailHelper;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {
private final RedisTemplate<String, Object> redisTemplate;
private final MailHelper mailHelper;
@Resource(name = "authTaskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
/**
* 发送短信验证码!
*
* @param sendVerificationCodeReqVO 发送验证码VO
* @return 响应
*/
@Override
public Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
// 邮箱
String email = sendVerificationCodeReqVO.getEmail();
//构建Redis的Key
String codeKey = RedisKeyConstants.buildVerificationCodeKey(email);
// 判断是否发送!
Boolean hasKey = redisTemplate.hasKey(codeKey);
if (hasKey) {
//若之前发送的验证码未过期,则提示发送频繁
throw new ApiException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
}
//生成六位数随机验证码
String verificationCode = RandomUtil.randomNumbers(6);
threadPoolTaskExecutor.submit(() -> mailHelper.sendMail(verificationCode, email));
log.info("==> 邮箱: {}, 已发送验证码:【{}】", email, verificationCode);
// 存储验证码到 redis, 并设置过期时间为 3 分钟
redisTemplate.opsForValue().set(codeKey, verificationCode, 3, TimeUnit.MINUTES);
return Response.success();
}
}

View File

@@ -0,0 +1,66 @@
package com.hanserwei.hannote.auth.utils;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.hannote.auth.enums.ResponseCodeEnum;
import jakarta.annotation.Resource;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.util.Arrays;
import java.util.Date;
@Slf4j
@Component
public class MailHelper {
@Resource
private JavaMailSender mailSender;
@Resource
private TemplateEngine templateEngine;
@Value("${spring.mail.username}")
private String username;
public boolean sendMail(String verificationCode, String email) {
Context context = new Context();
context.setVariable("verifyCode", Arrays.asList(verificationCode.split("")));
String process;
try {
// 确保这里的 templateEngine 能够正确处理 context
process = templateEngine.process("EmailVerificationCode.html", context);
} catch (Exception e) {
// 处理模板渲染失败的异常
throw new ApiException(ResponseCodeEnum.TEMPLATE_RENDER_ERROR);
}
MimeMessage mimeMessage = mailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("【Han-note】验证码");
helper.setFrom(username);
helper.setTo(email);
helper.setSentDate(new Date());
helper.setText(process, true);
mailSender.send(mimeMessage); // 可能会抛出 MailException (RuntimeException)
log.info("邮件发送成功!");
} catch (MessagingException | org.springframework.mail.MailException e) {
// 捕获 MimeMessageHelper 配置异常 和 mailSender 发送异常
log.error("邮件发送失败:{}", e.getMessage());
throw new ApiException(ResponseCodeEnum.MAIL_SEND_ERROR);
}
return true;
}
}

View File

@@ -9,6 +9,29 @@ spring:
url: ${spring.datasource.url}
username: ${spring.datasource.username} # 数据库用户名
password: ${spring.datasource.password} # 数据库密码
data:
redis:
database: 5 # Redis 数据库索引(默认为 0
host: 127.0.0.1 # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: redis # Redis 服务器连接密码(默认为空)
timeout: 5s # 读超时时间
connect-timeout: 5s # 链接超时时间
lettuce:
pool:
max-active: 200 # 连接池最大连接数
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
mail:
host: smtp.qq.com
port: 587
username: 2628273921@qq.com
password: ${MAIL_PASSWORD:}
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
mail.smtp.starttls.required: true
server:
port: 8080 # 项目启动的端口
mybatis-plus:
@@ -21,3 +44,19 @@ mybatis-plus:
logging:
level:
com.hanserwei.hannote.auth.domain.mapper: debug
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: true
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: simple-uuid
# 是否输出操作日志
is-log: true

View File

@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>邮箱验证码</title>
<style>
table {
width: 700px;
margin: 0 auto;
}
#top {
width: 700px;
border-bottom: 1px solid #ccc;
margin: 0 auto 30px;
}
#top table {
font: 12px Tahoma, Arial, ;
height: 40px;
}
#content {
width: 680px;
padding: 0 10px;
margin: 0 auto;
}
#content_top {
line-height: 1.5;
font-size: 14px;
margin-bottom: 25px;
color: #4d4d4d;
}
#content_top strong {
display: block;
margin-bottom: 15px;
}
#content_top strong span {
color: #f60;
font-size: 16px;
}
#verificationCode {
color: #f60;
font-size: 24px;
}
#content_bottom {
margin-bottom: 30px;
}
#content_bottom small {
display: block;
margin-bottom: 20px;
font-size: 12px;
color: #747474;
}
#bottom {
width: 700px;
margin: 0 auto;
}
#bottom div {
padding: 10px 10px 0;
border-top: 1px solid #ccc;
color: #747474;
margin-bottom: 20px;
line-height: 1.3em;
font-size: 12px;
}
#content_top strong span {
font-size: 18px;
color: #FE4F70;
}
#sign {
text-align: right;
font-size: 18px;
color: #FE4F70;
font-weight: bold;
}
#verificationCode {
height: 100px;
width: 680px;
text-align: center;
margin: 30px 0;
}
#verificationCode div {
height: 100px;
width: 680px;
}
.button {
color: #FE4F70;
margin-left: 10px;
height: 80px;
width: 80px;
resize: none;
font-size: 42px;
border: none;
outline: none;
padding: 10px 15px;
background: #ededed;
text-align: center;
border-radius: 17px;
box-shadow: 6px 6px 12px #cccccc,
-6px -6px 12px #ffffff;
}
.button:hover {
box-shadow: inset 6px 6px 4px #d1d1d1,
inset -6px -6px 4px #ffffff;
}
</style>
</head>
<body>
<table>
<tbody>
<tr>
<td>
<div id="top">
<table>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
</div>
<div id="content">
<div id="content_top">
<strong>尊敬的用户,您好!</strong>
<strong>
您正在使用验证码校验请在5分钟内填写如下验证码如非本人操作请忽略该邮件
</strong>
<div id="verificationCode">
<button class="button" th:each="a:${verifyCode}">[[${a}]]</button>
</div>
</div>
<div id="content_bottom">
<small>
注意:此操作可能会修改您的密码、登录邮箱或绑定手机。如非本人操作,请及时登录并修改密码以保证帐户安全
<br>(工作人员不会向你索取此验证码,请勿泄漏!)
</small>
</div>
</div>
<div id="bottom">
<div>
<p>此为系统邮件,请勿回复<br>
请保管好您的邮箱,避免账号被他人盗用
</p>
<p id="sign">——Han-note官方</p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>