From f5314c0ae1aa7a78f02fca3366ae0bdae2520e0b Mon Sep 17 00:00:00 2001 From: yang chen Date: Tue, 2 Dec 2025 10:36:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(biz):=20=E5=AE=9E=E7=8E=B0=E5=8F=AF?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E7=9A=84HTTP=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增RetryableHttpClient,支持自动登录和token刷新 - 实现线程安全的令牌管理机制 - 添加RestTemplate配置和条件化注入 - 创建业务HTTP客户端服务接口及实现类 - 集成Hutool工具库进行数据转换处理 - 实现API请求失败后的重试逻辑 - 添加详细的日志记录和异常处理机制 --- .../module/biz/config/HttpClientConfig.java | 22 +++ .../module/biz/http/RetryableHttpClient.java | 149 ++++++++++++++++++ .../biz/service/IBizHttpClientService.java | 18 +++ .../impl/BizHttpClientServiceImpl.java | 26 +++ 4 files changed, 215 insertions(+) create mode 100644 jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/config/HttpClientConfig.java create mode 100644 jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/http/RetryableHttpClient.java create mode 100644 jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/service/IBizHttpClientService.java create mode 100644 jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/service/impl/BizHttpClientServiceImpl.java diff --git a/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/config/HttpClientConfig.java b/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/config/HttpClientConfig.java new file mode 100644 index 0000000..5652f4f --- /dev/null +++ b/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/config/HttpClientConfig.java @@ -0,0 +1,22 @@ +package com.jeelowcode.module.biz.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * HttpClient配置类 + * + * @author lingma + */ +@Configuration +public class HttpClientConfig { + + @Bean + @ConditionalOnMissingBean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} \ No newline at end of file diff --git a/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/http/RetryableHttpClient.java b/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/http/RetryableHttpClient.java new file mode 100644 index 0000000..3209468 --- /dev/null +++ b/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/http/RetryableHttpClient.java @@ -0,0 +1,149 @@ +package com.jeelowcode.module.biz.http; + +import cn.hutool.core.convert.Convert; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 可重试的HttpClient,支持自动登录和token刷新 + * 线程安全的实现,可以在并发环境下使用 + */ +@Slf4j +@Component +@ConditionalOnProperty( + name = "jeelowcode.powerenv.baseurl", matchIfMissing = true) +public class RetryableHttpClient { + + @Value("${jeelowcode.powerenv.baseurl}") + private String baseUrl; + + // 访问令牌URL + private static final String ACCESS_TOKEN_URL = "/_campus/open/accessToken.json"; + + // API接口URL + private static final String API_URL = "/_campus/open/api/invoked.json"; + + // 应用凭证 + private static final String APP_KEY = "campusT0Yoh1U6tXD3ZVXy"; + private static final String APP_SECRET = "3d4d89b5-8a14-4b83-a9aa-715bdc8264a1"; + + // 存储访问令牌及相关信息 + private volatile String accessToken; + private volatile long tokenExpireTime; + + // 用于保护令牌更新操作的锁 + private final ReentrantLock tokenLock = new ReentrantLock(); + + // RestTemplate实例 + @Resource + private RestTemplate restTemplate; + + @PostConstruct + public void init() { + // 初始化时获取一次访问令牌 + refreshAccessToken(); + } + + /** + * 获取API数据 + * + * @return ResponseEntity 响应结果 + */ + public ResponseEntity getApiData() { + // 检查令牌是否过期,如果过期则刷新 + if (isTokenExpired()) { + refreshAccessToken(); + } + + // 构建带access_token参数的URL + String url = API_URL + "?access_token=" + accessToken; + + try { + // 尝试请求API + return restTemplate.getForEntity(url, String.class); + } catch (HttpClientErrorException.Unauthorized e) { + log.warn("API请求未授权,尝试刷新访问令牌后重试"); + // 如果是401未授权错误,则刷新令牌后重试 + refreshAccessToken(); + url = API_URL + "?access_token=" + accessToken; + return restTemplate.getForEntity(url, String.class); + } catch (Exception e) { + log.error("请求API时发生异常", e); + throw e; + } + } + + /** + * 刷新访问令牌 + */ + private void refreshAccessToken() { + tokenLock.lock(); + try { + // 再次检查令牌是否已经被其他线程更新 + if (!isTokenExpired()) { + return; + } + + log.info("开始刷新访问令牌"); + + // 构造获取令牌的参数 + String tokenUrl = baseUrl + ACCESS_TOKEN_URL + "?appKey=" + APP_KEY + "&" + APP_SECRET; + + try { + // 请求访问令牌 + ResponseEntity response = restTemplate.getForEntity(tokenUrl, Map.class); + + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + Map responseBody = Convert.toMap(String.class, Object.class, response.getBody()); + + // 检查响应是否成功 + Boolean success = (Boolean) responseBody.get("success"); + if (success != null && success) { + Map content = Convert.toMap(String.class, Object.class, responseBody.get("content")); + + // 更新访问令牌和过期时间 + accessToken = (String) content.get("access_token"); + Integer expiresIn = (Integer) content.get("expires_in"); + + // 设置过期时间(提前5分钟过期以确保安全) + tokenExpireTime = System.currentTimeMillis() + (expiresIn - 300) * 1000L; + + log.info("成功刷新访问令牌,新令牌将在 {} 过期", new java.util.Date(tokenExpireTime)); + } else { + log.error("获取访问令牌失败: {}", responseBody); + throw new RuntimeException("无法获取访问令牌: " + responseBody.get("msg")); + } + } else { + log.error("获取访问令牌失败,HTTP状态码: {}", response.getStatusCode()); + throw new RuntimeException("无法获取访问令牌,HTTP状态码: " + response.getStatusCode()); + } + } catch (Exception e) { + log.error("刷新访问令牌时发生异常", e); + throw new RuntimeException("刷新访问令牌失败", e); + } + } finally { + tokenLock.unlock(); + } + } + + /** + * 检查令牌是否已过期 + * + * @return boolean 如果令牌已过期返回true,否则返回false + */ + private boolean isTokenExpired() { + // 如果令牌为空或者已经过期 + return accessToken == null || System.currentTimeMillis() >= tokenExpireTime; + } + +} \ No newline at end of file diff --git a/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/service/IBizHttpClientService.java b/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/service/IBizHttpClientService.java new file mode 100644 index 0000000..db25ecb --- /dev/null +++ b/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/service/IBizHttpClientService.java @@ -0,0 +1,18 @@ +package com.jeelowcode.module.biz.service; + +import org.springframework.http.ResponseEntity; + +/** + * 业务HTTP客户端服务接口 + * + * @author lingma + */ +public interface IBizHttpClientService { + + /** + * 获取API数据 + * + * @return ResponseEntity 响应结果 + */ + ResponseEntity getApiData(); +} \ No newline at end of file diff --git a/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/service/impl/BizHttpClientServiceImpl.java b/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/service/impl/BizHttpClientServiceImpl.java new file mode 100644 index 0000000..58345d0 --- /dev/null +++ b/jeelowcode-module/jeelowcode-module-biz/src/main/java/com/jeelowcode/module/biz/service/impl/BizHttpClientServiceImpl.java @@ -0,0 +1,26 @@ +package com.jeelowcode.module.biz.service.impl; + +import com.jeelowcode.module.biz.http.RetryableHttpClient; +import com.jeelowcode.module.biz.service.IBizHttpClientService; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 业务HTTP客户端服务实现类 + * + * @author lingma + */ +@Service +public class BizHttpClientServiceImpl implements IBizHttpClientService { + + @Resource + private RetryableHttpClient retryableHttpClient; + + @Override + public ResponseEntity getApiData() { + return retryableHttpClient.getApiData(); + } + +} \ No newline at end of file