feat(biz): 实现动环系统HTTP客户端服务功能

- 新增园区、建筑及设备指标数据的获取方法
- 完善动环系统相关DTO类定义,包括分页数据结构
- 优化HTTP客户端的请求处理逻辑,增强错误处理机制
- 支持自动刷新访问令牌及请求重试机制
- 统一请求参数构建方式,提升代码可维护性
This commit is contained in:
2025-12-02 15:53:48 +08:00
parent 4cbb6c24d5
commit bd5ad3baf0
8 changed files with 277 additions and 34 deletions

View File

@@ -1,7 +0,0 @@
package com.jeelowcode.module.biz.dto;
// todo 待完善
public class PowerEnvBuildingDataDTO {
}

View File

@@ -0,0 +1,47 @@
package com.jeelowcode.module.biz.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 动环系统建筑数据
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统建筑数据")
public class PowerEnvBuildingItemDTO {
/**
* 建筑ID
*/
@Schema(description = "建筑ID")
private String buildingId;
/**
* 建筑名称
*/
@Schema(description = "建筑名称")
private String buildingName;
/**
* 园区ID
*/
@Schema(description = "园区ID")
private String campusId;
/**
* 园区名称
*/
@Schema(description = "园区名称")
private String campusName;
/**
* 状态(0:停用,1:启用)
*/
@Schema(description = "状态(0:停用,1:启用)")
private Integer status;
}

View File

@@ -1,9 +0,0 @@
package com.jeelowcode.module.biz.dto;
import lombok.Data;
// todo 待完善
@Data
public class PowerEnvCampusDataDTO {
}

View File

@@ -0,0 +1,35 @@
package com.jeelowcode.module.biz.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 动环系统园区信息
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统园区信息")
public class PowerEnvCampusItemDTO {
/**
* 园区编号
*/
@Schema(description = "园区编号")
private String campusId;
/**
* 园区名称
*/
@Schema(description = "园区名称")
private String campusName;
/**
* 状态(0:停用,1:启用)
*/
@Schema(description = "状态(0:停用,1:启用)")
private Integer status;
}

View File

@@ -0,0 +1,49 @@
package com.jeelowcode.module.biz.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 动环系统园区数据DTO
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统园区数据DTO")
public class PowerEnvPageDataDTO<T> {
/**
* 总页数
*/
@Schema(description = "总页数")
private Integer totalPage = 0;
/**
* 每页数量
*/
@Schema(description = "每页数量")
private Integer pageSize = 0;
/**
* 当前页码
*/
@Schema(description = "当前页码")
private Integer currentPage = 0;
/**
* 总记录数
*/
@Schema(description = "总记录数")
private Integer totalCount = 0;
/**
* 数据项
*/
@Schema(description = "数据项")
private List<T> items;
}

View File

