diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/domain/AjaxResult.java b/fastbee-common/src/main/java/com/fastbee/common/core/domain/AjaxResult.java index e5f1513f..c6d7c86f 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/domain/AjaxResult.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/domain/AjaxResult.java @@ -1,6 +1,7 @@ package com.fastbee.common.core.domain; import com.fastbee.common.constant.HttpStatus; +import com.fastbee.common.trace.TraceContext; import com.fastbee.common.utils.StringUtils; import java.util.HashMap; @@ -196,4 +197,20 @@ public class AjaxResult extends HashMap { super.put(key, value); return this; } + + /** + * 附带当前链路 TraceId + * + *

用于绕过 ResponseBodyAdvice 直接写响应的场景(Security 认证失败、重复提交拦截等), + * 这类响应不经过 TraceIdResponseAdvice,需要手动携带 TraceId 供前端排查。 + * + * @return 当前对象,便于链式调用 + */ + public AjaxResult withTraceId() { + String traceId = TraceContext.getTraceId(); + if (traceId != null) { + super.put(TraceContext.TRACE_ID_KEY, traceId); + } + return this; + } } diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/domain/CommonResult.java b/fastbee-common/src/main/java/com/fastbee/common/core/domain/CommonResult.java index 8c571e65..dd8bcefe 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/domain/CommonResult.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/domain/CommonResult.java @@ -34,6 +34,13 @@ public class CommonResult implements Serializable { * @see ErrorCode#getMsg() () */ private String msg; + /** + * 链路追踪ID,由 TraceIdResponseAdvice 统一回填,用于前后端联查日志 + * + *

属性名必须与 {@link com.fastbee.common.trace.TraceContext#TRACE_ID_KEY} 保持一致, + * 否则本类序列化出的 JSON 字段名会与 AjaxResult 注入的字段名不一致。 + */ + private String traceId; /** * 将传入的 result 对象,转换成另外一个泛型结果的对象 diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/domain/R.java b/fastbee-common/src/main/java/com/fastbee/common/core/domain/R.java index 49ce39d9..802601e8 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/domain/R.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/domain/R.java @@ -25,6 +25,14 @@ public class R implements Serializable { private T data; + /** + * 链路追踪ID,由 TraceIdResponseAdvice 统一回填,用于前后端联查日志 + * + *

属性名必须与 {@link com.fastbee.common.trace.TraceContext#TRACE_ID_KEY} 保持一致, + * 否则本类序列化出的 JSON 字段名会与 AjaxResult 注入的字段名不一致。 + */ + private String traceId; + public static R ok() { return restResult(null, SUCCESS, "操作成功"); } @@ -96,4 +104,12 @@ public class R implements Serializable { public void setData(T data) { this.data = data; } + + public String getTraceId() { + return traceId; + } + + public void setTraceId(String traceId) { + this.traceId = traceId; + } } diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/page/TableDataInfo.java b/fastbee-common/src/main/java/com/fastbee/common/core/page/TableDataInfo.java index a78d0b4e..2aec9863 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/page/TableDataInfo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/page/TableDataInfo.java @@ -31,6 +31,14 @@ public class TableDataInfo implements Serializable { */ private String msg; + /** + * 链路追踪ID,由 TraceIdResponseAdvice 统一回填,用于前后端联查日志 + * + *

属性名必须与 {@link com.fastbee.common.trace.TraceContext#TRACE_ID_KEY} 保持一致, + * 否则本类序列化出的 JSON 字段名会与 AjaxResult 注入的字段名不一致。 + */ + private String traceId; + /** * 表格数据对象 */ @@ -79,4 +87,12 @@ public class TableDataInfo implements Serializable { public void setMsg(String msg) { this.msg = msg; } + + public String getTraceId() { + return traceId; + } + + public void setTraceId(String traceId) { + this.traceId = traceId; + } } diff --git a/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java index 7c807628..06e663a3 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java +++ b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java @@ -44,10 +44,25 @@ import java.io.Serializable; public class TraceContext { /** - * TraceId 在 MDC 中的 key + * TraceId 的名称,同时用于日志 MDC 键名与 HTTP 响应体 JSON 字段名 + * + *

两者刻意共用同一个名称:日志里 {@code grep 'traceId=xxx'} 与前端看到的 + * {@code traceId: xxx} 是同一个值,避免排障时再做一次名称换算。 */ public static final String TRACE_ID_KEY = "traceId"; + /** + * TraceId 在 HTTP Header / MQ Header 中的名称 + * HTTP 传播沿用业界惯例的连字符风格,因此与 MDC / JSON 的字段名不同 + */ + public static final String TRACE_ID_HEADER = "X-Trace-Id"; + + /** + * TraceId 的合法字符集与长度上限 + * 用于校验上游传入的 TraceId,防止伪造或包含换行符导致日志注入 + */ + public static final String TRACE_ID_PATTERN = "^[A-Za-z0-9_-]{1,64}$"; + /** * TenantId 在 MDC 中的 key * 预留字段,暂未使用 diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/config/ResourcesConfig.java b/fastbee-framework/src/main/java/com/fastbee/framework/config/ResourcesConfig.java index de355449..b6752364 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/config/ResourcesConfig.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/config/ResourcesConfig.java @@ -2,12 +2,15 @@ package com.fastbee.framework.config; import com.fastbee.common.config.RuoYiConfig; import com.fastbee.common.constant.Constants; +import com.fastbee.common.trace.TraceContext; +import com.fastbee.framework.filter.TraceIdFilter; import com.fastbee.framework.interceptor.LanguageInterceptor; import com.fastbee.framework.interceptor.RepeatSubmitInterceptor; -import com.fastbee.framework.interceptor.TraceIdInterceptor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.http.CacheControl; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -30,8 +33,20 @@ public class ResourcesConfig implements WebMvcConfigurer { private RepeatSubmitInterceptor repeatSubmitInterceptor; @Resource private LanguageInterceptor languageInterceptor; - @Autowired - private TraceIdInterceptor traceIdInterceptor; + + /** + * TraceId 过滤器注册 + * 必须排在 Spring Security 过滤器链(-100)之前,保证 token 校验、鉴权失败等早期日志 + * 与 Controller 返回的 traceId 属于同一条链路 + */ + @Bean + public FilterRegistrationBean traceIdFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(new TraceIdFilter()); + registration.addUrlPatterns("/*"); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + registration.setName("traceIdFilter"); + return registration; + } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { @@ -51,8 +66,7 @@ public class ResourcesConfig implements WebMvcConfigurer { */ @Override public void addInterceptors(InterceptorRegistry registry) { - // TraceId拦截器放在最前面,确保所有请求都有TraceId - registry.addInterceptor(traceIdInterceptor).addPathPatterns("/**").order(0); + // TraceId 统一由 TraceIdFilter 在 Servlet 过滤器最前端生成,此处不再重复处理 registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); //这里配置国际化拦截器的白名单 registry.addInterceptor(languageInterceptor).addPathPatterns("/**").excludePathPatterns("/v2/api-docs", @@ -70,6 +84,8 @@ public class ResourcesConfig implements WebMvcConfigurer { config.addAllowedOriginPattern("*"); // 设置访问源请求头 config.addAllowedHeader("*"); + // 暴露 TraceId 响应头,跨域场景下前端 JS 才能读取 + config.addExposedHeader(TraceContext.TRACE_ID_HEADER); // 设置访问源请求方法 config.addAllowedMethod("*"); // 有效期 1800秒 diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/filter/TraceIdFilter.java b/fastbee-framework/src/main/java/com/fastbee/framework/filter/TraceIdFilter.java new file mode 100644 index 00000000..b1a85d74 --- /dev/null +++ b/fastbee-framework/src/main/java/com/fastbee/framework/filter/TraceIdFilter.java @@ -0,0 +1,80 @@ +package com.fastbee.framework.filter; + +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * TraceId 入口过滤器 + * + *

放在 Servlet 过滤器链最前端(早于 Spring Security 的 JwtAuthenticationTokenFilter), + * 保证 token 校验、鉴权失败等早期日志也带有 TraceId,与 Controller 返回的 TraceId 一致。 + * + *

职责: + *

+ * + * @author FastBee + */ +public class TraceIdFilter extends OncePerRequestFilter { + + private static final Logger log = LoggerFactory.getLogger(TraceIdFilter.class); + + private static final Pattern TRACE_ID_MATCHER = Pattern.compile(TraceContext.TRACE_ID_PATTERN); + + /** + * 不打请求日志的路径前缀 + * 过滤器作用域是 /*,静态资源与监控端点数量大且无排查价值,避免淹没业务日志 + */ + private static final Set NO_LOG_PATH_PREFIX = new HashSet<>(Arrays.asList( + "/profile", "/statics", "/webjars", "/actuator", "/druid", "/favicon.ico", "/ueditor")); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + String traceId = request.getHeader(TraceContext.TRACE_ID_HEADER); + // 上游 TraceId 不合法(含换行符等)时重新生成,避免日志注入 + if (traceId == null || traceId.isEmpty() || !TRACE_ID_MATCHER.matcher(traceId).matches()) { + traceId = TraceIdGenerator.generate(); + } + TraceContext.setTraceId(traceId); + response.setHeader(TraceContext.TRACE_ID_HEADER, traceId); + + String uri = request.getRequestURI(); + try { + if (isNeedLog(uri)) { + log.info("请求入口: {} {}", request.getMethod(), uri); + } + chain.doFilter(request, response); + } finally { + TraceContext.clear(); + } + } + + private boolean isNeedLog(String uri) { + if (uri == null) { + return false; + } + for (String prefix : NO_LOG_PATH_PREFIX) { + if (uri.startsWith(prefix)) { + return false; + } + } + return true; + } +} diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/RepeatSubmitInterceptor.java b/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/RepeatSubmitInterceptor.java index 812ccbd0..bb008200 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/RepeatSubmitInterceptor.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/RepeatSubmitInterceptor.java @@ -27,7 +27,7 @@ public abstract class RepeatSubmitInterceptor implements HandlerInterceptor { RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (this.isRepeatSubmit(request, annotation)) { - AjaxResult ajaxResult = AjaxResult.error(annotation.message()); + AjaxResult ajaxResult = AjaxResult.error(annotation.message()).withTraceId(); ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); return false; } diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java b/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java deleted file mode 100644 index af9675c8..00000000 --- a/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.fastbee.framework.interceptor; - -import com.fastbee.common.trace.TraceContext; -import com.fastbee.common.trace.TraceIdGenerator; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * 日志追踪ID拦截器 - * 用于生成和传递TraceId,方便同一个链路调用追查问题 - * - * @author Sumuxi - */ -@Component -public class TraceIdInterceptor implements HandlerInterceptor { - - private static final String TRACE_ID_HEADER = "X-Trace-Id"; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - // 获取请求URI - String requestURI = request.getRequestURI(); - - // 尝试从请求头中获取TraceId(支持分布式追踪) - String traceId = request.getHeader(TRACE_ID_HEADER); - - // 如果请求头中没有TraceId,则生成一个新的 - if (traceId == null || traceId.isEmpty()) { - traceId = TraceIdGenerator.generate(); - } - - // 设置到 TraceContext(自动同步到 MDC) - TraceContext.setTraceId(traceId); - - // 将TraceId放入响应头,方便前端获取 - response.setHeader(TRACE_ID_HEADER, traceId); - - // 打印请求路径日志 - org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TraceIdInterceptor.class); - log.info("拦截请求路径: {}", requestURI); - - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { - // 请求完成后清除 TraceContext,避免内存泄漏 - TraceContext.clear(); - } -} - diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/security/handle/AuthenticationEntryPointImpl.java b/fastbee-framework/src/main/java/com/fastbee/framework/security/handle/AuthenticationEntryPointImpl.java index 5c596260..a212af16 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/security/handle/AuthenticationEntryPointImpl.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/security/handle/AuthenticationEntryPointImpl.java @@ -23,20 +23,10 @@ import java.io.Serializable; public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -8970718410437077606L; - public static boolean isAjaxRequest(HttpServletRequest request) { - String ajaxFlag = request.getHeader("X-Requested-With"); - return ajaxFlag != null && "XMLHttpRequest".equals(ajaxFlag); - } - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) - throws IOException { -// if (isAjaxRequest(request)){ + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { int code = HttpStatus.UNAUTHORIZED; String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); - ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); -// }else { -// response.sendRedirect("/oauth/login"); -// } + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg).withTraceId())); } } diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/security/handle/LogoutSuccessHandlerImpl.java b/fastbee-framework/src/main/java/com/fastbee/framework/security/handle/LogoutSuccessHandlerImpl.java index 5c984243..09d9f942 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/security/handle/LogoutSuccessHandlerImpl.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -45,6 +45,6 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { // 记录用户退出日志 AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功")); } - ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功"))); + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功").withTraceId())); } } diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/web/advice/TraceIdResponseAdvice.java b/fastbee-framework/src/main/java/com/fastbee/framework/web/advice/TraceIdResponseAdvice.java new file mode 100644 index 00000000..0cf0b441 --- /dev/null +++ b/fastbee-framework/src/main/java/com/fastbee/framework/web/advice/TraceIdResponseAdvice.java @@ -0,0 +1,140 @@ +package com.fastbee.framework.web.advice; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fastbee.common.core.domain.CommonResult; +import com.fastbee.common.core.domain.R; +import com.fastbee.common.core.page.TableDataInfo; +import com.fastbee.common.trace.TraceContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * TraceId 响应体注入 + * + *

所有经 JSON 序列化的 Controller 返回值统一带上 traceId 字段,前端报错时可直接把 + * traceId 报给后端,用 {@code grep traceId=xxx logs/sys-all.log} 定位整条链路。 + * + *

覆盖范围:AjaxResult、R、CommonResult、TableDataInfo 以及直接返回 VO 的接口, + * 同时包含 {@code @RestControllerAdvice} 异常处理返回的 AjaxResult。 + * + *

处理策略: + *

+ * + * @author FastBee + */ +@RestControllerAdvice +// 最后执行:若有其它 ResponseBodyAdvice(如统一结果包装)先执行,本 advice 再在其结果上补 traceId +@Order() +public class TraceIdResponseAdvice implements ResponseBodyAdvice { + + /** + * 不注入响应体的路径前缀 + * Swagger/OpenAPI 文档是既定结构的元数据,注入业务字段会污染契约 + */ + private static final List EXCLUDE_PATH_PREFIX = Arrays.asList( + "/v2/api-docs", "/v3/api-docs", "/swagger-resources", "/webjars", "/actuator"); + + private static final Logger log = LoggerFactory.getLogger(TraceIdResponseAdvice.class); + + private final ObjectMapper objectMapper; + + public TraceIdResponseAdvice(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType); + } + + @Override + @SuppressWarnings("unchecked") + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + String traceId = TraceContext.getTraceId(); + if (traceId == null) { + return body; + } + // 响应头始终写入:即使响应体无法追加字段,前端仍可从 header 取到 TraceId + response.getHeaders().set(TraceContext.TRACE_ID_HEADER, traceId); + + if (body == null || isExcludePath(request.getURI().getPath())) { + return body; + } + // Map 子类无需 JSON 树转换,直接进入零开销路径 + if (body instanceof Map) { + writeToMap((Map) body, traceId, request.getURI().getPath()); + return body; + } + if (body instanceof R) { + ((R) body).setTraceId(traceId); + return body; + } + if (body instanceof CommonResult) { + ((CommonResult) body).setTraceId(traceId); + return body; + } + if (body instanceof TableDataInfo) { + ((TableDataInfo) body).setTraceId(traceId); + return body; + } + // 直接返回 VO 的场景:复用容器 ObjectMapper 转成 JSON 对象节点,保证序列化配置一致 + try { + JsonNode node = objectMapper.valueToTree(body); + if (node instanceof ObjectNode) { + ((ObjectNode) node).put(TraceContext.TRACE_ID_KEY, traceId); + return node; + } + } catch (RuntimeException e) { + // JSON 树转换失败(如存在循环引用)时保持原样,由响应头兜底,绝不让旁路功能影响业务响应 + log.warn("响应体注入 traceId 失败,已降级到响应头,uri={}", request.getURI().getPath(), e); + } + return body; + } + + /** + * 向 Map 响应体写入 traceId + * 不可变 Map(如 Collections.unmodifiableMap)会拒绝写入,此时由响应头兜底 + */ + private void writeToMap(Map body, String traceId, String uri) { + try { + body.put(TraceContext.TRACE_ID_KEY, traceId); + } catch (UnsupportedOperationException e) { + log.debug("响应体为不可变 Map,traceId 仅输出到响应头,uri={}", uri, e); + } + } + + private boolean isExcludePath(String path) { + if (path == null) { + return false; + } + for (String prefix : EXCLUDE_PATH_PREFIX) { + if (path.startsWith(prefix)) { + return true; + } + } + return false; + } +} diff --git a/yack-app/apis/http.interceptor.js b/yack-app/apis/http.interceptor.js index 48eb7711..28ee617b 100644 --- a/yack-app/apis/http.interceptor.js +++ b/yack-app/apis/http.interceptor.js @@ -7,6 +7,17 @@ const codeMessage = { 500: '服务器内部错误,无法完成请求', }; +/** + * 给错误提示附加 traceId,便于用户报错后到后端日志里检索整条链路 + * 优先取响应体 traceId,其次取 X-Trace-Id 响应头 + */ +const withTraceId = (msg, res) => { + // 小程序平台 response header 字段名不统一,两个都取 + const header = res.header || res.headers || {}; + const traceId = (res.data && res.data.traceId) || header['X-Trace-Id'] || header['x-trace-id'] || ''; + return traceId ? `${msg} [traceId:${traceId}]` : msg; +}; + const install = (Vue, vm) => { // 这个配置是一次配置,全局通用的,具体参数见 https://www.uviewui.com/js/http.html uni.$u.http.setConfig((config) => { @@ -115,10 +126,10 @@ const install = (Vue, vm) => { return result } else { console.log(result); - vm.$u.toast(result.msg); + vm.$u.toast(withTraceId(result.msg, res)); } } else { - vm.$u.toast(res.data.msg); + vm.$u.toast(withTraceId((res.data && res.data.msg) || '', res)); } return false; }); diff --git a/yack-view/src/api/axios.ts b/yack-view/src/api/axios.ts index 775094c5..1ce4fc70 100644 --- a/yack-view/src/api/axios.ts +++ b/yack-view/src/api/axios.ts @@ -71,8 +71,10 @@ axiosInstance.interceptors.response.use( return Promise.resolve(res.data) } - // 提示错误 - window['$message'].error(window['$t']((res.data as any).msg)) + // 提示错误(附带 traceId,便于用户报错后按此 ID 检索后端整条链路日志) + const { msg, traceId } = res.data as { msg: string; traceId?: string } + const message = window['$t'](msg) + window['$message'].error(traceId ? `${message} [traceId:${traceId}]` : message) return Promise.resolve(res.data) }, (err: AxiosError) => { diff --git a/yack-web/src/utils/request.js b/yack-web/src/utils/request.js index 95279d58..c7cc9854 100644 --- a/yack-web/src/utils/request.js +++ b/yack-web/src/utils/request.js @@ -11,6 +11,31 @@ let downloadLoadingInstance; // 是否显示重新登录 export let isRelogin = { show: false }; +/** + * 提取响应中的链路追踪ID + * 优先取响应体 traceId 字段,其次取 X-Trace-Id 响应头(二进制/网络异常场景) + * + * @param {Object} res axios 响应对象 + * @returns {String} traceId,取不到时返回空串 + */ +function getTraceId(res) { + if (!res) { + return ''; + } + return (res.data && res.data.traceId) || (res.headers && res.headers['x-trace-id']) || ''; +} + +/** + * 给错误提示附加 traceId,便于用户报错后直接到日志里检索整条链路 + * + * @param {String} message 原始提示 + * @param {String} traceId 链路追踪ID + * @returns {String} 拼接后的提示 + */ +function appendTraceId(message, traceId) { + return traceId ? `${message} [traceId:${traceId}]` : message; +} + axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'; // 创建axios实例 const service = axios.create({ @@ -102,13 +127,13 @@ service.interceptors.response.use( } return Promise.reject('无效的会话,或者会话已过期,请重新登录。'); } else if (code === 500) { - Message({ message: msg, type: 'error' }); + Message({ message: appendTraceId(msg, getTraceId(res)), type: 'error', duration: 5 * 1000 }); return Promise.reject('error'); } else if (code === 601) { - Message({ message: msg, type: 'warning' }); + Message({ message: appendTraceId(msg, getTraceId(res)), type: 'warning', duration: 5 * 1000 }); return Promise.reject('error'); } else if (code !== 200) { - Notification.error({ title: msg }); + Notification.error({ title: msg, message: getTraceId(res) ? 'traceId:' + getTraceId(res) : '', duration: 5 * 1000 }); return Promise.reject('error'); } else { return res.data; @@ -124,7 +149,7 @@ service.interceptors.response.use( } else if (message.includes('Request failed with status code')) { message = '系统接口' + message.substr(message.length - 3) + '异常'; } - Message({ message: message, type: 'error', duration: 5 * 1000 }); + Message({ message: appendTraceId(message, getTraceId(error.response)), type: 'error', duration: 5 * 1000 }); return Promise.reject(error); } ); @@ -152,7 +177,7 @@ export function download(url, params, filename, config) { const resText = await data.text(); const rspObj = JSON.parse(resText); const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']; - Message.error(errMsg); + Message.error({ message: appendTraceId(errMsg, rspObj.traceId), duration: 5 * 1000 }); } downloadLoadingInstance.close(); })