package com.tcm.training.admin.utils;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.MediaType;
|
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
import org.springframework.stereotype.Component;
|
import org.springframework.util.StringUtils;
|
import org.springframework.web.client.RestClientException;
|
import org.springframework.web.client.RestOperations;
|
import org.springframework.web.client.RestTemplate;
|
|
import java.nio.charset.StandardCharsets;
|
import java.security.MessageDigest;
|
import java.security.NoSuchAlgorithmException;
|
import java.time.*;
|
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeParseException;
|
import java.util.LinkedHashMap;
|
import java.util.Map;
|
|
/**
|
* 微赞开放平台HTTP工具,负责Token获取、缓存和自动刷新。
|
*/
|
@Slf4j
|
@Component
|
public class VzanHttpUtil {
|
|
private static final String TOKEN_URL = "https://openapi.njyqkj0ksyz.com/api/v2/token/get_token";
|
private static final String ACCOUNT = "Vzan1247381477";
|
private static final String SECRET_KEY = "b1bd524170574dc085a2d75adcd1c644 ";
|
private static final long REFRESH_AHEAD_MILLIS = 60_000L;
|
private static final DateTimeFormatter DATE_TIME_FORMATTER =
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
private final Object refreshLock = new Object();
|
private final RestOperations restOperations;
|
private final ObjectMapper objectMapper;
|
private final Clock clock;
|
|
private volatile TokenSnapshot tokenSnapshot;
|
|
@Autowired
|
public VzanHttpUtil(ObjectMapper objectMapper) {
|
this(createRestTemplate(), objectMapper, Clock.systemDefaultZone());
|
}
|
|
public VzanHttpUtil(RestOperations restOperations, ObjectMapper objectMapper, Clock clock) {
|
this.restOperations = restOperations;
|
this.objectMapper = objectMapper;
|
this.clock = clock;
|
}
|
|
/**
|
* 获取有效Token;Token不存在或即将过期时自动刷新。
|
*/
|
public String getToken() {
|
TokenSnapshot current = tokenSnapshot;
|
if (isUsable(current)) {
|
return current.token;
|
}
|
synchronized (refreshLock) {
|
current = tokenSnapshot;
|
if (isUsable(current)) {
|
return current.token;
|
}
|
return requestAndCacheToken().token;
|
}
|
}
|
|
/**
|
* 忽略当前缓存并立即获取新Token。
|
*/
|
public String refreshToken() {
|
synchronized (refreshLock) {
|
return requestAndCacheToken().token;
|
}
|
}
|
|
private TokenSnapshot requestAndCacheToken() {
|
validateCredentials();
|
long timestamp = clock.millis();
|
|
Map<String, String> body = new LinkedHashMap<String, String>();
|
body.put("account", ACCOUNT.trim());
|
body.put("signkey", md5Lowercase(SECRET_KEY.trim() + timestamp));
|
body.put("timestamp", String.valueOf(timestamp));
|
|
HttpHeaders headers = new HttpHeaders();
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
HttpEntity<Map<String, String>> request = new HttpEntity<Map<String, String>>(body, headers);
|
ResponseEntity<String> response = restOperations.postForEntity(TOKEN_URL, request, String.class);
|
TokenSnapshot received = parseTokenResponse(response.getBody());
|
tokenSnapshot = received;
|
return received;
|
}
|
|
private TokenSnapshot parseTokenResponse(String responseBody) {
|
if (!StringUtils.hasText(responseBody)) {
|
throw new IllegalStateException("微赞Token接口返回为空");
|
}
|
try {
|
JsonNode root = objectMapper.readTree(responseBody);
|
if (!"0".equals(root.path("code").asText())) {
|
throw new IllegalStateException("获取微赞Token失败: " + root.path("msg").asText("未知错误"));
|
}
|
JsonNode data = root.path("data");
|
String token = data.path("token").asText();
|
String tokenExpires = data.path("tokenExpires").asText();
|
if (!StringUtils.hasText(token) || !StringUtils.hasText(tokenExpires)) {
|
throw new IllegalStateException("微赞Token接口返回数据不完整");
|
}
|
return new TokenSnapshot(token, parseExpiresAt(tokenExpires));
|
} catch (IllegalStateException ex) {
|
throw ex;
|
} catch (Exception ex) {
|
throw new IllegalStateException("解析微赞Token接口响应失败", ex);
|
}
|
}
|
|
private Instant parseExpiresAt(String value) {
|
String normalized = value.trim();
|
try {
|
long numericValue = Long.parseLong(normalized);
|
if (numericValue < 10_000_000L) {
|
return clock.instant().plusSeconds(numericValue);
|
}
|
return numericValue < 10_000_000_000L
|
? Instant.ofEpochSecond(numericValue)
|
: Instant.ofEpochMilli(numericValue);
|
} catch (NumberFormatException ignored) {
|
// 继续兼容标准日期时间格式。
|
}
|
try {
|
return Instant.parse(normalized);
|
} catch (DateTimeParseException ignored) {
|
// 继续兼容带时区和无时区格式。
|
}
|
try {
|
return OffsetDateTime.parse(normalized).toInstant();
|
} catch (DateTimeParseException ignored) {
|
// 继续兼容微赞常用的本地日期时间格式。
|
}
|
try {
|
LocalDateTime localDateTime = LocalDateTime.parse(normalized, DATE_TIME_FORMATTER);
|
return localDateTime.atZone(ZoneId.systemDefault()).toInstant();
|
} catch (DateTimeParseException ex) {
|
throw new IllegalStateException("无法识别微赞Token过期时间: " + normalized, ex);
|
}
|
}
|
|
private boolean isUsable(TokenSnapshot snapshot) {
|
return snapshot != null
|
&& snapshot.expiresAt.toEpochMilli() - clock.millis() > REFRESH_AHEAD_MILLIS;
|
}
|
|
private void validateCredentials() {
|
if (!StringUtils.hasText(ACCOUNT) || !StringUtils.hasText(SECRET_KEY)) {
|
throw new IllegalStateException("请先配置微赞PAAS账号和客户密钥");
|
}
|
}
|
|
/**
|
* 创建微赞代理。接口返回顶层code或data.result非成功时抛出运行时异常。
|
*
|
* @return 微赞返回的代理业务数据
|
*/
|
public JsonNode createAgent(String agentMobile, String agentName, Integer levelCode,
|
String parentMobile, String storeName) {
|
String token = getToken();
|
Map<String, Object> body = new LinkedHashMap<String, Object>();
|
body.put("agent_mobile", requireText(agentMobile, "代理手机号不能为空"));
|
body.put("agent_name", requireText(agentName, "代理名称不能为空"));
|
if (levelCode == null) {
|
throw new IllegalArgumentException("代理等级不能为空");
|
}
|
body.put("level_code", levelCode);
|
putIfHasText(body, "parent_mobile", parentMobile);
|
putIfHasText(body, "store_name", storeName);
|
|
HttpHeaders headers = new HttpHeaders();
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setBearerAuth(token);
|
try {
|
ResponseEntity<String> response = restOperations.postForEntity(
|
"https://openapi.njyqkj0ksyz.com/api/v2/agent/createAgent",
|
new HttpEntity<Map<String, Object>>(body, headers), String.class);
|
return parseCreateAgentResponse(response.getBody());
|
} catch (RestClientException ex) {
|
throw new IllegalStateException("调用微赞创建代理接口失败", ex);
|
}
|
}
|
|
private JsonNode parseCreateAgentResponse(String responseBody) {
|
if (!StringUtils.hasText(responseBody)) {
|
throw new IllegalStateException("微赞创建代理接口返回为空");
|
}
|
try {
|
JsonNode root = objectMapper.readTree(responseBody);
|
if (!"0".equals(root.path("code").asText())) {
|
throw new IllegalStateException("微赞创建代理失败: " + root.path("msg").asText("未知错误"));
|
}
|
JsonNode data = root.path("data");
|
if (!data.isObject() || data.path("result").asInt(-1) != 0) {
|
throw new IllegalStateException("微赞创建代理失败: 业务处理未成功");
|
}
|
return data;
|
} catch (IllegalStateException ex) {
|
throw ex;
|
} catch (Exception ex) {
|
throw new IllegalStateException("解析微赞创建代理响应失败", ex);
|
}
|
}
|
|
private static String requireText(String value, String message) {
|
if (!StringUtils.hasText(value)) {
|
throw new IllegalArgumentException(message);
|
}
|
return value.trim();
|
}
|
|
private static void putIfHasText(Map<String, Object> body, String key, String value) {
|
if (StringUtils.hasText(value)) {
|
body.put(key, value.trim());
|
}
|
}
|
|
private static String md5Lowercase(String text) {
|
try {
|
MessageDigest digest = MessageDigest.getInstance("MD5");
|
byte[] bytes = digest.digest(text.getBytes(StandardCharsets.UTF_8));
|
StringBuilder result = new StringBuilder(bytes.length * 2);
|
for (byte value : bytes) {
|
result.append(String.format("%02x", value & 0xff));
|
}
|
return result.toString();
|
} catch (NoSuchAlgorithmException ex) {
|
throw new IllegalStateException("当前运行环境不支持MD5", ex);
|
}
|
}
|
|
private static RestTemplate createRestTemplate() {
|
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
requestFactory.setConnectTimeout(10_000);
|
requestFactory.setReadTimeout(10_000);
|
return new RestTemplate(requestFactory);
|
}
|
|
private static final class TokenSnapshot {
|
private final String token;
|
private final Instant expiresAt;
|
|
private TokenSnapshot(String token, Instant expiresAt) {
|
this.token = token;
|
this.expiresAt = expiresAt;
|
}
|
}
|
|
// /**
|
// * 启动时验证微赞Token接口配置和连通性,失败仅记录日志,不阻断应用启动。
|
// */
|
// @PostConstruct
|
// public void init() {
|
// if (!StringUtils.hasText(ACCOUNT) || !StringUtils.hasText(SECRET_KEY)) {
|
// log.warn("微赞Token连通性测试已跳过:PAAS账号或客户密钥未配置");
|
// return;
|
// }
|
// try {
|
// String token = getToken();
|
// log.info("微赞Token获取成功:{}", token);
|
// } catch (Exception ex) {
|
// log.error("微赞Token获取失败", ex);
|
// }
|
// }
|
}
|