Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-01-19 20:14:51 +08:00
22 changed files with 794 additions and 105 deletions

7
.gitignore vendored
View File

@@ -39,4 +39,9 @@ build/
### mybatis plus generator
/generator/
/generator/
### Uploads ###
/uploads/
### Logs ###
/logs/

View File

@@ -6,7 +6,7 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "user-service", path = "/api/v1/user")
@FeignClient(name = "aioj-user-service", path = "/api/v1/user")
public interface UserClient {
@GetMapping("/inner/get-by-username")

View File

@@ -1,6 +1,6 @@
spring:
application:
name: auth-service
name: aioj-auth-service
data:
redis:
host: 10.0.0.10

View File

@@ -1,6 +1,6 @@
spring:
application:
name: auth-service
name: aioj-auth-service
profiles:
active: @env@
devtools:
@@ -36,4 +36,7 @@ jwt:
enabled: true
secret: "12345678901234567890123456789012" # 至少32字节
access-expire: 900000 # 24小时
refresh-expire: 604800000 # 7天
refresh-expire: 604800000 # 7天
logging:
file:
path: ./logs/${spring.application.name}

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--debug="false" 表示关闭 Logback 框架自身的内部状态信息打印。-->
<configuration scan="true" scanPeriod="10 seconds" debug="false">
<!-- 引入 spring boot 默认日志颜色和基础配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义变量 APP_NAME从 Spring 环境变量中获取 spring.application.name 的值-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 定义时间格式yyyy-MM 表示年-yyyy-MM-dd 表示年---->
<timestamp key="time-month" datePattern="yyyy-MM"/>
<timestamp key="time-month-day" datePattern="yyyy-MM-dd"/>
<!-- 定义变量 LOG_FILE_PATH默认值为 ./logs/${APP_NAME},可以通过环境变量 LOG_PATH 覆盖 日志存储路径-->
<property name="LOG_FILE_PATH" value="${LOG_PATH:-./logs/${APP_NAME}}"/>
<!-- 定义日志格式
格式说明:
%d{yyyy-MM-dd HH:mm:ss.SSS}:日志记录时间,格式为年--日 时:分:秒.毫秒
[%thread]:日志记录线程名称
%-5level日志级别左对齐占用 5 个字符宽度
%logger{50}:日志记录器名称,最多显示 50 个字符
-[%X{traceId:-}]:从 MDC 中获取 traceId 变量值,如果不存在则显示为空
%msg%n日志消息内容换行符
-->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n"/>
<!-- appender 控制台输出-->
<!--
<appender>: 这是 Logback 配置的根标签之一,用于定义一个日志输出目的地。
name="CONSOLE": 为这个 Appender 赋予一个唯一的名称,方便在 <root> 或 <logger> 标签中引用它。
class="ch.qos.logback.core.ConsoleAppender": 指定使用的实现类。
ConsoleAppender 是 Logback 库中专门用于将日志事件写入 System.out (标准输出) 或 System.err (标准错误) 的类。
这是我们在本地开发和测试时最常用的 Appender。
CONSOLE_LOG_PATTERN 是上面include的默认日志格式这里直接引用即可
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 全量info日志-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 只记录 INFO 级别以及以上的日志的日志 -->
<level>INFO</level>
</filter>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 只记录 ERROR 级别以及以上的日志的日志 -->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步写入日志-->
<appender name="ASYNC_INFO"
class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="INFO_FILE"/>
</appender>
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="ERROR_FILE"/>
</appender>
<!-- =========== 环境配置 打印到控制台 ===========-->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
</configuration>

View File

@@ -1,6 +1,6 @@
spring:
application:
name: file-service
name: aioj-file-service
profiles:
active: @env@
servlet:

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--debug="false" 表示关闭 Logback 框架自身的内部状态信息打印。-->
<configuration scan="true" scanPeriod="10 seconds" debug="false">
<!-- 引入 spring boot 默认日志颜色和基础配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义变量 APP_NAME从 Spring 环境变量中获取 spring.application.name 的值-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 定义时间格式yyyy-MM 表示年-yyyy-MM-dd 表示年---->
<timestamp key="time-month" datePattern="yyyy-MM"/>
<timestamp key="time-month-day" datePattern="yyyy-MM-dd"/>
<!-- 定义变量 LOG_FILE_PATH默认值为 ./logs/${APP_NAME},可以通过环境变量 LOG_PATH 覆盖 日志存储路径-->
<property name="LOG_FILE_PATH" value="${LOG_PATH:-./logs/${APP_NAME}}"/>
<!-- 定义日志格式
格式说明:
%d{yyyy-MM-dd HH:mm:ss.SSS}:日志记录时间,格式为年--日 时:分:秒.毫秒
[%thread]:日志记录线程名称
%-5level日志级别左对齐占用 5 个字符宽度
%logger{50}:日志记录器名称,最多显示 50 个字符
-[%X{traceId:-}]:从 MDC 中获取 traceId 变量值,如果不存在则显示为空
%msg%n日志消息内容换行符
-->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n"/>
<!-- appender 控制台输出-->
<!--
<appender>: 这是 Logback 配置的根标签之一,用于定义一个日志输出目的地。
name="CONSOLE": 为这个 Appender 赋予一个唯一的名称,方便在 <root> 或 <logger> 标签中引用它。
class="ch.qos.logback.core.ConsoleAppender": 指定使用的实现类。
ConsoleAppender 是 Logback 库中专门用于将日志事件写入 System.out (标准输出) 或 System.err (标准错误) 的类。
这是我们在本地开发和测试时最常用的 Appender。
CONSOLE_LOG_PATTERN 是上面include的默认日志格式这里直接引用即可
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 全量info日志-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 只记录 INFO 级别以及以上的日志的日志 -->
<level>INFO</level>
</filter>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 只记录 ERROR 级别以及以上的日志的日志 -->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步写入日志-->
<appender name="ASYNC_INFO"
class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="INFO_FILE"/>
</appender>
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="ERROR_FILE"/>
</appender>
<!-- =========== 环境配置 打印到控制台 ===========-->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
</configuration>

View File

@@ -105,7 +105,7 @@ public class AuthGlobalFilter implements GlobalFilter, Ordered {
private Mono<Boolean> validateToken(String token) {
return webClientBuilder.build()
.post()
.uri("lb://auth-service/api/v1/auth/validate")
.uri("lb://aioj-auth-service/api/v1/auth/validate")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.retrieve()

View File

@@ -2,8 +2,9 @@ server:
port: 18085
error:
include-stacktrace: never
spring:
application:
name: aioj-backend-gateway
profiles:
active: @env@
cloud:
@@ -13,28 +14,28 @@ spring:
routes:
# auth服务 Swagger 文档路由
- id: auth-service-doc
uri: lb://auth-service
uri: lb://aioj-auth-service
predicates:
- Path=/auth-service/**
filters:
- StripPrefix=1
# user服务 Swagger 文档路由
- id: user-service-doc
uri: lb://user-service
uri: lb://aioj-user-service
predicates:
- Path=/user-service/**
filters:
- StripPrefix=1
# auth服务 Swagger 文档路由
- id: file-service-doc
uri: lb://file-service
uri: lb://aioj-file-service
predicates:
- Path=/file-service/**
filters:
- StripPrefix=1
# auth业务接口
# auth业务接口路由
- id: auth-service
uri: lb://auth-service
uri: lb://aioj-auth-service
predicates:
- Path=/api/v1/auth/**
filters:
@@ -46,7 +47,7 @@ spring:
firstBackoff: 50ms
maxBackoff: 500ms
- id: user-service
uri: lb://user-service
uri: lb://aioj-user-service
predicates:
- Path=/api/v1/user/**
filters:
@@ -58,7 +59,7 @@ spring:
firstBackoff: 50ms
maxBackoff: 500ms
- id: file-service
uri: lb://file-service
uri: lb://aioj-file-service
predicates:
- Path=/api/v1/file/**
filters:
@@ -71,13 +72,14 @@ spring:
maxBackoff: 500ms
# 文件访问路由(公开,直接转发不去前缀)
- id: file-access
uri: lb://file-service
uri: lb://aioj-file-service
predicates:
- Path=/api/file/**
# 设置应用启动后的就绪探针
lifecycle:
timeout-per-shutdown-phase: 30s
aioj-backend-gateway:
# 白名单配置
white-list:
@@ -93,3 +95,6 @@ aioj:
log:
enabled: true
max-length: 20000
logging:
file:
path: ./logs/${spring.application.name}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--debug="false" 表示关闭 Logback 框架自身的内部状态信息打印。-->
<configuration scan="true" scanPeriod="10 seconds" debug="false">
<!-- 引入 spring boot 默认日志颜色和基础配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义变量 APP_NAME从 Spring 环境变量中获取 spring.application.name 的值-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 定义时间格式yyyy-MM 表示年-yyyy-MM-dd 表示年---->
<timestamp key="time-month" datePattern="yyyy-MM"/>
<timestamp key="time-month-day" datePattern="yyyy-MM-dd"/>
<!-- 定义变量 LOG_FILE_PATH默认值为 ./logs/${APP_NAME},可以通过环境变量 LOG_PATH 覆盖 日志存储路径-->
<property name="LOG_FILE_PATH" value="${LOG_PATH:-./logs/${APP_NAME}}"/>
<!-- 定义日志格式
格式说明:
%d{yyyy-MM-dd HH:mm:ss.SSS}:日志记录时间,格式为年--日 时:分:秒.毫秒
[%thread]:日志记录线程名称
%-5level日志级别左对齐占用 5 个字符宽度
%logger{50}:日志记录器名称,最多显示 50 个字符
-[%X{traceId:-}]:从 MDC 中获取 traceId 变量值,如果不存在则显示为空
%msg%n日志消息内容换行符
-->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n"/>
<!-- appender 控制台输出-->
<!--
<appender>: 这是 Logback 配置的根标签之一,用于定义一个日志输出目的地。
name="CONSOLE": 为这个 Appender 赋予一个唯一的名称,方便在 <root> 或 <logger> 标签中引用它。
class="ch.qos.logback.core.ConsoleAppender": 指定使用的实现类。
ConsoleAppender 是 Logback 库中专门用于将日志事件写入 System.out (标准输出) 或 System.err (标准错误) 的类。
这是我们在本地开发和测试时最常用的 Appender。
CONSOLE_LOG_PATTERN 是上面include的默认日志格式这里直接引用即可
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 全量info日志-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 只记录 INFO 级别以及以上的日志的日志 -->
<level>INFO</level>
</filter>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 只记录 ERROR 级别以及以上的日志的日志 -->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步写入日志-->
<appender name="ASYNC_INFO"
class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="INFO_FILE"/>
</appender>
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="ERROR_FILE"/>
</appender>
<!-- =========== 环境配置 打印到控制台 ===========-->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
<appender name="USER_LOG" class="ch.qos.logback.core.FileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/auth.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name="cn.meowrain.aioj.backend.gateway.filter.AuthGlobalFilter">
<appender-ref ref="USER_LOG"/>
</logger>
</configuration>

View File

@@ -0,0 +1,40 @@
package cn.meowrain.aioj.backend.userservice.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步配置类
*
* @author meowrain
* @since 2026-01-18
*/
@Slf4j
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("emailExecutor")
public Executor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(2);
// 最大线程数
executor.setMaxPoolSize(5);
// 队列容量
executor.setQueueCapacity(100);
// 线程名前缀
executor.setThreadNamePrefix("email-async-");
// 拒绝策略:由调用线程执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
log.info("邮件异步线程池初始化完成: coreSize=2, maxSize=5, queueCapacity=100");
return executor;
}
}

View File

@@ -1,13 +1,10 @@
package cn.meowrain.aioj.backend.userservice.controller;
import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.userservice.dto.req.AvatarUpdateRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.req.BindEmailRequest;
import cn.meowrain.aioj.backend.userservice.dto.req.EmailSendCodeRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.req.*;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserProfileRespDTO;
import cn.meowrain.aioj.backend.userservice.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -15,7 +12,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RequiredArgsConstructor
@RestController()
@@ -60,7 +56,7 @@ public class UserController {
@Operation(summary = "发送验证码", description = "根据用户注册的邮箱发送验证码")
@GetMapping("/email/send-code")
public Result<Void> getVerifyCode(@Parameter(description = "邮箱信息", required = true)
@Valid @RequestParam EmailSendCodeRequestDTO request) {
@Valid @ModelAttribute EmailSendCodeRequestDTO request) {
userService.sendEmailCode(request.getEmail());
return Results.success(null);
}
@@ -73,8 +69,8 @@ public class UserController {
*/
@Operation(summary = "绑定邮箱", description = "根据用户注册的邮箱绑定邮箱")
@PostMapping("/email/bind")
public Result<Void> bindEmail(@RequestBody BindEmailRequest request) {
userService.bindEmail(request.getEmail(), request.getCode());
public Result<Void> bindEmail(@RequestBody @Valid BindEmailRequest request) {
userService.bindEmail(request.getEmail(), request.getVerifyCode());
return Results.success(null);
}
@@ -86,20 +82,22 @@ public class UserController {
@Operation(summary = "解绑邮箱", description = "根据用户注册的邮箱解绑邮箱")
@PostMapping("/email/unbind")
public Result<Void> unbindEmail() {
userService.unbindEmail(ContextHolderUtils.getCurrentUserId());
userService.unbindEmail();
return Results.success(null);
}
@Operation(summary = "个人资料管理", description = "获取完整个人资料")
@GetMapping("/profile")
public Result<Void> getUserProfile() {
return Results.success();
public Result<UserProfileRespDTO> getUserProfile() {
UserProfileRespDTO userProfileRespDTO = userService.getUserProfile();
return Results.success(userProfileRespDTO);
}
@Operation(summary = "个人资料管理-更新个人资料", description = "更新个人资料")
@PutMapping("/profile")
public Result<Void> updateUserProfile() {
public Result<Void> updateUserProfile(@RequestBody UserProfileUpdateRequestDTO dto) {
userService.updateUserProfile(dto);
return Results.success();
}
@@ -110,8 +108,10 @@ public class UserController {
return Results.success();
}
@PutMapping("/password")
@Operation(summary = "个人资料管理-修改密码",description = "修改密码")
public Result<Void> changePassword() {
public Result<Void> changePassword(@RequestBody ChangePasswordRequestDTO dto) {
userService.changePassword(dto);
return Results.success();
}
}

View File

@@ -12,5 +12,5 @@ public class BindEmailRequest {
@Schema(description = "邮箱",example = "123@qq.com")
private String email;
@Schema(description = "验证码",example = "123456")
private String code;
private String verifyCode;
}

View File

@@ -0,0 +1,13 @@
package cn.meowrain.aioj.backend.userservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "修改密码请求参数")
public class ChangePasswordRequestDTO {
@Schema(description = "旧密码")
private String oldPassword;
@Schema(description = "新密码")
private String newPassword;
}

View File

@@ -0,0 +1,13 @@
package cn.meowrain.aioj.backend.userservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户个人资料更新请求DTO")
public class UserProfileUpdateRequestDTO {
@Schema(description = "用户昵称")
private String userName;
@Schema(description = "用户简介")
private String userProfile;
}

View File

@@ -0,0 +1,10 @@
package cn.meowrain.aioj.backend.userservice.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户个人资料响应DTO")
public class UserProfileRespDTO {
}

View File

@@ -10,4 +10,11 @@ public interface EmailService {
* @param email 收件人邮箱
*/
void sendVerifyCode(String email);
/**
* 获取邮箱验证码
* @param email 收件人邮箱
* @return 验证码
*/
String getVerifyCode(String email);
}

View File

@@ -2,8 +2,11 @@ package cn.meowrain.aioj.backend.userservice.service;
import cn.meowrain.aioj.backend.userservice.dao.entity.User;
import cn.meowrain.aioj.backend.userservice.dto.req.ChangePasswordRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.req.UserProfileUpdateRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserProfileRespDTO;
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
@@ -42,7 +45,7 @@ public interface UserService extends IService<User> {
/**
* 解绑邮箱
*/
void unbindEmail(Long userId);
void unbindEmail();
/**
* 设置用户头像
@@ -50,4 +53,19 @@ public interface UserService extends IService<User> {
* @return
*/
void setProfileAvatar(Long fileId);
/**
* 修改密码
*/
void changePassword(ChangePasswordRequestDTO dto);
/**
* 更新用户个人资料
*/
void updateUserProfile(UserProfileUpdateRequestDTO dto);
/**
* 获取用户个人资料
*/
UserProfileRespDTO getUserProfile();
}

View File

@@ -1,5 +1,6 @@
package cn.meowrain.aioj.backend.userservice.service.impl;
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
import cn.meowrain.aioj.backend.framework.core.exception.ServiceException;
import cn.meowrain.aioj.backend.userservice.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.userservice.service.EmailService;
@@ -11,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.UnsupportedEncodingException;
@@ -95,6 +97,7 @@ public class EmailServiceImpl implements EmailService {
}
@Async("emailExecutor")
@Override
public void sendVerifyCode(String email) {
// 生成验证码
@@ -124,4 +127,22 @@ public class EmailServiceImpl implements EmailService {
}
}
/**
* 获取邮箱验证码
* @param email 收件人邮箱
* @return 验证码
*/
@Override
public String getVerifyCode(String email) {
String redisKey = String.format(RedisKeyConstants.EMAIL_CODE_PREFIX,email);
// 从redis里面获取验证码,用Object接收,因为redis里面可能存储的是null 比如过期这种情况
Object verifyCodeInSystem = redisTemplate.opsForValue().get(redisKey);
if(verifyCodeInSystem == null) {
throw new ClientException("验证码不存在或已过期");
}
// 转换为字符串
return verifyCodeInSystem.toString();
}
}

View File

@@ -10,8 +10,11 @@ import cn.meowrain.aioj.backend.userservice.dao.entity.User;
import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper;
import cn.meowrain.aioj.backend.userservice.dto.chains.context.UserRegisterRequestParamVerifyContext;
import cn.meowrain.aioj.backend.userservice.dto.req.ChangePasswordRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.req.UserProfileUpdateRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserProfileRespDTO;
import cn.meowrain.aioj.backend.userservice.service.EmailService;
import cn.meowrain.aioj.backend.userservice.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -29,90 +32,187 @@ import java.util.Date;
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
private final UserRegisterRequestParamVerifyContext userRegisterRequestParamVerifyContext;
private final UserRegisterRequestParamVerifyContext userRegisterRequestParamVerifyContext;
private final EmailService emailService;
private final EmailService emailService;
@Override
public Long userRegister(UserRegisterRequestDTO request) {
UserAuthRespDTO authInfoByUserAccount = findAuthInfoByUserAccount(request.getUserAccount());
if (authInfoByUserAccount != null) {
throw new ClientException("重复创建用户");
}
@Override
public Long userRegister(UserRegisterRequestDTO request) {
UserAuthRespDTO authInfoByUserAccount = findAuthInfoByUserAccount(request.getUserAccount());
if (authInfoByUserAccount != null) {
throw new ClientException("重复创建用户");
}
log.info("进行用户注册");
userRegisterRequestParamVerifyContext.handler(ChainMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(),
request);
// 使用 BCrypt 加密密码
Date now = new Date();
String salt = BCrypt.gensalt();
String encryptPassword = BCrypt.hashpw(request.getUserPassword(), salt);
User user = new User().setUserAccount(request.getUserAccount())
.setUserPassword(encryptPassword)
.setUserRole("user")
.setCreateTime(now)
.setUpdateTime(now);
try {
// 需要修改表,使得用户名是唯一的
this.save(user);
}
catch (DuplicateKeyException e) {
log.error("重复创建用户");
throw new ServiceException("用户名已存在", ErrorCode.SYSTEM_ERROR);
}
log.info("进行用户注册");
userRegisterRequestParamVerifyContext.handler(ChainMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(),
request);
// 使用 BCrypt 加密密码
Date now = new Date();
String salt = BCrypt.gensalt();
String encryptPassword = BCrypt.hashpw(request.getUserPassword(), salt);
User user = new User().setUserAccount(request.getUserAccount())
.setUserPassword(encryptPassword)
.setUserRole("user")
.setCreateTime(now)
.setUpdateTime(now);
try {
// 需要修改表,使得用户名是唯一的
this.save(user);
} catch (DuplicateKeyException e) {
log.error("重复创建用户");
throw new ServiceException("用户名已存在", ErrorCode.SYSTEM_ERROR);
}
return user.getId();
}
return user.getId();
}
@Override
public UserAuthRespDTO findAuthInfoByUserAccount(String userAccount) {
User one = this.lambdaQuery().eq(User::getUserAccount, userAccount).one();
UserAuthRespDTO userAuthDTO = new UserAuthRespDTO();
if (one != null) {
BeanUtils.copyProperties(one, userAuthDTO);
return userAuthDTO;
}
return null;
}
@Override
public UserAuthRespDTO findAuthInfoByUserAccount(String userAccount) {
User one = this.lambdaQuery().eq(User::getUserAccount, userAccount).one();
UserAuthRespDTO userAuthDTO = new UserAuthRespDTO();
if (one != null) {
BeanUtils.copyProperties(one, userAuthDTO);
return userAuthDTO;
}
return null;
}
@Override
public UserAuthRespDTO findAuthInfoByUserId(Long userId) {
User one = this.lambdaQuery().eq(User::getId, userId).one();
UserAuthRespDTO userAuthDTO = new UserAuthRespDTO();
if (one != null) {
BeanUtils.copyProperties(one, userAuthDTO);
return userAuthDTO;
}
return null;
@Override
public UserAuthRespDTO findAuthInfoByUserId(Long userId) {
User one = this.lambdaQuery().eq(User::getId, userId).one();
UserAuthRespDTO userAuthDTO = new UserAuthRespDTO();
if (one != null) {
BeanUtils.copyProperties(one, userAuthDTO);
return userAuthDTO;
}
return null;
}
}
@Override
public void sendEmailCode(String email) {
emailService.sendVerifyCode(email);
}
@Override
public void sendEmailCode(String email) {
emailService.sendVerifyCode(email);
}
@Override
public void bindEmail(String email, String code) {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
@Override
public void bindEmail(String email, String code) {
if (email == null || code == null) {
throw new ClientException("邮箱或验证码不能为空");
}
Long currentUserId = ContextHolderUtils.getCurrentUserId();
// 检查用户是否存在
User user = this.lambdaQuery().eq(User::getId, currentUserId).one();
if (user == null) {
throw new ClientException("用户不存在");
}
}
// 验证邮箱验证码
// 从redis里面获取验证码
String verifyCodeInSystem = emailService.getVerifyCode(email);
// 验证验证码是否匹配
if (!verifyCodeInSystem.equals(code)) {
throw new ClientException("验证码错误,请重新输入");
}
@Override
public void unbindEmail(Long userId) {
User one = this.lambdaQuery().eq(User::getId, userId).one();
// 绑定邮箱
user.setUserEmail(email);
user.setUpdateTime(new Date());
this.updateById(user);
}
}
@Override
public void unbindEmail() {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
User user = this.lambdaQuery().eq(User::getId, currentUserId).one();
if (user == null) {
throw new ClientException("用户不存在");
}
@Transactional(rollbackFor = Exception.class)
@Override
public void setProfileAvatar(Long fileId) {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
User user = this.lambdaQuery().eq(User::getId, currentUserId).one();
user.setUserAvatar(fileId);
user.setUpdateTime(new Date());
this.updateById(user);
}
if (user.getUserEmail() == null) {
throw new ClientException("邮箱未绑定");
}
// 这种方法避免了查询整个对象,并且能精确控制 NULL 值的更新。
boolean success = this.lambdaUpdate()
// 设置 userEmail 为 NULL
.set(User::getUserEmail, null)
// 设置 updateTime 为当前时间
.set(User::getUpdateTime, new Date())
// 筛选条件用户ID
.eq(User::getId, currentUserId)
// 执行更新
.update();
if (!success) {
throw new ClientException("解绑失败,请重试");
}
}
@Transactional(rollbackFor = Exception.class)
@Override
public void setProfileAvatar(Long fileId) {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
User user = this.lambdaQuery().eq(User::getId, currentUserId).one();
user.setUserAvatar(fileId);
user.setUpdateTime(new Date());
this.updateById(user);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void changePassword(ChangePasswordRequestDTO dto) {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
User user = this.lambdaQuery().eq(User::getId, currentUserId).one();
// 检查用户是否存在
if (user == null) {
throw new ClientException("用户不存在");
}
// 检查旧密码是否正确
String oldPassword = user.getUserPassword();
if (!BCrypt.checkpw(dto.getOldPassword(), oldPassword)) {
throw new ClientException("旧密码错误");
}
// 检查新密码是否与旧密码相同
if (BCrypt.checkpw(dto.getNewPassword(), oldPassword)) {
throw new ClientException("新密码不能与旧密码相同");
}
// 加密新密码
Date now = new Date();
String salt = BCrypt.gensalt();
String encryptPassword = BCrypt.hashpw(dto.getNewPassword(), salt);
// 更新用户密码
user.setUserPassword(encryptPassword);
user.setUpdateTime(now);
this.updateById(user);
}
@Override
public void updateUserProfile(UserProfileUpdateRequestDTO dto) {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
User user = this.lambdaQuery().eq(User::getId, currentUserId).one();
if (user == null) {
throw new ClientException("用户不存在");
}
// 更新用户个人资料
user.setUserName(dto.getUserName());
user.setUserProfile(dto.getUserProfile());
user.setUpdateTime(new Date());
this.updateById(user);
}
/**
* 获取用户个人资料
* @return 用户个人资料
*/
@Override
public UserProfileRespDTO getUserProfile() {
// TODO: 待实现 从数据库查询用户个人资料
return null;
}
}

View File

@@ -1,6 +1,6 @@
spring:
application:
name: user-service
name: aioj-user-service
profiles:
active: @env@
server:
@@ -42,4 +42,7 @@ jwt:
aioj:
log:
enabled: true
max-length: 20000
max-length: 20000
logging:
file:
path: ./logs/${spring.application.name}

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--debug="false" 表示关闭 Logback 框架自身的内部状态信息打印。-->
<configuration scan="true" scanPeriod="10 seconds" debug="false">
<!-- 引入 spring boot 默认日志颜色和基础配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义变量 APP_NAME从 Spring 环境变量中获取 spring.application.name 的值-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 定义时间格式yyyy-MM 表示年-yyyy-MM-dd 表示年---->
<timestamp key="time-month" datePattern="yyyy-MM"/>
<timestamp key="time-month-day" datePattern="yyyy-MM-dd"/>
<!-- 定义变量 LOG_FILE_PATH默认值为 ./logs/${APP_NAME},可以通过环境变量 LOG_PATH 覆盖 日志存储路径-->
<property name="LOG_FILE_PATH" value="${LOG_PATH:-./logs/${APP_NAME}}"/>
<!-- 定义日志格式
格式说明:
%d{yyyy-MM-dd HH:mm:ss.SSS}:日志记录时间,格式为年--日 时:分:秒.毫秒
[%thread]:日志记录线程名称
%-5level日志级别左对齐占用 5 个字符宽度
%logger{50}:日志记录器名称,最多显示 50 个字符
-[%X{traceId:-}]:从 MDC 中获取 traceId 变量值,如果不存在则显示为空
%msg%n日志消息内容换行符
-->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n"/>
<!-- appender 控制台输出-->
<!--
<appender>: 这是 Logback 配置的根标签之一,用于定义一个日志输出目的地。
name="CONSOLE": 为这个 Appender 赋予一个唯一的名称,方便在 <root> 或 <logger> 标签中引用它。
class="ch.qos.logback.core.ConsoleAppender": 指定使用的实现类。
ConsoleAppender 是 Logback 库中专门用于将日志事件写入 System.out (标准输出) 或 System.err (标准错误) 的类。
这是我们在本地开发和测试时最常用的 Appender。
CONSOLE_LOG_PATTERN 是上面include的默认日志格式这里直接引用即可
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 全量info日志-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 只记录 INFO 级别以及以上的日志的日志 -->
<level>INFO</level>
</filter>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 只记录 ERROR 级别以及以上的日志的日志 -->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步写入日志-->
<appender name="ASYNC_INFO"
class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="INFO_FILE"/>
</appender>
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="ERROR_FILE"/>
</appender>
<!-- =========== 环境配置 打印到控制台 ===========-->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
</configuration>