feat(powerenv): 实现动环系统API集成和访问令牌管理

- 添加动环系统相关DTO类,包括访问令牌、建筑、园区、设备等数据传输对象
- 实现基于注解的条件化HTTP客户端服务,支持动态启用/禁用
- 集成可重试HTTP客户端,支持自动刷新访问令牌和异常重试
- 移除旧的RestTemplate配置类,统一通过Spring Boot自动配置管理
- 更新API调用方式,支持POST请求和结构化响应处理
- 添加应用密钥和密钥的外部化配置支持
- 完善动环系统接口参数封装和响应解析逻辑
This commit is contained in:
2025-12-02 14:13:32 +08:00
parent f5314c0ae1
commit e8015fb33f
13 changed files with 405 additions and 54 deletions

View File

@@ -1,22 +0,0 @@
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();
}
}

View File

@@ -0,0 +1,32 @@
package com.jeelowcode.module.biz.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 动环系统访问令牌数据DTO
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统访问令牌数据DTO")
public class PowerEnvAccessTokenDataDTO {
/**
* 访问令牌
*/
@JsonProperty("access_token")
@Schema(description = "访问令牌")
private String accessToken;
/**
* 访问令牌过期时间
*/
@JsonProperty("expires_in")
@Schema(description = "访问令牌过期时间")
private Long expiresIn;
}

View File

@@ -0,0 +1,7 @@
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;
/**
* 动环系统建筑请求参数DTO
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统建筑请求参数DTO")
public class PowerEnvBuildingParamsDTO {
/**
* 每页数量
*/
@Schema(description = "每页数量")
private String pageSize;
/**
* 页码
*/
@Schema(description = "页码")
private String page;
/**
* 园区编号
*/
@Schema(description = "园区编号")
private String campusId;
/**
* 关键字
*/
@Schema(description = "关键字")
private String key;
/**
* 状态(0:停用,1:启用)
*/
@Schema(description = "状态(0:停用,1:启用)")
private String status;
}

View File

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

View File

@@ -0,0 +1,41 @@
package com.jeelowcode.module.biz.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 动环系统园区请求参数DTO
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统园区请求参数DTO")
public class PowerEnvCampusParamsDTO {
/**
* 每页数量
*/
@Schema(description = "每页数量")
private String pageSize;
/**
* 页码
*/
@Schema(description = "页码")
private String page;
/**
* 关键字
*/
@Schema(description = "关键字")
private String key;
/**
* 状态(0:停用,1:启用)
*/
@Schema(description = "状态(0:停用,1:启用)")
private String status;
}

View File

@@ -0,0 +1,31 @@
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;
/**
* 动环系统设备信息数据
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统设备信息数据")
public class PowerEnvDeviceDataDTO {
/**
* 设备唯一标识
*/
@Schema(description = "设备唯一标识")
private String deviceUid;
/**
* 设备运行数据列表
*/
@Schema(description = "设备运行数据列表")
private List<PowerEnvDeviceMetricDataDTO> propertyRunDataList;
}

View File

@@ -0,0 +1,74 @@
package com.jeelowcode.module.biz.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 动环系统设备运行数据DTO
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统设备运行数据DTO")
public class PowerEnvDeviceMetricDataDTO {
/**
* 设备运行数据ID
*/
@Schema(description = "设备运行数据ID")
private String metadataUid;
/**
* 设备运行数据编码
*/
@Schema(description = "设备运行数据编码")
private String metadataCode;
/**
* 设备运行数据名称
*/
@Schema(description = "设备运行数据名称")
private String metadataName;
/**
* 设备运行数据属性编码
*/
@Schema(description = "设备运行数据属性编码")
private String propertyCode;
/**
* 设备运行数据属性名称
*/
@Schema(description = "设备运行数据属性名称")
private String propertyName;
/**
* 设备运行数据单位编码
*/
@Schema(description = "设备运行数据单位编码")
private String unitCode;
/**
* 设备运行数据值类型
*/
@Schema(description = "设备运行数据值类型")
private Integer valueType;
/**
* 设备运行数据更新时间
*/
@Schema(description = "设备运行数据更新时间")
private Long updateTime;
/**
* 设备运行点位类型
*/
@Schema(description = "设备运行点位类型")
private Integer type;
/**
* 设备运行数据值
*/
@Schema(description = "设备运行数据值")
private String value;
/**
* 设备运行数据控制枚举值
*/
@Schema(description = "设备运行数据控制枚举值")
private String controlEnumValue;
}

View File

