|
|
@@ -0,0 +1,369 @@
|
|
|
+package com.zsElectric.boot.sdk;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
+import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.zsElectric.boot.sdk.model.SdkRequest;
|
|
|
+import com.zsElectric.boot.sdk.model.SdkResponse;
|
|
|
+import com.zsElectric.boot.sdk.model.SdkResult;
|
|
|
+import com.zsElectric.boot.sdk.util.SdkCryptoUtil;
|
|
|
+
|
|
|
+import java.io.BufferedReader;
|
|
|
+import java.io.InputStreamReader;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.net.HttpURLConnection;
|
|
|
+import java.net.URL;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 众水电力SDK客户端
|
|
|
+ * 提供第三方调用众水电力平台API的能力
|
|
|
+ *
|
|
|
+ * @author wzq
|
|
|
+ */
|
|
|
+public class ZsElectricClient {
|
|
|
+
|
|
|
+ private static final String API_PREFIX = "/third-party/v1";
|
|
|
+ private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
|
|
+
|
|
|
+ private final ZsElectricConfig config;
|
|
|
+ private final ObjectMapper objectMapper;
|
|
|
+ private final AtomicInteger seqCounter = new AtomicInteger(1);
|
|
|
+ private final String apiBaseUrl;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缓存的AccessToken
|
|
|
+ */
|
|
|
+ private volatile String accessToken;
|
|
|
+ private volatile long tokenExpireTime = 0;
|
|
|
+
|
|
|
+ public ZsElectricClient(ZsElectricConfig config) {
|
|
|
+ this.config = config;
|
|
|
+ this.objectMapper = new ObjectMapper();
|
|
|
+ this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
|
+
|
|
|
+ // 智能处理baseUrl:如果已包含API路径前缀,则不再拼接
|
|
|
+ String base = config.getBaseUrl();
|
|
|
+ if (base.contains("/third_party/") || base.contains("/third_party/")) {
|
|
|
+ // 统一路径分隔符为连字符格式
|
|
|
+ this.apiBaseUrl = base.replace("/third_party/", "/third_party/");
|
|
|
+ } else {
|
|
|
+ this.apiBaseUrl = base + API_PREFIX;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 公开API方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取Token
|
|
|
+ * Token作为全局唯一凭证,调用其他接口时需要携带
|
|
|
+ *
|
|
|
+ * @return Token信息
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> queryToken() {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("operatorId", config.getOperatorId());
|
|
|
+ requestData.put("operatorSecret", config.getOperatorSecret());
|
|
|
+
|
|
|
+ SdkResult<Map<String, Object>> result = executeRequest("/query_token", requestData, false);
|
|
|
+
|
|
|
+ // 缓存token
|
|
|
+ if (result.isSuccess() && result.getData() != null) {
|
|
|
+ Map<String, Object> data = result.getData();
|
|
|
+ Object token = data.get("accessToken");
|
|
|
+ Object expireTime = data.get("tokenExpirationTime");
|
|
|
+ if (token != null) {
|
|
|
+ this.accessToken = token.toString();
|
|
|
+ if (expireTime != null) {
|
|
|
+ // 提前60秒过期,避免边界问题
|
|
|
+ this.tokenExpireTime = System.currentTimeMillis() + (Long.parseLong(expireTime.toString()) - 60) * 1000;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取充值档位信息分页列表
|
|
|
+ *
|
|
|
+ * @param pageNum 页码
|
|
|
+ * @param pageSize 每页数量
|
|
|
+ * @return 充值档位列表
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> queryRechargeLevelPage(int pageNum, int pageSize) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("pageNum", pageNum);
|
|
|
+ requestData.put("pageSize", pageSize);
|
|
|
+ return executeRequest("/query_recharge_level_page", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据手机号获取用户信息
|
|
|
+ *
|
|
|
+ * @param mobile 手机号
|
|
|
+ * @return 用户信息
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> queryUserInfo(String mobile) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("mobile", mobile);
|
|
|
+ return executeRequest("/query_user_info", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 充点券购买
|
|
|
+ *
|
|
|
+ * @param mobile 用户手机号
|
|
|
+ * @param levelId 档位ID
|
|
|
+ * @param outOrderNo 外部订单号
|
|
|
+ * @return 购买结果
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> chargeOrderPay(String mobile, Long levelId, String outOrderNo) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("mobile", mobile);
|
|
|
+ requestData.put("levelId", levelId);
|
|
|
+ requestData.put("outOrderNo", outOrderNo);
|
|
|
+ return executeRequest("/charge_order_pay", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取充电站列表
|
|
|
+ *
|
|
|
+ * @param pageNum 页码
|
|
|
+ * @param pageSize 每页数量
|
|
|
+ * @param latitude 纬度 (可选)
|
|
|
+ * @param longitude 经度 (可选)
|
|
|
+ * @return 充电站列表
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> queryChargeStationList(int pageNum, int pageSize, Double latitude, Double longitude) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("pageNum", pageNum);
|
|
|
+ requestData.put("pageSize", pageSize);
|
|
|
+ if (latitude != null) {
|
|
|
+ requestData.put("latitude", latitude);
|
|
|
+ }
|
|
|
+ if (longitude != null) {
|
|
|
+ requestData.put("longitude", longitude);
|
|
|
+ }
|
|
|
+ return executeRequest("/query_charge_station_list", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取充电站详情与充电终端列表
|
|
|
+ *
|
|
|
+ * @param stationId 充电站ID
|
|
|
+ * @return 充电站详情
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> queryChargeStationDetail(Long stationId) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("stationId", stationId);
|
|
|
+ return executeRequest("/query_charge_station_detail", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取充电终端详情
|
|
|
+ *
|
|
|
+ * @param deviceId 设备ID
|
|
|
+ * @return 充电终端详情
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> queryChargeDeviceDetail(Long deviceId) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("deviceId", deviceId);
|
|
|
+ return executeRequest("/query_charge_device_detail", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动充电
|
|
|
+ *
|
|
|
+ * @param mobile 用户手机号
|
|
|
+ * @param connectorId 充电枪ID
|
|
|
+ * @param outOrderNo 外部订单号
|
|
|
+ * @return 启动结果
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> invokeCharge(String mobile, Long connectorId, String outOrderNo) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("mobile", mobile);
|
|
|
+ requestData.put("connectorId", connectorId);
|
|
|
+ requestData.put("outOrderNo", outOrderNo);
|
|
|
+ return executeRequest("/invoke_charge", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 停止充电
|
|
|
+ *
|
|
|
+ * @param orderNo 充电订单号
|
|
|
+ * @return 停止结果
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> stopCharge(String orderNo) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("orderNo", orderNo);
|
|
|
+ return executeRequest("/stop_charge", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询充电订单列表
|
|
|
+ *
|
|
|
+ * @param mobile 用户手机号 (可选)
|
|
|
+ * @param pageNum 页码
|
|
|
+ * @param pageSize 每页数量
|
|
|
+ * @return 订单列表
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> queryChargeOrderList(String mobile, int pageNum, int pageSize) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ if (mobile != null && !mobile.isEmpty()) {
|
|
|
+ requestData.put("mobile", mobile);
|
|
|
+ }
|
|
|
+ requestData.put("pageNum", pageNum);
|
|
|
+ requestData.put("pageSize", pageSize);
|
|
|
+ return executeRequest("/query_charge_order_list", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询充电订单详情
|
|
|
+ *
|
|
|
+ * @param orderNo 充电订单号
|
|
|
+ * @return 订单详情
|
|
|
+ */
|
|
|
+ public SdkResult<Map<String, Object>> queryChargeOrderInfo(String orderNo) {
|
|
|
+ Map<String, Object> requestData = new HashMap<>();
|
|
|
+ requestData.put("orderNo", orderNo);
|
|
|
+ return executeRequest("/query_charge_order_info", requestData, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行API请求
|
|
|
+ *
|
|
|
+ * @param apiPath API路径
|
|
|
+ * @param requestData 业务数据
|
|
|
+ * @param needToken 是否需要Token
|
|
|
+ * @return 响应结果
|
|
|
+ */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private SdkResult<Map<String, Object>> executeRequest(String apiPath, Map<String, Object> requestData, boolean needToken) {
|
|
|
+ try {
|
|
|
+ // 如果需要Token且Token已过期,先刷新
|
|
|
+ if (needToken && isTokenExpired()) {
|
|
|
+ SdkResult<Map<String, Object>> tokenResult = queryToken();
|
|
|
+ if (!tokenResult.isSuccess()) {
|
|
|
+ return SdkResult.error("获取Token失败: " + tokenResult.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建请求
|
|
|
+ String timeStamp = LocalDateTime.now().format(TIME_FORMATTER);
|
|
|
+ String seq = String.format("%04d", seqCounter.getAndIncrement() % 10000);
|
|
|
+
|
|
|
+ // 加密业务数据
|
|
|
+ String jsonData = objectMapper.writeValueAsString(requestData);
|
|
|
+ String encryptedData = SdkCryptoUtil.aesEncrypt(jsonData, config.getDataSecret(), config.getDataSecretIV());
|
|
|
+
|
|
|
+ // 生成签名
|
|
|
+ String sig = SdkCryptoUtil.genRequestSign(config.getOperatorId(), encryptedData, timeStamp, seq, config.getSigSecret());
|
|
|
+
|
|
|
+ // 构建请求体
|
|
|
+ SdkRequest request = new SdkRequest();
|
|
|
+ request.setOperatorId(config.getOperatorId());
|
|
|
+ request.setData(encryptedData);
|
|
|
+ request.setTimeStamp(timeStamp);
|
|
|
+ request.setSeq(seq);
|
|
|
+ request.setSig(sig);
|
|
|
+
|
|
|
+ // 发送HTTP请求
|
|
|
+ String url = apiBaseUrl + apiPath;
|
|
|
+ String requestBody = objectMapper.writeValueAsString(request);
|
|
|
+ String responseBody = sendHttpPost(url, requestBody, needToken ? accessToken : null);
|
|
|
+
|
|
|
+ // 解析响应
|
|
|
+ SdkResponse response = objectMapper.readValue(responseBody, SdkResponse.class);
|
|
|
+
|
|
|
+ // 检查响应是否有效(ret为null可能是服务端返回了非预期格式)
|
|
|
+ if (response.getRet() == null) {
|
|
|
+ return SdkResult.error("服务端响应格式异常,原始响应: " + responseBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!response.isSuccess()) {
|
|
|
+ return SdkResult.fail(response.getRet(), response.getMsg(), response);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解密响应数据
|
|
|
+ if (response.getData() != null && !response.getData().isEmpty()) {
|
|
|
+ String decryptedData = SdkCryptoUtil.aesDecrypt(response.getData(), config.getDataSecret(), config.getDataSecretIV());
|
|
|
+ Map<String, Object> resultData = objectMapper.readValue(decryptedData, Map.class);
|
|
|
+ return SdkResult.success(resultData, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ return SdkResult.success(new HashMap<>(), response);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ return SdkResult.error("请求异常: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送HTTP POST请求
|
|
|
+ */
|
|
|
+ private String sendHttpPost(String urlStr, String body, String token) throws Exception {
|
|
|
+ URL url = new URL(urlStr);
|
|
|
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
|
+ conn.setRequestMethod("POST");
|
|
|
+ conn.setConnectTimeout(config.getConnectTimeout());
|
|
|
+ conn.setReadTimeout(config.getReadTimeout());
|
|
|
+ conn.setDoOutput(true);
|
|
|
+ conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
|
|
|
+
|
|
|
+ if (token != null && !token.isEmpty()) {
|
|
|
+ conn.setRequestProperty("Authorization", "Bearer " + token);
|
|
|
+ }
|
|
|
+
|
|
|
+ try (OutputStream os = conn.getOutputStream()) {
|
|
|
+ os.write(body.getBytes(StandardCharsets.UTF_8));
|
|
|
+ os.flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ int responseCode = conn.getResponseCode();
|
|
|
+ BufferedReader reader;
|
|
|
+ if (responseCode >= 200 && responseCode < 300) {
|
|
|
+ reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
|
|
|
+ } else {
|
|
|
+ reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
+
|
|
|
+ StringBuilder response = new StringBuilder();
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
+ response.append(line);
|
|
|
+ }
|
|
|
+ reader.close();
|
|
|
+ conn.disconnect();
|
|
|
+
|
|
|
+ return response.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查Token是否过期
|
|
|
+ */
|
|
|
+ private boolean isTokenExpired() {
|
|
|
+ return accessToken == null || System.currentTimeMillis() >= tokenExpireTime;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手动设置AccessToken(用于外部管理Token的场景)
|
|
|
+ */
|
|
|
+ public void setAccessToken(String accessToken, long expireTimeSeconds) {
|
|
|
+ this.accessToken = accessToken;
|
|
|
+ this.tokenExpireTime = System.currentTimeMillis() + (expireTimeSeconds - 60) * 1000;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前AccessToken
|
|
|
+ */
|
|
|
+ public String getAccessToken() {
|
|
|
+ return accessToken;
|
|
|
+ }
|
|
|
+}
|