feat: 项目初始化及当前全部内容提交
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# 用户服务Dockerfile
|
||||
FROM openjdk:17-jdk-alpine
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的工具
|
||||
RUN apk add --no-cache curl tzdata && \
|
||||
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
# 复制Maven构建文件
|
||||
COPY pom.xml ./
|
||||
COPY emotion-common ./emotion-common
|
||||
COPY emotion-user ./emotion-user
|
||||
|
||||
# 安装Maven
|
||||
RUN apk add --no-cache maven
|
||||
|
||||
# 构建应用
|
||||
RUN mvn clean package -DskipTests -pl emotion-user -am -Dstart-class=com.emotionmuseum.user.UserApplication
|
||||
|
||||
# 创建运行用户
|
||||
RUN addgroup -g 1000 emotion && \
|
||||
adduser -D -s /bin/sh -u 1000 -G emotion emotion
|
||||
|
||||
# 复制jar文件
|
||||
RUN cp emotion-user/target/emotion-user-*.jar app.jar
|
||||
|
||||
# 设置文件权限
|
||||
RUN chown -R emotion:emotion /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER emotion
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:19001/actuator/health || exit 1
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 19001
|
||||
|
||||
# 启动命令
|
||||
ENTRYPOINT ["java", "-jar", \
|
||||
"-Xms512m", "-Xmx1024m", \
|
||||
"-Djava.security.egd=file:/dev/./urandom", \
|
||||
"-Dspring.profiles.active=local", \
|
||||
"app.jar"]
|
||||
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.emotionmuseum</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>emotion-user</artifactId>
|
||||
<name>emotion-user</name>
|
||||
<description>用户服务</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 内部模块依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.emotionmuseum</groupId>
|
||||
<artifactId>emotion-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud Discovery -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot DevTools for automatic restart -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Druid -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<dependency>
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
<artifactId>JustAuth</artifactId>
|
||||
<version>1.16.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>com.github.whvcse</groupId>
|
||||
<artifactId>easy-captcha</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 监控 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- API文档 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<configuration>
|
||||
<mainClass>com.emotionmuseum.user.UserApplication</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.emotionmuseum.user;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
|
||||
/**
|
||||
* 用户服务启动类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {"com.emotionmuseum"})
|
||||
@EnableDiscoveryClient
|
||||
@MapperScan("com.emotionmuseum.user.mapper")
|
||||
public class UserApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UserApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.emotionmuseum.user.config;
|
||||
|
||||
import com.wf.captcha.ArithmeticCaptcha;
|
||||
import com.wf.captcha.ChineseCaptcha;
|
||||
import com.wf.captcha.GifCaptcha;
|
||||
import com.wf.captcha.SpecCaptcha;
|
||||
import com.wf.captcha.base.Captcha;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 验证码配置
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Configuration
|
||||
public class CaptchaConfig {
|
||||
|
||||
/**
|
||||
* 算术验证码
|
||||
*/
|
||||
@Bean("arithmeticCaptcha")
|
||||
public Captcha arithmeticCaptcha() {
|
||||
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 48);
|
||||
captcha.setLen(2); // 几位数运算,默认是两位
|
||||
captcha.getArithmeticString(); // 获取运算的公式:3+2=?
|
||||
return captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文验证码
|
||||
*/
|
||||
@Bean("chineseCaptcha")
|
||||
public Captcha chineseCaptcha() {
|
||||
ChineseCaptcha captcha = new ChineseCaptcha(130, 48);
|
||||
captcha.setLen(4); // 几个汉字,默认5个
|
||||
return captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* GIF验证码
|
||||
*/
|
||||
@Bean("gifCaptcha")
|
||||
public Captcha gifCaptcha() {
|
||||
GifCaptcha captcha = new GifCaptcha(130, 48);
|
||||
captcha.setLen(4); // 几位数字,默认5位
|
||||
return captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* PNG验证码
|
||||
*/
|
||||
@Bean("specCaptcha")
|
||||
public Captcha specCaptcha() {
|
||||
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
|
||||
captcha.setCharType(Captcha.TYPE_DEFAULT); // 设置类型,纯数字、纯字母、字母数字混合
|
||||
return captcha;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.emotionmuseum.user.config;
|
||||
|
||||
import me.zhyd.oauth.config.AuthConfig;
|
||||
import me.zhyd.oauth.request.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 第三方登录配置
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Configuration
|
||||
public class OAuthConfig {
|
||||
|
||||
@Value("${oauth.wechat.client-id:}")
|
||||
private String wechatClientId;
|
||||
|
||||
@Value("${oauth.wechat.client-secret:}")
|
||||
private String wechatClientSecret;
|
||||
|
||||
@Value("${oauth.wechat.redirect-uri:}")
|
||||
private String wechatRedirectUri;
|
||||
|
||||
@Value("${oauth.qq.client-id:}")
|
||||
private String qqClientId;
|
||||
|
||||
@Value("${oauth.qq.client-secret:}")
|
||||
private String qqClientSecret;
|
||||
|
||||
@Value("${oauth.qq.redirect-uri:}")
|
||||
private String qqRedirectUri;
|
||||
|
||||
@Value("${oauth.wechat-mp.client-id:}")
|
||||
private String wechatMpClientId;
|
||||
|
||||
@Value("${oauth.wechat-mp.client-secret:}")
|
||||
private String wechatMpClientSecret;
|
||||
|
||||
@Value("${oauth.wechat-mp.redirect-uri:}")
|
||||
private String wechatMpRedirectUri;
|
||||
|
||||
/**
|
||||
* 微信开放平台登录
|
||||
*/
|
||||
@Bean
|
||||
public AuthWeChatOpenRequest weChatOpenRequest() {
|
||||
return new AuthWeChatOpenRequest(AuthConfig.builder()
|
||||
.clientId(wechatClientId)
|
||||
.clientSecret(wechatClientSecret)
|
||||
.redirectUri(wechatRedirectUri)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信公众平台登录
|
||||
*/
|
||||
@Bean
|
||||
public AuthWeChatMpRequest weChatMpRequest() {
|
||||
return new AuthWeChatMpRequest(AuthConfig.builder()
|
||||
.clientId(wechatMpClientId)
|
||||
.clientSecret(wechatMpClientSecret)
|
||||
.redirectUri(wechatMpRedirectUri)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* QQ登录
|
||||
*/
|
||||
@Bean
|
||||
public AuthQqRequest qqRequest() {
|
||||
return new AuthQqRequest(AuthConfig.builder()
|
||||
.clientId(qqClientId)
|
||||
.clientSecret(qqClientSecret)
|
||||
.redirectUri(qqRedirectUri)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.emotionmuseum.user.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis配置类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* 配置RedisTemplate
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 使用String序列化器作为key的序列化器
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
|
||||
// 使用JSON序列化器作为value的序列化器
|
||||
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
template.setValueSerializer(jsonRedisSerializer);
|
||||
template.setHashValueSerializer(jsonRedisSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.emotionmuseum.user.config;
|
||||
|
||||
import com.emotionmuseum.user.security.JwtAuthenticationFilter;
|
||||
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.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;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Spring Security配置类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*/
|
||||
@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配置
|
||||
*/
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOriginPatterns(Collections.singletonList("*"));
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Collections.singletonList("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全过滤器链
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用CSRF
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 配置CORS
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
|
||||
// 配置会话管理
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
|
||||
// 配置授权规则
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
// 公开接口
|
||||
.requestMatchers(
|
||||
"/user/register",
|
||||
"/user/login",
|
||||
"/user/refresh",
|
||||
"/user/check/**",
|
||||
"/captcha/**",
|
||||
"/oauth/**")
|
||||
.permitAll()
|
||||
|
||||
// 监控和文档接口
|
||||
.requestMatchers(
|
||||
"/actuator/**",
|
||||
"/swagger-ui/**",
|
||||
"/v3/api-docs/**",
|
||||
"/doc.html",
|
||||
"/swagger-resources/**",
|
||||
"/webjars/**",
|
||||
"/error")
|
||||
.permitAll()
|
||||
|
||||
// 其他接口需要认证
|
||||
.anyRequest().authenticated())
|
||||
|
||||
// 配置认证提供者
|
||||
.authenticationProvider(authenticationProvider())
|
||||
|
||||
// 添加JWT过滤器
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
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
@@ -0,0 +1,59 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
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;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
@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(
|
||||
@Parameter(description = "用户ID") @PathVariable String userId) {
|
||||
log.info("获取用户信息: {}", userId);
|
||||
UserInfoResponse response = userService.getUserInfo(userId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新用户信息")
|
||||
@PutMapping("/info/{userId}")
|
||||
public Result<UserInfoResponse> updateUserInfo(
|
||||
@Parameter(description = "用户ID") @PathVariable String userId,
|
||||
@Valid @RequestBody UserUpdateRequest request) {
|
||||
log.info("更新用户信息: {}", userId);
|
||||
UserInfoResponse response = userService.updateUserInfo(userId, request);
|
||||
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(
|
||||
@Parameter(description = "用户ID") @PathVariable String userId) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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
@@ -0,0 +1,37 @@
|
||||
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
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.emotionmuseum.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 用户信息更新请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "用户信息更新请求")
|
||||
public class UserUpdateRequest {
|
||||
|
||||
@Schema(description = "用户名", example = "新用户名")
|
||||
@Size(min = 2, max = 20, message = "用户名长度必须在2-20位之间")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "邮箱", example = "new@example.com")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号", example = "13900139000")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "头像URL", example = "https://example.com/avatar.jpg")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "昵称", example = "新昵称")
|
||||
@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;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.emotionmuseum.user.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.emotionmuseum.common.entity.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("user")
|
||||
public class User extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 账号
|
||||
*/
|
||||
@TableField("account")
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@TableField("password")
|
||||
@JsonIgnore
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@TableField("username")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@TableField("email")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@TableField("phone")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
@TableField("avatar")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@TableField("nickname")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
@TableField("birth_date")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate birthDate;
|
||||
|
||||
/**
|
||||
* 所在地
|
||||
*/
|
||||
@TableField("location")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 个人简介
|
||||
*/
|
||||
@TableField("bio")
|
||||
private String bio;
|
||||
|
||||
/**
|
||||
* 会员等级
|
||||
*/
|
||||
@TableField("member_level")
|
||||
private String memberLevel;
|
||||
|
||||
/**
|
||||
* 使用天数
|
||||
*/
|
||||
@TableField("total_days")
|
||||
private Integer totalDays;
|
||||
|
||||
/**
|
||||
* 自我感知
|
||||
*/
|
||||
@TableField("self_awareness")
|
||||
private BigDecimal selfAwareness;
|
||||
|
||||
/**
|
||||
* 情绪韧性
|
||||
*/
|
||||
@TableField("emotional_resilience")
|
||||
private BigDecimal emotionalResilience;
|
||||
|
||||
/**
|
||||
* 行动力
|
||||
*/
|
||||
@TableField("action_power")
|
||||
private BigDecimal actionPower;
|
||||
|
||||
/**
|
||||
* 共情力
|
||||
*/
|
||||
@TableField("empathy")
|
||||
private BigDecimal empathy;
|
||||
|
||||
/**
|
||||
* 生活热度
|
||||
*/
|
||||
@TableField("life_enthusiasm")
|
||||
private BigDecimal lifeEnthusiasm;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用,1-正常
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 是否已验证:0-未验证,1-已验证
|
||||
*/
|
||||
@TableField("is_verified")
|
||||
private Integer isVerified;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
@TableField("last_active_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime lastActiveTime;
|
||||
|
||||
/**
|
||||
* 第三方平台ID
|
||||
*/
|
||||
@TableField("third_party_id")
|
||||
private String thirdPartyId;
|
||||
|
||||
/**
|
||||
* 第三方平台类型
|
||||
*/
|
||||
@TableField("third_party_type")
|
||||
private String thirdPartyType;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.emotionmuseum.user.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotionmuseum.user.entity.User;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 用户Mapper
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@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);
|
||||
|
||||
/**
|
||||
* 更新最后活跃时间
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateLastActiveTime(@Param("userId") String userId);
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package com.emotionmuseum.user.security;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.emotionmuseum.common.util.JwtUtil;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final UserDetailsService userDetailsService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String TOKEN_PREFIX = "Bearer ";
|
||||
private static final String HEADER_NAME = "Authorization";
|
||||
private static final String REDIS_TOKEN_KEY_PREFIX = "auth:token:";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
try {
|
||||
String token = extractTokenFromRequest(request);
|
||||
|
||||
if (StrUtil.isNotBlank(token) && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
// 验证token有效性
|
||||
if (jwtUtil.validateToken(token)) {
|
||||
String userId = jwtUtil.getUserIdFromToken(token);
|
||||
|
||||
// 检查Redis中是否存在该token(用于登出功能)
|
||||
String redisKey = REDIS_TOKEN_KEY_PREFIX + userId;
|
||||
String redisToken = (String) redisTemplate.opsForValue().get(redisKey);
|
||||
|
||||
if (StrUtil.isNotBlank(redisToken) && redisToken.equals(token)) {
|
||||
// 加载用户详情
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
|
||||
|
||||
// 创建认证对象
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
// 设置到安全上下文
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
// 更新token在Redis中的过期时间
|
||||
redisTemplate.expire(redisKey, 24, TimeUnit.HOURS);
|
||||
|
||||
log.debug("JWT认证成功,用户ID: {}", userId);
|
||||
} else {
|
||||
log.debug("Redis中未找到有效token,用户ID: {}", userId);
|
||||
}
|
||||
} else {
|
||||
log.debug("JWT token无效");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("JWT认证过程中发生错误: {}", e.getMessage());
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取token
|
||||
*/
|
||||
private String extractTokenFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(HEADER_NAME);
|
||||
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
|
||||
return bearerToken.substring(TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否跳过JWT认证
|
||||
*/
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
|
||||
// 跳过认证的路径
|
||||
return path.startsWith("/user/register") ||
|
||||
path.startsWith("/user/login") ||
|
||||
path.startsWith("/user/refresh") ||
|
||||
path.startsWith("/user/check/") ||
|
||||
path.startsWith("/captcha/") ||
|
||||
path.startsWith("/oauth/") ||
|
||||
path.startsWith("/actuator/") ||
|
||||
path.startsWith("/swagger-ui/") ||
|
||||
path.startsWith("/v3/api-docs") ||
|
||||
path.startsWith("/doc.html") ||
|
||||
path.equals("/error");
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.emotionmuseum.user.security;
|
||||
|
||||
import com.emotionmuseum.user.entity.User;
|
||||
import com.emotionmuseum.user.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Spring Security用户详情服务实现
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-15
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
|
||||
log.debug("加载用户详情,用户ID: {}", userId);
|
||||
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
log.warn("用户不存在,用户ID: {}", userId);
|
||||
throw new UsernameNotFoundException("用户不存在: " + userId);
|
||||
}
|
||||
|
||||
if (user.getStatus() == 0) {
|
||||
log.warn("用户已被禁用,用户ID: {}", userId);
|
||||
throw new UsernameNotFoundException("用户已被禁用: " + userId);
|
||||
}
|
||||
|
||||
return new SecurityUser(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security用户详情实现类
|
||||
*/
|
||||
public static class SecurityUser implements UserDetails {
|
||||
|
||||
private final User user;
|
||||
|
||||
public SecurityUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
// 这里可以根据用户角色返回权限
|
||||
// 目前简单返回一个默认角色
|
||||
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return user.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return user.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户账号
|
||||
*/
|
||||
public String getAccount() {
|
||||
return user.getAccount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户昵称
|
||||
*/
|
||||
public String getNickname() {
|
||||
return user.getNickname();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户实体
|
||||
*/
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return user.getStatus() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return user.getStatus() == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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
@@ -0,0 +1,35 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 用户服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
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获取用户信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
UserInfoResponse getUserInfo(String userId);
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param request 更新请求
|
||||
* @return 用户信息
|
||||
*/
|
||||
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
@@ -0,0 +1,119 @@
|
||||
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
@@ -0,0 +1,182 @@
|
||||
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
@@ -0,0 +1,210 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@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());
|
||||
}
|
||||
return convertToUserInfoResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
BeanUtils.copyProperties(request, user, "id", "account", "password");
|
||||
updateById(user);
|
||||
|
||||
log.info("用户信息更新成功: {}", userId);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为用户信息响应
|
||||
*/
|
||||
private UserInfoResponse convertToUserInfoResponse(User user) {
|
||||
UserInfoResponse response = new UserInfoResponse();
|
||||
BeanUtils.copyProperties(user, response);
|
||||
|
||||
// 设置成长数据
|
||||
UserInfoResponse.GrowthStatsVO growthStats = new UserInfoResponse.GrowthStatsVO();
|
||||
growthStats.setSelfAwareness(user.getSelfAwareness());
|
||||
growthStats.setEmotionalResilience(user.getEmotionalResilience());
|
||||
growthStats.setActionPower(user.getActionPower());
|
||||
growthStats.setEmpathy(user.getEmpathy());
|
||||
growthStats.setLifeEnthusiasm(user.getLifeEnthusiasm());
|
||||
response.setGrowthStats(growthStats);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.emotionmuseum.user.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户信息响应
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "用户信息响应")
|
||||
public class UserInfoResponse {
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "账号")
|
||||
private String account;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "邮箱")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "头像URL")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "昵称")
|
||||
private String nickname;
|
||||
|
||||
@Schema(description = "生日")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate birthDate;
|
||||
|
||||
@Schema(description = "所在地")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "个人简介")
|
||||
private String bio;
|
||||
|
||||
@Schema(description = "会员等级")
|
||||
private String memberLevel;
|
||||
|
||||
@Schema(description = "使用天数")
|
||||
private Integer totalDays;
|
||||
|
||||
@Schema(description = "成长数据")
|
||||
private GrowthStatsVO growthStats;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "是否已验证")
|
||||
private Integer isVerified;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "最后活跃时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime lastActiveTime;
|
||||
|
||||
/**
|
||||
* 成长数据VO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "成长数据")
|
||||
public static class GrowthStatsVO {
|
||||
|
||||
@Schema(description = "自我感知")
|
||||
private BigDecimal selfAwareness;
|
||||
|
||||
@Schema(description = "情绪韧性")
|
||||
private BigDecimal emotionalResilience;
|
||||
|
||||
@Schema(description = "行动力")
|
||||
private BigDecimal actionPower;
|
||||
|
||||
@Schema(description = "共情力")
|
||||
private BigDecimal empathy;
|
||||
|
||||
@Schema(description = "生活热度")
|
||||
private BigDecimal lifeEnthusiasm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# 用户服务 Docker环境配置
|
||||
server:
|
||||
port: 9001
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
profiles:
|
||||
active: docker
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
config:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
file-extension: yml
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
pool-name: EmotionUserHikariCP
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
auto-commit: true
|
||||
idle-timeout: 30000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:redis}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password:
|
||||
database: 2
|
||||
timeout: 6000ms
|
||||
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
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: assign_uuid
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
banner: false
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: DEBUG
|
||||
com.emotionmuseum.user.mapper: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
|
||||
|
||||
# 管理端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,102 @@
|
||||
server:
|
||||
port: 19001
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
|
||||
# 数据源配置
|
||||
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
|
||||
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"
|
||||
file:
|
||||
name: logs/emotion-user-local.log
|
||||
|
||||
# 管理端点配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,75 @@
|
||||
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
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
ip: ${SERVER_IP:localhost}
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
environment: prod
|
||||
config:
|
||||
enabled: false
|
||||
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
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
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
|
||||
@@ -0,0 +1,94 @@
|
||||
server:
|
||||
port: 19001
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
profiles:
|
||||
active: dev
|
||||
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
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000ms
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace: emotion-dev
|
||||
group: DEFAULT_GROUP
|
||||
enabled: false
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: assign_uuid
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# 监控配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
com.baomidou.mybatisplus: debug
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: emotion-museum-secret-key-2025
|
||||
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:9001/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:9001/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:9001/oauth/callback/qq}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!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>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,80 @@
|
||||
# 用户服务 Docker环境配置
|
||||
server:
|
||||
port: 9001
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
profiles:
|
||||
active: docker
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
config:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
file-extension: yml
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
pool-name: EmotionUserHikariCP
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
auto-commit: true
|
||||
idle-timeout: 30000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:redis}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password:
|
||||
database: 2
|
||||
timeout: 6000ms
|
||||
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
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: assign_uuid
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
banner: false
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: DEBUG
|
||||
com.emotionmuseum.user.mapper: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
|
||||
|
||||
# 管理端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,102 @@
|
||||
server:
|
||||
port: 19001
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
|
||||
# 数据源配置
|
||||
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
|
||||
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"
|
||||
file:
|
||||
name: logs/emotion-user-local.log
|
||||
|
||||
# 管理端点配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,75 @@
|
||||
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
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
ip: ${SERVER_IP:localhost}
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
environment: prod
|
||||
config:
|
||||
enabled: false
|
||||
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
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
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
|
||||
@@ -0,0 +1,94 @@
|
||||
server:
|
||||
port: 19001
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-user
|
||||
profiles:
|
||||
active: dev
|
||||
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
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000ms
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace: emotion-dev
|
||||
group: DEFAULT_GROUP
|
||||
enabled: false
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: assign_uuid
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# 监控配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
com.baomidou.mybatisplus: debug
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: emotion-museum-secret-key-2025
|
||||
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:9001/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:9001/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:9001/oauth/callback/qq}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!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>
|
||||
|
||||
</mapper>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
com/emotionmuseum/user/vo/LoginResponse.class
|
||||
com/emotionmuseum/user/config/SecurityConfig.class
|
||||
com/emotionmuseum/user/config/RedisConfig.class
|
||||
com/emotionmuseum/user/service/impl/SliderCaptchaServiceImpl.class
|
||||
com/emotionmuseum/user/vo/UserInfoResponse.class
|
||||
com/emotionmuseum/user/config/OAuthConfig.class
|
||||
com/emotionmuseum/user/dto/UserUpdateRequest.class
|
||||
com/emotionmuseum/user/dto/OAuthLoginRequest.class
|
||||
com/emotionmuseum/user/dto/SliderCaptchaResponse.class
|
||||
com/emotionmuseum/user/controller/OAuthController.class
|
||||
com/emotionmuseum/user/UserApplication.class
|
||||
com/emotionmuseum/user/service/impl/OAuthServiceImpl.class
|
||||
com/emotionmuseum/user/dto/SliderCaptchaVerifyRequest.class
|
||||
com/emotionmuseum/user/service/impl/SliderCaptchaServiceImpl$SliderCaptchaData.class
|
||||
com/emotionmuseum/user/service/UserService.class
|
||||
com/emotionmuseum/user/service/impl/CaptchaServiceImpl.class
|
||||
com/emotionmuseum/user/entity/User.class
|
||||
com/emotionmuseum/user/service/impl/UserServiceImpl.class
|
||||
com/emotionmuseum/user/security/UserDetailsServiceImpl.class
|
||||
com/emotionmuseum/user/dto/CaptchaResponse.class
|
||||
com/emotionmuseum/user/service/CaptchaService.class
|
||||
com/emotionmuseum/user/security/UserDetailsServiceImpl$SecurityUser.class
|
||||
com/emotionmuseum/user/dto/RegisterRequest.class
|
||||
com/emotionmuseum/user/config/CaptchaConfig.class
|
||||
com/emotionmuseum/user/service/OAuthService.class
|
||||
com/emotionmuseum/user/controller/CaptchaController.class
|
||||
com/emotionmuseum/user/dto/LoginRequest.class
|
||||
com/emotionmuseum/user/controller/UserController.class
|
||||
com/emotionmuseum/user/security/JwtAuthenticationFilter.class
|
||||
com/emotionmuseum/user/service/SliderCaptchaService.class
|
||||
com/emotionmuseum/user/vo/UserInfoResponse$GrowthStatsVO.class
|
||||
com/emotionmuseum/user/mapper/UserMapper.class
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/config/SecurityConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/vo/LoginResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/dto/RegisterRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/service/impl/CaptchaServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/security/JwtAuthenticationFilter.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/dto/CaptchaResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/config/CaptchaConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/service/UserService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/service/OAuthService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/dto/SliderCaptchaVerifyRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/service/SliderCaptchaService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/service/impl/OAuthServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/service/impl/SliderCaptchaServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/vo/UserInfoResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/entity/User.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/mapper/UserMapper.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/security/UserDetailsServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/UserApplication.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/dto/LoginRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/controller/OAuthController.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/dto/OAuthLoginRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/service/impl/UserServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/service/CaptchaService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/controller/UserController.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/dto/SliderCaptchaResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/config/OAuthConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/config/RedisConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/dto/UserUpdateRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-user/src/main/java/com/emotionmuseum/user/controller/CaptchaController.java
|
||||
Reference in New Issue
Block a user