|
@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets;
|
|
|
import java.util.Arrays;
|
|
|
import java.util.Collections;
|
|
|
import java.util.Map;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
import java.util.function.Function;
|
|
|
import javax.crypto.Cipher;
|
|
|
|
|
@@ -14,15 +15,24 @@ import com.chelvc.framework.common.util.AssertUtils;
|
|
|
import com.chelvc.framework.common.util.JacksonUtils;
|
|
|
import com.chelvc.framework.common.util.ObjectUtils;
|
|
|
import com.chelvc.framework.common.util.StringUtils;
|
|
|
+import com.chelvc.framework.common.util.ThreadUtils;
|
|
|
+import com.chelvc.framework.redis.context.RedisContextHolder;
|
|
|
+import com.chelvc.framework.wechat.WechatAccessToken;
|
|
|
import com.chelvc.framework.wechat.WechatPublicHandler;
|
|
|
import com.chelvc.framework.wechat.WechatResponse;
|
|
|
+import com.chelvc.framework.wechat.config.WechatProperties;
|
|
|
import com.github.wxpay.sdk.WXPayUtil;
|
|
|
import com.google.common.collect.Maps;
|
|
|
import lombok.NonNull;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.codec.binary.Base64;
|
|
|
+import org.apache.commons.lang3.tuple.Pair;
|
|
|
+import org.springframework.boot.ApplicationArguments;
|
|
|
+import org.springframework.boot.ApplicationRunner;
|
|
|
import org.springframework.core.ParameterizedTypeReference;
|
|
|
import org.springframework.http.HttpEntity;
|
|
|
import org.springframework.http.HttpMethod;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
/**
|
|
@@ -31,15 +41,127 @@ import org.springframework.web.client.RestTemplate;
|
|
|
* @author Woody
|
|
|
* @date 2024/1/30
|
|
|
*/
|
|
|
-public final class WechatContextHolder {
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public final class WechatContextHolder implements ApplicationRunner {
|
|
|
+ /**
|
|
|
+ * 获取访问令牌接口地址
|
|
|
+ */
|
|
|
+ private static final String ACCESS_TOKEN_URL =
|
|
|
+ "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 应用标识/令牌映射表
|
|
|
+ */
|
|
|
+ private static final Map<String, String> APP_TOKEN_MAPPING = Maps.newConcurrentMap();
|
|
|
+
|
|
|
/**
|
|
|
* 微信公众号处理器映射表
|
|
|
*/
|
|
|
private static Map<String, WechatPublicHandler> WECHAT_PUBLIC_HANDLER_MAPPING;
|
|
|
|
|
|
+ /**
|
|
|
+ * 工具类初始化标记
|
|
|
+ */
|
|
|
+ private volatile boolean initialized;
|
|
|
+
|
|
|
private WechatContextHolder() {
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public void run(ApplicationArguments args) throws Exception {
|
|
|
+ if (!this.initialized) {
|
|
|
+ synchronized (this) {
|
|
|
+ if (!this.initialized) {
|
|
|
+ initialize(ApplicationContextHolder.getBean(WechatProperties.class));
|
|
|
+ this.initialized = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化
|
|
|
+ *
|
|
|
+ * @param properties 微信配置属性
|
|
|
+ */
|
|
|
+ private static void initialize(WechatProperties properties) {
|
|
|
+ if (!properties.getToken().isRefreshable() || (ObjectUtils.isEmpty(properties.getApplets())
|
|
|
+ && ObjectUtils.isEmpty(properties.getPublics()))) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化令牌刷新服务线程
|
|
|
+ ThreadUtils.run(() -> {
|
|
|
+ while (!Thread.currentThread().isInterrupted()) {
|
|
|
+ // 刷新小程序访问令牌
|
|
|
+ if (ObjectUtils.notEmpty(properties.getApplets())) {
|
|
|
+ properties.getApplets().forEach(config -> refreshAccessToken(
|
|
|
+ config.getAppid(), config.getSecret()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 刷新公众号访问令牌
|
|
|
+ if (ObjectUtils.notEmpty(properties.getPublics())) {
|
|
|
+ properties.getPublics().forEach(config -> refreshAccessToken(
|
|
|
+ config.getAppid(), config.getSecret()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ ThreadUtils.sleep(60000);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新访问令牌
|
|
|
+ *
|
|
|
+ * @param appid AppID
|
|
|
+ * @param secret AppSecret
|
|
|
+ */
|
|
|
+ private static void refreshAccessToken(String appid, String secret) {
|
|
|
+ String key = "wechat:token:" + appid;
|
|
|
+ try {
|
|
|
+ RedisContextHolder.tryLockAround(key + ":lock", () -> {
|
|
|
+ // 获取令牌当前有效期(秒)
|
|
|
+ Long ttl = RedisContextHolder.execute(
|
|
|
+ RedisContextHolder.getDefaultConnectionFactory(),
|
|
|
+ connection -> connection.ttl(key.getBytes(StandardCharsets.UTF_8))
|
|
|
+ );
|
|
|
+
|
|
|
+ // 如果令牌有效期小于120秒则刷新令牌
|
|
|
+ if (ttl == null || ttl < 120) {
|
|
|
+ String url = String.format(ACCESS_TOKEN_URL, appid, secret);
|
|
|
+ WechatAccessToken accessToken = get(url, WechatAccessToken.class);
|
|
|
+ String token = accessToken.getToken();
|
|
|
+ long duration = accessToken.getDuration();
|
|
|
+ RedisContextHolder.getDefaultTemplate().opsForValue().set(key, token, duration, TimeUnit.SECONDS);
|
|
|
+ APP_TOKEN_MAPPING.put(appid, token);
|
|
|
+ log.info("Wechat access token refreshed: {}", appid);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("Wechat access token refresh failed: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取微信访问令牌
|
|
|
+ *
|
|
|
+ * @param appid AppID
|
|
|
+ * @return 访问令牌
|
|
|
+ */
|
|
|
+ public static String getAccessToken(@NonNull String appid) {
|
|
|
+ return APP_TOKEN_MAPPING.computeIfAbsent(appid, k -> {
|
|
|
+ String key = "wechat:token:" + appid;
|
|
|
+ String token = (String) RedisContextHolder.getDefaultTemplate().opsForValue().get(key);
|
|
|
+ if (StringUtils.isEmpty(token)) {
|
|
|
+ throw new IllegalStateException("Wechat access token has not been initialized: " + appid);
|
|
|
+ }
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取微信公众号处理器
|
|
|
*
|
|
@@ -47,7 +169,7 @@ public final class WechatContextHolder {
|
|
|
*/
|
|
|
public static Map<String, WechatPublicHandler> getWechatPublicHandlers() {
|
|
|
if (WECHAT_PUBLIC_HANDLER_MAPPING == null) {
|
|
|
- synchronized (WechatContextHolder.class) {
|
|
|
+ synchronized (WechatPublicHandler.class) {
|
|
|
if (WECHAT_PUBLIC_HANDLER_MAPPING == null) {
|
|
|
Map<String, WechatPublicHandler> handlers =
|
|
|
ApplicationContextHolder.getBeans(WechatPublicHandler.class);
|
|
@@ -136,16 +258,20 @@ public final class WechatContextHolder {
|
|
|
/**
|
|
|
* 数据解密
|
|
|
*
|
|
|
- * @param cipher 密码处理器
|
|
|
* @param ciphertext 密文
|
|
|
+ * @param secret 密钥
|
|
|
* @return 明文
|
|
|
*/
|
|
|
- public static String decrypt(@NonNull Cipher cipher, String ciphertext) {
|
|
|
+ public static String decrypt(String ciphertext, @NonNull String secret) {
|
|
|
if (StringUtils.isEmpty(ciphertext)) {
|
|
|
return ciphertext;
|
|
|
}
|
|
|
|
|
|
// 解密数据
|
|
|
+ Cipher cipher = AESUtils.lookupDecrypter(AESUtils.CBC_NOPADDING + secret, () -> {
|
|
|
+ Pair<byte[], byte[]> pair = AESUtils.getDefaultCBCCertificate(secret);
|
|
|
+ return AESUtils.getCipher(AESUtils.CBC_NOPADDING, Cipher.DECRYPT_MODE, pair.getLeft(), pair.getRight());
|
|
|
+ });
|
|
|
byte[] original = AESUtils.codec(cipher, Base64.decodeBase64(ciphertext));
|
|
|
|
|
|
// 删除解密后明文的补位字符
|
|
@@ -168,19 +294,13 @@ public final class WechatContextHolder {
|
|
|
* 执行接口请求
|
|
|
*
|
|
|
* @param function 请求回调函数
|
|
|
- * @param retry 请求重试回调函数
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T execute(@NonNull Function<RestTemplate, T> function,
|
|
|
- @NonNull Function<RestTemplate, T> retry) {
|
|
|
+ public static <T extends WechatResponse> T execute(@NonNull Function<RestTemplate, T> function) {
|
|
|
T response = RestContextHolder.execute(function);
|
|
|
int status = ObjectUtils.ifNull(response.getErrcode(), 0);
|
|
|
- if (status == 40001 || status == 42001) {
|
|
|
- // 如果访问令牌过期或无效则更新访问令牌后重试
|
|
|
- response = RestContextHolder.execute(retry);
|
|
|
- }
|
|
|
- if ((status = ObjectUtils.ifNull(response.getErrcode(), 0)) != 0) {
|
|
|
+ if (status != 0) {
|
|
|
throw new IllegalStateException(status + ", " + response.getErrmsg());
|
|
|
}
|
|
|
return response;
|
|
@@ -195,10 +315,9 @@ public final class WechatContextHolder {
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T get(@NonNull Function<Boolean, String> url, @NonNull Class<T> type,
|
|
|
+ public static <T extends WechatResponse> T get(@NonNull String url, @NonNull Class<T> type,
|
|
|
@NonNull Object... parameters) {
|
|
|
- return execute(rest -> rest.getForObject(url.apply(true), type, parameters),
|
|
|
- rest -> rest.getForObject(url.apply(false), type, parameters));
|
|
|
+ return execute(rest -> rest.getForObject(url, type, parameters));
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -210,10 +329,9 @@ public final class WechatContextHolder {
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T get(@NonNull Function<Boolean, String> url, @NonNull Class<T> type,
|
|
|
+ public static <T extends WechatResponse> T get(@NonNull String url, @NonNull Class<T> type,
|
|
|
@NonNull Map<String, ?> parameters) {
|
|
|
- return execute(rest -> rest.getForObject(url.apply(true), type, parameters),
|
|
|
- rest -> rest.getForObject(url.apply(false), type, parameters));
|
|
|
+ return execute(rest -> rest.getForObject(url, type, parameters));
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -226,10 +344,9 @@ public final class WechatContextHolder {
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T post(@NonNull Function<Boolean, String> url, Object request,
|
|
|
- @NonNull Class<T> type, @NonNull Object... parameters) {
|
|
|
- return execute(rest -> rest.postForObject(url.apply(true), request, type, parameters),
|
|
|
- rest -> rest.postForObject(url.apply(false), request, type, parameters));
|
|
|
+ public static <T extends WechatResponse> T post(@NonNull String url, Object request, @NonNull Class<T> type,
|
|
|
+ @NonNull Object... parameters) {
|
|
|
+ return execute(rest -> rest.postForObject(url, request, type, parameters));
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -242,10 +359,9 @@ public final class WechatContextHolder {
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T post(@NonNull Function<Boolean, String> url, Object request,
|
|
|
- @NonNull Class<T> type, @NonNull Map<String, ?> parameters) {
|
|
|
- return execute(rest -> rest.postForObject(url.apply(true), request, type, parameters),
|
|
|
- rest -> rest.postForObject(url.apply(false), request, type, parameters));
|
|
|
+ public static <T extends WechatResponse> T post(@NonNull String url, Object request, @NonNull Class<T> type,
|
|
|
+ @NonNull Map<String, ?> parameters) {
|
|
|
+ return execute(rest -> rest.postForObject(url, request, type, parameters));
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -259,11 +375,10 @@ public final class WechatContextHolder {
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T exchange(@NonNull Function<Boolean, String> url,
|
|
|
- @NonNull HttpMethod method, HttpEntity<?> entity,
|
|
|
- @NonNull Class<T> type, @NonNull Object... parameters) {
|
|
|
- return execute(rest -> rest.exchange(url.apply(true), method, entity, type, parameters).getBody(),
|
|
|
- rest -> rest.exchange(url.apply(false), method, entity, type, parameters).getBody());
|
|
|
+ public static <T extends WechatResponse> T exchange(@NonNull String url, @NonNull HttpMethod method,
|
|
|
+ HttpEntity<?> entity, @NonNull Class<T> type,
|
|
|
+ @NonNull Object... parameters) {
|
|
|
+ return execute(rest -> rest.exchange(url, method, entity, type, parameters).getBody());
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -277,11 +392,10 @@ public final class WechatContextHolder {
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T exchange(@NonNull Function<Boolean, String> url,
|
|
|
- @NonNull HttpMethod method, HttpEntity<?> entity,
|
|
|
- @NonNull Class<T> type, @NonNull Map<String, ?> parameters) {
|
|
|
- return execute(rest -> rest.exchange(url.apply(true), method, entity, type, parameters).getBody(),
|
|
|
- rest -> rest.exchange(url.apply(false), method, entity, type, parameters).getBody());
|
|
|
+ public static <T extends WechatResponse> T exchange(@NonNull String url, @NonNull HttpMethod method,
|
|
|
+ HttpEntity<?> entity, @NonNull Class<T> type,
|
|
|
+ @NonNull Map<String, ?> parameters) {
|
|
|
+ return execute(rest -> rest.exchange(url, method, entity, type, parameters).getBody());
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -295,12 +409,11 @@ public final class WechatContextHolder {
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T exchange(@NonNull Function<Boolean, String> url,
|
|
|
- @NonNull HttpMethod method, HttpEntity<?> entity,
|
|
|
+ public static <T extends WechatResponse> T exchange(@NonNull String url, @NonNull HttpMethod method,
|
|
|
+ HttpEntity<?> entity,
|
|
|
@NonNull ParameterizedTypeReference<T> type,
|
|
|
@NonNull Object... parameters) {
|
|
|
- return execute(rest -> rest.exchange(url.apply(true), method, entity, type, parameters).getBody(),
|
|
|
- rest -> rest.exchange(url.apply(false), method, entity, type, parameters).getBody());
|
|
|
+ return execute(rest -> rest.exchange(url, method, entity, type, parameters).getBody());
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -314,11 +427,10 @@ public final class WechatContextHolder {
|
|
|
* @param <T> 结果类型
|
|
|
* @return 微信响应结果
|
|
|
*/
|
|
|
- public static <T extends WechatResponse> T exchange(@NonNull Function<Boolean, String> url,
|
|
|
- @NonNull HttpMethod method, HttpEntity<?> entity,
|
|
|
+ public static <T extends WechatResponse> T exchange(@NonNull String url, @NonNull HttpMethod method,
|
|
|
+ HttpEntity<?> entity,
|
|
|
@NonNull ParameterizedTypeReference<T> type,
|
|
|
@NonNull Map<String, ?> parameters) {
|
|
|
- return execute(rest -> rest.exchange(url.apply(true), method, entity, type, parameters).getBody(),
|
|
|
- rest -> rest.exchange(url.apply(false), method, entity, type, parameters).getBody());
|
|
|
+ return execute(rest -> rest.exchange(url, method, entity, type, parameters).getBody());
|
|
|
}
|
|
|
}
|