Selaa lähdekoodia

feat: 日志体系优化——支付回调按异常类型区分应答并显式打印订单关键字段,审计切面失败请求也落库,logback增加保留上限/全日期/ERROR单独文件/异步写盘,MDC新增uid便于按用户串联日志

skyline 3 viikkoa sitten
vanhempi
säilyke
13d4e975f0

+ 4 - 0
admin/src/main/java/com/kym/admin/config/SaTokenConfigure.java

@@ -2,6 +2,7 @@ package com.kym.admin.config;
 
 import cn.dev33.satoken.interceptor.SaInterceptor;
 import cn.dev33.satoken.stp.StpUtil;
+import com.kym.common.interceptor.UserIdMdcInterceptor;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -33,6 +34,9 @@ public class SaTokenConfigure implements WebMvcConfigurer {
                         "/**/*.ico",
                         "/"
                 );
+        // 将登录用户ID写入MDC,便于按用户串联日志
+        registry.addInterceptor(new UserIdMdcInterceptor())
+                .addPathPatterns("/**");
         // TODO: 2023-07-11 具体功能权限校验后续开发
     }
 

+ 41 - 6
admin/src/main/resources/logback-spring.xml

@@ -2,22 +2,56 @@
 <configuration scan="true" scanPeriod="60 seconds" debug="false">
     <contextName>logback</contextName>
     <property name="log.path" value="logs/admin/admin.log"/>
-    <property name="CONSOLE_LOG_PATTERN"
-              value="%date{yyyy-MM-dd HH:mm:ss} | %highlight(%-5level) | %boldYellow(%thread) | %boldGreen(%logger) | %msg%n"/>
+    <property name="error.path" value="logs/admin/admin-error.log"/>
+    <property name="FILE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%thread|%-5level|%F:%L|%X{ip}|%X{seq}|%X{uid}|%msg%n"/>
     <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
-            <pattern>%d{HH:mm:ss.SSS}|%thread|%-5level|%F:%L|%X{ip}|%X{seq}|%msg%n</pattern>
+            <pattern>%d{HH:mm:ss.SSS}|%thread|%-5level|%F:%L|%X{ip}|%X{seq}|%X{uid}|%msg%n</pattern>
         </encoder>
     </appender>
     <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
         <file>${log.path}</file>
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <fileNamePattern>./logs/admin/admin-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
         </rollingPolicy>
         <encoder>
-            <pattern>%d{HH:mm:ss.SSS}|%thread|%-5level|%F:%L|%X{ip}|%X{seq}|%msg%n</pattern>
+            <pattern>${FILE_PATTERN}</pattern>
         </encoder>
     </appender>
+    <!-- ERROR单独落盘,便于告警对接与问题定位 -->
+    <appender name="errorFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <filter class="ch.qos.logback.core.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+        <file>${error.path}</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>./logs/admin/admin-error-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>2GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${FILE_PATTERN}</pattern>
+        </encoder>
+    </appender>
+    <!-- 异步写盘;includeCallerData必须开启,否则%F:%L打印为空 -->
+    <appender name="asyncFile" class="ch.qos.logback.classic.AsyncAppender">
+        <queueSize>1024</queueSize>
+        <discardingThreshold>0</discardingThreshold>
+        <neverBlock>true</neverBlock>
+        <includeCallerData>true</includeCallerData>
+        <appender-ref ref="file"/>
+    </appender>
+    <appender name="asyncErrorFile" class="ch.qos.logback.classic.AsyncAppender">
+        <queueSize>1024</queueSize>
+        <discardingThreshold>0</discardingThreshold>
+        <neverBlock>true</neverBlock>
+        <includeCallerData>true</includeCallerData>
+        <appender-ref ref="errorFile"/>
+    </appender>
     <!-- <logger name="org.springframework.web" level="DEBUG"/> -->
     <logger name="org.springframework" level="info"/>
     <logger name="org.hibernate" level="info"/>
