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 body = new LinkedHashMap(); 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> request = new HttpEntity>(body, headers); ResponseEntity 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 body = new LinkedHashMap(); 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 response = restOperations.postForEntity( "https://openapi.njyqkj0ksyz.com/api/v2/agent/createAgent", new HttpEntity>(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 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); // } // } }