From 6c588ce15bc8d7457d0109404a6aa61eba07ce4d Mon Sep 17 00:00:00 2001
From: huangxz <huangxz@xiaopeng.com>
Date: 星期六, 07 三月 2026 21:00:56 +0800
Subject: [PATCH] feat: 添加&更新Feign调用日志切面和请求/响应日志过滤器

---
 xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/config/log/FeignLoggingAspect.java    |   22 -
 xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/config/log/RequestResponseLoggingFilter.java                 |  139 ++++++++++
 xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/config/log/RequestResponseLoggingFilter.java |  139 ++++++++++
 xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/config/log/FeignLoggingAspect.java               |   22 -
 xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/config/log/FeignLoggingAspect.java                   |   83 ++++++
 xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/config/log/FeignLoggingAspect.java      |   22 -
 xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/config/log/FeignLoggingAspect.java           |   83 ++++++
 xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/config/log/FeignLoggingAspect.java                           |   83 ++++++
 xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/config/log/RequestResponseLoggingFilter.java         |  139 ++++++++++
 9 files changed, 681 insertions(+), 51 deletions(-)

diff --git a/xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/config/log/FeignLoggingAspect.java b/xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/config/log/FeignLoggingAspect.java
new file mode 100644
index 0000000..f39b76d
--- /dev/null
+++ b/xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/config/log/FeignLoggingAspect.java
@@ -0,0 +1,83 @@
+package com.xiaopeng.account.config.log;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * Feign 调用日志切面
+ *
+ * <p>拦截所有带 @FeignClient 注解的接口方法,记录:
+ * <ul>
+ *   <li>调用的接口类名 + 方法名</li>
+ *   <li>入参(JSON 序列化)</li>
+ *   <li>出参(JSON 序列化)</li>
+ *   <li>耗时(ms)</li>
+ *   <li>异常信息(含完整堆栈)</li>
+ * </ul>
+ */
+@Slf4j
+@Aspect
+@Component
+public class FeignLoggingAspect {
+
+    /**
+     * 拦截所有带 @FeignClient 注解的接口的所有方法
+     */
+    @Around("within(@org.springframework.cloud.openfeign.FeignClient *)")
+    public Object logFeignCall(ProceedingJoinPoint pjp) throws Throwable {
+        return doLog(pjp);
+    }
+
+    private Object doLog(ProceedingJoinPoint pjp) throws Throwable {
+        MethodSignature signature = (MethodSignature) pjp.getSignature();
+        Method method = signature.getMethod();
+        // 使用接口声明类名,避免打印动态代理类名(如 $Proxy270)
+        String className = signature.getDeclaringType().getSimpleName();
+        String methodName = method.getName();
+        String fullMethod = className + "#" + methodName;
+
+        Object[] args = pjp.getArgs();
+        String argsJson = safeToJson(args);
+
+        log.info("[Feign] >>> 调用开始 | method={} | args={}", fullMethod, argsJson);
+
+        long start = System.currentTimeMillis();
+        try {
+            Object result = pjp.proceed();
+            long elapsed = System.currentTimeMillis() - start;
+            String resultJson = safeToJson(result);
+            log.info("[Feign] <<< 调用成功 | method={} | elapsed={}ms | result={}", fullMethod, elapsed, resultJson);
+            return result;
+        } catch (Throwable ex) {
+            long elapsed = System.currentTimeMillis() - start;
+            log.error("[Feign] <<< 调用异常 | method={} | elapsed={}ms | error={} | args={}",
+                    fullMethod, elapsed, ex.getMessage(), argsJson, ex);
+            throw ex;
+        }
+    }
+
+    /**
+     * 安全序列化为 JSON,避免序列化失败导致日志切面本身抛异常
+     */
+    private String safeToJson(Object obj) {
+        if (obj == null) {
+            return "null";
+        }
+        try {
+            if (obj instanceof Object[]) {
+                return JSON.toJSONString(Arrays.asList((Object[]) obj));
+            }
+            return JSON.toJSONString(obj);
+        } catch (Exception e) {
+            return "[序列化失败: " + obj.getClass().getSimpleName() + "]";
+        }
+    }
+}
diff --git a/xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/config/log/RequestResponseLoggingFilter.java b/xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/config/log/RequestResponseLoggingFilter.java
new file mode 100644
index 0000000..04054bf
--- /dev/null
+++ b/xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/config/log/RequestResponseLoggingFilter.java
@@ -0,0 +1,139 @@
+package com.xiaopeng.account.config.log;
+
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+import org.springframework.web.util.ContentCachingResponseWrapper;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 请求/响应日志过滤器
+ * 打印:traceId、method、uri、queryString、请求体、响应状态、耗时、响应体
+ */
+@Slf4j
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE + 10)
+public class RequestResponseLoggingFilter extends OncePerRequestFilter {
+
+    /** 不打印 body 的路径前缀(健康检查、actuator 等) */
+    private static final Set<String> EXCLUDE_PATHS = new HashSet<>(Arrays.asList(
+            "/actuator", "/healthcheck", "/health", "/prometheus"
+    ));
+
+    /** 只对这些 Content-Type 打印 body(避免打印二进制流) */
+    private static final Set<String> LOGGABLE_CONTENT_TYPES = new HashSet<>(Arrays.asList(
+            MediaType.APPLICATION_JSON_VALUE,
+            MediaType.APPLICATION_JSON_UTF8_VALUE,
+            MediaType.TEXT_PLAIN_VALUE,
+            MediaType.TEXT_XML_VALUE,
+            MediaType.APPLICATION_XML_VALUE,
+            "application/x-www-form-urlencoded"
+    ));
+
+    private static final int MAX_BODY_LENGTH = 2000;
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String uri = request.getRequestURI();
+        return EXCLUDE_PATHS.stream().anyMatch(uri::startsWith);
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+                                    FilterChain chain) throws ServletException, IOException {
+        // 初始化 MDC(Filter 先于 Interceptor 执行,在此统一设置)
+        String traceId = request.getHeader("X-Trace-Id");
+        if (traceId == null || traceId.isEmpty()) {
+            traceId = java.util.UUID.randomUUID().toString().replace("-", "");
+        }
+        MDC.put("traceId",    traceId);
+        MDC.put("clientIp",   getClientIp(request));
+        MDC.put("requestUri", request.getRequestURI());
+        response.setHeader("X-Trace-Id", traceId);
+
+        ContentCachingRequestWrapper reqWrapper  = new ContentCachingRequestWrapper(request);
+        ContentCachingResponseWrapper respWrapper = new ContentCachingResponseWrapper(response);
+
+        long start = System.currentTimeMillis();
+        try {
+            chain.doFilter(reqWrapper, respWrapper);
+        } finally {
+            long cost = System.currentTimeMillis() - start;
+            logRequest(reqWrapper);
+            logResponse(respWrapper, cost);
+            // 必须把响应体写回,否则客户端收不到数据
+            respWrapper.copyBodyToResponse();
+            MDC.clear();
+        }
+    }
+
+    private String getClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
+            return ip.split(",")[0].trim();
+        }
+        ip = request.getHeader("X-Real-IP");
+        if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
+            return ip;
+        }
+        return request.getRemoteAddr();
+    }
+
+    private void logRequest(ContentCachingRequestWrapper request) {
+        String traceId = MDC.get("traceId");
+        String uri     = request.getRequestURI();
+        String query   = request.getQueryString();
+        String method  = request.getMethod();
+        String body    = extractBody(request.getContentAsByteArray(), request.getContentType());
+
+        log.info(">>> [{}] {} {}{} | body={}",
+                traceId,
+                method,
+                uri,
+                query != null ? "?" + query : "",
+                body);
+    }
+
+    private void logResponse(ContentCachingResponseWrapper response, long cost) {
+        String traceId = MDC.get("traceId");
+        int    status  = response.getStatus();
+        String body    = extractBody(response.getContentAsByteArray(), response.getContentType());
+
+        log.info("<<< [{}] status={} cost={}ms | body={}",
+                traceId, status, cost, body);
+    }
+
+    private String extractBody(byte[] bodyBytes, String contentType) {
+        if (bodyBytes == null || bodyBytes.length == 0) {
+            return "";
+        }
+        if (!isLoggableContentType(contentType)) {
+            return "[binary, " + bodyBytes.length + " bytes]";
+        }
+        String body = new String(bodyBytes, StandardCharsets.UTF_8);
+        if (body.length() > MAX_BODY_LENGTH) {
+            body = body.substring(0, MAX_BODY_LENGTH) + "...(truncated)";
+        }
+        return body;
+    }
+
+    private boolean isLoggableContentType(String contentType) {
+        if (contentType == null) return true;
+        String ct = contentType.toLowerCase();
+        return LOGGABLE_CONTENT_TYPES.stream().anyMatch(ct::contains);
+    }
+}
diff --git a/xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/config/log/FeignLoggingAspect.java b/xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/config/log/FeignLoggingAspect.java
index 56caae6..16b07d3 100644
--- a/xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/config/log/FeignLoggingAspect.java
+++ b/xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/config/log/FeignLoggingAspect.java
@@ -14,7 +14,7 @@
 /**
  * Feign 调用日志切面
  *
- * <p>拦截 com.ros.feign 包下所有 Feign 接口方法,记录:
+ * <p>拦截所有带 @FeignClient 注解的接口方法,记录:
  * <ul>
  *   <li>调用的接口类名 + 方法名</li>
  *   <li>入参(JSON 序列化)</li>
@@ -22,9 +22,6 @@
  *   <li>耗时(ms)</li>
  *   <li>异常信息(含完整堆栈)</li>
  * </ul>
- *
- * <p>同时覆盖任何实现了 Feign 接口的 bean(包括 SecureGatewayFeignService 等外部引入的客户端),
- * 通过第二个切点 {@code feignBeanMethods()} 拦截。
  */
 @Slf4j
 @Aspect