@@ -33,6 +67,7 @@
     <logger name="io.lettuce" level="warn"/>
     <root level="debug">
         <appender-ref ref="console"/>
-        <appender-ref ref="file"/>
+        <appender-ref ref="asyncFile"/>
+        <appender-ref ref="asyncErrorFile"/>
     </root>
-</configuration>
+</configuration>

+ 35 - 0
common/src/main/java/com/kym/common/interceptor/UserIdMdcInterceptor.java

@@ -0,0 +1,35 @@
+package com.kym.common.interceptor;
+
+import cn.dev33.satoken.stp.StpUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.MDC;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+/**
+ * 将登录用户ID写入MDC(key: uid),便于按用户串联日志
+ *
+ * @author kym
+ */
+public class UserIdMdcInterceptor implements HandlerInterceptor {
+
+    private static final String UID = "uid";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+        try {
+            Object loginId = StpUtil.getLoginIdDefaultNull();
+            if (loginId != null) {
+                MDC.put(UID, String.valueOf(loginId));
+            }
+        } catch (Exception ignored) {
+            // 未登录或鉴权上下文异常时不影响请求
+        }
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+        MDC.remove(UID);
+    }
+}

+ 10 - 4
miniapp/src/main/java/com/kym/miniapp/aspect/AppLogAspect.java

@@ -51,10 +51,16 @@ public class AppLogAspect {
     @Around("logPointCut()")
     Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
         var startTime = System.currentTimeMillis();
-        Object result = joinPoint.proceed();
-        var executeTime = System.currentTimeMillis() - startTime;
-        saveApiLog(joinPoint, executeTime);
-        return result;
+        try {
+            Object result = joinPoint.proceed();
+            saveApiLog(joinPoint, System.currentTimeMillis() - startTime);
+            return result;
+        } catch (Throwable e) {
+            // 失败请求同样落审计日志,并以ERROR记录堆栈
+            LOGGER.error("ApiLog:方法{}执行异常,耗时{}ms", joinPoint.getSignature().toShortString(), System.currentTimeMillis() - startTime, e);
+            saveApiLog(joinPoint, System.currentTimeMillis() - startTime);
+            throw e;
+        }
     }
 
 

+ 4 - 0
miniapp/src/main/java/com/kym/miniapp/config/SaTokenConfigure.java

@@ -3,6 +3,7 @@ package com.kym.miniapp.config;
 import cn.dev33.satoken.interceptor.SaInterceptor;
 import cn.dev33.satoken.router.SaRouter;
 import cn.dev33.satoken.stp.StpUtil;
+import com.kym.common.interceptor.UserIdMdcInterceptor;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -55,5 +56,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
                             .check(r -> StpUtil.checkLogin());
                 }))
                 .addPathPatterns("/**");
+        // 将登录用户ID写入MDC,便于按用户串联日志
+        registry.addInterceptor(new UserIdMdcInterceptor())
+                .addPathPatterns("/**");
     }
 }

+ 41 - 6
miniapp/src/main/resources/logback-spring.xml

@@ -2,22 +2,56 @@
 <configuration scan="true" scanPeriod="60 seconds" debug="false">
     <contextName>logback</contextName>
     <property name="log.path" value="logs/miniapp/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="error.path" value="logs/miniapp/miniapp-error.log"/>
+    <property name="FILE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%thread|%-5level|%F:%L|%X{ip}|%X{seq}|%X{uid}|%msg%n"/>
     <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
-            <pattern>%d{HH:mm:ss.SSS}|%thread|%-5level|%F:%L|%X{ip}|%X{seq}|%msg%n</pattern>
+            <pattern>%d{HH:mm:ss.SSS}|%thread|%-5level|%F:%L|%X{ip}|%X{seq}|%X{uid}|%msg%n</pattern>
         </encoder>
     </appender>
     <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
         <file>${log.path}</file>
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <fileNamePattern>./logs/miniapp/miniapp-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
         </rollingPolicy>
         <encoder>
