Browse Source

日志优化

skyline 1 tháng trước cách đây
mục cha
commit
b416045096
21 tập tin đã thay đổi với 936 bổ sung49 xóa
  1. 22 0
      haha-admin/src/main/java/com/haha/admin/aspect/OperationLogAspect.java
  2. 78 0
      haha-admin/src/main/java/com/haha/admin/config/AsyncConfig.java
  3. 87 18
      haha-admin/src/main/java/com/haha/admin/config/GlobalExceptionHandler.java
  4. 34 0
      haha-admin/src/main/java/com/haha/admin/config/WebMvcConfig.java
  5. 8 0
      haha-admin/src/main/java/com/haha/admin/task/StatisticsTask.java
  6. 22 0
      haha-admin/src/main/java/com/haha/admin/task/TimedDiscountTask.java
  7. 9 0
      haha-admin/src/main/resources/application.yml
  8. 2 2
      haha-admin/src/main/resources/logback-spring.xml
  9. 14 0
      haha-common/pom.xml
  10. 9 7
      haha-common/src/main/java/com/haha/common/config/CommonConfig.java
  11. 30 0
      haha-common/src/main/java/com/haha/common/config/LogProperties.java
  12. 34 0
      haha-common/src/main/java/com/haha/common/config/MdcTaskDecorator.java
  13. 34 0
      haha-common/src/main/java/com/haha/common/constant/LogConstants.java
  14. 248 0
      haha-common/src/main/java/com/haha/common/interceptor/RequestLogInterceptor.java
  15. 100 0
      haha-common/src/main/java/com/haha/common/utils/SensitiveDataUtils.java
  16. 44 0
      haha-common/src/main/java/com/haha/common/utils/TraceIdUtils.java
  17. 5 0
      haha-entity/src/main/java/com/haha/entity/OperationLog.java
  18. 87 18
      haha-miniapp/src/main/java/com/haha/miniapp/config/GlobalExceptionHandler.java
  19. 34 0
      haha-miniapp/src/main/java/com/haha/miniapp/config/WebMvcConfig.java
  20. 9 0
      haha-miniapp/src/main/resources/application.yml
  21. 26 4
      haha-miniapp/src/main/resources/logback-spring.xml

+ 22 - 0
haha-admin/src/main/java/com/haha/admin/aspect/OperationLogAspect.java

@@ -4,6 +4,7 @@ import cn.dev33.satoken.stp.StpUtil;
 import com.alibaba.fastjson2.JSON;
 import com.haha.admin.utils.IpUtils;
 import com.haha.common.annotation.Log;
+import com.haha.common.utils.TraceIdUtils;
 import com.haha.entity.OperationLog;
 import com.haha.service.OperationLogService;
 import jakarta.servlet.http.HttpServletRequest;
@@ -14,6 +15,7 @@ import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.AfterThrowing;
 import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
 import org.aspectj.lang.annotation.Pointcut;
 import org.aspectj.lang.reflect.MethodSignature;
 import org.springframework.scheduling.annotation.Async;
@@ -49,6 +51,14 @@ public class OperationLogAspect {
     public void logPointcut() {
     }
 
+    /**
+     * 方法执行前记录开始时间
+     */
+    @Before("logPointcut()")
+    public void doBefore(JoinPoint joinPoint) {
+        START_TIME.set(System.currentTimeMillis());
+    }
+
     /**
      * 方法执行后记录日志
      */
@@ -82,6 +92,18 @@ public class OperationLogAspect {
             // 创建日志对象
             OperationLog operationLog = new OperationLog();
             
+            // 设置TraceId
+            String traceId = TraceIdUtils.getTraceId();
+            operationLog.setTraceId(traceId);
+            
+            // 计算处理时长
+            Long startTime = START_TIME.get();
+            if (startTime != null) {
+                long duration = System.currentTimeMillis() - startTime;
+                operationLog.setCostTime(duration);
+                START_TIME.remove();
+            }
+            
             // 设置基本信息
             operationLog.setModule(logAnnotation.module());
             operationLog.setOperationType(logAnnotation.operation().getDescription());

+ 78 - 0
haha-admin/src/main/java/com/haha/admin/config/AsyncConfig.java

@@ -0,0 +1,78 @@
+package com.haha.admin.config;
+
+import com.haha.common.config.MdcTaskDecorator;
+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;
+
+/**
+ * 异步任务配置类
+ * 配置异步线程池,支持 MDC 上下文传递
+ */
+@Configuration
+@EnableAsync
+public class AsyncConfig {
+
+    /**
+     * 核心线程数
+     */
+    private static final int CORE_POOL_SIZE = 5;
+
+    /**
+     * 最大线程数
+     */
+    private static final int MAX_POOL_SIZE = 20;
+
+    /**
+     * 队列容量
+     */
+    private static final int QUEUE_CAPACITY = 100;
+
+    /**
+     * 线程名前缀
+     */
+    private static final String THREAD_NAME_PREFIX = "async-task-";
+
+    /**
+     * 线程空闲时间(秒)
+     */
+    private static final int KEEP_ALIVE_SECONDS = 60;
+
+    /**
+     * 配置异步任务执行器
+     * 使用 MdcTaskDecorator 确保 MDC 上下文能够传递到子线程
+     */
+    @Bean("taskExecutor")
+    public Executor taskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        
+        // 核心线程数
+        executor.setCorePoolSize(CORE_POOL_SIZE);
+        // 最大线程数
+        executor.setMaxPoolSize(MAX_POOL_SIZE);
+        // 队列容量
+        executor.setQueueCapacity(QUEUE_CAPACITY);
+        // 线程名前缀
+        executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
+        // 线程空闲时间
+        executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
+        
+        // 设置任务装饰器,传递 MDC 上下文
+        executor.setTaskDecorator(new MdcTaskDecorator());
+        
+        // 拒绝策略:由调用线程处理
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        
+        // 等待所有任务完成后再关闭线程池
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        // 等待时间
+        executor.setAwaitTerminationSeconds(60);
+        
+        executor.initialize();
+        return executor;
+    }
+}

+ 87 - 18
haha-admin/src/main/java/com/haha/admin/config/GlobalExceptionHandler.java

@@ -2,7 +2,9 @@ package com.haha.admin.config;
 
 import cn.dev33.satoken.exception.NotLoginException;
 import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.stp.StpUtil;
 import com.haha.common.exception.BusinessException;
+import com.haha.common.utils.TraceIdUtils;
 import com.haha.common.vo.Result;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
@@ -15,6 +17,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
 
+import jakarta.servlet.http.HttpServletRequest;
 import java.util.stream.Collectors;
 
 /**
@@ -29,8 +32,13 @@ public class GlobalExceptionHandler {
      * 处理业务异常
      */
     @ExceptionHandler(BusinessException.class)
-    public Result<Void> handleBusinessException(BusinessException e) {
-        log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
+    public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 业务异常 - 路径: {}, 异常代码: {}, 异常消息: {}", 
+                traceId, requestPath, e.getCode(), e.getMessage());
+        
         return Result.error(e.getCode(), e.getMessage());
     }
 