@@ -32,27 +29,18 @@
 public class FeignLoggingAspect {
 
     /**
-     * 切点1:com.ros.feign 包下所有类的所有方法
-     * 覆盖 UserCenterFeign、RobotFeign、MappingServiceFeign、VmpRobotFeign 等
-     */
-    @Around("execution(* com.ros.feign..*(..))")
-    public Object logRosFeignCall(ProceedingJoinPoint pjp) throws Throwable {
-        return doLog(pjp);
-    }
-
-    /**
-     * 切点2:所有带 @FeignClient 注解的接口的实现 bean 的所有方法
-     * 覆盖 SecureGatewayFeignService 等外部包引入的 Feign 客户端
+     * 拦截所有带 @FeignClient 注解的接口的所有方法
      */
     @Around("within(@org.springframework.cloud.openfeign.FeignClient *)")
-    public Object logExternalFeignCall(ProceedingJoinPoint pjp) throws Throwable {
+    public Object logFeignCall(ProceedingJoinPoint pjp) throws Throwable {
         return doLog(pjp);
     }
 
     private Object doLog(ProceedingJoinPoint pjp) throws Throwable {
         MethodSignature signature = (MethodSignature) pjp.getSignature();
         Method method = signature.getMethod();
-        String className = pjp.getTarget().getClass().getSimpleName();
+        // 使用接口声明类名,避免打印动态代理类名(如 $Proxy270)
+        String className = signature.getDeclaringType().getSimpleName();
         String methodName = method.getName();
         String fullMethod = className + "#" + methodName;
 
diff --git a/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/config/log/FeignLoggingAspect.java b/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/config/log/FeignLoggingAspect.java
new file mode 100644
index 0000000..79b77d3
--- /dev/null
+++ b/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/config/log/FeignLoggingAspect.java
@@ -0,0 +1,83 @@
+package com.xiaopeng.maintenance.config.log;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * Feign 调用日志切面
+ *
+ * <p>拦截所有带 @FeignClient 注解的接口方法,记录:
+ * <ul>
+ *   <li>调用的接口类名 + 方法名</li>
+ *   <li>入参(JSON 序列化)</li>
+ *   <li>出参(JSON 序列化)</li>
+ *   <li>耗时(ms)</li>
+ *   <li>异常信息(含完整堆栈)</li>
+ * </ul>
+ */
+@Slf4j
+@Aspect
+@Component
+public class FeignLoggingAspect {
+
+    /**
+     * 拦截所有带 @FeignClient 注解的接口的所有方法
+     */
+    @Around("within(@org.springframework.cloud.openfeign.FeignClient *)")
+    public Object logFeignCall(ProceedingJoinPoint pjp) throws Throwable {
+        return doLog(pjp);
+    }
+
+    private Object doLog(ProceedingJoinPoint pjp) throws Throwable {
+        MethodSignature signature = (MethodSignature) pjp.getSignature();
+        Method method = signature.getMethod();
+        // 使用接口声明类名,避免打印动态代理类名(如 $Proxy270)
+        String className = signature.getDeclaringType().getSimpleName();
+        String methodName = method.getName();
+        String fullMethod = className + "#" + methodName;
+
+        Object[] args = pjp.getArgs();
+        String argsJson = safeToJson(args);
+
+        log.info("[Feign] >>> 调用开始 | method={} | args={}", fullMethod, argsJson);
+
+        long start = System.currentTimeMillis();
+        try {
+            Object result = pjp.proceed();
+            long elapsed = System.currentTimeMillis() - start;
+            String resultJson = safeToJson(result);
+            log.info("[Feign] <<< 调用成功 | method={} | elapsed={}ms | result={}", fullMethod, elapsed, resultJson);
+            return result;
+        } catch (Throwable ex) {
+            long elapsed = System.currentTimeMillis() - start;
+            log.error("[Feign] <<< 调用异常 | method={} | elapsed={}ms | error={} | args={}",
+                    fullMethod, elapsed, ex.getMessage(), argsJson, ex);
+            throw ex;
+        }
+    }
+
+    /**
+     * 安全序列化为 JSON,避免序列化失败导致日志切面本身抛异常
+     */
+    private String safeToJson(Object obj) {
+        if (obj == null) {
+            return "null";
+        }
+        try {
+            if (obj instanceof Object[]) {
+                return JSON.toJSONString(Arrays.asList((Object[]) obj));
+            }
+            return JSON.toJSONString(obj);
+        } catch (Exception e) {
+            return "[序列化失败: " + obj.getClass().getSimpleName() + "]";
+        }
+    }
+}
diff --git a/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/config/log/RequestResponseLoggingFilter.java b/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/config/log/RequestResponseLoggingFilter.java
new file mode 100644
index 0000000..f78bb74
--- /dev/null
+++ b/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/config/log/RequestResponseLoggingFilter.java
@@ -0,0 +1,139 @@
+package com.xiaopeng.maintenance.config.log;
+
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+import org.springframework.web.util.ContentCachingResponseWrapper;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 请求/响应日志过滤器
+ * 打印:traceId、method、uri、queryString、请求体、响应状态、耗时、响应体
+ */
+@Slf4j
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE + 10)
+public class RequestResponseLoggingFilter extends OncePerRequestFilter {
+
+    /** 不打印 body 的路径前缀(健康检查、actuator 等) */
+    private static final Set<String> EXCLUDE_PATHS = new HashSet<>(Arrays.asList(
+            "/actuator", "/healthcheck", "/health", "/prometheus"
+    ));
+
+    /** 只对这些 Content-Type 打印 body(避免打印二进制流) */
+    private static final Set<String> LOGGABLE_CONTENT_TYPES = new HashSet<>(Arrays.asList(
+            MediaType.APPLICATION_JSON_VALUE,
+            MediaType.APPLICATION_JSON_UTF8_VALUE,
+            MediaType.TEXT_PLAIN_VALUE,
+            MediaType.TEXT_XML_VALUE,
+            MediaType.APPLICATION_XML_VALUE,
+            "application/x-www-form-urlencoded"
+    ));
+
+    private static final int MAX_BODY_LENGTH = 2000;
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String uri = request.getRequestURI();
+        return EXCLUDE_PATHS.stream().anyMatch(uri::startsWith);
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+                                    FilterChain chain) throws ServletException, IOException {
+        // 初始化 MDC(Filter 先于 Interceptor 执行,在此统一设置)
+        String traceId = request.getHeader("X-Trace-Id");
+        if (traceId == null || traceId.isEmpty()) {
+            traceId = java.util.UUID.randomUUID().toString().replace("-", "");
+        }
+        MDC.put("traceId",    traceId);
+        MDC.put("clientIp",   getClientIp(request));
+        MDC.put("requestUri", request.getRequestURI());
+        response.setHeader("X-Trace-Id", traceId);
+
+        ContentCachingRequestWrapper reqWrapper  = new ContentCachingRequestWrapper(request);
+        ContentCachingResponseWrapper respWrapper = new ContentCachingResponseWrapper(response);
+
+        long start = System.currentTimeMillis();
+        try {
+            chain.doFilter(reqWrapper, respWrapper);
+        } finally {
+            long cost = System.currentTimeMillis() - start;
+            logRequest(reqWrapper);
+            logResponse(respWrapper, cost);
+            // 必须把响应体写回,否则客户端收不到数据
+            respWrapper.copyBodyToResponse();
+            MDC.clear();
+        }
+    }
+
+    private String getClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
+            return ip.split(",")[0].trim();
+        }
+        ip = request.getHeader("X-Real-IP");
+        if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
+            return ip;
+        }
+        return request.getRemoteAddr();
+    }
+
+    private void logRequest(ContentCachingRequestWrapper request) {
+        String traceId = MDC.get("traceId");
+        String uri     = request.getRequestURI();
+        String query   = request.getQueryString();
+        String method  = request.getMethod();
+        String body    = extractBody(request.getContentAsByteArray(), request.getContentType());
+
+        log.info(">>> [{}] {} {}{} | body={}",
+                traceId,
+                method,
+                uri,
+                query != null ? "?" + query : "",
+                body);
+    }
+
+    private void logResponse(ContentCachingResponseWrapper response, long cost) {
+        String traceId = MDC.get("traceId");
+        int    status  = response.getStatus();
+        String body    = extractBody(response.getContentAsByteArray(), response.getContentType());
+
+        log.info("<<< [{}] status={} cost={}ms | body={}",
+                traceId, status, cost, body);
+    }
+
+    private String extractBody(byte[] bodyBytes, String contentType) {
+        if (bodyBytes == null || bodyBytes.length == 0) {
+            return "";
+        }
+        if (!isLoggableContentType(contentType)) {
+            return "[binary, " + bodyBytes.length + " bytes]";
+        }
+        String body = new String(bodyBytes, StandardCharsets.UTF_8);
+        if (body.length() > MAX_BODY_LENGTH) {
+            body = body.substring(0, MAX_BODY_LENGTH) + "...(truncated)";
+        }
+        return body;
+    }
+
+    private boolean isLoggableContentType(String contentType) {
+        if (contentType == null) return true;
+        String ct = contentType.toLowerCase();
+        return LOGGABLE_CONTENT_TYPES.stream().anyMatch(ct::contains);
+    }
+}
diff --git a/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/config/log/FeignLoggingAspect.java b/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/config/log/FeignLoggingAspect.java
new file mode 100644
index 0000000..700ba2e
--- /dev/null
+++ b/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/config/log/FeignLoggingAspect.java
@@ -0,0 +1,83 @@
+package com.xiaopeng.map.config.log;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * Feign 调用日志切面
+ *
+ * <p>拦截所有带 @FeignClient 注解的接口方法,记录:
+ * <ul>
+ *   <li>调用的接口类名 + 方法名</li>
+ *   <li>入参(JSON 序列化)</li>
+ *   <li>出参(JSON 序列化)</li>
+ *   <li>耗时(ms)</li>
+ *   <li>异常信息(含完整堆栈)</li>
+ * </ul>
+ */
+@Slf4j
+@Aspect
+@Component
+public class FeignLoggingAspect {
+
+    /**
+     * 拦截所有带 @FeignClient 注解的接口的所有方法
+     */
+    @Around("within(@org.springframework.cloud.openfeign.FeignClient *)")
+    public Object logFeignCall(ProceedingJoinPoint pjp) throws Throwable {
+        return doLog(pjp);
+    }
+
+    private Object doLog(ProceedingJoinPoint pjp) throws Throwable {
+        MethodSignature signature = (MethodSignature) pjp.getSignature();
+        Method method = signature.getMethod();
+        // 使用接口声明类名,避免打印动态代理类名(如 $Proxy270)
+        String className = signature.getDeclaringType().getSimpleName();
+        String methodName = method.getName();
+        String fullMethod = className + "#" + methodName;
+
+        Object[] args = pjp.getArgs();
+        String argsJson = safeToJson(args);
+
+        log.info("[Feign] >>> 调用开始 | method={} | args={}", fullMethod, argsJson);
+
+        long start = System.currentTimeMillis();
+        try {
+            Object result = pjp.proceed();
+            long elapsed = System.currentTimeMillis() - start;
+            String resultJson = safeToJson(result);
+            log.info("[Feign] <<< 调用成功 | method={} | elapsed={}ms | result={}", fullMethod, elapsed, resultJson);
+            return result;
+        } catch (Throwable ex) {
+            long elapsed = System.currentTimeMillis() - start;
+            log.error("[Feign] <<< 调用异常 | method={} | elapsed={}ms | error={} | args={}",
+                    fullMethod, elapsed, ex.getMessage(), argsJson, ex);
+            throw ex;
+        }
+    }
+
+    /**
+     * 安全序列化为 JSON,避免序列化失败导致日志切面本身抛异常
+     */
+    private String safeToJson(Object obj) {
+        if (obj == null) {
+            return "null";
+        }
+        try {
+            if (obj instanceof Object[]) {
+                return JSON.toJSONString(Arrays.asList((Object[]) obj));
+            }
+            return JSON.toJSONString(obj);
+        } catch (Exception e) {
+            return "[序列化失败: " + obj.getClass().getSimpleName() + "]";
+        }
+    }
+}
diff --git a/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/config/log/RequestResponseLoggingFilter.java b/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/config/log/RequestResponseLoggingFilter.java
new file mode 100644
index 0000000..3ea1fdf
--- /dev/null
+++ b/xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/config/log/RequestResponseLoggingFilter.java
@@ -0,0 +1,139 @@
+package com.xiaopeng.map.config.log;
+
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+import org.springframework.web.util.ContentCachingResponseWrapper;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 请求/响应日志过滤器
+ * 打印:traceId、method、uri、queryString、请求体、响应状态、耗时、响应体
+ */
+@Slf4j
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE + 10)
+public class RequestResponseLoggingFilter extends OncePerRequestFilter {
+
+    /** 不打印 body 的路径前缀(健康检查、actuator 等) */
+    private static final Set<String> EXCLUDE_PATHS = new HashSet<>(Arrays.asList(
+            "/actuator", "/healthcheck", "/health", "/prometheus"
+    ));
+
+    /** 只对这些 Content-Type 打印 body(避免打印二进制流) */
+    private static final Set<String> LOGGABLE_CONTENT_TYPES = new HashSet<>(Arrays.asList(
+            MediaType.APPLICATION_JSON_VALUE,
+            MediaType.APPLICATION_JSON_UTF8_VALUE,
+            MediaType.TEXT_PLAIN_VALUE,
+            MediaType.TEXT_XML_VALUE,
+            MediaType.APPLICATION_XML_VALUE,
+            "application/x-www-form-urlencoded"
+    ));
+
+    private static final int MAX_BODY_LENGTH = 2000;
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String uri = request.getRequestURI();
+        return EXCLUDE_PATHS.stream().anyMatch(uri::startsWith);
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+                                    FilterChain chain) throws ServletException, IOException {
+        // 初始化 MDC(Filter 先于 Interceptor 执行,在此统一设置)
+        String traceId = request.getHeader("X-Trace-Id");
+        if (traceId == null || traceId.isEmpty()) {
+            traceId = java.util.UUID.randomUUID().toString().replace("-", "");
+        }
+        MDC.put("traceId",    traceId);
+        MDC.put("clientIp",   getClientIp(request));
+        MDC.put("requestUri", request.getRequestURI());
+        response.setHeader("X-Trace-Id", traceId);
+
+        ContentCachingRequestWrapper reqWrapper  = new ContentCachingRequestWrapper(request);
+        ContentCachingResponseWrapper respWrapper = new ContentCachingResponseWrapper(response);
+
+        long start = System.currentTimeMillis();
+        try {
+            chain.doFilter(reqWrapper, respWrapper);
+        } finally {
+            long cost = System.currentTimeMillis() - start;
+            logRequest(reqWrapper);
+            logResponse(respWrapper, cost);
+            // 必须把响应体写回,否则客户端收不到数据
+            respWrapper.copyBodyToResponse();
+            MDC.clear();
+        }
+    }
+
+    private String getClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
+            return ip.split(",")[0].trim();
+        }
+        ip = request.getHeader("X-Real-IP");
+        if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
+            return ip;
+        }
+        return request.getRemoteAddr();
+    }
+
+    private void logRequest(ContentCachingRequestWrapper request) {
+        String traceId = MDC.get("traceId");
+        String uri     = request.getRequestURI();
+        String query   = request.getQueryString();
+        String method  = request.getMethod();
+        String body    = extractBody(request.getContentAsByteArray(), request.getContentType());
+
+        log.info(">>> [{}] {} {}{} | body={}",
+                traceId,
+                method,
+                uri,
+                query != null ? "?" + query : "",
+                body);
+    }
+
+    private void logResponse(ContentCachingResponseWrapper response, long cost) {
+        String traceId = MDC.get("traceId");
+        int    status  = response.getStatus();
+        String body    = extractBody(response.getContentAsByteArray(), response.getContentType());
+
+        log.info("<<< [{}] status={} cost={}ms | body={}",
+                traceId, status, cost, body);
+    }
+
+    private String extractBody(byte[] bodyBytes, String contentType) {
+        if (bodyBytes == null || bodyBytes.length == 0) {
+            return "";
+        }
+        if (!isLoggableContentType(contentType)) {
+            return "[binary, " + bodyBytes.length + " bytes]";
+        }
+        String body = new String(bodyBytes, StandardCharsets.UTF_8);
+        if (body.length() > MAX_BODY_LENGTH) {
+            body = body.substring(0, MAX_BODY_LENGTH) + "...(truncated)";
+        }
+        return body;
+    }
+
+    private boolean isLoggableContentType(String contentType) {
+        if (contentType == null) return true;
+        String ct = contentType.toLowerCase();
+        return LOGGABLE_CONTENT_TYPES.stream().anyMatch(ct::contains);
+    }
+}
diff --git a/xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/config/log/FeignLoggingAspect.java b/xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/config/log/FeignLoggingAspect.java
index 24b8f5d..5a8b54c 100644
--- a/xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/config/log/FeignLoggingAspect.java
+++ b/xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/config/log/FeignLoggingAspect.java
@@ -14,7 +14,7 @@
 /**
  * Feign 调用日志切面
  *
- * <p>拦截 com.ros.feign 包下所有 Feign 接口方法,记录:
+ * <p>拦截所有带 @FeignClient 注解的接口方法,记录:
  * <ul>
  *   <li>调用的接口类名 + 方法名</li>
  *   <li>入参(JSON 序列化)</li>
@@ -22,9 +22,6 @@
  *   <li>耗时(ms)</li>
  *   <li>异常信息(含完整堆栈)</li>
  * </ul>
- *
- * <p>同时覆盖任何实现了 Feign 接口的 bean(包括 SecureGatewayFeignService 等外部引入的客户端),
- * 通过第二个切点 {@code feignBeanMethods()} 拦截。
  */
 @Slf4j
 @Aspect
@@ -32,27 +29,18 @@
 public class FeignLoggingAspect {
 
     /**
-     * 切点1:com.ros.feign 包下所有类的所有方法
-     * 覆盖 UserCenterFeign、RobotFeign、MappingServiceFeign、VmpRobotFeign 等
-     */
-    @Around("execution(* com.ros.feign..*(..))")
-    public Object logRosFeignCall(ProceedingJoinPoint pjp) throws Throwable {
-        return doLog(pjp);
-    }
-
-    /**
-     * 切点2:所有带 @FeignClient 注解的接口的实现 bean 的所有方法
-     * 覆盖 SecureGatewayFeignService 等外部包引入的 Feign 客户端
+     * 拦截所有带 @FeignClient 注解的接口的所有方法
      */
     @Around("within(@org.springframework.cloud.openfeign.FeignClient *)")
-    public Object logExternalFeignCall(ProceedingJoinPoint pjp) throws Throwable {
+    public Object logFeignCall(ProceedingJoinPoint pjp) throws Throwable {
         return doLog(pjp);
     }
 
     private Object doLog(ProceedingJoinPoint pjp) throws Throwable {
         MethodSignature signature = (MethodSignature) pjp.getSignature();
         Method method = signature.getMethod();
-        String className = pjp.getTarget().getClass().getSimpleName();
+        // 使用接口声明类名,避免打印动态代理类名(如 $Proxy270)
+        String className = signature.getDeclaringType().getSimpleName();
         String methodName = method.getName();
         String fullMethod = className + "#" + methodName;
 
diff --git a/xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/config/log/FeignLoggingAspect.java b/xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/config/log/FeignLoggingAspect.java
index 777a2ad..ae3c775 100644
--- a/xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/config/log/FeignLoggingAspect.java
+++ b/xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/config/log/FeignLoggingAspect.java
@@ -14,7 +14,7 @@
 /**
  * Feign 调用日志切面
  *
- * <p>拦截 com.ros.feign 包下所有 Feign 接口方法,记录:
+ * <p>拦截所有带 @FeignClient 注解的接口方法,记录:
  * <ul>
  *   <li>调用的接口类名 + 方法名</li>
  *   <li>入参(JSON 序列化)</li>
@@ -22,9 +22,6 @@
  *   <li>耗时(ms)</li>
  *   <li>异常信息(含完整堆栈)</li>
  * </ul>
- *
- * <p>同时覆盖任何实现了 Feign 接口的 bean(包括 SecureGatewayFeignService 等外部引入的客户端),
- * 通过第二个切点 {@code feignBeanMethods()} 拦截。
  */
 @Slf4j
 @Aspect
@@ -32,27 +29,18 @@
 public class FeignLoggingAspect {
 
     /**
-     * 切点1:com.ros.feign 包下所有类的所有方法
-     * 覆盖 UserCenterFeign、RobotFeign、MappingServiceFeign、VmpRobotFeign 等
-     */
-    @Around("execution(* com.ros.feign..*(..))")
-    public Object logRosFeignCall(ProceedingJoinPoint pjp) throws Throwable {
-        return doLog(pjp);
-    }
-
-    /**
-     * 切点2:所有带 @FeignClient 注解的接口的实现 bean 的所有方法
-     * 覆盖 SecureGatewayFeignService 等外部包引入的 Feign 客户端
+     * 拦截所有带 @FeignClient 注解的接口的所有方法
      */
     @Around("within(@org.springframework.cloud.openfeign.FeignClient *)")
-    public Object logExternalFeignCall(ProceedingJoinPoint pjp) throws Throwable {
+    public Object logFeignCall(ProceedingJoinPoint pjp) throws Throwable {
         return doLog(pjp);
     }
 
     private Object doLog(ProceedingJoinPoint pjp) throws Throwable {
         MethodSignature signature = (MethodSignature) pjp.getSignature();
         Method method = signature.getMethod();
-        String className = pjp.getTarget().getClass().getSimpleName();
+        // 使用接口声明类名,避免打印动态代理类名(如 $Proxy270)
+        String className = signature.getDeclaringType().getSimpleName();
         String methodName = method.getName();
         String fullMethod = className + "#" + methodName;
 

--
Gitblit v1.7.1