@@ -1,6 +1,7 @@
package com.jeelowcode.module.biz.http;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.jeelowcode.module.biz.dto.PowerEnvRequestParamsDTO;
import com.jeelowcode.module.biz.dto.PowerEnvResponseDataDTO;
import lombok.extern.slf4j.Slf4j;
@@ -9,6 +10,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
@@ -17,23 +19,35 @@ import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重试的HttpClient支持自动登录和token刷新
* 线程安全的实现,可以在并发环境下使用
*
* @author yangchenjj
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "jeelowcode.powerenv.baseurl")
public class RetryableHttpClient {
/**
* 动环系统基础URL
*/
@Value("${jeelowcode.powerenv.baseurl}")
private String baseUrl;
/**
* 动环系统应用Key
*/
@Value("${jeelowcode.powerenv.appKey:campusT0Yoh1U6tXD3ZVXy}")
private String appKey;
/**
* 动环系统应用密钥
*/
@Value("${jeelowcode.powerenv.appSecret:3d4d89b5-8a14-4b83-a9aa-715bdc8264a1}")
private String appSecret;
@@ -62,12 +76,10 @@ public class RetryableHttpClient {
/**
* 发送GET请求并获取响应数据
*
* @param apiPath API接口路径
* @param requestParams 请求参数
* @return 响应数据
*/
public ResponseEntity<PowerEnvResponseDataDTO<?>> getData(
String apiPath, PowerEnvRequestParamsDTO<?> requestParams) {
public ResponseEntity<PowerEnvResponseDataDTO<?>> getData(PowerEnvRequestParamsDTO<?> requestParams) {
// 检查令牌是否过期,如果过期则刷新
if (isTokenExpired()) {
refreshAccessToken();
@@ -75,18 +87,49 @@ public class RetryableHttpClient {
// 构建带access_token参数的URL
String url = baseUrl + API_URL + "?access_token=" + accessToken;
try {
return restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(requestParams),
// 请求数据
ResponseEntity<PowerEnvResponseDataDTO<?>> response = restTemplate
.exchange(url, HttpMethod.POST, new HttpEntity<>(requestParams),
new ParameterizedTypeReference<PowerEnvResponseDataDTO<?>>() {
});
} catch (HttpClientErrorException.Unauthorized e) {
log.warn("API请求未授权尝试刷新访问令牌后重试");
// 如果是401未授权错误则刷新令牌后重试
// 检查响应状态码
if (!Objects.equals(response.getStatusCode(), HttpStatus.OK)) {
// 如果不是200则抛出异常
throw new HttpClientErrorException(response.getStatusCode(), "API请求失败");
}
// 如果请求返回200,则检查响应内容
PowerEnvResponseDataDTO<?> responseData = response.getBody();
if (!Objects.requireNonNull(responseData).getSuccess()) {
// 如果响应消息为false则还需要进一步查看错误信息
if (StrUtil.equalsAny(responseData.getCode(), "OPEN_API_PARAM_ERROR", "ACCESS_TOKEN_ERROR")) {
// 需要刷新Token并且重试
refreshAccessToken();
url = baseUrl + API_URL + "?access_token=" + accessToken;
return restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(requestParams),
// 获得重试结果
ResponseEntity<PowerEnvResponseDataDTO<?>> retryResponse =
restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(requestParams),
new ParameterizedTypeReference<PowerEnvResponseDataDTO<?>>() {
});
if (Objects.equals(retryResponse.getStatusCode(), HttpStatus.OK) &&
Objects.requireNonNull(retryResponse.getBody()).getSuccess()) {
// 只放请求成功的结果
return retryResponse;
} else {
// 重试如果除了任何异常,则抛出异常
throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, responseData.getMsg());
}
} else {
// 直接抛出异常
throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, responseData.getMsg());
}
}
// 如果成功了,则直接返回响应数据
return response;
} catch (Exception e) {
// 如果发生异常,则在日志中记录异常,并且抛出
log.error("请求API时发生异常", e);
throw e;
}

View File

@@ -1,11 +1,36 @@
package com.jeelowcode.module.biz.service;
import com.jeelowcode.module.biz.dto.*;
/**
* 业务HTTP客户端服务接口
* 动环系统客户端服务接口
*
* @author lingma
* @author yangchenjj
*/
public interface IBizHttpClientService {
/**
* 获取园区列表
*
* @param params 园区查询参数
* @return 园区数据列表
*/
PowerEnvPageDataDTO<PowerEnvCampusItemDTO> listCampus(PowerEnvCampusParamsDTO params);
/**
* 获取建筑列表
*
* @param params 建筑查询参数
* @return 建筑数据列表
*/
PowerEnvPageDataDTO<PowerEnvBuildingItemDTO> listBuilding(PowerEnvBuildingParamsDTO params);
/**
* 获取设备指标数据列表
*
* @param deviceId 设备ID
* @return 设备指标数据列表
*/
PowerEnvDeviceDataDTO listDeviceMetrics(String deviceId);
}

View File

@@ -1,11 +1,17 @@
package com.jeelowcode.module.biz.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import com.jeelowcode.module.biz.dto.*;
import com.jeelowcode.module.biz.http.RetryableHttpClient;
import com.jeelowcode.module.biz.service.IBizHttpClientService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Optional;
/**
* 业务HTTP客户端服务实现类
@@ -19,4 +25,58 @@ public class BizHttpClientServiceImpl implements IBizHttpClientService {
@Resource
private RetryableHttpClient retryableHttpClient;
/**
* 获取园区分页数据
*
* @param params 园区查询参数
* @return 园区分页数据
*/
@Override
public PowerEnvPageDataDTO<PowerEnvCampusItemDTO> listCampus(PowerEnvCampusParamsDTO params) {
PowerEnvResponseDataDTO<?> response = retryableHttpClient.getData(
PowerEnvRequestParamsDTO.buildCampusRequestParams(params)).getBody();
return Optional.ofNullable(response)
.map(PowerEnvResponseDataDTO::getContent)
.map(content -> Convert.convert(
new TypeReference<PowerEnvPageDataDTO<PowerEnvCampusItemDTO>>() {
}, content))
.orElse(new PowerEnvPageDataDTO<PowerEnvCampusItemDTO>().setItems(Collections.emptyList()));
}
/**
* 获取建筑分页数据
*
* @param params 建筑查询参数
* @return 建筑分页数据
*/
@Override
public PowerEnvPageDataDTO<PowerEnvBuildingItemDTO> listBuilding(PowerEnvBuildingParamsDTO params) {
PowerEnvResponseDataDTO<?> response = retryableHttpClient.getData(
PowerEnvRequestParamsDTO.buildBuildingRequestParams(params)).getBody();
return Optional.ofNullable(response)
.map(PowerEnvResponseDataDTO::getContent)
.map(content -> Convert.convert(
new TypeReference<PowerEnvPageDataDTO<PowerEnvBuildingItemDTO>>() {
}, content))
.orElse(new PowerEnvPageDataDTO<PowerEnvBuildingItemDTO>().setItems(Collections.emptyList()));
}
/**
* 获取设备指标数据
*
* @param deviceId 设备ID
* @return 设备指标数据
*/
@Override
public PowerEnvDeviceDataDTO listDeviceMetrics(String deviceId) {
PowerEnvResponseDataDTO<?> response = retryableHttpClient.getData(
PowerEnvRequestParamsDTO.buildDeviceRunDataRequestParams(deviceId)).getBody();
return Optional.ofNullable(response)
.map(PowerEnvResponseDataDTO::getContent)
.map(content -> BeanUtil.toBean(content, PowerEnvDeviceDataDTO.class))
.orElse(new PowerEnvDeviceDataDTO().setDeviceUid(deviceId).setPropertyRunDataList(Collections.emptyList()));
}
}