@@ -39,8 +47,13 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(NotLoginException.class)
     @ResponseStatus(HttpStatus.UNAUTHORIZED)
-    public Result<Void> handleNotLoginException(NotLoginException e) {
-        log.warn("未登录异常: {}", e.getMessage());
+    public Result<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 未登录异常 - 路径: {}, 异常类型: {}", 
+                traceId, requestPath, e.getType());
+        
         String message;
         if (e.getType().equals(NotLoginException.NOT_TOKEN)) {
             message = "未提供token";
@@ -59,8 +72,13 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(NotPermissionException.class)
     @ResponseStatus(HttpStatus.FORBIDDEN)
-    public Result<Void> handleNotPermissionException(NotPermissionException e) {
-        log.warn("无权限异常: {}", e.getMessage());
+    public Result<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 无权限异常 - 路径: {}, 需要权限: {}", 
+                traceId, requestPath, e.getPermission());
+        
         return Result.error(403, "无访问权限");
     }
 
@@ -69,11 +87,21 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(MethodArgumentNotValidException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+    public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        String fieldErrors = e.getBindingResult().getFieldErrors().stream()
+                .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
+                .collect(Collectors.joining("; "));
+        
         String message = e.getBindingResult().getFieldErrors().stream()
                 .map(FieldError::getDefaultMessage)
                 .collect(Collectors.joining(", "));
-        log.warn("参数校验失败: {}", message);
+        
+        log.warn("[TraceId: {}] 参数校验失败 - 路径: {}, 校验失败字段: {}", 
+                traceId, requestPath, fieldErrors);
+        
         return Result.error(400, message);
     }
 
@@ -82,11 +110,21 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(BindException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleBindException(BindException e) {
+    public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        String fieldErrors = e.getBindingResult().getFieldErrors().stream()
+                .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
+                .collect(Collectors.joining("; "));
+        
         String message = e.getBindingResult().getFieldErrors().stream()
                 .map(FieldError::getDefaultMessage)
                 .collect(Collectors.joining(", "));
-        log.warn("参数绑定失败: {}", message);
+        
+        log.warn("[TraceId: {}] 参数绑定失败 - 路径: {}, 绑定失败字段: {}", 
+                traceId, requestPath, fieldErrors);
+        
         return Result.error(400, message);
     }
 
@@ -95,8 +133,13 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(MissingServletRequestParameterException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
-        log.warn("缺少必需参数: {}", e.getParameterName());
+    public Result<Void> handleMissingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 缺少必需参数 - 路径: {}, 参数名: {}, 参数类型: {}", 
+                traceId, requestPath, e.getParameterName(), e.getParameterType());
+        
         return Result.error(400, "缺少必需参数: " + e.getParameterName());
     }
 
@@ -105,8 +148,14 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(MethodArgumentTypeMismatchException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
-        log.warn("参数类型不匹配: {}={}", e.getName(), e.getValue());
+    public Result<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 参数类型不匹配 - 路径: {}, 参数名: {}, 参数值: {}, 需要类型: {}", 
+                traceId, requestPath, e.getName(), e.getValue(), 
+                e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "未知");
+        
         return Result.error(400, "参数类型错误: " + e.getName());
     }
 
@@ -115,8 +164,13 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(IllegalArgumentException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
-        log.warn("非法参数: {}", e.getMessage());
+    public Result<Void> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 非法参数 - 路径: {}, 异常消息: {}", 
+                traceId, requestPath, e.getMessage());
+        
         return Result.error(400, e.getMessage());
     }
 
