feat:优化邮件验证功能并添加日志记录
- 优化邮件验证逻辑,增加对 verifyCode 是否为空的判断 - 添加 log4j2 配置文件,实现日志记录功能 - 新增 WebSocket 相关配置和处理类 - 优化 TokenInterceptor 类,使用 Lombok 注解 - 新增 TraceIdAspect 方面类,用于添加 traceId 到日志 - 更新 RedisServiceImpl 类,简化 exists 方法的实现 - 新增 SchedulerConfig 配置类,用于配置定时任务 - 更新 SecurityConfig 配置类,添加跨域请求配置
This commit is contained in:
parent
ae5b7cbd1a
commit
8455307731
31
qodana.yaml
Normal file
31
qodana.yaml
Normal 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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
45
src/main/resources/log4j2-spring.xml
Normal file
45
src/main/resources/log4j2-spring.xml
Normal 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>
|
||||
Reference in New Issue
Block a user