feat:优化邮件验证功能并添加日志记录

- 优化邮件验证逻辑,增加对 verifyCode 是否为空的判断
- 添加 log4j2 配置文件,实现日志记录功能
- 新增 WebSocket 相关配置和处理类
- 优化 TokenInterceptor 类,使用 Lombok 注解
- 新增 TraceIdAspect 方面类,用于添加 traceId 到日志
- 更新 RedisServiceImpl 类,简化 exists 方法的实现
- 新增 SchedulerConfig 配置类,用于配置定时任务
- 更新 SecurityConfig 配置类,添加跨域请求配置
This commit is contained in:
LingandRX 2025-04-26 11:40:54 +08:00
parent ae5b7cbd1a
commit 8455307731
15 changed files with 285 additions and 22 deletions

31
qodana.yaml Normal file
View File

@ -0,0 +1,31 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
projectJDK: "17" #(Applied in CI/CD pipeline)
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-jvm:2025.1

View File

@ -0,0 +1,21 @@
package com.example.copykamanotes.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Aspect
@Component
public class PutTraceIdAspect {
private static final String TRACE_ID_KEY = "traceId";
@Before("execution(* com.example.copykamanotes..*(..))")
public void addTraceIdToLog() {
if (MDC.get(TRACE_ID_KEY) == null) {
String traceId = UUID.randomUUID().toString();
MDC.put(TRACE_ID_KEY, traceId);
}
}
}

View File

@ -0,0 +1,19 @@
package com.example.copykamanotes.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("task-");
return scheduler;
}
}

View File