@@ -0,0 +1,89 @@
package com.jeelowcode.module.biz.dto;
import com.google.common.collect.Lists;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 动环请求参数
*
* @param <P> 参数值
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统请求参数DTO")
public class PowerEnvRequestParamsDTO<P> {
/**
* 获取校区列表接口地址
*/
private final static String API_LIST_CAMPUS = "/space/campus/list";
/**
* 获取楼栋列表接口地址
*/
private final static String API_LIST_BUILDING = "/space/building/list";
/**
* 获取设备运行数据接口地址
*/
private final static String API_LIST_DEVICE_RUN_DATA = "/device/device/getDeviceRunData";
/**
* 接口地址
*/
@Schema(description = "接口地址")
private String api;
/**
* 请求参数
*/
@Schema(description = "参数值")
private List<P> params;
/**
* 构建请求参数对象
*
* @param api 接口地址
* @param param 请求参数
* @param <P> 参数类型
* @return PowerEnvRequestParamsDTO<P> 请求参数对象
*/
public static <P> PowerEnvRequestParamsDTO<P> buildRequestParams(String api, P param) {
return new PowerEnvRequestParamsDTO<P>().setApi(api).setParams(Lists.newArrayList(param));
}
/**
* 构建获取校区列表的请求参数对象
*
* @param param 校区参数
* @return PowerEnvRequestParamsDTO<PowerEnvCampusParamsDTO> 请求参数对象
*/
public static PowerEnvRequestParamsDTO<PowerEnvCampusParamsDTO> buildCampusRequestParams(PowerEnvCampusParamsDTO param) {
return buildRequestParams(API_LIST_CAMPUS, param);
}
/**
* 构建获取楼栋列表的请求参数对象
*
* @param param 楼栋参数
* @return PowerEnvRequestParamsDTO<PowerEnvBuildingParamsDTO> 请求参数对象
*/
public static PowerEnvRequestParamsDTO<PowerEnvBuildingParamsDTO> buildBuildingRequestParams(PowerEnvBuildingParamsDTO param) {
return buildRequestParams(API_LIST_BUILDING, param);
}
/**
* 构建获取设备运行数据的请求参数对象
*
* @param param 设备运行数据参数
* @return PowerEnvRequestParamsDTO<String> 请求参数对象
*/
public static PowerEnvRequestParamsDTO<String> buildDeviceRunDataRequestParams(String param) {
return buildRequestParams(API_LIST_DEVICE_RUN_DATA, param);
}
}

View File

@@ -0,0 +1,45 @@
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;
/**
* 动环系统接口响应体
*
* @author yangchenjj
*/
@Data
@Accessors(chain = true)
@Schema(description = "动环系统接口响应体")
public class PowerEnvResponseDataDTO<T> {
/**
* 响应消息
*/
@Schema(description = "响应消息")
private String msg;
/**
* 响应码
*/
@Schema(description = "响应码")
private String code;
/**
* 请求ID
*/
@Schema(description = "请求ID")
private String requestId;
/**
* 是否成功
*/
@Schema(description = "是否成功")
private Boolean success;
/**
* 响应内容
*/
@Schema(description = "响应内容")
private List<T> content;
}

View File