@@ -125,8 +179,23 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(Exception.class)
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
-    public Result<Void> handleException(Exception e) {
-        log.error("系统异常: {}", e.getMessage(), e);
+    public Result<Void> handleException(Exception e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        // 尝试获取当前登录用户信息
+        String userInfo = "未登录";
+        try {
+            if (StpUtil.isLogin()) {
+                userInfo = "用户ID: " + StpUtil.getLoginIdAsString();
+            }
+        } catch (Exception ex) {
+            // 忽略获取用户信息时的异常
+        }
+        
+        log.error("[TraceId: {}] 系统异常 - 路径: {}, 用户信息: {}, 异常消息: {}", 
+                traceId, requestPath, userInfo, e.getMessage(), e);
+        
         return Result.error(500, "系统繁忙,请稍后重试");
     }
 }

+ 34 - 0
haha-admin/src/main/java/com/haha/admin/config/WebMvcConfig.java

@@ -0,0 +1,34 @@
+package com.haha.admin.config;
+
+import com.haha.common.config.LogProperties;
+import com.haha.common.constant.LogConstants;
+import com.haha.common.interceptor.RequestLogInterceptor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * Web MVC 配置类
+ * 用于配置拦截器、跨域、静态资源等
+ *
+ * @author haha
+ */
+@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    private final RequestLogInterceptor requestLogInterceptor;
+
+    private final LogProperties logProperties;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册请求日志拦截器
+        if (logProperties.getEnableRequestLog()) {
+            registry.addInterceptor(requestLogInterceptor)
+                    .addPathPatterns("/**")
+                    .excludePathPatterns(LogConstants.EXCLUDE_PATHS);
+        }
+    }
+}

+ 8 - 0
haha-admin/src/main/java/com/haha/admin/task/StatisticsTask.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.haha.entity.*;
 import com.haha.mapper.*;
 import com.haha.common.constant.OrderConstants;
