1. 新增分页接口聚合查询注解支持
2. 优化 databus api 日志记录的字段缺失问题 3. 新增 eplat sso 页面登录校验 4. 用户、部门编辑新增 seata 事务支持 5. 新增 iwork 流程发起接口 6. 新增 eban 同步用户时的岗位处理逻辑 7. 新增无 skywalking 时的 traceId 支持
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package com.zt.plat.framework.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 标记分页结果中需要求和的字段。
|
||||
* <p>
|
||||
* 未显式指定列名时,会默认使用实体字段对应的数据库列。
|
||||
* <p>
|
||||
* {@link #exist()} 可以用于声明该字段并不存在于表结构中,相当于为字段添加
|
||||
* {@code @TableField(exist = false)},方便在 DO 中声明专用于汇总结果的临时字段。
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PageSum {
|
||||
|
||||
/**
|
||||
* 自定义求和的数据库列名或表达式,未设置时默认使用实体字段对应的列。
|
||||
*/
|
||||
String column() default "";
|
||||
|
||||
/**
|
||||
* 是否在实体字段上声明真实存在的数据库列。
|
||||
* <p>
|
||||
* 设为 {@code false} 时,框架会自动为该字段提供 {@code @TableField(exist = false)} 的能力,
|
||||
* 适用于只在分页响应中返回的临时统计字段。
|
||||
*/
|
||||
boolean exist() default false;
|
||||
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
package com.zt.plat.framework.common.pojo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "分页结果")
|
||||
@Data
|
||||
@@ -15,19 +22,31 @@ public final class PageResult<T> implements Serializable {
|
||||
private List<T> list;
|
||||
|
||||
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("total")
|
||||
@JsonAlias({"totalCount"})
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "汇总信息(字段需使用 @PageSum 标注)")
|
||||
@JsonProperty("summary")
|
||||
private Map<String, BigDecimal> summary;
|
||||
|
||||
public PageResult() {
|
||||
this.list = new ArrayList<>();
|
||||
this.summary = Collections.emptyMap();
|
||||
}
|
||||
|
||||
public PageResult(List<T> list, Long total) {
|
||||
this(list, total, null);
|
||||
}
|
||||
|
||||
public PageResult(List<T> list, Long total, Map<String, BigDecimal> summary) {
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
setSummaryInternal(summary);
|
||||
}
|
||||
|
||||
public PageResult(Long total) {
|
||||
this.list = new ArrayList<>();
|
||||
this.total = total;
|
||||
this(new ArrayList<>(), total, null);
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> empty() {
|
||||
@@ -38,4 +57,30 @@ public final class PageResult<T> implements Serializable {
|
||||
return new PageResult<>(total);
|
||||
}
|
||||
|
||||
public void setSummary(Map<String, BigDecimal> summary) {
|
||||
setSummaryInternal(summary);
|
||||
}
|
||||
|
||||
private void setSummaryInternal(Map<String, BigDecimal> summary) {
|
||||
if (summary == null || summary.isEmpty()) {
|
||||
this.summary = Collections.emptyMap();
|
||||
return;
|
||||
}
|
||||
this.summary = new LinkedHashMap<>(summary);
|
||||
}
|
||||
|
||||
public <R> PageResult<R> convert(List<R> newList) {
|
||||
return new PageResult<>(newList, total, summary);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Long getTotalCount() {
|
||||
return total;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public void setTotalCount(Long totalCount) {
|
||||
this.total = totalCount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.zt.plat.framework.common.util.integration;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 配置参数,控制对接 ePlat 共享服务的请求行为。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "eplat.share")
|
||||
public class ShareServiceProperties {
|
||||
|
||||
private static final String DEFAULT_TOKEN_ENDPOINT_PATH = "/eplat/oauth/token";
|
||||
|
||||
/**
|
||||
* 共享服务基础地址,例如:https://example.com/share。
|
||||
*/
|
||||
private String urlPrefix;
|
||||
|
||||
/**
|
||||
* OAuth 客户端标识。
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* OAuth 客户端密钥。
|
||||
*/
|
||||
private String clientSecret;
|
||||
|
||||
/**
|
||||
* OAuth scope,默认 read。
|
||||
*/
|
||||
private String scope = "read";
|
||||
|
||||
/**
|
||||
* 访问 token 在 Redis 中的缓存 key。
|
||||
*/
|
||||
private String tokenCacheKey = "eplat:cache:shareToken";
|
||||
|
||||
/**
|
||||
* 刷新 token 在 Redis 中的缓存 key。
|
||||
*/
|
||||
private String refreshTokenCacheKey = "eplat:cache:shareRefreshToken";
|
||||
|
||||
/**
|
||||
* 调用共享服务时携带 token 的请求头名称。
|
||||
*/
|
||||
private String tokenHeaderName = "Xplat-Token";
|
||||
|
||||
/**
|
||||
* 获取 token 的接口路径,默认 /eplat/oauth/token。
|
||||
*/
|
||||
private String tokenEndpointPath = DEFAULT_TOKEN_ENDPOINT_PATH;
|
||||
|
||||
/**
|
||||
* 访问 token 默认有效期,默认 5000 秒,建议略小于服务端实际过期时间。
|
||||
*/
|
||||
private Duration tokenTtl = Duration.ofSeconds(5000);
|
||||
|
||||
/**
|
||||
* 刷新 token 默认有效期,若未设置则取访问 token 的 2 倍。
|
||||
*/
|
||||
private Duration refreshTokenTtl;
|
||||
|
||||
/**
|
||||
* 构造具体服务的请求地址。
|
||||
*
|
||||
* @param serviceNo 服务号
|
||||
* @return 完整请求地址
|
||||
*/
|
||||
public String buildServiceUrl(String serviceNo) {
|
||||
return normalizeBaseUrl(urlPrefix) + "/service/" + serviceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造获取 token 的请求地址。
|
||||
*
|
||||
* @return token 请求地址
|
||||
*/
|
||||
public String buildTokenUrl() {
|
||||
String base = normalizeBaseUrl(urlPrefix);
|
||||
String path = StrUtil.prependIfMissing(tokenEndpointPath, "/");
|
||||
return base + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 token 的缓存有效期。
|
||||
*
|
||||
* @return 刷新 token 有效期
|
||||
*/
|
||||
public Duration getRefreshTokenTtl() {
|
||||
if (refreshTokenTtl != null) {
|
||||
return refreshTokenTtl;
|
||||
}
|
||||
return tokenTtl.multipliedBy(2);
|
||||
}
|
||||
|
||||
private static String normalizeBaseUrl(String url) {
|
||||
if (StrUtil.isBlank(url)) {
|
||||
throw new IllegalArgumentException("共享服务地址不能为空");
|
||||
}
|
||||
return StrUtil.removeSuffix(url.trim(), "/");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package com.zt.plat.framework.common.util.integration;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.common.util.json.JsonUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* ePlat 共享服务调用工具,负责发送请求与自动刷新访问 token。
|
||||
*/
|
||||
@Slf4j
|
||||
public final class ShareServiceUtils {
|
||||
|
||||
private static final Duration MIN_CACHE_TTL = Duration.ofSeconds(1);
|
||||
private static final ConcurrentMap<String, Lock> TOKEN_REFRESH_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
private ShareServiceUtils() {
|
||||
}
|
||||
|
||||
public static String callShareService(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
String serviceNo,
|
||||
String requestBody) {
|
||||
return callShareService(restTemplate, redisTemplate, properties, serviceNo, (Object) requestBody);
|
||||
}
|
||||
|
||||
public static String callShareService(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
String serviceNo,
|
||||
Object requestBody) {
|
||||
Assert.notNull(restTemplate, "RestTemplate 不能为空");
|
||||
Assert.notNull(redisTemplate, "StringRedisTemplate 不能为空");
|
||||
Assert.notNull(properties, "ShareServiceProperties 不能为空");
|
||||
Assert.hasText(serviceNo, "服务号不能为空");
|
||||
|
||||
String url = properties.buildServiceUrl(serviceNo);
|
||||
String payload = convertRequestBody(requestBody);
|
||||
log.info("共享服务调用地址:[{}],请求体:[{}]", url, payload);
|
||||
|
||||
String token = obtainAccessToken(restTemplate, redisTemplate, properties);
|
||||
log.debug("共享服务服务号 [{}] 使用的 token 已获取", serviceNo);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set(properties.getTokenHeaderName(), token);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
|
||||
return Objects.requireNonNullElse(response.getBody(), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取共享服务的访问 token,可复用于自定义调用场景。
|
||||
*
|
||||
* @param restTemplate 用于请求共享服务的 {@link RestTemplate}
|
||||
* @param redisTemplate 缓存 token 的 {@link StringRedisTemplate}
|
||||
* @param properties 共享服务配置
|
||||
* @return 访问共享服务的 token
|
||||
*/
|
||||
public static String getAccessToken(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties) {
|
||||
Assert.notNull(restTemplate, "RestTemplate 不能为空");
|
||||
Assert.notNull(redisTemplate, "StringRedisTemplate 不能为空");
|
||||
Assert.notNull(properties, "ShareServiceProperties 不能为空");
|
||||
return obtainAccessToken(restTemplate, redisTemplate, properties);
|
||||
}
|
||||
|
||||
private static String convertRequestBody(Object requestBody) {
|
||||
if (requestBody == null) {
|
||||
return "";
|
||||
}
|
||||
if (requestBody instanceof String str) {
|
||||
return str;
|
||||
}
|
||||
if (requestBody instanceof byte[] bytes) {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
return JsonUtils.toJsonString(requestBody);
|
||||
}
|
||||
|
||||
private static String obtainAccessToken(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties) {
|
||||
// 直接从 Redis 读取可复用的 token
|
||||
ValueOperations<String, String> valueOps = redisTemplate.opsForValue();
|
||||
String token = valueOps.get(properties.getTokenCacheKey());
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return token;
|
||||
}
|
||||
// 针对同一个缓存 key 做细粒度加锁,避免并发刷新问题
|
||||
Lock lock = TOKEN_REFRESH_LOCKS.computeIfAbsent(properties.getTokenCacheKey(), key -> new ReentrantLock());
|
||||
lock.lock();
|
||||
try {
|
||||
token = valueOps.get(properties.getTokenCacheKey());
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return token;
|
||||
}
|
||||
return refreshAccessToken(restTemplate, redisTemplate, properties, valueOps);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static String refreshAccessToken(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
ValueOperations<String, String> valueOps) {
|
||||
String refreshToken = valueOps.get(properties.getRefreshTokenCacheKey());
|
||||
if (StrUtil.isNotBlank(refreshToken)) {
|
||||
try {
|
||||
return requestToken(restTemplate, redisTemplate, properties,
|
||||
buildRefreshTokenParams(properties, refreshToken));
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("刷新共享服务 token 失败,准备回退为 client_credentials 模式", ex);
|
||||
redisTemplate.delete(properties.getRefreshTokenCacheKey());
|
||||
}
|
||||
}
|
||||
return requestToken(restTemplate, redisTemplate, properties,
|
||||
buildClientCredentialsParams(properties));
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> buildClientCredentialsParams(ShareServiceProperties properties) {
|
||||
MultiValueMap<String, String> params = baseTokenParams(properties);
|
||||
params.add("grant_type", "client_credentials");
|
||||
if (StrUtil.isNotBlank(properties.getScope())) {
|
||||
params.add("scope", properties.getScope());
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> buildRefreshTokenParams(ShareServiceProperties properties,
|
||||
String refreshToken) {
|
||||
MultiValueMap<String, String> params = baseTokenParams(properties);
|
||||
params.add("grant_type", "refresh_token");
|
||||
params.add("refresh_token", refreshToken);
|
||||
return params;
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> baseTokenParams(ShareServiceProperties properties) {
|
||||
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||
Assert.hasText(properties.getClientId(), "clientId 不能为空");
|
||||
Assert.hasText(properties.getClientSecret(), "clientSecret 不能为空");
|
||||
params.add("client_id", properties.getClientId());
|
||||
params.add("client_secret", properties.getClientSecret());
|
||||
return params;
|
||||
}
|
||||
|
||||
private static String requestToken(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
MultiValueMap<String, String> body) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
|
||||
String tokenUrl = properties.buildTokenUrl();
|
||||
log.info("共享服务获取 token 地址:[{}],授权方式:[{}]", tokenUrl, body.getFirst("grant_type"));
|
||||
ResponseEntity<String> response;
|
||||
try {
|
||||
response = restTemplate.postForEntity(tokenUrl, entity, String.class);
|
||||
} catch (RestClientException ex) {
|
||||
throw new IllegalStateException("请求共享服务 token 失败", ex);
|
||||
}
|
||||
String responseBody = response.getBody();
|
||||
if (StrUtil.isBlank(responseBody)) {
|
||||
throw new IllegalStateException("共享服务返回的 token 内容为空");
|
||||
}
|
||||
TokenResponse tokenResponse = parseTokenResponse(responseBody);
|
||||
cacheTokens(redisTemplate, properties, tokenResponse);
|
||||
return tokenResponse.accessToken();
|
||||
}
|
||||
|
||||
private static TokenResponse parseTokenResponse(String body) {
|
||||
var node = JsonUtils.parseTree(body);
|
||||
String accessToken = node.path("access_token").asText(null);
|
||||
if (StrUtil.isBlank(accessToken)) {
|
||||
throw new IllegalStateException("共享服务返回结果缺少 access_token 字段");
|
||||
}
|
||||
String refreshToken = node.path("refresh_token").asText(null);
|
||||
long expiresIn = node.path("expires_in").asLong(-1);
|
||||
long refreshExpiresIn = node.path("refresh_expires_in").asLong(-1);
|
||||
return new TokenResponse(accessToken, refreshToken, expiresIn, refreshExpiresIn);
|
||||
}
|
||||
|
||||
private static void cacheTokens(StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
TokenResponse tokenResponse) {
|
||||
// 将最新的 token 与刷新 token 写回缓存
|
||||
ValueOperations<String, String> valueOps = redisTemplate.opsForValue();
|
||||
Duration tokenTtl = resolveTtl(tokenResponse.expiresIn(), properties.getTokenTtl());
|
||||
valueOps.set(properties.getTokenCacheKey(), tokenResponse.accessToken(), tokenTtl);
|
||||
if (StrUtil.isNotBlank(tokenResponse.refreshToken())) {
|
||||
Duration refreshTtl = resolveTtl(tokenResponse.refreshExpiresIn(), properties.getRefreshTokenTtl());
|
||||
valueOps.set(properties.getRefreshTokenCacheKey(), tokenResponse.refreshToken(), refreshTtl);
|
||||
}
|
||||
}
|
||||
|
||||
private static Duration resolveTtl(long expiresInSeconds, Duration fallback) {
|
||||
Duration effectiveFallback = fallback;
|
||||
if (effectiveFallback == null || effectiveFallback.compareTo(MIN_CACHE_TTL) < 0) {
|
||||
effectiveFallback = Duration.ofMinutes(5);
|
||||
}
|
||||
if (expiresInSeconds > 0) {
|
||||
Duration candidate = Duration.ofSeconds(expiresInSeconds);
|
||||
if (candidate.compareTo(MIN_CACHE_TTL) < 0) {
|
||||
candidate = MIN_CACHE_TTL;
|
||||
}
|
||||
return candidate.compareTo(effectiveFallback) < 0 ? candidate : effectiveFallback;
|
||||
}
|
||||
return effectiveFallback;
|
||||
}
|
||||
|
||||
private record TokenResponse(String accessToken, String refreshToken, long expiresIn, long refreshExpiresIn) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
package com.zt.plat.framework.common.util.monitor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 链路追踪工具类
|
||||
@@ -9,7 +17,33 @@ import org.apache.skywalking.apm.toolkit.trace.TraceContext;
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class TracerUtils {
|
||||
public final class TracerUtils {
|
||||
|
||||
/**
|
||||
* SkyWalking 在未接入 Agent 时返回的默认占位值
|
||||
*/
|
||||
private static final String SKY_WALKING_PLACEHOLDER = "N/A";
|
||||
|
||||
/**
|
||||
* SkyWalking 在忽略追踪时返回的占位值
|
||||
*/
|
||||
private static final String SKY_WALKING_IGNORED = "Ignored_Trace";
|
||||
|
||||
private static final String MDC_TRACE_ID_KEY = "traceId";
|
||||
private static final String REQUEST_ATTRIBUTE_KEY = TracerUtils.class.getName() + ".TRACE_ID";
|
||||
private static final String[] HEADER_CANDIDATES = {
|
||||
"trace-id",
|
||||
"Trace-Id",
|
||||
"x-trace-id",
|
||||
"X-Trace-Id",
|
||||
"x-request-id",
|
||||
"X-Request-Id"
|
||||
};
|
||||
|
||||
/**
|
||||
* 兜底的 traceId,保证在未接入链路追踪时依旧具备追踪能力
|
||||
*/
|
||||
private static final InheritableThreadLocal<String> FALLBACK_TRACE_ID = new InheritableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 私有化构造方法
|
||||
@@ -18,13 +52,121 @@ public class TracerUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
|
||||
* 如果不存在的话为空字符串!!!
|
||||
* 获得链路追踪编号。
|
||||
* <p>
|
||||
* 优先返回 SkyWalking 的 TraceId;在缺少链路上下文或者未接入 SkyWalking 时,会优先复用来自请求上下文的 TraceId,
|
||||
* 否则生成一个新的兜底 TraceId,并在当前线程、请求上下文与日志 MDC 中缓存,确保后续组件能够复用。
|
||||
*
|
||||
* @return 链路追踪编号
|
||||
*/
|
||||
public static String getTraceId() {
|
||||
return TraceContext.traceId();
|
||||
String traceId = TraceContext.traceId();
|
||||
if (isValidTraceId(traceId)) {
|
||||
cacheTraceId(traceId);
|
||||
return traceId;
|
||||
}
|
||||
String cached = resolveCachedTraceId();
|
||||
if (StringUtils.isNotBlank(cached)) {
|
||||
return cached;
|
||||
}
|
||||
String generated = generateFallbackTraceId();
|
||||
cacheTraceId(generated);
|
||||
return generated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动绑定外部传入的 TraceId,例如消费消息、处理异步任务时。
|
||||
*
|
||||
* @param traceId 链路编号
|
||||
*/
|
||||
public static void bindTraceId(String traceId) {
|
||||
if (StringUtils.isBlank(traceId)) {
|
||||
return;
|
||||
}
|
||||
cacheTraceId(traceId.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理当前线程关联的兜底 traceId,避免线程复用导致污染。
|
||||
*/
|
||||
public static void clear() {
|
||||
FALLBACK_TRACE_ID.remove();
|
||||
MDC.remove(MDC_TRACE_ID_KEY);
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request != null) {
|
||||
request.removeAttribute(REQUEST_ATTRIBUTE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidTraceId(String traceId) {
|
||||
if (StringUtils.isBlank(traceId)) {
|
||||
return false;
|
||||
}
|
||||
if (StringUtils.equalsIgnoreCase(traceId, SKY_WALKING_PLACEHOLDER)) {
|
||||
return false;
|
||||
}
|
||||
return !StringUtils.equalsIgnoreCase(traceId, SKY_WALKING_IGNORED);
|
||||
}
|
||||
|
||||
private static String resolveCachedTraceId() {
|
||||
String cached = FALLBACK_TRACE_ID.get();
|
||||
if (StringUtils.isNotBlank(cached)) {
|
||||
return cached;
|
||||
}
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request != null) {
|
||||
Object attribute = request.getAttribute(REQUEST_ATTRIBUTE_KEY);
|
||||
if (attribute instanceof String attrValue && StringUtils.isNotBlank(attrValue)) {
|
||||
cacheTraceId(attrValue);
|
||||
return attrValue;
|
||||
}
|
||||
String headerValue = resolveTraceIdFromHeader(request);
|
||||
if (StringUtils.isNotBlank(headerValue)) {
|
||||
cacheTraceId(headerValue);
|
||||
return headerValue;
|
||||
}
|
||||
}
|
||||
String mdcTraceId = MDC.get(MDC_TRACE_ID_KEY);
|
||||
if (StringUtils.isNotBlank(mdcTraceId)) {
|
||||
cacheTraceId(mdcTraceId);
|
||||
return mdcTraceId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void cacheTraceId(String traceId) {
|
||||
if (StringUtils.isBlank(traceId)) {
|
||||
return;
|
||||
}
|
||||
String trimmed = traceId.trim();
|
||||
FALLBACK_TRACE_ID.set(trimmed);
|
||||
MDC.put(MDC_TRACE_ID_KEY, trimmed);
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request != null) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_KEY, trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpServletRequest currentRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes instanceof ServletRequestAttributes servletRequestAttributes) {
|
||||
return servletRequestAttributes.getRequest();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String resolveTraceIdFromHeader(HttpServletRequest request) {
|
||||
for (String header : HEADER_CANDIDATES) {
|
||||
String value = request.getHeader(header);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String generateFallbackTraceId() {
|
||||
return StringUtils.replace(UUID.randomUUID().toString(), "-", "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.framework.common.util.collection.CollectionUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
@@ -69,10 +70,13 @@ public class BeanUtils {
|
||||
return null;
|
||||
}
|
||||
List<T> list = toBean(source.getList(), targetType);
|
||||
if (list == null) {
|
||||
list = Collections.emptyList();
|
||||
}
|
||||
if (peek != null) {
|
||||
list.forEach(peek);
|
||||
}
|
||||
return new PageResult<>(list, source.getTotal());
|
||||
return new PageResult<>(list, source.getTotal(), source.getSummary());
|
||||
}
|
||||
|
||||
public static void copyProperties(Object source, Object target) {
|
||||
|
||||
Reference in New Issue
Block a user