feat: 完成Nacos配置优化和WebSocket集成
主要更新: 1. 统一所有微服务端口配置(19000-19008) 2. 为所有服务创建本地/测试/生产三套环境配置 3. 配置Nacos认证密码(本地:Peanut2817*#, 测试/生产:EmotionMuseum2025) 4. 优化网关路由配置,支持负载均衡和WebSocket 5. 新增emotion-websocket模块,支持实时聊天 6. 前端集成WebSocket,替代HTTP轮询 7. 添加配置验证和管理工具脚本 技术特性: - 完整的环境隔离和服务发现 - WebSocket实时通信支持 - 负载均衡路由配置 - 跨域和安全配置 - 自动重连和心跳检测
This commit is contained in:
+53
@@ -0,0 +1,53 @@
|
||||
package com.emotionmuseum.user.config;
|
||||
|
||||
import com.emotionmuseum.user.security.UserDetailsServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
/**
|
||||
* 认证配置类
|
||||
* 独立的认证配置,避免循环依赖
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class AuthenticationConfig {
|
||||
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证提供者
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ public class CaptchaConfig {
|
||||
public Captcha arithmeticCaptcha() {
|
||||
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 48);
|
||||
captcha.setLen(2); // 几位数运算,默认是两位
|
||||
captcha.getArithmeticString(); // 获取运算的公式:3+2=?
|
||||
return captcha;
|
||||
}
|
||||
|
||||
|
||||
+8
-34
@@ -3,19 +3,17 @@ package com.emotionmuseum.user.config;
|
||||
import com.emotionmuseum.user.security.JwtAuthenticationFilter;
|
||||
import com.emotionmuseum.user.security.UserDetailsServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
@@ -37,35 +35,10 @@ import java.util.Collections;
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final AuthenticationProvider authenticationProvider;
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证提供者
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS配置
|
||||
@@ -126,10 +99,11 @@ public class SecurityConfig {
|
||||
.anyRequest().authenticated())
|
||||
|
||||
// 配置认证提供者
|
||||
.authenticationProvider(authenticationProvider())
|
||||
.authenticationProvider(authenticationProvider)
|
||||
|
||||
// 添加JWT过滤器
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
.addFilterBefore(applicationContext.getBean(JwtAuthenticationFilter.class),
|
||||
UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
package com.emotionmuseum.user.controller;
|
||||
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
import com.emotionmuseum.user.dto.CaptchaResponse;
|
||||
import com.emotionmuseum.user.dto.SliderCaptchaResponse;
|
||||
import com.emotionmuseum.user.dto.SliderCaptchaVerifyRequest;
|
||||
import com.emotionmuseum.user.service.CaptchaService;
|
||||
import com.emotionmuseum.user.service.SliderCaptchaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 验证码控制器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/captcha")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "验证码管理", description = "验证码生成和验证接口")
|
||||
public class CaptchaController {
|
||||
|
||||
private final CaptchaService captchaService;
|
||||
private final SliderCaptchaService sliderCaptchaService;
|
||||
|
||||
@Operation(summary = "生成验证码")
|
||||
@GetMapping("/generate")
|
||||
public Result<CaptchaResponse> generateCaptcha(
|
||||
@Parameter(description = "验证码类型", example = "arithmetic") @RequestParam(defaultValue = "arithmetic") String type) {
|
||||
log.info("生成验证码请求,类型: {}", type);
|
||||
CaptchaResponse response = captchaService.generateCaptcha(type);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "验证验证码")
|
||||
@PostMapping("/verify")
|
||||
public Result<Boolean> verifyCaptcha(
|
||||
@Parameter(description = "验证码ID") @RequestParam String captchaId,
|
||||
@Parameter(description = "验证码") @RequestParam String captcha) {
|
||||
log.info("验证验证码请求,ID: {}", captchaId);
|
||||
boolean isValid = captchaService.verifyCaptcha(captchaId, captcha);
|
||||
return Result.success(isValid);
|
||||
}
|
||||
|
||||
@Operation(summary = "生成滑块验证码")
|
||||
@GetMapping("/slider/generate")
|
||||
public Result<SliderCaptchaResponse> generateSliderCaptcha() {
|
||||
log.info("生成滑块验证码请求");
|
||||
SliderCaptchaResponse response = sliderCaptchaService.generateSliderCaptcha();
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "验证滑块验证码")
|
||||
@PostMapping("/slider/verify")
|
||||
public Result<Boolean> verifySliderCaptcha(@RequestBody SliderCaptchaVerifyRequest request) {
|
||||
log.info("验证滑块验证码请求,ID: {}", request.getCaptchaId());
|
||||
boolean isValid = sliderCaptchaService.verifySliderCaptcha(request);
|
||||
return Result.success(isValid);
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package com.emotionmuseum.user.controller;
|
||||
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
import com.emotionmuseum.user.dto.OAuthLoginRequest;
|
||||
import com.emotionmuseum.user.service.OAuthService;
|
||||
import com.emotionmuseum.user.vo.LoginResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 第三方登录控制器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/oauth")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "第三方登录", description = "微信、QQ等第三方登录接口")
|
||||
public class OAuthController {
|
||||
|
||||
private final OAuthService oauthService;
|
||||
|
||||
@Operation(summary = "获取第三方登录授权URL")
|
||||
@GetMapping("/auth-url/{platform}")
|
||||
public Result<String> getAuthUrl(
|
||||
@Parameter(description = "平台类型", example = "wechat")
|
||||
@PathVariable String platform) {
|
||||
log.info("获取第三方登录授权URL: {}", platform);
|
||||
String authUrl = oauthService.getAuthUrl(platform);
|
||||
return Result.success(authUrl);
|
||||
}
|
||||
|
||||
@Operation(summary = "第三方登录")
|
||||
@PostMapping("/login")
|
||||
public Result<LoginResponse> oauthLogin(@Valid @RequestBody OAuthLoginRequest request) {
|
||||
log.info("第三方登录请求: {}", request.getPlatform());
|
||||
LoginResponse response = oauthService.oauthLogin(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取第三方用户信息")
|
||||
@GetMapping("/user-info/{platform}")
|
||||
public Result<Object> getOAuthUserInfo(
|
||||
@Parameter(description = "平台类型") @PathVariable String platform,
|
||||
@Parameter(description = "授权码") @RequestParam String code,
|
||||
@Parameter(description = "状态码") @RequestParam(required = false) String state) {
|
||||
log.info("获取第三方用户信息: {}", platform);
|
||||
Object userInfo = oauthService.getOAuthUserInfo(platform, code, state);
|
||||
return Result.success(userInfo);
|
||||
}
|
||||
}
|
||||
+1
-62
@@ -1,11 +1,8 @@
|
||||
package com.emotionmuseum.user.controller;
|
||||
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
import com.emotionmuseum.user.dto.LoginRequest;
|
||||
import com.emotionmuseum.user.dto.RegisterRequest;
|
||||
import com.emotionmuseum.user.dto.UserUpdateRequest;
|
||||
import com.emotionmuseum.user.service.UserService;
|
||||
import com.emotionmuseum.user.vo.LoginResponse;
|
||||
import com.emotionmuseum.user.vo.UserInfoResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -28,36 +25,11 @@ import jakarta.validation.Valid;
|
||||
@RequestMapping("/user")
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
@Tag(name = "用户管理", description = "用户注册、登录、信息管理")
|
||||
@Tag(name = "用户管理", description = "用户信息管理")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Operation(summary = "用户注册")
|
||||
@PostMapping("/register")
|
||||
public Result<UserInfoResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||
log.info("用户注册请求: {}", request.getAccount());
|
||||
UserInfoResponse response = userService.register(request);
|
||||
return Result.success("注册成功", response);
|
||||
}
|
||||
|
||||
@Operation(summary = "用户登录")
|
||||
@PostMapping("/login")
|
||||
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
log.info("用户登录请求: {}", request.getAccount());
|
||||
LoginResponse response = userService.login(request);
|
||||
return Result.success("登录成功", response);
|
||||
}
|
||||
|
||||
@Operation(summary = "刷新Token")
|
||||
@PostMapping("/refresh")
|
||||
public Result<LoginResponse> refreshToken(
|
||||
@Parameter(description = "刷新Token") @RequestParam String refreshToken) {
|
||||
log.info("刷新Token请求");
|
||||
LoginResponse response = userService.refreshToken(refreshToken);
|
||||
return Result.success("Token刷新成功", response);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取用户信息")
|
||||
@GetMapping("/info/{userId}")
|
||||
public Result<UserInfoResponse> getUserInfo(
|
||||
@@ -77,30 +49,6 @@ public class UserController {
|
||||
return Result.success("更新成功", response);
|
||||
}
|
||||
|
||||
@Operation(summary = "检查账号是否存在")
|
||||
@GetMapping("/check/account")
|
||||
public Result<Boolean> checkAccount(
|
||||
@Parameter(description = "账号") @RequestParam String account) {
|
||||
boolean exists = userService.existsByAccount(account);
|
||||
return Result.success(exists);
|
||||
}
|
||||
|
||||
@Operation(summary = "检查邮箱是否存在")
|
||||
@GetMapping("/check/email")
|
||||
public Result<Boolean> checkEmail(
|
||||
@Parameter(description = "邮箱") @RequestParam String email) {
|
||||
boolean exists = userService.existsByEmail(email);
|
||||
return Result.success(exists);
|
||||
}
|
||||
|
||||
@Operation(summary = "检查手机号是否存在")
|
||||
@GetMapping("/check/phone")
|
||||
public Result<Boolean> checkPhone(
|
||||
@Parameter(description = "手机号") @RequestParam String phone) {
|
||||
boolean exists = userService.existsByPhone(phone);
|
||||
return Result.success(exists);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新最后活跃时间")
|
||||
@PostMapping("/active/{userId}")
|
||||
public Result<Void> updateLastActiveTime(
|
||||
@@ -108,13 +56,4 @@ public class UserController {
|
||||
userService.updateLastActiveTime(userId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "用户登出")
|
||||
@PostMapping("/logout/{userId}")
|
||||
public Result<Void> logout(
|
||||
@Parameter(description = "用户ID") @PathVariable String userId) {
|
||||
log.info("用户登出请求: {}", userId);
|
||||
userService.logout(userId);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.emotionmuseum.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 验证码响应
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "验证码响应")
|
||||
public class CaptchaResponse {
|
||||
|
||||
@Schema(description = "验证码ID")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "验证码图片Base64")
|
||||
private String captchaImage;
|
||||
|
||||
@Schema(description = "验证码类型", example = "arithmetic")
|
||||
private String captchaType;
|
||||
|
||||
@Schema(description = "过期时间(秒)", example = "300")
|
||||
private Long expireTime;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.emotionmuseum.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 用户登录请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "用户登录请求")
|
||||
public class LoginRequest {
|
||||
|
||||
@Schema(description = "账号(支持账号/邮箱/手机号)", example = "test_user")
|
||||
@NotBlank(message = "账号不能为空")
|
||||
private String account;
|
||||
|
||||
@Schema(description = "密码", example = "123456")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "验证码ID", example = "captcha_123")
|
||||
@NotBlank(message = "验证码ID不能为空")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "验证码", example = "1234")
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String captcha;
|
||||
|
||||
@Schema(description = "记住我", example = "false")
|
||||
private Boolean rememberMe = false;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.emotionmuseum.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 第三方登录请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "第三方登录请求")
|
||||
public class OAuthLoginRequest {
|
||||
|
||||
@Schema(description = "第三方平台类型", example = "wechat")
|
||||
@NotBlank(message = "平台类型不能为空")
|
||||
private String platform;
|
||||
|
||||
@Schema(description = "授权码", example = "auth_code_123")
|
||||
@NotBlank(message = "授权码不能为空")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "状态码", example = "state_123")
|
||||
private String state;
|
||||
|
||||
@Schema(description = "验证码ID", example = "captcha_123")
|
||||
@NotBlank(message = "验证码ID不能为空")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "验证码", example = "1234")
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String captcha;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package com.emotionmuseum.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 用户注册请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "用户注册请求")
|
||||
public class RegisterRequest {
|
||||
|
||||
@Schema(description = "账号", example = "test_user")
|
||||
@NotBlank(message = "账号不能为空")
|
||||
@Size(min = 4, max = 20, message = "账号长度必须在4-20位之间")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "账号只能包含字母、数字和下划线")
|
||||
private String account;
|
||||
|
||||
@Schema(description = "密码", example = "123456")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "确认密码", example = "123456")
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
|
||||
@Schema(description = "验证码ID", example = "captcha_123")
|
||||
@NotBlank(message = "验证码ID不能为空")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "验证码", example = "1234")
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String captcha;
|
||||
|
||||
@Schema(description = "用户名", example = "测试用户")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 2, max = 20, message = "用户名长度必须在2-20位之间")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "邮箱", example = "test@example.com")
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "昵称", example = "小测试")
|
||||
@NotBlank(message = "昵称不能为空")
|
||||
@Size(min = 1, max = 20, message = "昵称长度必须在1-20位之间")
|
||||
private String nickname;
|
||||
|
||||
@Schema(description = "生日", example = "1990-01-01")
|
||||
private LocalDate birthDate;
|
||||
|
||||
@Schema(description = "所在地", example = "北京市")
|
||||
@Size(max = 50, message = "所在地长度不能超过50位")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "个人简介", example = "这是一个测试用户")
|
||||
@Size(max = 200, message = "个人简介长度不能超过200位")
|
||||
private String bio;
|
||||
|
||||
/**
|
||||
* 验证密码一致性
|
||||
*/
|
||||
public boolean isPasswordMatch() {
|
||||
return password != null && password.equals(confirmPassword);
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package com.emotionmuseum.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 滑块验证码响应
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "滑块验证码响应")
|
||||
public class SliderCaptchaResponse {
|
||||
|
||||
@Schema(description = "验证码ID")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "背景图片Base64")
|
||||
private String backgroundImage;
|
||||
|
||||
@Schema(description = "滑块图片Base64")
|
||||
private String sliderImage;
|
||||
|
||||
@Schema(description = "滑块X坐标")
|
||||
private Integer sliderX;
|
||||
|
||||
@Schema(description = "滑块Y坐标")
|
||||
private Integer sliderY;
|
||||
|
||||
@Schema(description = "过期时间(秒)")
|
||||
private Long expireTime;
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package com.emotionmuseum.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 滑块验证码验证请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "滑块验证码验证请求")
|
||||
public class SliderCaptchaVerifyRequest {
|
||||
|
||||
@Schema(description = "验证码ID")
|
||||
@NotBlank(message = "验证码ID不能为空")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "滑块X坐标")
|
||||
@NotNull(message = "滑块X坐标不能为空")
|
||||
private Integer x;
|
||||
|
||||
@Schema(description = "滑块Y坐标")
|
||||
@NotNull(message = "滑块Y坐标不能为空")
|
||||
private Integer y;
|
||||
|
||||
@Schema(description = "滑动轨迹")
|
||||
private String track;
|
||||
}
|
||||
@@ -14,30 +14,6 @@ import org.apache.ibatis.annotations.Param;
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
|
||||
/**
|
||||
* 根据账号查询用户
|
||||
*
|
||||
* @param account 账号
|
||||
* @return 用户信息
|
||||
*/
|
||||
User selectByAccount(@Param("account") String account);
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @return 用户信息
|
||||
*/
|
||||
User selectByEmail(@Param("email") String email);
|
||||
|
||||
/**
|
||||
* 根据手机号查询用户
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 用户信息
|
||||
*/
|
||||
User selectByPhone(@Param("phone") String phone);
|
||||
|
||||
/**
|
||||
* 更新最后活跃时间
|
||||
*
|
||||
|
||||
+2
@@ -8,6 +8,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
@@ -32,6 +33,7 @@ import java.util.concurrent.TimeUnit;
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
@Lazy
|
||||
private final UserDetailsService userDetailsService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ import com.emotionmuseum.user.entity.User;
|
||||
import com.emotionmuseum.user.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
@@ -25,6 +26,7 @@ import java.util.Collections;
|
||||
@RequiredArgsConstructor
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
@Lazy
|
||||
private final UserService userService;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.emotionmuseum.user.service;
|
||||
|
||||
import com.emotionmuseum.user.dto.CaptchaResponse;
|
||||
|
||||
/**
|
||||
* 验证码服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
public interface CaptchaService {
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @param type 验证码类型 (arithmetic, chinese, gif, spec)
|
||||
* @return 验证码响应
|
||||
*/
|
||||
CaptchaResponse generateCaptcha(String type);
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
*
|
||||
* @param captchaId 验证码ID
|
||||
* @param captcha 用户输入的验证码
|
||||
* @return 是否验证成功
|
||||
*/
|
||||
boolean verifyCaptcha(String captchaId, String captcha);
|
||||
|
||||
/**
|
||||
* 删除验证码
|
||||
*
|
||||
* @param captchaId 验证码ID
|
||||
*/
|
||||
void removeCaptcha(String captchaId);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.emotionmuseum.user.service;
|
||||
|
||||
import com.emotionmuseum.user.dto.OAuthLoginRequest;
|
||||
import com.emotionmuseum.user.vo.LoginResponse;
|
||||
|
||||
/**
|
||||
* 第三方登录服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
public interface OAuthService {
|
||||
|
||||
/**
|
||||
* 获取第三方登录授权URL
|
||||
*
|
||||
* @param platform 平台类型 (wechat, qq, wechat-mp)
|
||||
* @return 授权URL
|
||||
*/
|
||||
String getAuthUrl(String platform);
|
||||
|
||||
/**
|
||||
* 第三方登录
|
||||
*
|
||||
* @param request 第三方登录请求
|
||||
* @return 登录响应
|
||||
*/
|
||||
LoginResponse oauthLogin(OAuthLoginRequest request);
|
||||
|
||||
/**
|
||||
* 获取第三方用户信息
|
||||
*
|
||||
* @param platform 平台类型
|
||||
* @param code 授权码
|
||||
* @param state 状态码
|
||||
* @return 用户信息
|
||||
*/
|
||||
Object getOAuthUserInfo(String platform, String code, String state);
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
package com.emotionmuseum.user.service;
|
||||
|
||||
import com.emotionmuseum.user.dto.SliderCaptchaResponse;
|
||||
import com.emotionmuseum.user.dto.SliderCaptchaVerifyRequest;
|
||||
|
||||
/**
|
||||
* 滑块验证码服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
public interface SliderCaptchaService {
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*
|
||||
* @return 滑块验证码响应
|
||||
*/
|
||||
SliderCaptchaResponse generateSliderCaptcha();
|
||||
|
||||
/**
|
||||
* 验证滑块验证码
|
||||
*
|
||||
* @param request 验证请求
|
||||
* @return 是否验证成功
|
||||
*/
|
||||
boolean verifySliderCaptcha(SliderCaptchaVerifyRequest request);
|
||||
|
||||
/**
|
||||
* 删除滑块验证码
|
||||
*
|
||||
* @param captchaId 验证码ID
|
||||
*/
|
||||
void removeSliderCaptcha(String captchaId);
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
package com.emotionmuseum.user.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotionmuseum.user.dto.LoginRequest;
|
||||
import com.emotionmuseum.user.dto.RegisterRequest;
|
||||
import com.emotionmuseum.user.dto.UserUpdateRequest;
|
||||
import com.emotionmuseum.user.entity.User;
|
||||
import com.emotionmuseum.user.vo.LoginResponse;
|
||||
import com.emotionmuseum.user.vo.UserInfoResponse;
|
||||
|
||||
/**
|
||||
@@ -16,30 +13,6 @@ import com.emotionmuseum.user.vo.UserInfoResponse;
|
||||
*/
|
||||
public interface UserService extends IService<User> {
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param request 注册请求
|
||||
* @return 用户信息
|
||||
*/
|
||||
UserInfoResponse register(RegisterRequest request);
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param request 登录请求
|
||||
* @return 登录响应
|
||||
*/
|
||||
LoginResponse login(LoginRequest request);
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*
|
||||
* @param refreshToken 刷新Token
|
||||
* @return 登录响应
|
||||
*/
|
||||
LoginResponse refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 根据用户ID获取用户信息
|
||||
*
|
||||
@@ -57,49 +30,10 @@ public interface UserService extends IService<User> {
|
||||
*/
|
||||
UserInfoResponse updateUserInfo(String userId, UserUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 检查账号是否存在
|
||||
*
|
||||
* @param account 账号
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean existsByAccount(String account);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
/**
|
||||
* 检查手机号是否存在
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean existsByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 更新最后活跃时间
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
void updateLastActiveTime(String userId);
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
void logout(String userId);
|
||||
|
||||
/**
|
||||
* 根据第三方平台ID查找用户
|
||||
*
|
||||
* @param thirdPartyId 第三方平台ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
User findByThirdPartyId(String thirdPartyId);
|
||||
}
|
||||
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
package com.emotionmuseum.user.service.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.emotionmuseum.user.dto.CaptchaResponse;
|
||||
import com.emotionmuseum.user.dto.SliderCaptchaResponse;
|
||||
import com.emotionmuseum.user.dto.SliderCaptchaVerifyRequest;
|
||||
import com.emotionmuseum.user.service.CaptchaService;
|
||||
import com.emotionmuseum.user.service.SliderCaptchaService;
|
||||
import com.wf.captcha.base.Captcha;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 验证码服务实现
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CaptchaServiceImpl implements CaptchaService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private static final String CAPTCHA_KEY_PREFIX = "captcha:";
|
||||
private static final long CAPTCHA_EXPIRE_TIME = 300; // 5分钟
|
||||
|
||||
@Override
|
||||
public CaptchaResponse generateCaptcha(String type) {
|
||||
try {
|
||||
// 根据类型获取验证码Bean
|
||||
String beanName = getBeanNameByType(type);
|
||||
Captcha captcha = (Captcha) applicationContext.getBean(beanName);
|
||||
|
||||
// 生成验证码
|
||||
String captchaId = IdUtil.simpleUUID();
|
||||
String captchaText = captcha.text();
|
||||
String captchaImage = captcha.toBase64();
|
||||
|
||||
// 存储到Redis
|
||||
String redisKey = CAPTCHA_KEY_PREFIX + captchaId;
|
||||
redisTemplate.opsForValue().set(redisKey, captchaText.toLowerCase(), CAPTCHA_EXPIRE_TIME, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("生成验证码成功,ID: {}, 内容: {}", captchaId, captchaText);
|
||||
|
||||
return new CaptchaResponse(captchaId, captchaImage, type, CAPTCHA_EXPIRE_TIME);
|
||||
} catch (Exception e) {
|
||||
log.error("生成验证码失败: {}", e.getMessage());
|
||||
throw new RuntimeException("生成验证码失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyCaptcha(String captchaId, String captcha) {
|
||||
if (StrUtil.isBlank(captchaId) || StrUtil.isBlank(captcha)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
String redisKey = CAPTCHA_KEY_PREFIX + captchaId;
|
||||
String storedCaptcha = (String) redisTemplate.opsForValue().get(redisKey);
|
||||
|
||||
if (StrUtil.isBlank(storedCaptcha)) {
|
||||
log.warn("验证码已过期或不存在,ID: {}", captchaId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证码不区分大小写
|
||||
boolean isValid = storedCaptcha.equalsIgnoreCase(captcha.trim());
|
||||
|
||||
if (isValid) {
|
||||
// 验证成功后删除验证码
|
||||
redisTemplate.delete(redisKey);
|
||||
log.debug("验证码验证成功,ID: {}", captchaId);
|
||||
} else {
|
||||
log.warn("验证码验证失败,ID: {}, 期望: {}, 实际: {}", captchaId, storedCaptcha, captcha);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
} catch (Exception e) {
|
||||
log.error("验证验证码失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCaptcha(String captchaId) {
|
||||
if (StrUtil.isNotBlank(captchaId)) {
|
||||
String redisKey = CAPTCHA_KEY_PREFIX + captchaId;
|
||||
redisTemplate.delete(redisKey);
|
||||
log.debug("删除验证码,ID: {}", captchaId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型获取Bean名称
|
||||
*/
|
||||
private String getBeanNameByType(String type) {
|
||||
String defaultType = StrUtil.blankToDefault(type, "spec");
|
||||
switch (defaultType) {
|
||||
case "arithmetic":
|
||||
return "arithmeticCaptcha";
|
||||
case "chinese":
|
||||
return "chineseCaptcha";
|
||||
case "gif":
|
||||
return "gifCaptcha";
|
||||
default:
|
||||
return "specCaptcha";
|
||||
}
|
||||
}
|
||||
}
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
package com.emotionmuseum.user.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.emotionmuseum.common.result.ResultCode;
|
||||
import com.emotionmuseum.common.util.JwtUtil;
|
||||
import com.emotionmuseum.user.dto.OAuthLoginRequest;
|
||||
import com.emotionmuseum.user.entity.User;
|
||||
import com.emotionmuseum.user.service.CaptchaService;
|
||||
import com.emotionmuseum.user.service.OAuthService;
|
||||
import com.emotionmuseum.user.service.UserService;
|
||||
import com.emotionmuseum.user.vo.LoginResponse;
|
||||
import com.emotionmuseum.user.vo.UserInfoResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthQqRequest;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import me.zhyd.oauth.request.AuthWeChatMpRequest;
|
||||
import me.zhyd.oauth.request.AuthWeChatOpenRequest;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 第三方登录服务实现
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OAuthServiceImpl implements OAuthService {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final CaptchaService captchaService;
|
||||
private final UserService userService;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String REDIS_TOKEN_KEY_PREFIX = "auth:token:";
|
||||
|
||||
@Override
|
||||
public String getAuthUrl(String platform) {
|
||||
try {
|
||||
AuthRequest authRequest = getAuthRequest(platform);
|
||||
return authRequest.authorize();
|
||||
} catch (Exception e) {
|
||||
log.error("获取第三方登录授权URL失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取授权URL失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginResponse oauthLogin(OAuthLoginRequest request) {
|
||||
// 验证验证码
|
||||
if (!captchaService.verifyCaptcha(request.getCaptchaId(), request.getCaptcha())) {
|
||||
throw new RuntimeException(ResultCode.CAPTCHA_ERROR.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取第三方用户信息
|
||||
AuthUser authUser = (AuthUser) getOAuthUserInfo(request.getPlatform(), request.getCode(),
|
||||
request.getState());
|
||||
|
||||
if (authUser == null) {
|
||||
throw new RuntimeException("获取第三方用户信息失败");
|
||||
}
|
||||
|
||||
// 查找或创建用户
|
||||
User user = findOrCreateUser(authUser, request.getPlatform());
|
||||
|
||||
// 生成Token
|
||||
String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
|
||||
|
||||
// 将token存储到Redis中
|
||||
String redisKey = REDIS_TOKEN_KEY_PREFIX + user.getId();
|
||||
redisTemplate.opsForValue().set(redisKey, accessToken, 24, TimeUnit.HOURS);
|
||||
|
||||
// 更新最后活跃时间
|
||||
userService.updateLastActiveTime(user.getId());
|
||||
|
||||
// 构建响应
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setAccessToken(accessToken);
|
||||
response.setRefreshToken(refreshToken);
|
||||
response.setExpiresIn(86400L); // 24小时
|
||||
response.setUserInfo(convertToUserInfoResponse(user));
|
||||
response.setLoginTime(LocalDateTime.now());
|
||||
|
||||
log.info("第三方登录成功: {} - {}", request.getPlatform(), user.getId());
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("第三方登录失败: {}", e.getMessage());
|
||||
throw new RuntimeException("第三方登录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getOAuthUserInfo(String platform, String code, String state) {
|
||||
try {
|
||||
AuthRequest authRequest = getAuthRequest(platform);
|
||||
AuthCallback callback = AuthCallback.builder()
|
||||
.code(code)
|
||||
.state(state)
|
||||
.build();
|
||||
|
||||
AuthResponse<AuthUser> response = authRequest.login(callback);
|
||||
|
||||
if (response.ok()) {
|
||||
return response.getData();
|
||||
} else {
|
||||
log.error("第三方登录失败: {}", response.getMsg());
|
||||
throw new RuntimeException("第三方登录失败: " + response.getMsg());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取第三方用户信息失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取第三方用户信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据平台类型获取AuthRequest
|
||||
*/
|
||||
private AuthRequest getAuthRequest(String platform) {
|
||||
switch (platform.toLowerCase()) {
|
||||
case "wechat":
|
||||
return applicationContext.getBean(AuthWeChatOpenRequest.class);
|
||||
case "wechat-mp":
|
||||
return applicationContext.getBean(AuthWeChatMpRequest.class);
|
||||
case "qq":
|
||||
return applicationContext.getBean(AuthQqRequest.class);
|
||||
default:
|
||||
throw new RuntimeException("不支持的第三方平台: " + platform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找或创建用户
|
||||
*/
|
||||
private User findOrCreateUser(AuthUser authUser, String platform) {
|
||||
// 根据第三方平台ID查找用户
|
||||
String thirdPartyId = platform + "_" + authUser.getUuid();
|
||||
User existingUser = userService.findByThirdPartyId(thirdPartyId);
|
||||
|
||||
if (existingUser != null) {
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
User newUser = new User();
|
||||
newUser.setUsername(authUser.getNickname());
|
||||
newUser.setNickname(authUser.getNickname());
|
||||
newUser.setAvatar(authUser.getAvatar());
|
||||
newUser.setEmail(authUser.getEmail());
|
||||
newUser.setThirdPartyId(thirdPartyId);
|
||||
newUser.setThirdPartyType(platform);
|
||||
newUser.setStatus(1); // 启用状态
|
||||
|
||||
// 保存用户
|
||||
userService.save(newUser);
|
||||
|
||||
log.info("创建第三方登录用户: {} - {}", platform, newUser.getId());
|
||||
return newUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为用户信息响应
|
||||
*/
|
||||
private UserInfoResponse convertToUserInfoResponse(User user) {
|
||||
UserInfoResponse response = new UserInfoResponse();
|
||||
BeanUtils.copyProperties(user, response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
-210
@@ -1,210 +0,0 @@
|
||||
package com.emotionmuseum.user.service.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.emotionmuseum.user.dto.SliderCaptchaResponse;
|
||||
import com.emotionmuseum.user.dto.SliderCaptchaVerifyRequest;
|
||||
import com.emotionmuseum.user.service.SliderCaptchaService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 滑块验证码服务实现
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SliderCaptchaServiceImpl implements SliderCaptchaService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String SLIDER_CAPTCHA_KEY_PREFIX = "slider_captcha:";
|
||||
private static final long SLIDER_CAPTCHA_EXPIRE_TIME = 300; // 5分钟
|
||||
private static final int BACKGROUND_WIDTH = 300;
|
||||
private static final int BACKGROUND_HEIGHT = 150;
|
||||
private static final int SLIDER_WIDTH = 60;
|
||||
private static final int SLIDER_HEIGHT = 60;
|
||||
private static final int TOLERANCE = 5; // 容错范围
|
||||
|
||||
@Override
|
||||
public SliderCaptchaResponse generateSliderCaptcha() {
|
||||
try {
|
||||
String captchaId = IdUtil.simpleUUID();
|
||||
|
||||
// 生成随机位置
|
||||
Random random = new Random();
|
||||
int sliderX = random.nextInt(BACKGROUND_WIDTH - SLIDER_WIDTH - 50) + 50;
|
||||
int sliderY = random.nextInt(BACKGROUND_HEIGHT - SLIDER_HEIGHT - 20) + 20;
|
||||
|
||||
// 生成背景图片
|
||||
BufferedImage backgroundImage = generateBackgroundImage(sliderX, sliderY);
|
||||
String backgroundBase64 = imageToBase64(backgroundImage);
|
||||
|
||||
// 生成滑块图片
|
||||
BufferedImage sliderImage = generateSliderImage();
|
||||
String sliderBase64 = imageToBase64(sliderImage);
|
||||
|
||||
// 存储到Redis
|
||||
String redisKey = SLIDER_CAPTCHA_KEY_PREFIX + captchaId;
|
||||
SliderCaptchaData data = new SliderCaptchaData(sliderX, sliderY);
|
||||
redisTemplate.opsForValue().set(redisKey, data, SLIDER_CAPTCHA_EXPIRE_TIME, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("生成滑块验证码成功,ID: {}, 位置: ({}, {})", captchaId, sliderX, sliderY);
|
||||
|
||||
return new SliderCaptchaResponse(captchaId, backgroundBase64, sliderBase64, 0, sliderY, SLIDER_CAPTCHA_EXPIRE_TIME);
|
||||
} catch (Exception e) {
|
||||
log.error("生成滑块验证码失败: {}", e.getMessage());
|
||||
throw new RuntimeException("生成滑块验证码失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifySliderCaptcha(SliderCaptchaVerifyRequest request) {
|
||||
if (StrUtil.isBlank(request.getCaptchaId()) || request.getX() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
String redisKey = SLIDER_CAPTCHA_KEY_PREFIX + request.getCaptchaId();
|
||||
SliderCaptchaData data = (SliderCaptchaData) redisTemplate.opsForValue().get(redisKey);
|
||||
|
||||
if (data == null) {
|
||||
log.warn("滑块验证码已过期或不存在,ID: {}", request.getCaptchaId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证X坐标是否在容错范围内
|
||||
boolean isValid = Math.abs(data.getSliderX() - request.getX()) <= TOLERANCE;
|
||||
|
||||
if (isValid) {
|
||||
// 验证成功后删除验证码
|
||||
redisTemplate.delete(redisKey);
|
||||
log.debug("滑块验证码验证成功,ID: {}", request.getCaptchaId());
|
||||
} else {
|
||||
log.warn("滑块验证码验证失败,ID: {}, 期望X: {}, 实际X: {}",
|
||||
request.getCaptchaId(), data.getSliderX(), request.getX());
|
||||
}
|
||||
|
||||
return isValid;
|
||||
} catch (Exception e) {
|
||||
log.error("验证滑块验证码失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSliderCaptcha(String captchaId) {
|
||||
if (StrUtil.isNotBlank(captchaId)) {
|
||||
String redisKey = SLIDER_CAPTCHA_KEY_PREFIX + captchaId;
|
||||
redisTemplate.delete(redisKey);
|
||||
log.debug("删除滑块验证码,ID: {}", captchaId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成背景图片
|
||||
*/
|
||||
private BufferedImage generateBackgroundImage(int sliderX, int sliderY) {
|
||||
BufferedImage image = new BufferedImage(BACKGROUND_WIDTH, BACKGROUND_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
|
||||
// 设置抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制渐变背景
|
||||
GradientPaint gradient = new GradientPaint(0, 0, new Color(135, 206, 250),
|
||||
BACKGROUND_WIDTH, BACKGROUND_HEIGHT, new Color(70, 130, 180));
|
||||
g2d.setPaint(gradient);
|
||||
g2d.fillRect(0, 0, BACKGROUND_WIDTH, BACKGROUND_HEIGHT);
|
||||
|
||||
// 绘制一些装饰性图形
|
||||
Random random = new Random();
|
||||
g2d.setColor(new Color(255, 255, 255, 100));
|
||||
for (int i = 0; i < 20; i++) {
|
||||
int x = random.nextInt(BACKGROUND_WIDTH);
|
||||
int y = random.nextInt(BACKGROUND_HEIGHT);
|
||||
int size = random.nextInt(20) + 5;
|
||||
g2d.fillOval(x, y, size, size);
|
||||
}
|
||||
|
||||
// 绘制滑块缺口
|
||||
g2d.setColor(new Color(0, 0, 0, 150));
|
||||
g2d.fillRoundRect(sliderX, sliderY, SLIDER_WIDTH, SLIDER_HEIGHT, 10, 10);
|
||||
|
||||
g2d.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成滑块图片
|
||||
*/
|
||||
private BufferedImage generateSliderImage() {
|
||||
BufferedImage image = new BufferedImage(SLIDER_WIDTH, SLIDER_HEIGHT, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
|
||||
// 设置抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制滑块
|
||||
g2d.setColor(new Color(70, 130, 180));
|
||||
g2d.fillRoundRect(0, 0, SLIDER_WIDTH, SLIDER_HEIGHT, 10, 10);
|
||||
|
||||
// 绘制边框
|
||||
g2d.setColor(new Color(255, 255, 255));
|
||||
g2d.setStroke(new BasicStroke(2));
|
||||
g2d.drawRoundRect(1, 1, SLIDER_WIDTH - 2, SLIDER_HEIGHT - 2, 10, 10);
|
||||
|
||||
// 绘制箭头
|
||||
g2d.setColor(Color.WHITE);
|
||||
int[] xPoints = {SLIDER_WIDTH/2 - 8, SLIDER_WIDTH/2 + 8, SLIDER_WIDTH/2};
|
||||
int[] yPoints = {SLIDER_HEIGHT/2, SLIDER_HEIGHT/2, SLIDER_HEIGHT/2 - 8};
|
||||
g2d.fillPolygon(xPoints, yPoints, 3);
|
||||
|
||||
g2d.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片转Base64
|
||||
*/
|
||||
private String imageToBase64(BufferedImage image) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "PNG", baos);
|
||||
byte[] bytes = baos.toByteArray();
|
||||
return "data:image/png;base64," + Base64.getEncoder().encodeToString(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑块验证码数据
|
||||
*/
|
||||
public static class SliderCaptchaData {
|
||||
private int sliderX;
|
||||
private int sliderY;
|
||||
|
||||
public SliderCaptchaData() {}
|
||||
|
||||
public SliderCaptchaData(int sliderX, int sliderY) {
|
||||
this.sliderX = sliderX;
|
||||
this.sliderY = sliderY;
|
||||
}
|
||||
|
||||
public int getSliderX() { return sliderX; }
|
||||
public void setSliderX(int sliderX) { this.sliderX = sliderX; }
|
||||
public int getSliderY() { return sliderY; }
|
||||
public void setSliderY(int sliderY) { this.sliderY = sliderY; }
|
||||
}
|
||||
}
|
||||
+2
-239
@@ -1,30 +1,17 @@
|
||||
package com.emotionmuseum.user.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotionmuseum.common.result.ResultCode;
|
||||
import com.emotionmuseum.common.util.JwtUtil;
|
||||
import com.emotionmuseum.user.dto.LoginRequest;
|
||||
import com.emotionmuseum.user.dto.RegisterRequest;
|
||||
import com.emotionmuseum.user.dto.UserUpdateRequest;
|
||||
import com.emotionmuseum.user.entity.User;
|
||||
import com.emotionmuseum.user.mapper.UserMapper;
|
||||
import com.emotionmuseum.user.service.UserService;
|
||||
import com.emotionmuseum.user.vo.LoginResponse;
|
||||
import com.emotionmuseum.user.vo.UserInfoResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import com.emotionmuseum.user.service.CaptchaService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
*
|
||||
@@ -36,156 +23,11 @@ import java.time.LocalDateTime;
|
||||
@RequiredArgsConstructor
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
private static final String REDIS_TOKEN_KEY_PREFIX = "auth:token:";
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public UserInfoResponse register(RegisterRequest request) {
|
||||
// 验证验证码
|
||||
if (!captchaService.verifyCaptcha(request.getCaptchaId(), request.getCaptcha())) {
|
||||
throw new RuntimeException(ResultCode.CAPTCHA_ERROR.getMessage());
|
||||
}
|
||||
|
||||
// 验证密码一致性
|
||||
if (!request.isPasswordMatch()) {
|
||||
throw new RuntimeException(ResultCode.PARAM_VALIDATION_ERROR.getMessage() + ": 两次密码不一致");
|
||||
}
|
||||
|
||||
// 检查账号是否存在
|
||||
if (existsByAccount(request.getAccount())) {
|
||||
throw new RuntimeException(ResultCode.ACCOUNT_ALREADY_EXISTS.getMessage());
|
||||
}
|
||||
|
||||
// 检查邮箱是否存在
|
||||
if (existsByEmail(request.getEmail())) {
|
||||
throw new RuntimeException(ResultCode.EMAIL_ALREADY_EXISTS.getMessage());
|
||||
}
|
||||
|
||||
// 检查手机号是否存在
|
||||
if (StrUtil.isNotBlank(request.getPhone()) && existsByPhone(request.getPhone())) {
|
||||
throw new RuntimeException(ResultCode.PHONE_ALREADY_EXISTS.getMessage());
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
User user = new User();
|
||||
BeanUtils.copyProperties(request, user);
|
||||
|
||||
// 加密密码
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
|
||||
// 设置默认值
|
||||
user.setMemberLevel("free");
|
||||
user.setTotalDays(0);
|
||||
user.setSelfAwareness(new BigDecimal("50.00"));
|
||||
user.setEmotionalResilience(new BigDecimal("50.00"));
|
||||
user.setActionPower(new BigDecimal("50.00"));
|
||||
user.setEmpathy(new BigDecimal("50.00"));
|
||||
user.setLifeEnthusiasm(new BigDecimal("50.00"));
|
||||
user.setStatus(1);
|
||||
user.setIsVerified(0);
|
||||
user.setLastActiveTime(LocalDateTime.now());
|
||||
|
||||
// 保存用户
|
||||
save(user);
|
||||
|
||||
log.info("用户注册成功: {}", user.getAccount());
|
||||
return convertToUserInfoResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginResponse login(LoginRequest request) {
|
||||
// 验证验证码
|
||||
if (!captchaService.verifyCaptcha(request.getCaptchaId(), request.getCaptcha())) {
|
||||
throw new RuntimeException(ResultCode.CAPTCHA_ERROR.getMessage());
|
||||
}
|
||||
|
||||
// 查找用户(支持账号/邮箱/手机号登录)
|
||||
User user = findUserByAccount(request.getAccount());
|
||||
if (user == null) {
|
||||
throw new RuntimeException(ResultCode.USER_NOT_FOUND.getMessage());
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
||||
throw new RuntimeException(ResultCode.INVALID_CREDENTIALS.getMessage());
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.getStatus() == 0) {
|
||||
throw new RuntimeException(ResultCode.USER_DISABLED.getMessage());
|
||||
}
|
||||
|
||||
// 生成Token
|
||||
String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
|
||||
|
||||
// 将token存储到Redis中(用于登出和token管理)
|
||||
String redisKey = REDIS_TOKEN_KEY_PREFIX + user.getId();
|
||||
redisTemplate.opsForValue().set(redisKey, accessToken, 24, java.util.concurrent.TimeUnit.HOURS);
|
||||
|
||||
// 更新最后活跃时间
|
||||
updateLastActiveTime(user.getId());
|
||||
|
||||
// 构建响应
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setAccessToken(accessToken);
|
||||
response.setRefreshToken(refreshToken);
|
||||
response.setExpiresIn(86400L); // 24小时
|
||||
response.setUserInfo(convertToUserInfoResponse(user));
|
||||
response.setLoginTime(LocalDateTime.now());
|
||||
|
||||
log.info("用户登录成功: {}", user.getAccount());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginResponse refreshToken(String refreshToken) {
|
||||
if (!jwtUtil.validateToken(refreshToken)) {
|
||||
throw new RuntimeException(ResultCode.REFRESH_TOKEN_INVALID.getMessage());
|
||||
}
|
||||
|
||||
String userId = jwtUtil.getUserIdFromToken(refreshToken);
|
||||
String username = jwtUtil.getUsernameFromToken(refreshToken);
|
||||
|
||||
if (StrUtil.isBlank(userId) || StrUtil.isBlank(username)) {
|
||||
throw new RuntimeException(ResultCode.REFRESH_TOKEN_INVALID.getMessage());
|
||||
}
|
||||
|
||||
// 生成新Token
|
||||
String newAccessToken = jwtUtil.generateToken(userId, username);
|
||||
String newRefreshToken = jwtUtil.generateRefreshToken(userId, username);
|
||||
|
||||
// 更新Redis中的token
|
||||
String redisKey = REDIS_TOKEN_KEY_PREFIX + userId;
|
||||
redisTemplate.opsForValue().set(redisKey, newAccessToken, 24, java.util.concurrent.TimeUnit.HOURS);
|
||||
|
||||
// 获取用户信息
|
||||
User user = getById(userId);
|
||||
if (user == null) {
|
||||
throw new RuntimeException(ResultCode.USER_NOT_FOUND.getMessage());
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setAccessToken(newAccessToken);
|
||||
response.setRefreshToken(newRefreshToken);
|
||||
response.setExpiresIn(86400L);
|
||||
response.setUserInfo(convertToUserInfoResponse(user));
|
||||
response.setLoginTime(LocalDateTime.now());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserInfoResponse getUserInfo(String userId) {
|
||||
User user = getById(userId);
|
||||
if (user == null) {
|
||||
throw new RuntimeException(ResultCode.USER_NOT_FOUND.getMessage());
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
return convertToUserInfoResponse(user);
|
||||
}
|
||||
@@ -195,21 +37,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
public UserInfoResponse updateUserInfo(String userId, UserUpdateRequest request) {
|
||||
User user = getById(userId);
|
||||
if (user == null) {
|
||||
throw new RuntimeException(ResultCode.USER_NOT_FOUND.getMessage());
|
||||
}
|
||||
|
||||
// 检查邮箱是否被其他用户使用
|
||||
if (StrUtil.isNotBlank(request.getEmail()) && !request.getEmail().equals(user.getEmail())) {
|
||||
if (existsByEmail(request.getEmail())) {
|
||||
throw new RuntimeException(ResultCode.EMAIL_ALREADY_EXISTS.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否被其他用户使用
|
||||
if (StrUtil.isNotBlank(request.getPhone()) && !request.getPhone().equals(user.getPhone())) {
|
||||
if (existsByPhone(request.getPhone())) {
|
||||
throw new RuntimeException(ResultCode.PHONE_ALREADY_EXISTS.getMessage());
|
||||
}
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
@@ -220,76 +48,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return convertToUserInfoResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByAccount(String account) {
|
||||
return baseMapper.selectByAccount(account) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByEmail(String email) {
|
||||
return baseMapper.selectByEmail(email) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByPhone(String phone) {
|
||||
return StrUtil.isNotBlank(phone) && baseMapper.selectByPhone(phone) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLastActiveTime(String userId) {
|
||||
baseMapper.updateLastActiveTime(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(String userId) {
|
||||
// 从Redis中删除token
|
||||
String redisKey = REDIS_TOKEN_KEY_PREFIX + userId;
|
||||
redisTemplate.delete(redisKey);
|
||||
log.info("用户登出成功: {}", userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User findByThirdPartyId(String thirdPartyId) {
|
||||
if (StrUtil.isBlank(thirdPartyId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getThirdPartyId, thirdPartyId);
|
||||
queryWrapper.eq(User::getIsDeleted, 0);
|
||||
|
||||
return getOne(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据账号查找用户(支持账号/邮箱/手机号)
|
||||
*/
|
||||
private User findUserByAccount(String account) {
|
||||
// 先按账号查找
|
||||
User user = baseMapper.selectByAccount(account);
|
||||
if (user != null) {
|
||||
return user;
|
||||
}
|
||||
|
||||
// 按邮箱查找
|
||||
if (account.contains("@")) {
|
||||
user = baseMapper.selectByEmail(account);
|
||||
if (user != null) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
// 按手机号查找
|
||||
if (account.matches("^1[3-9]\\d{9}$")) {
|
||||
user = baseMapper.selectByPhone(account);
|
||||
if (user != null) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为用户信息响应
|
||||
*/
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.emotionmuseum.user.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "登录响应")
|
||||
public class LoginResponse {
|
||||
|
||||
@Schema(description = "访问Token")
|
||||
private String accessToken;
|
||||
|
||||
@Schema(description = "刷新Token")
|
||||
private String refreshToken;
|
||||
|
||||
@Schema(description = "Token类型", example = "Bearer")
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
@Schema(description = "Token过期时间(秒)", example = "86400")
|
||||
private Long expiresIn;
|
||||
|
||||
@Schema(description = "用户信息")
|
||||
private UserInfoResponse userInfo;
|
||||
|
||||
@Schema(description = "登录时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime loginTime;
|
||||
}
|
||||
@@ -1,102 +1,55 @@
|
||||
server:
|
||||
port: 19001
|
||||
# 本地开发环境配置
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
username: nacos
|
||||
password: Peanut2817*#
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
zone: local
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
username: nacos
|
||||
password: Peanut2817*#
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
password:
|
||||
database: 0
|
||||
timeout: 10000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# Nacos配置
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
register-enabled: true
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: emotion-museum-secret-key-2025-local
|
||||
expiration: 86400
|
||||
refresh-expiration: 604800
|
||||
|
||||
# 第三方登录配置
|
||||
oauth:
|
||||
wechat:
|
||||
client-id: ${WECHAT_CLIENT_ID:your_wechat_client_id}
|
||||
client-secret: ${WECHAT_CLIENT_SECRET:your_wechat_client_secret}
|
||||
redirect-uri: ${WECHAT_REDIRECT_URI:http://localhost:19001/oauth/callback/wechat}
|
||||
wechat-mp:
|
||||
client-id: ${WECHAT_MP_CLIENT_ID:your_wechat_mp_client_id}
|
||||
client-secret: ${WECHAT_MP_CLIENT_SECRET:your_wechat_mp_client_secret}
|
||||
redirect-uri: ${WECHAT_MP_REDIRECT_URI:http://localhost:19001/oauth/callback/wechat-mp}
|
||||
qq:
|
||||
client-id: ${QQ_CLIENT_ID:your_qq_client_id}
|
||||
client-secret: ${QQ_CLIENT_SECRET:your_qq_client_secret}
|
||||
redirect-uri: ${QQ_REDIRECT_URI:http://localhost:19001/oauth/callback/qq}
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
org.springframework.security: debug
|
||||
org.springframework.web: debug
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
com.baomidou.mybatisplus: debug
|
||||
com.alibaba.nacos: info
|
||||
file:
|
||||
name: logs/emotion-user-local.log
|
||||
|
||||
# 管理端点配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# 本地开发环境配置
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
username: nacos
|
||||
password: nacos
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
zone: local
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
username: nacos
|
||||
password: nacos
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
com.baomidou.mybatisplus: debug
|
||||
com.alibaba.nacos: info
|
||||
file:
|
||||
name: logs/emotion-user-local.log
|
||||
@@ -1,75 +1,55 @@
|
||||
server:
|
||||
port: 9001
|
||||
# 生产环境配置
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
|
||||
namespace: public
|
||||
server-addr: 47.111.10.27:8848
|
||||
namespace: prod
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
ip: ${SERVER_IP:localhost}
|
||||
username: nacos
|
||||
password: EmotionMuseum2025
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
environment: prod
|
||||
zone: prod
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: 47.111.10.27:8848
|
||||
namespace: prod
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
username: nacos
|
||||
password: EmotionMuseum2025
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:emotion_museum}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: ${MYSQL_USERNAME:emotion}
|
||||
password: ${MYSQL_PASSWORD:EmotionDB2024!}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
url: jdbc:mysql://47.111.10.27:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: EmotionMuseum2025*#
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
host: 47.111.10.27
|
||||
port: 6379
|
||||
password: EmotionMuseum2025*#
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000ms
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: input
|
||||
logic-delete-field: is_deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:mapper/*.xml
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: INFO
|
||||
com.baomidou.mybatisplus: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 管理端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
com.emotionmuseum: warn
|
||||
com.baomidou.mybatisplus: warn
|
||||
com.alibaba.nacos: error
|
||||
file:
|
||||
name: logs/emotion-user-prod.log
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# 测试环境配置
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 47.111.10.27:8848
|
||||
namespace: test
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
username: nacos
|
||||
password: EmotionMuseum2025
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
zone: test
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: 47.111.10.27:8848
|
||||
namespace: test
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
username: nacos
|
||||
password: EmotionMuseum2025
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://47.111.10.27:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: EmotionMuseum2025*#
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: 47.111.10.27
|
||||
port: 6379
|
||||
password: EmotionMuseum2025*#
|
||||
database: 0
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: info
|
||||
com.baomidou.mybatisplus: info
|
||||
com.alibaba.nacos: warn
|
||||
file:
|
||||
name: logs/emotion-user-test.log
|
||||
@@ -4,40 +4,74 @@ server:
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
|
||||
# 配置文件激活
|
||||
profiles:
|
||||
active: dev
|
||||
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||
|
||||
# 允许Bean覆盖和循环引用
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
allow-circular-references: true
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:emotion_museum}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:123456}
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
idle-timeout: 600000
|
||||
idle-timeout: 30000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
connection-test-query: SELECT 1
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
timeout: 10000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000ms
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# Nacos配置
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace: emotion-dev
|
||||
group: DEFAULT_GROUP
|
||||
enabled: false
|
||||
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
enabled: ${NACOS_DISCOVERY_ENABLED:true}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
zone: ${NACOS_ZONE:default}
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
enabled: false
|
||||
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
file-extension: yml
|
||||
enabled: ${NACOS_CONFIG_ENABLED:false}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
|
||||
@@ -2,18 +2,6 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.emotionmuseum.user.mapper.UserMapper">
|
||||
|
||||
<!-- 根据账号查询用户 -->
|
||||
<select id="selectByAccount" resultType="com.emotionmuseum.user.entity.User"> SELECT * FROM user
|
||||
WHERE account = #{account} AND is_deleted = 0 </select>
|
||||
|
||||
<!-- 根据邮箱查询用户 -->
|
||||
<select id="selectByEmail" resultType="com.emotionmuseum.user.entity.User"> SELECT * FROM user
|
||||
WHERE email = #{email} AND is_deleted = 0 </select>
|
||||
|
||||
<!-- 根据手机号查询用户 -->
|
||||
<select id="selectByPhone" resultType="com.emotionmuseum.user.entity.User"> SELECT * FROM user
|
||||
WHERE phone = #{phone} AND is_deleted = 0 </select>
|
||||
|
||||
<!-- 更新最后活跃时间 -->
|
||||
<update id="updateLastActiveTime"> UPDATE user SET last_active_time = NOW(), update_time = NOW()
|
||||
WHERE id = #{userId} AND is_deleted = 0 </update>
|
||||
|
||||
Reference in New Issue
Block a user