|
|
@@ -0,0 +1,207 @@
|
|
|
+package com.fs.erp.utils;
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.fs.erp.domain.WeizouApiPushOrderParam;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import okhttp3.*;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Qualifier;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.cache.annotation.Cacheable;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Optional;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 微走云联客服订单推送工具类 (Java 8)
|
|
|
+ *
|
|
|
+ * 接口文档:https://my.feishu.cn/docx/Stn6dvLZlon81mx8A65cEkgAnRf
|
|
|
+ *
|
|
|
+ * 使用前需要补充:
|
|
|
+ * 1. 实际的 APP_ID 和 APP_SECRET
|
|
|
+ * 2. 正式的接口地址(文档中标记为“待更新”)
|
|
|
+ * 3. 请求参数的具体字段
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class WeizouApiClient {
|
|
|
+
|
|
|
+ // 1. 声明静态变量
|
|
|
+ private static RedisTemplate redisTemplate;
|
|
|
+
|
|
|
+ // 2. 注入实例到非静态变量
|
|
|
+ @Autowired
|
|
|
+ @Qualifier("redisTemplate")
|
|
|
+ private RedisTemplate redisTemplateInstance;
|
|
|
+
|
|
|
+ // 3. @PostConstruct 将实例赋值给静态变量
|
|
|
+ @PostConstruct
|
|
|
+ public void initStatic() {
|
|
|
+ redisTemplate = this.redisTemplateInstance;
|
|
|
+ log.info("RedisTemplate 静态初始化完成");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final String BASE_URL = "https://test-api.weizou.com:9702"; // TODO: 替换为文档中的正式环境地址
|
|
|
+
|
|
|
+ // TODO: 替换为平台提供的实际应用ID和密钥
|
|
|
+ private static final String APP_ID = "005fcddb9b664eeca80a13b62e823b63";
|
|
|
+ private static final String APP_SECRET = "a6cd645383a3405d842ce00da106ded0";
|
|
|
+
|
|
|
+ private static final String WEIZOU_API_CLIENT_KEY = "weizou:api:client:key";
|
|
|
+
|
|
|
+ private static final OkHttpClient httpClient = new OkHttpClient.Builder()
|
|
|
+ .connectTimeout(10, TimeUnit.SECONDS)
|
|
|
+ .writeTimeout(10, TimeUnit.SECONDS)
|
|
|
+ .readTimeout(30, TimeUnit.SECONDS)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ private static final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ // 存储当前有效的token(实际生产环境建议用Redis等分布式缓存)
|
|
|
+ private static String accessToken;
|
|
|
+ private static long tokenExpireTime; // 过期时间戳(毫秒)
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取有效的访问令牌(自动处理缓存和刷新)
|
|
|
+ */
|
|
|
+ public static String getAccessToken() throws IOException {
|
|
|
+ // 如果token有效则直接返回
|
|
|
+ Map<String, Object> tokenMap = redisTemplate.opsForHash().entries(WEIZOU_API_CLIENT_KEY);
|
|
|
+ if (!tokenMap.isEmpty()){
|
|
|
+ String token = (String) tokenMap.get("client_token");
|
|
|
+ String expireTime = (String) tokenMap.get("expires_in");
|
|
|
+ if (token != null && expireTime != null) {
|
|
|
+ long expireTimeL = Long.parseLong(expireTime);
|
|
|
+ if (System.currentTimeMillis() < expireTimeL) {
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则重新获取
|
|
|
+ return refreshAccessToken();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新访问令牌
|
|
|
+ * POST /auth/oauth2/client_token
|
|
|
+ */
|
|
|
+ private static synchronized String refreshAccessToken() throws IOException {
|
|
|
+ // 双重检查,防止并发刷新
|
|
|
+ Map<String, Object> tokenMap = redisTemplate.opsForHash().entries(WEIZOU_API_CLIENT_KEY);
|
|
|
+ if (!tokenMap.isEmpty()){
|
|
|
+ String token = (String) tokenMap.get("client_token");
|
|
|
+ Long expireTime = (Long) tokenMap.get("expires_in");
|
|
|
+ if (token != null && expireTime != null) {
|
|
|
+ if (System.currentTimeMillis() < expireTime) {
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Request request = new Request.Builder()
|
|
|
+ .url(BASE_URL + "/auth/oauth2/client_token"+ "?client_id=" + APP_ID +
|
|
|
+ "&client_secret=" + APP_SECRET)
|
|
|
+// .post(requestBody)
|
|
|
+ .addHeader("Content-Type", "application/json")
|
|
|
+ .build();
|
|
|
+
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
+ throw new IOException("获取token失败,HTTP状态码: " + response.code());
|
|
|
+ }
|
|
|
+
|
|
|
+ String responseBody = response.body().string();
|
|
|
+ JsonNode jsonNode = objectMapper.readTree(responseBody);
|
|
|
+
|
|
|
+ // TODO: 根据实际返回格式解析token和过期时间
|
|
|
+ JsonNode jsonResult = jsonNode.get("result");
|
|
|
+ String token = jsonResult.get("client_token").asText();
|
|
|
+ Long expiresIn = jsonResult.get("expires_in").asLong(); // 有效期(秒)
|
|
|
+
|
|
|
+ // 计算过期时间(提前5分钟过期,避免边界问题)
|
|
|
+ Long expireTime = System.currentTimeMillis() + (expiresIn - 60) * 1000L;
|
|
|
+ Map<String, String> hash = new HashMap<>();
|
|
|
+ hash.put("client_token", token);
|
|
|
+ hash.put("expires_in", String.valueOf(expireTime));
|
|
|
+ redisTemplate.opsForHash().putAll(WEIZOU_API_CLIENT_KEY, hash);
|
|
|
+ redisTemplate.expire(WEIZOU_API_CLIENT_KEY, expiresIn, TimeUnit.SECONDS);
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 推送客服订单
|
|
|
+ * POST /cs/api/storeOrder/sync
|
|
|
+ *
|
|
|
+ * @param orderData 订单数据(根据实际接口文档构造)
|
|
|
+ * @return 接口返回结果
|
|
|
+ */
|
|
|
+ public static String pushOrder(WeizouApiPushOrderParam orderData) throws IOException {
|
|
|
+ String token = getAccessToken();
|
|
|
+
|
|
|
+ String orderJson = objectMapper.writeValueAsString(orderData);
|
|
|
+
|
|
|
+ Request request = new Request.Builder()
|
|
|
+ .url(BASE_URL + "/cs/api/storeOrder/sync")
|
|
|
+ .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), orderJson))
|
|
|
+ .addHeader("Content-Type", "application/json")
|
|
|
+ .addHeader("token", token) // 假设使用Bearer Token
|
|
|
+ // TODO: 根据文档添加其他必要的Headers
|
|
|
+ .build();
|
|
|
+
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
+ throw new IOException("订单推送失败,HTTP状态码: " + response.code());
|
|
|
+ }
|
|
|
+ return response.body().string();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 内部辅助类 ==========
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Token请求参数(根据实际文档调整)
|
|
|
+ */
|
|
|
+ public static class TokenRequest {
|
|
|
+ private String client_id; // 或 app_id
|
|
|
+ private String client_secret; // 或 app_secret
|
|
|
+// private String grant_type = "client_credentials"; // OAuth2标准参数
|
|
|
+
|
|
|
+ public TokenRequest(String clientId, String clientSecret) {
|
|
|
+ this.client_id = clientId;
|
|
|
+ this.client_secret = clientSecret;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Getters and setters
|
|
|
+ public String getClient_id() { return client_id; }
|
|
|
+ public void setClient_id(String client_id) { this.client_id = client_id; }
|
|
|
+ public String getClient_secret() { return client_secret; }
|
|
|
+ public void setClient_secret(String client_secret) { this.client_secret = client_secret; }
|
|
|
+// public String getGrant_type() { return grant_type; }
|
|
|
+// public void setGrant_type(String grant_type) { this.grant_type = grant_type; }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Token响应(根据实际文档调整)
|
|
|
+ */
|
|
|
+ public static class TokenResponse {
|
|
|
+ private String access_token;
|
|
|
+ private int expires_in;
|
|
|
+ private String token_type;
|
|
|
+
|
|
|
+ // Getters and setters
|
|
|
+ public String getAccess_token() { return access_token; }
|
|
|
+ public void setAccess_token(String access_token) { this.access_token = access_token; }
|
|
|
+ public int getExpires_in() { return expires_in; }
|
|
|
+ public void setExpires_in(int expires_in) { this.expires_in = expires_in; }
|
|
|
+ public String getToken_type() { return token_type; }
|
|
|
+ public void setToken_type(String token_type) { this.token_type = token_type; }
|
|
|
+ }
|
|
|
+}
|