feat: 实现邮箱验证码和邮箱绑定功能
- 添加邮件发送服务实现(EmailService/EmailServiceImpl) - 新增发送验证码、绑定邮箱、解绑邮箱接口 - 用户实体新增邮箱相关字段(userEmail/userEmailVerified) - 添加邮件配置和JavaMailSender Bean - 放行邮箱验证码接口(/v1/user/email/send-code) - 新增ContextHolderUtils工具类用于获取当前用户上下文 - 完善Swagger文档注解 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2
.idea/CoolRequestCommonStatePersistent.xml
generated
2
.idea/CoolRequestCommonStatePersistent.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CoolRequestCommonStatePersistent">
|
<component name="CoolRequestCommonStatePersistent">
|
||||||
<option name="searchCache" value="UserRegisterRequestDTO" />
|
<option name="searchCache" value="JwtAuthenticationFilter" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -27,7 +27,7 @@ public class SecurityConfiguration {
|
|||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/v1/auth/**", "/oauth2/**", "/.well-known/**", "/doc.html", "/swagger-ui/**",
|
.requestMatchers("/v1/auth/**", "/oauth2/**", "/.well-known/**", "/doc.html", "/swagger-ui/**",
|
||||||
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico")
|
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico","/v1/user/email/send-code")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated())
|
.authenticated())
|
||||||
|
|||||||
@@ -67,5 +67,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Spring Security (optional) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package cn.meowrain.aioj.backend.framework.core.utils;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security Context 工具类
|
||||||
|
* 用于获取当前登录用户信息
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@UtilityClass
|
||||||
|
public class ContextHolderUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户ID
|
||||||
|
* @return 用户ID
|
||||||
|
* @throws IllegalStateException 如果用户未登录
|
||||||
|
*/
|
||||||
|
public static Long getCurrentUserId() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
throw new IllegalStateException("用户未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = authentication.getName();
|
||||||
|
try {
|
||||||
|
return Long.valueOf(userId);
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e) {
|
||||||
|
log.error("解析用户ID失败: {}", userId, e);
|
||||||
|
throw new IllegalStateException("无效的用户ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户ID(可选返回)
|
||||||
|
* @return 用户ID,未登录返回 null
|
||||||
|
*/
|
||||||
|
public static Long getCurrentUserIdOrNull() {
|
||||||
|
try {
|
||||||
|
return getCurrentUserId();
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户角色
|
||||||
|
* @return 用户角色,如 "USER", "ADMIN", "BAN"
|
||||||
|
*/
|
||||||
|
public static String getCurrentUserRole() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
throw new IllegalStateException("用户未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
return authentication.getAuthorities()
|
||||||
|
.stream()
|
||||||
|
.findFirst()
|
||||||
|
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
|
||||||
|
.orElse("USER");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否已登录
|
||||||
|
* @return true-已登录, false-未登录
|
||||||
|
*/
|
||||||
|
public static boolean isAuthenticated() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
return authentication != null && authentication.isAuthenticated()
|
||||||
|
&& !"anonymousUser".equals(authentication.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否为管理员
|
||||||
|
* @return true-是管理员, false-不是管理员
|
||||||
|
*/
|
||||||
|
public static boolean isAdmin() {
|
||||||
|
return "ADMIN".equals(getCurrentUserRole());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,6 +18,11 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!--引入spring boot email-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-mail</artifactId>
|
||||||
|
</dependency>
|
||||||
<!-- SpringDoc OpenAPI - 显式指定版本以兼容 Spring Boot 3.5.x -->
|
<!-- SpringDoc OpenAPI - 显式指定版本以兼容 Spring Boot 3.5.x -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
|
|||||||
@@ -1,8 +1,44 @@
|
|||||||
package cn.meowrain.aioj.backend.userservice.config;
|
package cn.meowrain.aioj.backend.userservice.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class FrameworkConfiguration {
|
public class FrameworkConfiguration {
|
||||||
|
|
||||||
|
@Value("${spring.mail.host}")
|
||||||
|
private String mailHost;
|
||||||
|
|
||||||
|
@Value("${spring.mail.port}")
|
||||||
|
private Integer mailPort;
|
||||||
|
|
||||||
|
@Value("${spring.mail.username}")
|
||||||
|
private String mailUsername;
|
||||||
|
|
||||||
|
@Value("${spring.mail.password}")
|
||||||
|
private String mailPassword;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JavaMailSender javaMailSender() {
|
||||||
|
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
|
||||||
|
mailSender.setHost(mailHost);
|
||||||
|
mailSender.setPort(mailPort);
|
||||||
|
mailSender.setUsername(mailUsername);
|
||||||
|
mailSender.setPassword(mailPassword);
|
||||||
|
|
||||||
|
Properties props = mailSender.getJavaMailProperties();
|
||||||
|
props.put("mail.transport.protocol", "smtp");
|
||||||
|
props.put("mail.smtp.auth", "true");
|
||||||
|
props.put("mail.smtp.ssl.enable", "true");
|
||||||
|
props.put("mail.smtp.starttls.enable", "true");
|
||||||
|
props.put("mail.debug", "false");
|
||||||
|
|
||||||
|
return mailSender;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,76 @@ package cn.meowrain.aioj.backend.userservice.controller;
|
|||||||
|
|
||||||
import cn.meowrain.aioj.backend.framework.core.web.Result;
|
import cn.meowrain.aioj.backend.framework.core.web.Result;
|
||||||
import cn.meowrain.aioj.backend.framework.core.web.Results;
|
import cn.meowrain.aioj.backend.framework.core.web.Results;
|
||||||
|
import cn.meowrain.aioj.backend.userservice.dto.req.EmailSendCodeRequestDTO;
|
||||||
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
|
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
|
||||||
import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
|
import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
|
||||||
import cn.meowrain.aioj.backend.userservice.service.UserService;
|
import cn.meowrain.aioj.backend.userservice.service.UserService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RestController()
|
@RestController()
|
||||||
@RequestMapping("/v1/user")
|
@RequestMapping("/v1/user")
|
||||||
|
@Tag(name = "用户管理", description = "用户注册、查询等接口")
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
@PostMapping("/register")
|
|
||||||
public Result<Long> register(@RequestBody UserRegisterRequestDTO userRegisterRequest) {
|
|
||||||
Long l = userService.userRegister(userRegisterRequest);
|
|
||||||
return Results.success(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/inner/get-by-username")
|
@PostMapping("/register")
|
||||||
public Result<UserAuthRespDTO> getUserByUserName(@RequestParam("userAccount") String userAccount) {
|
@Operation(summary = "用户注册", description = "根据用户注册信息创建新用户")
|
||||||
UserAuthRespDTO userAuthDTO = userService.findAuthInfoByUserAccount(userAccount);
|
public Result<Long> register(
|
||||||
return Results.success(userAuthDTO);
|
@Parameter(description = "用户注册信息", required = true)
|
||||||
}
|
@RequestBody UserRegisterRequestDTO userRegisterRequest) {
|
||||||
|
Long l = userService.userRegister(userRegisterRequest);
|
||||||
|
return Results.success(l);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/inner/get-by-userid")
|
@GetMapping("/inner/get-by-username")
|
||||||
public Result<UserAuthRespDTO> getUserById(@RequestParam("userId") String userid) {
|
@Operation(summary = "根据用户名查询用户", description = "通过用户账号获取用户认证信息(内部接口)")
|
||||||
UserAuthRespDTO userAuthRespDTO = userService.findAuthInfoByUserId(userid);
|
public Result<UserAuthRespDTO> getUserByUserName(
|
||||||
return Results.success(userAuthRespDTO);
|
@Parameter(description = "用户账号", required = true, example = "admin")
|
||||||
}
|
@RequestParam("userAccount") String userAccount) {
|
||||||
|
UserAuthRespDTO userAuthDTO = userService.findAuthInfoByUserAccount(userAccount);
|
||||||
|
return Results.success(userAuthDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/inner/get-by-userid")
|
||||||
|
@Operation(summary = "根据用户ID查询用户", description = "通过用户ID获取用户认证信息(内部接口)")
|
||||||
|
public Result<UserAuthRespDTO> getUserById(
|
||||||
|
@Parameter(description = "用户ID", required = true, example = "1")
|
||||||
|
@RequestParam("userId") String userid) {
|
||||||
|
UserAuthRespDTO userAuthRespDTO = userService.findAuthInfoByUserId(userid);
|
||||||
|
return Results.success(userAuthRespDTO);
|
||||||
|
}
|
||||||
|
/* ================================邮箱相关接口 ======================*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送验证码
|
||||||
|
*/
|
||||||
|
@GetMapping("/email/send-code")
|
||||||
|
public Result<Void> getVerifyCode(@Parameter(description = "邮箱信息", required = true)
|
||||||
|
@Valid @RequestParam EmailSendCodeRequestDTO request) {
|
||||||
|
userService.sendEmailCode(request.getEmail());
|
||||||
|
return Results.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定邮箱
|
||||||
|
*/
|
||||||
|
@PostMapping("/email/bind")
|
||||||
|
public Result<Void> bindEmail(@RequestBody String email) {
|
||||||
|
// TODO: 待实现
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/email/unbind")
|
||||||
|
public Result<Void> unbindEmail() {
|
||||||
|
// TODO: 待实现
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,70 +12,80 @@ import java.util.Date;
|
|||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class User implements Serializable {
|
public class User implements Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* id
|
* id
|
||||||
*/
|
*/
|
||||||
@TableId(type = IdType.ASSIGN_ID)
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户账号
|
* 用户账号
|
||||||
*/
|
*/
|
||||||
private String userAccount;
|
private String userAccount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户密码
|
* 用户密码
|
||||||
*/
|
*/
|
||||||
private String userPassword;
|
private String userPassword;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开放平台id
|
* 开放平台id
|
||||||
*/
|
*/
|
||||||
private String unionId;
|
private String unionId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公众号openId
|
* 公众号openId
|
||||||
*/
|
*/
|
||||||
private String mpOpenId;
|
private String mpOpenId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户昵称
|
* 用户昵称
|
||||||
*/
|
*/
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户头像
|
* 用户头像
|
||||||
*/
|
*/
|
||||||
private String userAvatar;
|
private String userAvatar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户简介
|
* 用户简介
|
||||||
*/
|
*/
|
||||||
private String userProfile;
|
private String userProfile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户角色:user/admin/ban
|
* 用户角色:user/admin/ban
|
||||||
*/
|
*/
|
||||||
private String userRole;
|
private String userRole;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 用户邮箱
|
||||||
*/
|
*/
|
||||||
@TableField(fill = FieldFill.INSERT)
|
private String userEmail;
|
||||||
private Date createTime;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新时间
|
* 用户邮箱是否验证 0 未验证 1已验证
|
||||||
*/
|
*/
|
||||||
private Date updateTime;
|
private Integer userEmailVerified;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否删除
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
@TableLogic
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private Integer isDelete;
|
private Date createTime;
|
||||||
|
|
||||||
@TableField(exist = false)
|
/**
|
||||||
private static final long serialVersionUID = 1L;
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否删除
|
||||||
|
*/
|
||||||
|
@TableLogic
|
||||||
|
private Integer isDelete;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package cn.meowrain.aioj.backend.userservice.dto.req;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册的邮箱 请求对象
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class EmailSendCodeRequestDTO {
|
||||||
|
@NotNull
|
||||||
|
@Email
|
||||||
|
String email;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package cn.meowrain.aioj.backend.userservice.dto.resp;
|
package cn.meowrain.aioj.backend.userservice.dto.resp;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -8,61 +9,85 @@ import java.util.Date;
|
|||||||
* 用户认证响应体
|
* 用户认证响应体
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "用户认证响应体")
|
||||||
public class UserAuthRespDTO {
|
public class UserAuthRespDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* id
|
* id
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "用户ID", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户账号
|
* 用户账号
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "用户账号", example = "admin")
|
||||||
private String userAccount;
|
private String userAccount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户密码
|
* 用户密码
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "用户密码", example = "123456")
|
||||||
private String userPassword;
|
private String userPassword;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开放平台id
|
* 开放平台id
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "开放平台ID", example = "wx_union_id_123")
|
||||||
private String unionId;
|
private String unionId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公众号openId
|
* 公众号openId
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "公众号OpenID", example = "wx_openid_123")
|
||||||
private String mpOpenId;
|
private String mpOpenId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户昵称
|
* 用户昵称
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "用户昵称", example = "张三")
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户头像
|
* 用户头像
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "用户头像URL", example = "https://example.com/avatar.jpg")
|
||||||
private String userAvatar;
|
private String userAvatar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户简介
|
* 用户简介
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "用户简介", example = "这是我的个人简介")
|
||||||
private String userProfile;
|
private String userProfile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户角色:user/admin/ban
|
* 用户角色:user/admin/ban
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "用户角色", example = "user", allowableValues = {"user", "admin", "ban"})
|
||||||
private String userRole;
|
private String userRole;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户邮箱
|
||||||
|
*/
|
||||||
|
@Schema(description = "用户邮箱", example = "user@example.com")
|
||||||
|
private String userEmail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户邮箱是否验证 0 未验证 1已验证
|
||||||
|
*/
|
||||||
|
@Schema(description = "用户邮箱是否验证", example = "1", allowableValues = {"0", "1"})
|
||||||
|
private Integer userEmailVerified;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "创建时间", example = "2025-01-01T00:00:00")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新时间
|
* 更新时间
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "更新时间", example = "2025-01-01T12:00:00")
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package cn.meowrain.aioj.backend.userservice.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件服务接口
|
||||||
|
*/
|
||||||
|
public interface EmailService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码
|
||||||
|
* @param email 收件人邮箱
|
||||||
|
*/
|
||||||
|
void sendVerifyCode(String email);
|
||||||
|
}
|
||||||
@@ -28,4 +28,19 @@ public interface UserService extends IService<User> {
|
|||||||
*/
|
*/
|
||||||
UserAuthRespDTO findAuthInfoByUserId(String userId);
|
UserAuthRespDTO findAuthInfoByUserId(String userId);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码
|
||||||
|
*/
|
||||||
|
void sendEmailCode(String email);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定邮箱
|
||||||
|
*/
|
||||||
|
void bindEmail(String email, String code);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑邮箱
|
||||||
|
*/
|
||||||
|
void unbindEmail(Long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package cn.meowrain.aioj.backend.userservice.service.impl;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ServiceException;
|
||||||
|
import cn.meowrain.aioj.backend.userservice.common.constants.RedisKeyConstants;
|
||||||
|
import cn.meowrain.aioj.backend.userservice.service.EmailService;
|
||||||
|
import jakarta.mail.MessagingException;
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EmailServiceImpl implements EmailService {
|
||||||
|
|
||||||
|
private final JavaMailSender mailSender;
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
@Value("${spring.mail.username}")
|
||||||
|
private String FROM_EMAIL;
|
||||||
|
|
||||||
|
private static final String SUBJECT = "【AIOJ】邮箱验证码";
|
||||||
|
|
||||||
|
|
||||||
|
private static final int CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成6位随机验证码
|
||||||
|
*/
|
||||||
|
private String generateCode() {
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||||
|
stringBuilder.append(random.nextInt(10));
|
||||||
|
}
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 构建邮件内容
|
||||||
|
*/
|
||||||
|
private String buildEmailContent(String code) {
|
||||||
|
return String.format("""
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, Helvetica, 'Microsoft YaHei', sans-serif; background-color: #f4f6f8; margin: 0; padding: 0;">
|
||||||
|
<table width="100%%" cellpadding="0" cellspacing="0" style="padding: 30px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="600" cellpadding="0" cellspacing="0" style="background: #ffffff; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.06);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px 32px; border-bottom: 1px solid #e6e8eb;">
|
||||||
|
<h2 style="margin: 0; font-size: 20px; font-weight: 600; color: #1f2937;">AIOJ 邮箱验证</h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px 32px; color: #374151; font-size: 14px;">
|
||||||
|
<p style="margin: 0 0 12px 0;">您好,</p>
|
||||||
|
<p style="margin: 0 0 16px 0;">您正在进行邮箱安全校验,为保障账户安全,请使用以下验证码:</p>
|
||||||
|
|
||||||
|
<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px; text-align: center; margin: 20px 0;">
|
||||||
|
<span style="font-size: 28px; font-weight: bold; letter-spacing: 2px; color: #2563eb;">%s</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin: 0 0 16px 0;">验证码有效期为 <strong>1 分钟</strong>,请尽快完成操作。</p>
|
||||||
|
<p style="margin: 0 0 8px 0; color: #6b7280;">若非本人操作,请忽略本邮件。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 20px 32px; border-top: 1px solid #e6e8eb; background: #fafbfc; color: #9ca3af; font-size: 12px; text-align: center;">
|
||||||
|
<p style="margin: 0 0 4px 0;">此邮件由系统自动发送,请勿回复</p>
|
||||||
|
<p style="margin: 0;">AIOJ © %s All Rights Reserved.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""", code, java.time.Year.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendVerifyCode(String email) {
|
||||||
|
// 生成验证码
|
||||||
|
String code = generateCode();
|
||||||
|
// 验证码存redis里面,1分钟过期时间
|
||||||
|
String redisKey = String.format(RedisKeyConstants.EMAIL_CODE_PREFIX,email);
|
||||||
|
redisTemplate.opsForValue().set(redisKey,code,RedisKeyConstants.EMAIL_CODE_TTL,TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
// 发送邮件
|
||||||
|
try {
|
||||||
|
MimeMessage message = mailSender.createMimeMessage();
|
||||||
|
// ⭐ 显式指定 UTF-8 编码,解决中文乱码问题
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||||
|
helper.setFrom(FROM_EMAIL);
|
||||||
|
helper.setTo(email);
|
||||||
|
helper.setSubject(SUBJECT);
|
||||||
|
helper.setText(buildEmailContent(code), true);
|
||||||
|
|
||||||
|
// ⭐ 发送邮件
|
||||||
|
mailSender.send(message);
|
||||||
|
log.info("验证码邮件发送成功: 邮箱={}, 验证码={}", email, code);
|
||||||
|
}catch (MessagingException e) {
|
||||||
|
log.error("验证码邮件发送失败: 邮箱={}", email, e);
|
||||||
|
throw new ServiceException("邮件发送失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import cn.hutool.crypto.digest.BCrypt;
|
|||||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
import cn.meowrain.aioj.backend.framework.core.exception.ServiceException;
|
import cn.meowrain.aioj.backend.framework.core.exception.ServiceException;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils;
|
||||||
import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums;
|
import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums;
|
||||||
import cn.meowrain.aioj.backend.userservice.dao.entity.User;
|
import cn.meowrain.aioj.backend.userservice.dao.entity.User;
|
||||||
import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper;
|
import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper;
|
||||||
@@ -11,6 +12,7 @@ import cn.meowrain.aioj.backend.userservice.dto.chains.context.UserRegisterReque
|
|||||||
|
|
||||||
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
|
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
|
||||||
import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
|
import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
|
||||||
|
import cn.meowrain.aioj.backend.userservice.service.EmailService;
|
||||||
import cn.meowrain.aioj.backend.userservice.service.UserService;
|
import cn.meowrain.aioj.backend.userservice.service.UserService;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -28,6 +30,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
|
|
||||||
private final UserRegisterRequestParamVerifyContext userRegisterRequestParamVerifyContext;
|
private final UserRegisterRequestParamVerifyContext userRegisterRequestParamVerifyContext;
|
||||||
|
|
||||||
|
private final EmailService emailService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long userRegister(UserRegisterRequestDTO request) {
|
public Long userRegister(UserRegisterRequestDTO request) {
|
||||||
UserAuthRespDTO authInfoByUserAccount = findAuthInfoByUserAccount(request.getUserAccount());
|
UserAuthRespDTO authInfoByUserAccount = findAuthInfoByUserAccount(request.getUserAccount());
|
||||||
@@ -82,4 +86,21 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendEmailCode(String email) {
|
||||||
|
emailService.sendVerifyCode(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bindEmail(String email, String code) {
|
||||||
|
Long currentUserId = ContextHolderUtils.getCurrentUserId();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unbindEmail(Long userId) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
spring:
|
spring:
|
||||||
|
mail:
|
||||||
|
host: smtp.qq.com
|
||||||
|
port: 465
|
||||||
|
username: 2705356115@qq.com
|
||||||
|
# 这里使用授权码
|
||||||
|
password: yohcndfrlxwcdfed
|
||||||
|
default-encoding: UTF-8
|
||||||
|
protocol: smtp
|
||||||
|
properties:
|
||||||
|
mail:
|
||||||
|
smtp:
|
||||||
|
ssl:
|
||||||
|
enable: true # 在 properties 中明确指定
|
||||||
|
auth: true
|
||||||
|
starttls:
|
||||||
|
enable: true # QQ邮箱也支持STARTTLS,但使用465端口时,ssl.enable=true是必须的
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: 10.0.0.10
|
host: 10.0.0.10
|
||||||
|
|||||||
Reference in New Issue
Block a user