+import com.haha.common.utils.TraceIdUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
@@ -53,6 +54,10 @@ public class StatisticsTask {
 
     @Scheduled(cron = "0 5 0 * * ?")
     public void aggregateDailyStatistics() {
+        // 生成独立的 TraceId 用于日志追踪
+        String traceId = TraceIdUtils.generateTraceId();
+        TraceIdUtils.setTraceId(traceId);
+        
         LocalDate yesterday = LocalDate.now().minusDays(1);
         log.info("开始执行每日统计汇总任务,统计日期:{}", yesterday);
         
@@ -66,6 +71,9 @@ public class StatisticsTask {
             log.info("每日统计汇总任务执行完成");
         } catch (Exception e) {
             log.error("每日统计汇总任务执行失败", e);
+        } finally {
+            // 清理 MDC 上下文
+            TraceIdUtils.removeTraceId();
         }
     }
 

+ 22 - 0
haha-admin/src/main/java/com/haha/admin/task/TimedDiscountTask.java

@@ -1,5 +1,6 @@
 package com.haha.admin.task;
 
+import com.haha.common.utils.TraceIdUtils;
 import com.haha.service.TimedDiscountService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -15,34 +16,55 @@ public class TimedDiscountTask {
 
     @Scheduled(cron = "0 0/5 * * * ?")
     public void executeTimedDiscount() {
+        // 生成独立的 TraceId 用于日志追踪
+        String traceId = TraceIdUtils.generateTraceId();
+        TraceIdUtils.setTraceId(traceId);
+        
         log.info("开始检查定时折扣活动执行");
         try {
             timedDiscountService.executeAllEnabledActivities();
             log.info("定时折扣活动检查完成");
         } catch (Exception e) {
             log.error("定时折扣活动执行失败", e);
+        } finally {
+            // 清理 MDC 上下文
+            TraceIdUtils.removeTraceId();
         }
     }
 
     @Scheduled(cron = "0 0 6 * * ?")
     public void restorePrices() {
+        // 生成独立的 TraceId 用于日志追踪
+        String traceId = TraceIdUtils.generateTraceId();
+        TraceIdUtils.setTraceId(traceId);
+        
         log.info("开始执行价格恢复任务");
         try {
             timedDiscountService.restorePrices();
             log.info("价格恢复任务执行完成");
         } catch (Exception e) {
             log.error("价格恢复任务执行失败", e);
+        } finally {
+            // 清理 MDC 上下文
+            TraceIdUtils.removeTraceId();
         }
     }
 
     @Scheduled(cron = "0 5 * * * ?")
     public void updateSalesStatistics() {
+        // 生成独立的 TraceId 用于日志追踪
+        String traceId = TraceIdUtils.generateTraceId();
+        TraceIdUtils.setTraceId(traceId);
+        
         log.info("开始执行销售统计更新任务");
         try {
             timedDiscountService.updateSalesStatistics();
             log.info("销售统计更新任务执行完成");
         } catch (Exception e) {
             log.error("销售统计更新任务执行失败", e);
+        } finally {
+            // 清理 MDC 上下文
+            TraceIdUtils.removeTraceId();
         }
     }
 }

+ 9 - 0
haha-admin/src/main/resources/application.yml

@@ -100,6 +100,15 @@ haha:
     app-id: 2601051549145878
     app-secret: 06e1be59332b00de0baad82002cdbcb5
     base-url: http://api.hahabianli.com/
+  
+  # 日志配置
+  log:
+    # 慢请求阈值(毫秒)
+    slow-request-threshold: 3000
+    # 是否启用敏感信息脱敏
+    enable-sensitive-mask: true
+    # 是否启用请求日志
+    enable-request-log: true
 
 # 日志配置
 logging:

+ 2 - 2
haha-admin/src/main/resources/logback-spring.xml

@@ -7,8 +7,8 @@
     
     <property name="log.path" value="${LOG_PATH}/${APP_NAME}.log"/>
     
-    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n"/>
-    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n"/>
+    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n"/>
 
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>

+ 14 - 0
haha-common/pom.xml

@@ -22,6 +22,20 @@
             <artifactId>spring-web</artifactId>
             <scope>provided</scope>
         </dependency>
+        
+        <!-- Spring WebMVC (用于拦截器) - provided scope -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <!-- Jakarta Servlet API - provided scope -->
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
 </project>

+ 9 - 7
haha-common/src/main/java/com/haha/common/config/CommonConfig.java

@@ -1,20 +1,22 @@
 package com.haha.common.config;
 
+import com.haha.common.interceptor.RequestLogInterceptor;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
-/**
- * 公共配置类
- */
 @Data
 @Configuration
+@EnableConfigurationProperties(LogProperties.class)
 @ConfigurationProperties(prefix = "haha.common")
 public class CommonConfig {
     
-    /**
-     * 图片域名前缀
-     * 用于给商品图片链接添加域名前缀
-     */
     private String imageDomainPrefix = "https://img.hahabianli.com/";
+
+    @Bean
+    public RequestLogInterceptor requestLogInterceptor(LogProperties logProperties) {
+        return new RequestLogInterceptor(logProperties);
+    }
 }

+ 30 - 0
haha-common/src/main/java/com/haha/common/config/LogProperties.java

@@ -0,0 +1,30 @@
+package com.haha.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 日志配置属性类
+ */
+@Data
+@ConfigurationProperties(prefix = "haha.log")
+public class LogProperties {
+
+    /**
+     * 慢请求阈值(毫秒)
+     * 超过此阈值的请求将被记录为慢请求
+     */
+    private Long slowRequestThreshold = 3000L;
+
+    /**
+     * 是否启用敏感信息脱敏
+     * 启用后,日志中的敏感信息(如手机号、身份证等)将被脱敏处理
+     */
+    private Boolean enableSensitiveMask = true;
+
+    /**
+     * 是否启用请求日志
+     * 启用后,将记录所有HTTP请求的详细信息
+     */
+    private Boolean enableRequestLog = true;
+}

+ 34 - 0
haha-common/src/main/java/com/haha/common/config/MdcTaskDecorator.java

@@ -0,0 +1,34 @@
+package com.haha.common.config;
+
+import org.slf4j.MDC;
+import org.springframework.core.task.TaskDecorator;
+
+import java.util.Map;
+
+/**
+ * MDC 任务装饰器
+ * 用于在异步任务中传递父线程的 MDC 上下文到子线程
+ * 确保日志追踪信息(如 traceId)能够正确传递
+ */
+public class MdcTaskDecorator implements TaskDecorator {
+
+    @Override
+    public Runnable decorate(Runnable runnable) {
+        // 获取父线程的 MDC 上下文
+        Map<String, String> contextMap = MDC.getCopyOfContextMap();
+        
+        return () -> {
+            try {
+                // 将父线程的 MDC 上下文设置到子线程
+                if (contextMap != null) {
+                    MDC.setContextMap(contextMap);
+                }
+                // 执行实际任务
+                runnable.run();
+            } finally {
+                // 任务执行完毕后清理 MDC,防止内存泄漏
+                MDC.clear();
+            }
+        };
+    }
+}

+ 34 - 0
haha-common/src/main/java/com/haha/common/constant/LogConstants.java

@@ -0,0 +1,34 @@
+package com.haha.common.constant;
+
+public class LogConstants {
+
+    private LogConstants() {
+    }
+
+    public static final String LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n";
+    
+    public static final String TRACE_ID_KEY = "traceId";
+    
+    public static final long DEFAULT_SLOW_REQUEST_THRESHOLD = 3000L;
+    
+    public static final String[] EXCLUDE_PATHS = {
+        "/static/**",
+        "/css/**",
+        "/js/**",
+        "/images/**",
+        "/fonts/**",
+        "/favicon.ico",
+        "/webjars/**",
+        "/actuator/**",
+        "/swagger-ui/**",
+        "/v3/api-docs/**"
+    };
+    
+    public static final String[] STATIC_RESOURCE_EXTENSIONS = {
+        ".css", ".js", ".jpg", ".jpeg", ".png", ".gif", ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot", ".map"
+    };
+    
+    public static final int MAX_PARAM_LENGTH = 1000;
+    
+    public static final int MAX_RESPONSE_LENGTH = 500;
+}

+ 248 - 0
haha-common/src/main/java/com/haha/common/interceptor/RequestLogInterceptor.java

@@ -0,0 +1,248 @@
+package com.haha.common.interceptor;
+
+import com.haha.common.config.LogProperties;
+import com.haha.common.constant.LogConstants;
+import com.haha.common.utils.SensitiveDataUtils;
+import com.haha.common.utils.TraceIdUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+public class RequestLogInterceptor implements HandlerInterceptor {
+
+    private final LogProperties logProperties;
+
+    public RequestLogInterceptor(LogProperties logProperties) {
+        this.logProperties = logProperties;
+    }
+
+    private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
+
+    private static final String START_TIME_ATTRIBUTE = "requestStartTime";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 检查是否为静态资源请求
+        if (isStaticResource(request)) {
+            return true;
+        }
+
+        // 生成 TraceId 并设置到 MDC
+        String traceId = TraceIdUtils.getOrGenerateTraceId();
+        response.setHeader(LogConstants.TRACE_ID_KEY, traceId);
+
+        // 记录请求开始时间
+        long startTime = System.currentTimeMillis();
+        START_TIME.set(startTime);
+        request.setAttribute(START_TIME_ATTRIBUTE, startTime);
+
+        // 获取请求信息
+        String method = request.getMethod();
+        String requestUri = request.getRequestURI();
+        String queryString = request.getQueryString();
+        String clientIp = getClientIp(request);
+        String userAgent = request.getHeader("User-Agent");
+
+        // 获取请求参数并脱敏
+        Map<String, String[]> parameterMap = request.getParameterMap();
+        String params = maskParameters(parameterMap);
+
+        // 记录请求开始日志
+        log.info("[请求开始] TraceId: {}, 方法: {}, 路径: {}, 参数: {}, IP: {}, User-Agent: {}",
+                traceId, method, requestUri + (queryString != null ? "?" + queryString : ""),
+                params, clientIp, userAgent);
+
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+        try {
+            // 检查是否为静态资源请求
+            if (isStaticResource(request)) {
+                return;
+            }
+
+            // 获取 TraceId
+            String traceId = TraceIdUtils.getTraceId();
+
+            // 计算请求处理时长
+            Long startTime = START_TIME.get();
+            long costTime = 0L;
+            if (startTime != null) {
+                costTime = System.currentTimeMillis() - startTime;
+            }
+
+            // 获取响应状态
+            int status = response.getStatus();
+
+            // 获取请求信息
+            String method = request.getMethod();
+            String requestUri = request.getRequestURI();
+
+            // 判断是否为慢请求
+            long slowRequestThreshold = logProperties.getSlowRequestThreshold() != null 
+                ? logProperties.getSlowRequestThreshold() 
+                : LogConstants.DEFAULT_SLOW_REQUEST_THRESHOLD;
+            if (costTime > slowRequestThreshold) {
+                log.warn("[请求结束-慢请求] TraceId: {}, 方法: {}, 路径: {}, 状态: {}, 耗时: {}ms",
+                        traceId, method, requestUri, status, costTime);
+            } else {
+                log.info("[请求结束] TraceId: {}, 方法: {}, 路径: {}, 状态: {}, 耗时: {}ms",
+                        traceId, method, requestUri, status, costTime);
+            }
+
+            // 如果有异常,记录异常信息
+            if (ex != null) {
+                log.error("[请求异常] TraceId: {}, 方法: {}, 路径: {}, 异常: {}",
+                        traceId, method, requestUri, ex.getMessage(), ex);
+            }
+        } finally {
+            // 清理 ThreadLocal
+            START_TIME.remove();
+
+            // 清理 MDC 中的 TraceId
+            TraceIdUtils.removeTraceId();
+        }
+    }
+
+    /**
+     * 判断是否为静态资源请求
+     *
+     * @param request HTTP 请求
+     * @return 是否为静态资源
+     */
+    private boolean isStaticResource(HttpServletRequest request) {
+        String requestUri = request.getRequestURI();
+        if (requestUri == null) {
+            return false;
+        }
+
+        // 检查文件扩展名
+        String[] staticExtensions = LogConstants.STATIC_RESOURCE_EXTENSIONS;
+        for (String extension : staticExtensions) {
+            if (requestUri.toLowerCase().endsWith(extension.toLowerCase())) {
+                return true;
+            }
+        }
+
+        // 检查排除路径
+        String[] excludePaths = LogConstants.EXCLUDE_PATHS;
+        for (String excludePath : excludePaths) {
+            if (excludePath.endsWith("/**")) {
+                String basePath = excludePath.substring(0, excludePath.length() - 3);
+                if (requestUri.startsWith(basePath)) {
+                    return true;
+                }
+            } else if (requestUri.equals(excludePath)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 获取客户端真实 IP 地址
+     *
+     * @param request HTTP 请求
+     * @return 客户端 IP
+     */
+    private String getClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        // 对于多个代理的情况,第一个 IP 为客户端真实 IP
+        if (ip != null && ip.contains(",")) {
+            ip = ip.split(",")[0].trim();
+        }
+        return ip;
+    }
+
+    /**
+     * 对请求参数进行脱敏处理
+     *
+     * @param parameterMap 请求参数 Map
+     * @return 脱敏后的参数字符串
+     */
+    private String maskParameters(Map<String, String[]> parameterMap) {
+        if (parameterMap == null || parameterMap.isEmpty()) {
+            return "{}";
+        }
+
+        Map<String, Object> maskedParams = new HashMap<>();
+        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
+            String key = entry.getKey();
+            String[] values = entry.getValue();
+
+            if (values == null || values.length == 0) {
+                maskedParams.put(key, "");
+            } else if (values.length == 1) {
+                String value = values[0];
+                if (isSensitiveParam(key) && Boolean.TRUE.equals(logProperties.getEnableSensitiveMask())) {
+                    value = SensitiveDataUtils.mask(value);
+                }
+                if (value != null && value.length() > LogConstants.MAX_PARAM_LENGTH) {
+                    value = value.substring(0, LogConstants.MAX_PARAM_LENGTH) + "...";
+                }
+                maskedParams.put(key, value);
+            } else {
+                String value = String.join(",", values);
+                if (isSensitiveParam(key) && Boolean.TRUE.equals(logProperties.getEnableSensitiveMask())) {
+                    value = SensitiveDataUtils.mask(value);
+                }
+                if (value != null && value.length() > LogConstants.MAX_PARAM_LENGTH) {
+                    value = value.substring(0, LogConstants.MAX_PARAM_LENGTH) + "...";
+                }
+                maskedParams.put(key, value);
+            }
+        }
+
+        return maskedParams.toString();
+    }
+
+    /**
+     * 判断参数名是否为敏感参数
+     *
+     * @param paramName 参数名
+     * @return 是否为敏感参数
+     */
+    private boolean isSensitiveParam(String paramName) {
+        if (paramName == null) {
+            return false;
+        }
+        String lowerParamName = paramName.toLowerCase();
+        return lowerParamName.contains("password")
+                || lowerParamName.contains("pwd")
+                || lowerParamName.contains("passwd")
+                || lowerParamName.contains("secret")
+                || lowerParamName.contains("token")
+                || lowerParamName.contains("key")
+                || lowerParamName.contains("phone")
+                || lowerParamName.contains("mobile")
+                || lowerParamName.contains("idcard")
+                || lowerParamName.contains("id_card")
+                || lowerParamName.contains("bankcard")
+                || lowerParamName.contains("bank_card");
+    }
+}

+ 100 - 0
haha-common/src/main/java/com/haha/common/utils/SensitiveDataUtils.java

@@ -0,0 +1,100 @@
+package com.haha.common.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SensitiveDataUtils {
+
+    private static final Pattern PHONE_PATTERN = Pattern.compile("(1[3-9]\\d)\\d{4}(\\d{4})");
+    private static final Pattern ID_CARD_PATTERN = Pattern.compile("(\\d{3})\\d{11}(\\d{4})");
+    private static final Pattern BANK_CARD_PATTERN = Pattern.compile("(\\d{4})\\d+(\\d{4})");
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("(password|pwd|passwd)['\"]?\\s*[:=]\\s*['\"]?([^'\"\\s,}]+)['\"]?", Pattern.CASE_INSENSITIVE);
+
+    private SensitiveDataUtils() {
+    }
+
+    public static String maskPhone(String phone) {
+        if (phone == null || phone.isEmpty()) {
+            return phone;
+        }
+        Matcher matcher = PHONE_PATTERN.matcher(phone);
+        if (matcher.matches()) {
+            return matcher.group(1) + "****" + matcher.group(2);
+        }
+        return phone;
+    }
+
+    public static String maskIdCard(String idCard) {
+        if (idCard == null || idCard.isEmpty()) {
+            return idCard;
+        }
+        Matcher matcher = ID_CARD_PATTERN.matcher(idCard);
+        if (matcher.matches()) {
+            return matcher.group(1) + "***********" + matcher.group(2);
+        }
+        return idCard;
+    }
+
+    public static String maskBankCard(String bankCard) {
+        if (bankCard == null || bankCard.isEmpty()) {
+            return bankCard;
+        }
+        String digits = bankCard.replaceAll("\\D", "");
+        if (digits.length() >= 8) {
+            int length = digits.length();
+            int start = 4;
+            int end = length - 4;
+            return digits.substring(0, start) + "****" + digits.substring(end);
+        }
+        return bankCard;
+    }
+
+    public static String maskPassword(String text) {
+        if (text == null || text.isEmpty()) {
+            return text;
+        }
+        Matcher matcher = PASSWORD_PATTERN.matcher(text);
+        return matcher.replaceAll("$1=******");
+    }
+
+    public static String mask(String text) {
+        if (text == null || text.isEmpty()) {
+            return text;
+        }
+        String result = text;
+        result = maskPhoneInText(result);
+        result = maskIdCardInText(result);
+        result = maskBankCardInText(result);
+        result = maskPassword(result);
+        return result;
+    }
+
+    private static String maskPhoneInText(String text) {
+        if (text == null || text.isEmpty()) {
+            return text;
+        }
+        return PHONE_PATTERN.matcher(text).replaceAll("$1****$2");
+    }
+
+    private static String maskIdCardInText(String text) {
+        if (text == null || text.isEmpty()) {
+            return text;
+        }
+        return ID_CARD_PATTERN.matcher(text).replaceAll("$1***********$2");
+    }
+
+    private static String maskBankCardInText(String text) {
+        if (text == null || text.isEmpty()) {
+            return text;
+        }
+        Matcher matcher = BANK_CARD_PATTERN.matcher(text);
+        StringBuffer sb = new StringBuffer();
+        while (matcher.find()) {
+            String group = matcher.group(0);
+            String masked = maskBankCard(group);
+            matcher.appendReplacement(sb, masked);
+        }
+        matcher.appendTail(sb);
+        return sb.toString();
+    }
+}

+ 44 - 0
haha-common/src/main/java/com/haha/common/utils/TraceIdUtils.java

@@ -0,0 +1,44 @@
+package com.haha.common.utils;
+
+import org.slf4j.MDC;
+
+import java.util.UUID;
+
+public class TraceIdUtils {
+
+    private static final String TRACE_ID_KEY = "traceId";
+
+    private TraceIdUtils() {
+    }
+
+    public static String generateTraceId() {
+        return UUID.randomUUID().toString().replace("-", "");
+    }
+
+    public static void setTraceId(String traceId) {
+        if (traceId != null && !traceId.isEmpty()) {
+            MDC.put(TRACE_ID_KEY, traceId);
+        }
+    }
+
+    public static String getTraceId() {
+        return MDC.get(TRACE_ID_KEY);
+    }
+
+    public static void removeTraceId() {
+        MDC.remove(TRACE_ID_KEY);
+    }
+
+    public static void clear() {
+        MDC.clear();
+    }
+
+    public static String getOrGenerateTraceId() {
+        String traceId = getTraceId();
+        if (traceId == null || traceId.isEmpty()) {
+            traceId = generateTraceId();
+            setTraceId(traceId);
+        }
+        return traceId;
+    }
+}

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/OperationLog.java

@@ -88,6 +88,11 @@ public class OperationLog implements Serializable {
      */
     private String os;
 
+    /**
+     * 链路追踪ID
+     */
+    private String traceId;
+
     /**
      * 操作状态:1-成功,0-失败
      */

+ 87 - 18
haha-miniapp/src/main/java/com/haha/miniapp/config/GlobalExceptionHandler.java

@@ -2,7 +2,9 @@ package com.haha.miniapp.config;
 
 import cn.dev33.satoken.exception.NotLoginException;
 import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.stp.StpUtil;
 import com.haha.common.exception.BusinessException;
+import com.haha.common.utils.TraceIdUtils;
 import com.haha.common.vo.Result;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
@@ -15,6 +17,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
 
+import jakarta.servlet.http.HttpServletRequest;
 import java.util.stream.Collectors;
 
 /**
@@ -29,8 +32,13 @@ public class GlobalExceptionHandler {
      * 处理业务异常
      */
     @ExceptionHandler(BusinessException.class)
-    public Result<Void> handleBusinessException(BusinessException e) {
-        log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
+    public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 业务异常 - 路径: {}, 异常代码: {}, 异常消息: {}", 
+                traceId, requestPath, e.getCode(), e.getMessage());
+        
         return Result.error(e.getCode(), e.getMessage());
     }
 
@@ -39,8 +47,13 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(NotLoginException.class)
     @ResponseStatus(HttpStatus.UNAUTHORIZED)
-    public Result<Void> handleNotLoginException(NotLoginException e) {
-        log.warn("未登录异常: {}", e.getMessage());
+    public Result<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 未登录异常 - 路径: {}, 异常类型: {}", 
+                traceId, requestPath, e.getType());
+        
         String message;
         if (e.getType().equals(NotLoginException.NOT_TOKEN)) {
             message = "未提供token";
@@ -59,8 +72,13 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(NotPermissionException.class)
     @ResponseStatus(HttpStatus.FORBIDDEN)
-    public Result<Void> handleNotPermissionException(NotPermissionException e) {
-        log.warn("无权限异常: {}", e.getMessage());
+    public Result<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 无权限异常 - 路径: {}, 需要权限: {}", 
+                traceId, requestPath, e.getPermission());
+        
         return Result.error(403, "无访问权限");
     }
 
@@ -69,11 +87,21 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(MethodArgumentNotValidException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+    public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        String fieldErrors = e.getBindingResult().getFieldErrors().stream()
+                .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
+                .collect(Collectors.joining("; "));
+        
         String message = e.getBindingResult().getFieldErrors().stream()
                 .map(FieldError::getDefaultMessage)
                 .collect(Collectors.joining(", "));
-        log.warn("参数校验失败: {}", message);
+        
+        log.warn("[TraceId: {}] 参数校验失败 - 路径: {}, 校验失败字段: {}", 
+                traceId, requestPath, fieldErrors);
+        
         return Result.error(400, message);
     }
 
@@ -82,11 +110,21 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(BindException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleBindException(BindException e) {
+    public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        String fieldErrors = e.getBindingResult().getFieldErrors().stream()
+                .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
+                .collect(Collectors.joining("; "));
+        
         String message = e.getBindingResult().getFieldErrors().stream()
                 .map(FieldError::getDefaultMessage)
                 .collect(Collectors.joining(", "));
-        log.warn("参数绑定失败: {}", message);
+        
+        log.warn("[TraceId: {}] 参数绑定失败 - 路径: {}, 绑定失败字段: {}", 
+                traceId, requestPath, fieldErrors);
+        
         return Result.error(400, message);
     }
 
@@ -95,8 +133,13 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(MissingServletRequestParameterException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
-        log.warn("缺少必需参数: {}", e.getParameterName());
+    public Result<Void> handleMissingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 缺少必需参数 - 路径: {}, 参数名: {}, 参数类型: {}", 
+                traceId, requestPath, e.getParameterName(), e.getParameterType());
+        
         return Result.error(400, "缺少必需参数: " + e.getParameterName());
     }
 
@@ -105,8 +148,14 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(MethodArgumentTypeMismatchException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
-        log.warn("参数类型不匹配: {}={}", e.getName(), e.getValue());
+    public Result<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 参数类型不匹配 - 路径: {}, 参数名: {}, 参数值: {}, 需要类型: {}", 
+                traceId, requestPath, e.getName(), e.getValue(), 
+                e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "未知");
+        
         return Result.error(400, "参数类型错误: " + e.getName());
     }
 