@ -9,6 +9,9 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@ -29,6 +32,19 @@ public class SecurityConfig {
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder() {

View File

@ -1,7 +1,7 @@
package com.example.copykamanotes.config;
import com.example.copykamanotes.filter.TraceIdFilter;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@ -14,9 +14,10 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.example.copykamanotes.interceptor.TokenInterceptor;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;
private final TokenInterceptor tokenInterceptor;
@Value("${upload.path:D:/kamaNotes/upload")
private String uploadPath;

View File

@ -0,0 +1,41 @@
package com.example.copykamanotes.config;
import com.example.copykamanotes.utils.JwtUtil;
import com.example.copykamanotes.websocket.MessageWebSocketHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
@Configuration
@EnableWebSocket
public class WebSocketConfig {
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
public WebSocketConfig(JwtUtil jwtUtil, ObjectMapper objectMapper) {
this.jwtUtil = jwtUtil;
this.objectMapper = objectMapper;
}
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(messageWebSocketHandler(), "ws/message")
.setAllowedOrigins("*");
}
@Bean
public MessageWebSocketHandler messageWebSocketHandler() {
return new MessageWebSocketHandler(jwtUtil, objectMapper);
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}

View File

@ -11,7 +11,7 @@ import jakarta.servlet.ServletResponse;
import java.io.IOException;
public class TraceIdFilter implements Filter {
private static final String TRACE_ID_KEY = "trace_id";
private static final String TRACE_ID_KEY = "traceId";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

View File

@ -5,6 +5,7 @@ import com.example.copykamanotes.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@ -19,7 +20,7 @@ public class TokenInterceptor implements HandlerInterceptor {
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null) {

View File

@ -1,10 +1,19 @@
package com.example.copykamanotes.model.base;
import lombok.Getter;
/**
* PaginationApiResponse类用于处理分页的API响应
* @param <T> 泛型类型表示分页数据类型
*/
@Getter
public class PaginationApiResponse<T> extends ApiResponse<T> {
/**
* -- GETTER --
* 获取分页对象
*
* @return 分页对象
*/
// 分页对象
private final Pagination pagination;
@ -13,12 +22,4 @@ public class PaginationApiResponse<T> extends ApiResponse<T> {
this.pagination = pagination;
}
/**
* 获取分页对象
*
* @return 分页对象
*/
public Pagination getPagination() {
return pagination;
}
}

View File

@ -81,15 +81,17 @@ public class EmailServiceImpl implements EmailService {
return false;
}
if (verifyCode.getExpiredAt().isBefore(LocalDateTime.now())) {
if (verifyCode != null && verifyCode.getExpiredAt().isBefore(LocalDateTime.now())) {
return false;
}
if (!verifyCode.getCode().equals(code)) {
if (verifyCode != null && !verifyCode.getCode().equals(code)) {
return false;
}
emailVerifyCodeMapper.markAsUsed(verifyCode.getId());
if (verifyCode != null) {
emailVerifyCodeMapper.markAsUsed(verifyCode.getId());
}
return true;
}

View File

@ -32,7 +32,7 @@ public class RedisServiceImpl implements RedisService {
@Override
public boolean exists(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
return redisTemplate.hasKey(key);
}
@Override

View File

@ -73,7 +73,7 @@ public class MarkdownAST {
StringBuilder text = new StringBuilder();
if (node instanceof Text) {
text.append(((Text) node).getChars());
text.append((node).getChars());
}
for (Node child : node.getChildren()) {

View File

@ -2,12 +2,10 @@ package com.example.copykamanotes.utils;
public class MarkdownUtils {
public static boolean needCollapsed(String markdown) {
MarkdownAST ast = new MarkdownAST(markdown);
return ast.shouldCollapse(250);
return MarkdownUtil.needCollapsed(markdown);
}
public static String extractIntroduction(String markdown) {
MarkdownAST ast = new MarkdownAST(markdown);
return ast.extractIntroduction(250);
return MarkdownUtil.extractIntroduction(markdown);
}
}

View File

@ -0,0 +1,87 @@
package com.example.copykamanotes.websocket;
import com.example.copykamanotes.utils.JwtUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@RequiredArgsConstructor
public class MessageWebSocketHandler extends TextWebSocketHandler {
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
private static final Map<Long, WebSocketSession> USER_SESSIONS = new ConcurrentHashMap<>();
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Long userId = getUserIdFromSession(session);
if (userId != null) {
log.info("User disconnected: {}", userId);
USER_SESSIONS.put(userId, session);
}
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
try {
String messageText = (String) message.getPayload();
log.debug("Received message: {}", messageText);
Long userId = getUserIdFromSession(session);
} catch (Exception e) {
log.error("Error handling message: {}", message.getPayload(), e);
}
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Long userId = getUserIdFromSession(session);
if (userId != null) {
log.info("用户[{}]断开WebSocket连接", userId);
USER_SESSIONS.remove(userId);
}
}
public void sendMessage(Long userId, String message) {
WebSocketSession session = USER_SESSIONS.get(userId);
if (session != null && session.isOpen()) {
try {
String messageText = objectMapper.writeValueAsString(message);
session.sendMessage(new TextMessage(messageText));
log.debug("Message sent to user: {}", userId);
} catch (Exception e) {
log.error("Error sending message to user: {}", userId, e);
}
}
}
public Long getUserIdFromSession(WebSocketSession session) {
try {
String token = session.getHandshakeHeaders().getFirst("Authorization");
if (token != null && token.startsWith("Bearer ")) {
String jwtToken = token.substring(7);
return jwtUtil.getUserIdFromToken(jwtToken);
}
} catch (Exception e) {
log.error("Error getting user ID from session: {}", session.getId(), e);
}
return null;
}
public int getOnlineUserCount() {
return USER_SESSIONS.size();
}
public boolean isUserOnline(Long userId) {
return USER_SESSIONS.containsKey(userId);
}
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<!--
status="WARN": Log4j2 自身的日志输出级别(仅用来调试日志系统本身的配置)
monitorInterval="30": 每隔 30 秒自动检测配置文件是否有修改,若有修改会重新加载
-->
<Appenders>
<!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout
pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%highlight{%-5level}] [%thread] [traceId=%X{traceId}] %logger{36} - %msg%n" />
</Console>
<!-- 轮盘日志 -->
<RollingFile name="File"
fileName="logs/app.log"
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout
pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%highlight{%-5level}] [%thread] [traceId=%X{traceId}] %logger{36} - %msg%n" />
<Policies>
<!-- 时间滚动策略,每日创建一个日志文件 -->
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<!-- 大小滚动策略,单个文件达到 10MB 切分 -->
<SizeBasedTriggeringPolicy size="10MB" />
</Policies>
<!-- 控制保留多少历史文件,超过数量会自动删除 -->
<DefaultRolloverStrategy max="30" />
</RollingFile>
</Appenders>
<!-- 2. 配置日志记录器Logger -->
<Loggers>
<!-- 指定某些包或类的日志级别,如果需要可以继续添加 -->
<Logger name="com.kama" level="debug" additivity="false">
<AppenderRef ref="Console" />
<AppenderRef ref="File" />
</Logger>
<!-- 3. Root Logger默认日志级别为 info -->
<Root level="info">
<AppenderRef ref="Console" />
<AppenderRef ref="File" />
</Root>
</Loggers>
</Configuration>