From 845530773110dffa65685f857f05e9fadf1e1eaa Mon Sep 17 00:00:00 2001 From: LingandRX Date: Sat, 26 Apr 2025 11:40:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化邮件验证逻辑,增加对 verifyCode 是否为空的判断 - 添加 log4j2 配置文件,实现日志记录功能 - 新增 WebSocket 相关配置和处理类 - 优化 TokenInterceptor 类,使用 Lombok 注解 - 新增 TraceIdAspect 方面类,用于添加 traceId 到日志 - 更新 RedisServiceImpl 类,简化 exists 方法的实现 - 新增 SchedulerConfig 配置类,用于配置定时任务 - 更新 SecurityConfig 配置类,添加跨域请求配置 --- qodana.yaml | 31 +++++++ .../aspect/PutTraceIdAspect.java | 21 +++++ .../copykamanotes/config/SchedulerConfig.java | 19 ++++ .../copykamanotes/config/SecurityConfig.java | 16 ++++ .../copykamanotes/config/WebConfig.java | 7 +- .../copykamanotes/config/WebSocketConfig.java | 41 +++++++++ .../copykamanotes/filter/TraceIdFilter.java | 2 +- .../interceptor/TokenInterceptor.java | 3 +- .../model/base/PaginationApiResponse.java | 17 ++-- .../service/impl/EmailServiceImpl.java | 8 +- .../service/impl/RedisServiceImpl.java | 2 +- .../copykamanotes/utils/MarkdownAST.java | 2 +- .../copykamanotes/utils/MarkdownUtils.java | 6 +- .../websocket/MessageWebSocketHandler.java | 87 +++++++++++++++++++ src/main/resources/log4j2-spring.xml | 45 ++++++++++ 15 files changed, 285 insertions(+), 22 deletions(-) create mode 100644 qodana.yaml create mode 100644 src/main/java/com/example/copykamanotes/aspect/PutTraceIdAspect.java create mode 100644 src/main/java/com/example/copykamanotes/config/SchedulerConfig.java create mode 100644 src/main/java/com/example/copykamanotes/config/WebSocketConfig.java create mode 100644 src/main/java/com/example/copykamanotes/websocket/MessageWebSocketHandler.java create mode 100644 src/main/resources/log4j2-spring.xml diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..faa34b8 --- /dev/null +++ b/qodana.yaml @@ -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: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +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 can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-jvm:2025.1 diff --git a/src/main/java/com/example/copykamanotes/aspect/PutTraceIdAspect.java b/src/main/java/com/example/copykamanotes/aspect/PutTraceIdAspect.java new file mode 100644 index 0000000..b037404 --- /dev/null +++ b/src/main/java/com/example/copykamanotes/aspect/PutTraceIdAspect.java @@ -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); + } + } +} diff --git a/src/main/java/com/example/copykamanotes/config/SchedulerConfig.java b/src/main/java/com/example/copykamanotes/config/SchedulerConfig.java new file mode 100644 index 0000000..035643e --- /dev/null +++ b/src/main/java/com/example/copykamanotes/config/SchedulerConfig.java @@ -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; + } +} diff --git a/src/main/java/com/example/copykamanotes/config/SecurityConfig.java b/src/main/java/com/example/copykamanotes/config/SecurityConfig.java index dcca82a..65ce69d 100644 --- a/src/main/java/com/example/copykamanotes/config/SecurityConfig.java +++ b/src/main/java/com/example/copykamanotes/config/SecurityConfig.java @@ -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() { diff --git a/src/main/java/com/example/copykamanotes/config/WebConfig.java b/src/main/java/com/example/copykamanotes/config/WebConfig.java index 13f34cc..4ee04a0 100644 --- a/src/main/java/com/example/copykamanotes/config/WebConfig.java +++ b/src/main/java/com/example/copykamanotes/config/WebConfig.java @@ -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; diff --git a/src/main/java/com/example/copykamanotes/config/WebSocketConfig.java b/src/main/java/com/example/copykamanotes/config/WebSocketConfig.java new file mode 100644 index 0000000..f45fac8 --- /dev/null +++ b/src/main/java/com/example/copykamanotes/config/WebSocketConfig.java @@ -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; + } +} diff --git a/src/main/java/com/example/copykamanotes/filter/TraceIdFilter.java b/src/main/java/com/example/copykamanotes/filter/TraceIdFilter.java index 8795002..4245d1c 100644 --- a/src/main/java/com/example/copykamanotes/filter/TraceIdFilter.java +++ b/src/main/java/com/example/copykamanotes/filter/TraceIdFilter.java @@ -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 { diff --git a/src/main/java/com/example/copykamanotes/interceptor/TokenInterceptor.java b/src/main/java/com/example/copykamanotes/interceptor/TokenInterceptor.java index 2ff072f..06389a6 100644 --- a/src/main/java/com/example/copykamanotes/interceptor/TokenInterceptor.java +++ b/src/main/java/com/example/copykamanotes/interceptor/TokenInterceptor.java @@ -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) { diff --git a/src/main/java/com/example/copykamanotes/model/base/PaginationApiResponse.java b/src/main/java/com/example/copykamanotes/model/base/PaginationApiResponse.java index db188e8..ea22149 100644 --- a/src/main/java/com/example/copykamanotes/model/base/PaginationApiResponse.java +++ b/src/main/java/com/example/copykamanotes/model/base/PaginationApiResponse.java @@ -1,10 +1,19 @@ package com.example.copykamanotes.model.base; +import lombok.Getter; + /** * PaginationApiResponse类用于处理分页的API响应。 * @param 泛型类型,表示分页数据类型。 */ +@Getter public class PaginationApiResponse extends ApiResponse { + /** + * -- GETTER -- + * 获取分页对象。 + * + * @return 分页对象 + */ // 分页对象 private final Pagination pagination; @@ -13,12 +22,4 @@ public class PaginationApiResponse extends ApiResponse { this.pagination = pagination; } - /** - * 获取分页对象。 - * - * @return 分页对象 - */ - public Pagination getPagination() { - return pagination; - } } diff --git a/src/main/java/com/example/copykamanotes/service/impl/EmailServiceImpl.java b/src/main/java/com/example/copykamanotes/service/impl/EmailServiceImpl.java index 07c29b2..3d36f65 100644 --- a/src/main/java/com/example/copykamanotes/service/impl/EmailServiceImpl.java +++ b/src/main/java/com/example/copykamanotes/service/impl/EmailServiceImpl.java @@ -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; } diff --git a/src/main/java/com/example/copykamanotes/service/impl/RedisServiceImpl.java b/src/main/java/com/example/copykamanotes/service/impl/RedisServiceImpl.java index 7989bae..8f023c9 100644 --- a/src/main/java/com/example/copykamanotes/service/impl/RedisServiceImpl.java +++ b/src/main/java/com/example/copykamanotes/service/impl/RedisServiceImpl.java @@ -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 diff --git a/src/main/java/com/example/copykamanotes/utils/MarkdownAST.java b/src/main/java/com/example/copykamanotes/utils/MarkdownAST.java index 3c9a8c1..db2c96b 100644 --- a/src/main/java/com/example/copykamanotes/utils/MarkdownAST.java +++ b/src/main/java/com/example/copykamanotes/utils/MarkdownAST.java @@ -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()) { diff --git a/src/main/java/com/example/copykamanotes/utils/MarkdownUtils.java b/src/main/java/com/example/copykamanotes/utils/MarkdownUtils.java index f884aa7..5e66525 100644 --- a/src/main/java/com/example/copykamanotes/utils/MarkdownUtils.java +++ b/src/main/java/com/example/copykamanotes/utils/MarkdownUtils.java @@ -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); } } diff --git a/src/main/java/com/example/copykamanotes/websocket/MessageWebSocketHandler.java b/src/main/java/com/example/copykamanotes/websocket/MessageWebSocketHandler.java new file mode 100644 index 0000000..3fa39f2 --- /dev/null +++ b/src/main/java/com/example/copykamanotes/websocket/MessageWebSocketHandler.java @@ -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 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); + } +} diff --git a/src/main/resources/log4j2-spring.xml b/src/main/resources/log4j2-spring.xml new file mode 100644 index 0000000..1601c8b --- /dev/null +++ b/src/main/resources/log4j2-spring.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +