Просмотр исходного кода

feat(core): 统一响应结构新增链路追踪字段

- 在 R<T> 类中新增 timestamp、requestId 和 traceId 字段
- 自动填充时间戳与请求/追踪 ID,便于日志关联与问题追踪
- 新增 TraceInterceptor 拦截器处理请求头并注入 MDC
- 工具类 TraceUtils 提供 ID 获取与生成逻辑
- 更新 logback 配置,在日志格式中加入 requestId 与 traceId
- 文档同步更新,说明新字段用途及使用方式
mcbaiyun 1 месяц назад
Родитель
Сommit
4beba25077

+ 33 - 0
docs/DevRule/06-日志和错误处理规范.md

@@ -186,3 +186,36 @@ try {
   ```
   ```
 
 
 注意:日志中以字符串记录 ID 并不改变数据库或实体类型,仅是记录/展示层的一致性约定。
 注意:日志中以字符串记录 ID 并不改变数据库或实体类型,仅是记录/展示层的一致性约定。
+
+### 响应体中新增链路追踪字段(timestamp / requestId / traceId)
+
+为便于调用方与日志系统做关联,API 的统一返回结构 `R<T>` 已新增 `timestamp`、`requestId`、`traceId` 三个字段:
+
+- timestamp:返回时的 Unix 毫秒时间戳,用于对照日志时间或做简单耗时统计。
+- requestId:HTTP 请求级别的唯一 ID,建议客户端设置 `X-Request-Id`,如果客户端未设置,服务端会自动生成。该 ID 也会保存在日志的 MDC 中(日志格式中使用 `%X{requestId}`),便于将一条响应与该请求在日志中关联起来。
+- traceId:分布式链路追踪 ID(用于跨服务链路分析),建议由客户端或上游服务传入 `X-Trace-Id`;若未传入,服务端会生成一个 UUID。该字段也写入 MDC(`%X{traceId}`),便于配合 Zipkin/Jaeger 等链路追踪系统排查问题。
+
+示例响应:
+
+```json
+{
+  "code": 1000,
+  "message": "ok",
+  "timestamp": 1697028512345,
+  "requestId": "123e4567-e89b-12d3-a456-426614174000",
+  "traceId": "abcd1234efgh5678",
+  "data": { ... }
+}
+```
+
+日志例子(logback pattern 建议包含 `%X{requestId}` 与 `%X{traceId}`):
+
+```
+2025-11-14 10:00:00.000 [http-nio-8080-exec-1] [123e4567-e89b-12d3-a456-426614174000] [abcd1234efgh5678] INFO work.baiyun.chronicdiseaseapp.service.UserService - user created id=100
+```
+
+注意:
+
+- 不要在 `requestId` 或 `traceId` 中携带敏感信息(如 token、身份证号等)。这些字段只用于追踪和关联日志,非鉴权或隐私字段。
+- 如果系统已接入 Spring Cloud Sleuth / OpenTelemetry 等链路追踪框架,优先使用它们提供的 traceId;本约定的 `traceId` 与第三方追踪工具应尽量保持一致(例如使用 `X-B3-TraceId` 或 `traceparent`)。
+- 以上字段的加入旨在提升链路追踪与诊断效率,不影响现有字段的含义;若调用方不需要,可忽略这些字段。

+ 19 - 0
src/main/java/work/baiyun/chronicdiseaseapp/common/R.java

@@ -1,12 +1,19 @@
 package work.baiyun.chronicdiseaseapp.common;
 package work.baiyun.chronicdiseaseapp.common;
 
 
 import lombok.Data;
 import lombok.Data;
+import work.baiyun.chronicdiseaseapp.util.TraceUtils;
 
 
 @Data
 @Data
 public class R<T> {
 public class R<T> {
     private Integer code;
     private Integer code;
     private String message;
     private String message;
     private T data;
     private T data;
+    /** 时间戳(毫秒) */
+    private Long timestamp;
+    /** 请求 id (可用于追踪原始请求) */
+    private String requestId;
+    /** 链路 trace id(可用于分布式追踪) */
+    private String traceId;
 
 
     /**
     /**
     * @param code
     * @param code
@@ -17,6 +24,9 @@ public class R<T> {
         R<Void> result = new R<>();
         R<Void> result = new R<>();
         result.code = code;
         result.code = code;
         result.message = message;
         result.message = message;
+        result.timestamp = System.currentTimeMillis();
+        result.requestId = TraceUtils.getRequestId();
+        result.traceId = TraceUtils.getTraceId();
         return result;
         return result;
     }
     }
 
 
@@ -25,6 +35,9 @@ public class R<T> {
         result.code = code;
         result.code = code;
         result.message = message;
         result.message = message;
         result.data = data;
         result.data = data;
+        result.timestamp = System.currentTimeMillis();
+        result.requestId = TraceUtils.getRequestId();
+        result.traceId = TraceUtils.getTraceId();
         return result;
         return result;
     }
     }
 
 
@@ -32,6 +45,9 @@ public class R<T> {
         R<Void> result = new R<>();
         R<Void> result = new R<>();
         result.code = code;
         result.code = code;
         result.message = message;
         result.message = message;
+        result.timestamp = System.currentTimeMillis();
+        result.requestId = TraceUtils.getRequestId();
+        result.traceId = TraceUtils.getTraceId();
         return result;
         return result;
     }
     }
     public static <T> R<T> fail(Integer code, String message, T data){
     public static <T> R<T> fail(Integer code, String message, T data){
@@ -39,6 +55,9 @@ public class R<T> {
         result.code = code;
         result.code = code;
         result.message = message;
         result.message = message;
         result.data = data;
         result.data = data;
+        result.timestamp = System.currentTimeMillis();
+        result.requestId = TraceUtils.getRequestId();
+        result.traceId = TraceUtils.getTraceId();
         return result;
         return result;
     }
     }
 }
 }

+ 48 - 0
src/main/java/work/baiyun/chronicdiseaseapp/config/TraceInterceptor.java

@@ -0,0 +1,48 @@
+package work.baiyun.chronicdiseaseapp.config;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.MDC;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.UUID;
+
+@Component
+public class TraceInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
+        // 从 header 中优先读取
+        String requestId = request.getHeader("X-Request-Id");
+        if (requestId == null || requestId.isEmpty()) {
+            requestId = request.getHeader("X-RequestId");
+        }
+        if (requestId == null || requestId.isEmpty()) {
+            requestId = UUID.randomUUID().toString();
+        }
+
+        String traceId = request.getHeader("X-Trace-Id");
+        if (traceId == null || traceId.isEmpty()) {
+            traceId = request.getHeader("traceId");
+        }
+        if (traceId == null || traceId.isEmpty()) {
+            traceId = UUID.randomUUID().toString();
+        }
+
+        // 放入 Request attribute & MDC, 便于后续使用
+        request.setAttribute("requestId", requestId);
+        request.setAttribute("traceId", traceId);
+        MDC.put("requestId", requestId);
+        MDC.put("traceId", traceId);
+
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) throws Exception {
+        MDC.remove("requestId");
+        MDC.remove("traceId");
+    }
+}

+ 9 - 2
src/main/java/work/baiyun/chronicdiseaseapp/config/WebMvcConfig.java

@@ -10,16 +10,23 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 public class WebMvcConfig implements WebMvcConfigurer {
 public class WebMvcConfig implements WebMvcConfigurer {
 
 
     private final AuthInterceptor authInterceptor;
     private final AuthInterceptor authInterceptor;
+    private final TraceInterceptor traceInterceptor;
 
 
     @Autowired
     @Autowired
-    public WebMvcConfig(AuthInterceptor authInterceptor) {
+    public WebMvcConfig(AuthInterceptor authInterceptor, TraceInterceptor traceInterceptor) {
         this.authInterceptor = authInterceptor;
         this.authInterceptor = authInterceptor;
+        this.traceInterceptor = traceInterceptor;
     }
     }
 
 
     @Override
     @Override
     public void addInterceptors(@NonNull InterceptorRegistry registry) {
     public void addInterceptors(@NonNull InterceptorRegistry registry) {
         // 拦截所有请求,但排除登录和 Swagger/OpenAPI 相关路径
         // 拦截所有请求,但排除登录和 Swagger/OpenAPI 相关路径
-        registry.addInterceptor(authInterceptor)
+    // TraceInterceptor should be first to populate MDC/request attributes
+    registry.addInterceptor(traceInterceptor)
+        .addPathPatterns("/**")
+        .excludePathPatterns("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/doc.html", "/webjars/**", "/favicon.ico");
+
+    registry.addInterceptor(authInterceptor)
                 .addPathPatterns("/**")
                 .addPathPatterns("/**")
                 .excludePathPatterns(
                 .excludePathPatterns(
                         "/", 
                         "/", 

+ 51 - 0
src/main/java/work/baiyun/chronicdiseaseapp/util/TraceUtils.java

@@ -0,0 +1,51 @@
+package work.baiyun.chronicdiseaseapp.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.MDC;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.UUID;
+
+/**
+ * TraceUtils - 从请求或 MDC 中获取 requestId 与 traceId(若无则生成),用于填充 API 返回值以便于追踪。
+ */
+public class TraceUtils {
+
+    public static String getRequestId() {
+        // 优先从 MDC
+        String rid = MDC.get("requestId");
+        if (rid != null && !rid.isEmpty()) return rid;
+
+        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
+        if (attrs instanceof ServletRequestAttributes) {
+            HttpServletRequest req = ((ServletRequestAttributes) attrs).getRequest();
+            rid = req.getHeader("X-Request-Id");
+            if (rid != null && !rid.isEmpty()) return rid;
+            Object attr = req.getAttribute("requestId");
+            if (attr != null) return String.valueOf(attr);
+        }
+        // fallback generate
+        return generateId();
+    }
+
+    public static String getTraceId() {
+        String tid = MDC.get("traceId");
+        if (tid != null && !tid.isEmpty()) return tid;
+
+        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
+        if (attrs instanceof ServletRequestAttributes) {
+            HttpServletRequest req = ((ServletRequestAttributes) attrs).getRequest();
+            tid = req.getHeader("X-Trace-Id");
+            if (tid != null && !tid.isEmpty()) return tid;
+            Object attr = req.getAttribute("traceId");
+            if (attr != null) return String.valueOf(attr);
+        }
+        return generateId();
+    }
+
+    private static String generateId() {
+        return UUID.randomUUID().toString();
+    }
+}

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

@@ -3,7 +3,7 @@
 
 
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{requestId}] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
         </encoder>
         </encoder>
     </appender>
     </appender>
 
 
@@ -14,7 +14,7 @@
             <maxHistory>30</maxHistory>
             <maxHistory>30</maxHistory>
         </rollingPolicy>
         </rollingPolicy>
         <encoder>
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{requestId}] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
         </encoder>
         </encoder>
     </appender>
     </appender>