@@ -115,8 +164,13 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(IllegalArgumentException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
-        log.warn("非法参数: {}", e.getMessage());
+    public Result<Void> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        log.warn("[TraceId: {}] 非法参数 - 路径: {}, 异常消息: {}", 
+                traceId, requestPath, e.getMessage());
+        
         return Result.error(400, e.getMessage());
     }
 
@@ -125,8 +179,23 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(Exception.class)
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
-    public Result<Void> handleException(Exception e) {
-        log.error("系统异常: {}", e.getMessage(), e);
+    public Result<Void> handleException(Exception e, HttpServletRequest request) {
+        String traceId = TraceIdUtils.getTraceId();
+        String requestPath = request.getRequestURI();
+        
+        // 尝试获取当前登录用户信息
+        String userInfo = "未登录";
+        try {
+            if (StpUtil.isLogin()) {
+                userInfo = "用户ID: " + StpUtil.getLoginIdAsString();
+            }
+        } catch (Exception ex) {
+            // 忽略获取用户信息时的异常
+        }
+        
+        log.error("[TraceId: {}] 系统异常 - 路径: {}, 用户信息: {}, 异常消息: {}", 
+                traceId, requestPath, userInfo, e.getMessage(), e);
+        
         return Result.error(500, "系统繁忙,请稍后重试");
     }
 }