@@ -1,9 +1,14 @@
package com.jeelowcode.module.biz.http; package com.jeelowcode.module.biz.http;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import com.jeelowcode.module.biz.dto.PowerEnvRequestParamsDTO;
import com.jeelowcode.module.biz.dto.PowerEnvResponseDataDTO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 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.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpClientErrorException;
@@ -20,23 +25,24 @@ import java.util.concurrent.locks.ReentrantLock;
*/ */
@Slf4j @Slf4j
@Component @Component
@ConditionalOnProperty( @ConditionalOnProperty(name = "jeelowcode.powerenv.baseurl")
name = "jeelowcode.powerenv.baseurl", matchIfMissing = true)
public class RetryableHttpClient { public class RetryableHttpClient {
@Value("${jeelowcode.powerenv.baseurl}") @Value("${jeelowcode.powerenv.baseurl}")
private String baseUrl; private String baseUrl;
@Value("${jeelowcode.powerenv.appKey:campusT0Yoh1U6tXD3ZVXy}")
private String appKey;
@Value("${jeelowcode.powerenv.appSecret:3d4d89b5-8a14-4b83-a9aa-715bdc8264a1}")
private String appSecret;
// 访问令牌URL // 访问令牌URL
private static final String ACCESS_TOKEN_URL = "/_campus/open/accessToken.json"; private static final String ACCESS_TOKEN_URL = "/_campus/open/accessToken.json";
// API接口URL // API接口URL
private static final String API_URL = "/_campus/open/api/invoked.json"; 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 String accessToken;
private volatile long tokenExpireTime; private volatile long tokenExpireTime;
@@ -44,7 +50,6 @@ public class RetryableHttpClient {
// 用于保护令牌更新操作的锁 // 用于保护令牌更新操作的锁
private final ReentrantLock tokenLock = new ReentrantLock(); private final ReentrantLock tokenLock = new ReentrantLock();
// RestTemplate实例
@Resource @Resource
private RestTemplate restTemplate; private RestTemplate restTemplate;
@@ -55,28 +60,32 @@ public class RetryableHttpClient {
} }
/** /**
* 获取API数据 * 发送GET请求并获取响应数据
* *
* @return ResponseEntity<String> 响应结果 * @param apiPath API接口路径
* @param requestParams 请求参数
* @return 响应数据
*/ */
public ResponseEntity<String> getApiData() { public ResponseEntity<PowerEnvResponseDataDTO<?>> getData(
String apiPath, PowerEnvRequestParamsDTO<?> requestParams) {
// 检查令牌是否过期,如果过期则刷新 // 检查令牌是否过期,如果过期则刷新
if (isTokenExpired()) { if (isTokenExpired()) {
refreshAccessToken(); refreshAccessToken();
} }
// 构建带access_token参数的URL // 构建带access_token参数的URL
String url = API_URL + "?access_token=" + accessToken; String url = baseUrl + API_URL + "?access_token=" + accessToken;
try { try {
// 尝试请求API return restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(requestParams),
return restTemplate.getForEntity(url, String.class); new ParameterizedTypeReference<PowerEnvResponseDataDTO<?>>() {
});
} catch (HttpClientErrorException.Unauthorized e) { } catch (HttpClientErrorException.Unauthorized e) {
log.warn("API请求未授权尝试刷新访问令牌后重试"); log.warn("API请求未授权尝试刷新访问令牌后重试");
// 如果是401未授权错误则刷新令牌后重试 // 如果是401未授权错误则刷新令牌后重试
refreshAccessToken(); refreshAccessToken();
url = API_URL + "?access_token=" + accessToken; url = baseUrl + API_URL + "?access_token=" + accessToken;
return restTemplate.getForEntity(url, String.class); return restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(requestParams),
new ParameterizedTypeReference<PowerEnvResponseDataDTO<?>>() {
});
} catch (Exception e) { } catch (Exception e) {
log.error("请求API时发生异常", e); log.error("请求API时发生异常", e);
throw e; throw e;
@@ -97,7 +106,7 @@ public class RetryableHttpClient {
log.info("开始刷新访问令牌"); log.info("开始刷新访问令牌");
// 构造获取令牌的参数 // 构造获取令牌的参数
String tokenUrl = baseUrl + ACCESS_TOKEN_URL + "?appKey=" + APP_KEY + "&" + APP_SECRET; String tokenUrl = baseUrl + ACCESS_TOKEN_URL + "?appKey=" + appKey + "&" + appSecret;
try { try {
// 请求访问令牌 // 请求访问令牌

View File

@@ -1,7 +1,5 @@
package com.jeelowcode.module.biz.service; package com.jeelowcode.module.biz.service;
import org.springframework.http.ResponseEntity;
/** /**
* 业务HTTP客户端服务接口 * 业务HTTP客户端服务接口
* *
@@ -9,10 +7,5 @@ import org.springframework.http.ResponseEntity;
*/ */
public interface IBizHttpClientService { public interface IBizHttpClientService {
/**
* 获取API数据
*
* @return ResponseEntity<String> 响应结果
*/
ResponseEntity<String> getApiData();
} }

View File

@@ -2,7 +2,7 @@ package com.jeelowcode.module.biz.service.impl;
import com.jeelowcode.module.biz.http.RetryableHttpClient; import com.jeelowcode.module.biz.http.RetryableHttpClient;
import com.jeelowcode.module.biz.service.IBizHttpClientService; import com.jeelowcode.module.biz.service.IBizHttpClientService;
import org.springframework.http.ResponseEntity; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@@ -13,14 +13,10 @@ import javax.annotation.Resource;
* @author lingma * @author lingma
*/ */
@Service @Service
@ConditionalOnProperty(name = "jeelowcode.powerenv.baseurl")
public class BizHttpClientServiceImpl implements IBizHttpClientService { public class BizHttpClientServiceImpl implements IBizHttpClientService {
@Resource @Resource
private RetryableHttpClient retryableHttpClient; private RetryableHttpClient retryableHttpClient;
@Override
public ResponseEntity<String> getApiData() {
return retryableHttpClient.getApiData();
}
} }