-            <pattern>%d{HH:mm:ss.SSS}|%thread|%-5level|%F:%L|%X{ip}|%X{seq}|%msg%n</pattern>
+            <pattern>${FILE_PATTERN}</pattern>
         </encoder>
     </appender>
+    <!-- ERROR单独落盘,便于告警对接与问题定位 -->
+    <appender name="errorFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <filter class="ch.qos.logback.core.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+        <file>${error.path}</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>./logs/miniapp/miniapp-error-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>2GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${FILE_PATTERN}</pattern>
+        </encoder>
+    </appender>
+    <!-- 异步写盘;includeCallerData必须开启,否则%F:%L打印为空 -->
+    <appender name="asyncFile" class="ch.qos.logback.classic.AsyncAppender">
+        <queueSize>1024</queueSize>
+        <discardingThreshold>0</discardingThreshold>
+        <neverBlock>true</neverBlock>
+        <includeCallerData>true</includeCallerData>
+        <appender-ref ref="file"/>
+    </appender>
+    <appender name="asyncErrorFile" class="ch.qos.logback.classic.AsyncAppender">
+        <queueSize>1024</queueSize>
+        <discardingThreshold>0</discardingThreshold>
+        <neverBlock>true</neverBlock>
+        <includeCallerData>true</includeCallerData>
+        <appender-ref ref="errorFile"/>
+    </appender>
     <!-- <logger name="org.springframework.web" level="DEBUG"/> -->
     <logger name="org.springframework" level="info"/>
     <logger name="org.hibernate" level="info"/>
@@ -32,6 +66,7 @@
     <logger name="io.lettuce" level="warn"/>
     <root level="debug">
         <appender-ref ref="console"/>
-        <appender-ref ref="file"/>
+        <appender-ref ref="asyncFile"/>
+        <appender-ref ref="asyncErrorFile"/>
     </root>
-</configuration>
+</configuration>

+ 11 - 4
service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -298,7 +298,10 @@ public class WxPayServiceImpl implements WxPayService {
             var notifyRes = handleWxNotify(request);
             // 以支付通知回调为例,验签、解密并转换成 Transaction
             Transaction transaction = ((NotificationParser) notifyRes[1]).parse((RequestParam) notifyRes[0], Transaction.class);
-            LOGGER.info("微信支付回调{}:验签解密完毕,数据:\n{}", notifyRes[2], transaction);
+            LOGGER.info("微信支付回调{}:验签解密完毕,outTradeNo:{},金额:{}分,交易状态:{},支付时间:{}",
+                    notifyRes[2], transaction.getOutTradeNo(),
+                    transaction.getAmount() == null ? null : transaction.getAmount().getTotal(),
+                    transaction.getTradeState(), transaction.getSuccessTime());
             // 判断是否已经接收处理过通知(快路径)
             if (payLogService.lambdaQuery().eq(PayLog::getOutTradeNo, transaction.getOutTradeNo()).one() != null) {
                 return ResponseEntity.status(HttpStatus.OK).build();
@@ -378,11 +381,15 @@ public class WxPayServiceImpl implements WxPayService {
             if (e instanceof ValidationException) {
                 // 签名验证失败,返回 401 UNAUTHORIZED 状态码
                 LOGGER.error("微信支付通知验签失败", e);
+                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("code", HttpStatus.UNAUTHORIZED, "message", "验签失败"));
             }
-            if (e instanceof BusinessException) {
-                LOGGER.error("业务异常", e);
+            // 业务/系统异常返回 500,微信会按策略重试;与验签失败区分便于监控告警
+            if (e instanceof BusinessException biz) {
+                LOGGER.error("微信支付回调业务异常:{}", biz.getMessage(), biz);
+            } else {
+                LOGGER.error("微信支付回调处理异常", e);
             }
-            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("code", HttpStatus.UNAUTHORIZED, "message", "验签失败"));
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "处理失败,等待微信重试"));
         }
     }