+ 34 - 0
haha-miniapp/src/main/java/com/haha/miniapp/config/WebMvcConfig.java

@@ -0,0 +1,34 @@
+package com.haha.miniapp.config;
+
+import com.haha.common.config.LogProperties;
+import com.haha.common.constant.LogConstants;
+import com.haha.common.interceptor.RequestLogInterceptor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * Web MVC 配置类
+ * 用于配置拦截器、跨域、静态资源等
+ *
+ * @author haha
+ */
+@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    private final RequestLogInterceptor requestLogInterceptor;
+
+    private final LogProperties logProperties;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册请求日志拦截器
+        if (logProperties.getEnableRequestLog()) {
+            registry.addInterceptor(requestLogInterceptor)
+                    .addPathPatterns("/**")
+                    .excludePathPatterns(LogConstants.EXCLUDE_PATHS);
+        }
+    }
+}

+ 9 - 0
haha-miniapp/src/main/resources/application.yml

@@ -101,6 +101,15 @@ haha:
   common:
     # 图片域名前缀
     image-domain-prefix: https://img.hahabianli.com/
+  
+  # 日志配置
+  log:
+    # 慢请求阈值(毫秒)
+    slow-request-threshold: 3000
+    # 是否启用敏感信息脱敏
+    enable-sensitive-mask: true
+    # 是否启用请求日志
+    enable-request-log: true
 
 # Sa-Token 配置
 sa-token:

+ 26 - 4
haha-miniapp/src/main/resources/logback-spring.xml

@@ -5,13 +5,13 @@
     <!-- 使用 Spring Boot 的日志路径配置 -->
     <springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="./logs"/>
     <property name="log.path" value="${LOG_PATH}/haha-miniapp.log"/>
-    <property name="CONSOLE_LOG_PATTERN"
-              value="%date{yyyy-MM-dd HH:mm:ss} | %highlight(%-5level) | %boldYellow(%thread) | %boldGreen(%logger) | %msg%n"/>
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n"/>
+    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n"/>
     
     <!-- 控制台输出 -->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
-            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -24,16 +24,37 @@
             <maxHistory>30</maxHistory>
         </rollingPolicy>
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
             <charset>UTF-8</charset>
         </encoder>
     </appender>
 
+    <!-- ERROR 级别日志文件输出 -->
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_PATH}/haha-miniapp-error.log</file>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_PATH}/haha-miniapp-error-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>60</maxHistory>
+            <totalSizeCap>5GB</totalSizeCap>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
     <!-- 开发环境:控制台输出 -->
     <springProfile name="dev,default">
         <root level="INFO">
             <appender-ref ref="CONSOLE"/>
             <appender-ref ref="FILE"/>
+            <appender-ref ref="ERROR_FILE"/>
         </root>
         <logger name="com.haha.miniapp" level="DEBUG"/>
         <logger name="cn.dev33.satoken" level="DEBUG"/>
@@ -44,6 +65,7 @@
         <root level="INFO">
             <appender-ref ref="CONSOLE"/>
             <appender-ref ref="FILE"/>
+            <appender-ref ref="ERROR_FILE"/>
         </root>
         <logger name="com.haha.miniapp" level="INFO"/>
         <logger name="cn.dev33.satoken" level="INFO"/>