Просмотр исходного кода

优化微信操作逻辑;修复微信登陆异常问题;

woody 11 месяцев назад
Родитель
Сommit
44e25c2b32
15 измененных файлов с 882 добавлено и 668 удалено
  1. 165 14
      framework-base/src/main/java/com/chelvc/framework/base/context/RestContextHolder.java
  2. 4 5
      framework-database/src/main/java/com/chelvc/framework/database/interceptor/Expressions.java
  3. 20 21
      framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java
  4. 1 2
      framework-sms/src/main/java/com/chelvc/framework/sms/support/DefaultCaptchaSmsHandler.java
  5. 130 0
      framework-wechat/src/main/java/com/chelvc/framework/wechat/DefaultWechatAppletHandler.java
  6. 0 324
      framework-wechat/src/main/java/com/chelvc/framework/wechat/DefaultWechatHandler.java
  7. 173 0
      framework-wechat/src/main/java/com/chelvc/framework/wechat/DefaultWechatPaymentHandler.java
  8. 42 67
      framework-wechat/src/main/java/com/chelvc/framework/wechat/DefaultWechatPublicHandler.java
  9. 66 0
      framework-wechat/src/main/java/com/chelvc/framework/wechat/WechatAppletHandler.java
  10. 0 130
      framework-wechat/src/main/java/com/chelvc/framework/wechat/WechatHandler.java
  11. 38 0
      framework-wechat/src/main/java/com/chelvc/framework/wechat/WechatPaymentHandler.java
  12. 19 40
      framework-wechat/src/main/java/com/chelvc/framework/wechat/WechatPublicHandler.java
  13. 30 15
      framework-wechat/src/main/java/com/chelvc/framework/wechat/config/WechatConfigurer.java
  14. 38 6
      framework-wechat/src/main/java/com/chelvc/framework/wechat/config/WechatProperties.java
  15. 156 44
      framework-wechat/src/main/java/com/chelvc/framework/wechat/context/WechatContextHolder.java

+ 165 - 14
framework-base/src/main/java/com/chelvc/framework/base/context/RestContextHolder.java

@@ -1,11 +1,14 @@
 package com.chelvc.framework.base.context;
 
 import java.net.URI;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.function.Function;
 
 import com.chelvc.framework.common.util.ErrorUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
 import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.http.NoHttpResponseException;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.HttpEntity;
@@ -19,6 +22,7 @@ import org.springframework.web.client.RestTemplate;
  * @author Woody
  * @date 2024/6/5
  */
+@Slf4j
 public final class RestContextHolder {
     /**
      * RestTemplate实例
@@ -56,6 +60,7 @@ public final class RestContextHolder {
             return function.apply(getTemplate());
         } catch (Exception e) {
             if (ErrorUtils.contains(e, NoHttpResponseException.class)) {
+                log.warn("Rest invoke failed and retry later: {}", e.getMessage());
                 // 请求重试
                 return function.apply(new RestTemplate());
             }
@@ -72,7 +77,17 @@ public final class RestContextHolder {
      * @return 请求结果
      */
     public static <T> T get(@NonNull URI url, @NonNull Class<T> type) {
-        return execute(rest -> rest.getForObject(url, type));
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                log.debug("Rest invoke: {}", url);
+            }
+            T response = rest.getForObject(url, type);
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -85,7 +100,17 @@ public final class RestContextHolder {
      * @return 请求结果
      */
     public static <T> T get(@NonNull String url, @NonNull Class<T> type, @NonNull Object... parameters) {
-        return execute(rest -> rest.getForObject(url, type, parameters));
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                log.debug("Rest invoke: {}, {}", url, Arrays.toString(parameters));
+            }
+            T response = rest.getForObject(url, type, parameters);
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -98,7 +123,17 @@ public final class RestContextHolder {
      * @return 请求结果
      */
     public static <T> T get(@NonNull String url, @NonNull Class<T> type, @NonNull Map<String, ?> parameters) {
-        return execute(rest -> rest.getForObject(url, type, parameters));
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                log.debug("Rest invoke: {}, {}", url, parameters);
+            }
+            T response = rest.getForObject(url, type, parameters);
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -111,7 +146,17 @@ public final class RestContextHolder {
      * @return 请求结果
      */
     public static <T> T post(@NonNull URI url, Object request, @NonNull Class<T> type) {
-        return execute(rest -> rest.postForObject(url, request, type));
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                log.debug("Rest invoke: {}, {}", url, request);
+            }
+            T response = rest.postForObject(url, request, type);
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -126,7 +171,17 @@ public final class RestContextHolder {
      */
     public static <T> T post(@NonNull String url, Object request, @NonNull Class<T> type,
                              @NonNull Object... parameters) {
-        return execute(rest -> rest.postForObject(url, request, type, parameters));
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                log.debug("Rest invoke: {}, {}, {}", url, request, Arrays.toString(parameters));
+            }
+            T response = rest.postForObject(url, request, type, parameters);
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -141,7 +196,17 @@ public final class RestContextHolder {
      */
     public static <T> T post(@NonNull String url, Object request, @NonNull Class<T> type,
                              @NonNull Map<String, ?> parameters) {
-        return execute(rest -> rest.postForObject(url, request, type, parameters));
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                log.debug("Rest invoke: {}, {}, {}", url, request, parameters);
+            }
+            T response = rest.postForObject(url, request, type, parameters);
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -153,7 +218,17 @@ public final class RestContextHolder {
      * @return 请求结果
      */
     public static <T> T exchange(@NonNull RequestEntity<?> entity, @NonNull Class<T> type) {
-        return execute(rest -> rest.exchange(entity, type).getBody());
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                log.debug("Rest invoke: {}", entity.getUrl());
+            }
+            T response = rest.exchange(entity, type).getBody();
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -165,7 +240,17 @@ public final class RestContextHolder {
      * @return 请求结果
      */
     public static <T> T exchange(@NonNull RequestEntity<?> entity, @NonNull ParameterizedTypeReference<T> type) {
-        return execute(rest -> rest.exchange(entity, type).getBody());
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                log.debug("Rest invoke: {}", entity.getUrl());
+            }
+            T response = rest.exchange(entity, type).getBody();
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -180,7 +265,18 @@ public final class RestContextHolder {
      */
     public static <T> T exchange(@NonNull URI url, @NonNull HttpMethod method, HttpEntity<?> entity,
                                  @NonNull Class<T> type) {
-        return execute(rest -> rest.exchange(url, method, entity, type).getBody());
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                Object body = ObjectUtils.ifNull(entity, HttpEntity::getBody);
+                log.debug("Rest invoke: {}, {}", url, body);
+            }
+            T response = rest.exchange(url, method, entity, type).getBody();
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -196,7 +292,18 @@ public final class RestContextHolder {
      */
     public static <T> 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());
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                Object body = ObjectUtils.ifNull(entity, HttpEntity::getBody);
+                log.debug("Rest invoke: {}, {}, {}", url, body, Arrays.toString(parameters));
+            }
+            T response = rest.exchange(url, method, entity, type, parameters).getBody();
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -212,7 +319,18 @@ public final class RestContextHolder {
      */
     public static <T> 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());
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                Object body = ObjectUtils.ifNull(entity, HttpEntity::getBody);
+                log.debug("Rest invoke: {}, {}, {}", url, body, parameters);
+            }
+            T response = rest.exchange(url, method, entity, type, parameters).getBody();
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -227,7 +345,18 @@ public final class RestContextHolder {
      */
     public static <T> T exchange(@NonNull URI url, @NonNull HttpMethod method, HttpEntity<?> entity,
                                  @NonNull ParameterizedTypeReference<T> type) {
-        return execute(rest -> rest.exchange(url, method, entity, type).getBody());
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                Object body = ObjectUtils.ifNull(entity, HttpEntity::getBody);
+                log.debug("Rest invoke: {}, {}", url, body);
+            }
+            T response = rest.exchange(url, method, entity, type).getBody();
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -243,7 +372,18 @@ public final class RestContextHolder {
      */
     public static <T> T exchange(@NonNull String url, @NonNull HttpMethod method, HttpEntity<?> entity,
                                  @NonNull ParameterizedTypeReference<T> type, @NonNull Object... parameters) {
-        return execute(rest -> rest.exchange(url, method, entity, type, parameters).getBody());
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                Object body = ObjectUtils.ifNull(entity, HttpEntity::getBody);
+                log.debug("Rest invoke: {}, {}, {}", url, body, Arrays.toString(parameters));
+            }
+            T response = rest.exchange(url, method, entity, type, parameters).getBody();
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 
     /**
@@ -259,6 +399,17 @@ public final class RestContextHolder {
      */
     public static <T> T exchange(@NonNull String url, @NonNull HttpMethod method, HttpEntity<?> entity,
                                  @NonNull ParameterizedTypeReference<T> type, @NonNull Map<String, ?> parameters) {
-        return execute(rest -> rest.exchange(url, method, entity, type, parameters).getBody());
+        return execute(rest -> {
+            boolean debug = log.isDebugEnabled();
+            if (debug) {
+                Object body = ObjectUtils.ifNull(entity, HttpEntity::getBody);
+                log.debug("Rest invoke: {}, {}, {}", url, body, parameters);
+            }
+            T response = rest.exchange(url, method, entity, type, parameters).getBody();
+            if (debug) {
+                log.debug("Rest response: {}", response);
+            }
+            return response;
+        });
     }
 }

+ 4 - 5
framework-database/src/main/java/com/chelvc/framework/database/interceptor/Expressions.java

@@ -210,10 +210,9 @@ final class Expressions {
      * @return 表名称/字段名/值映射表
      */
     public static Map<String, Map<String, ExpressionList>> getSelectAdditionalConditions() {
-        return ApplicationContextHolder.getProperty(ENUM_OPTION_LIMIT_PROPERTY, config -> {
-            Map<String, Map<String, ExpressionList>> conditions = analyseSelectAdditionalCondition((String) config);
-            log.info("Select additional condition refreshed: {}", conditions.size());
-            return conditions;
-        });
+        return ApplicationContextHolder.getProperty(
+                ENUM_OPTION_LIMIT_PROPERTY,
+                config -> analyseSelectAdditionalCondition((String) config)
+        );
     }
 }

+ 20 - 21
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java

@@ -576,7 +576,7 @@ public final class RedisContextHolder {
      * @param name 锁名称
      * @return 锁标识
      */
-    public static String lock(String name) {
+    public static String lock(@NonNull String name) {
         return lock(name, Duration.ofSeconds(DEFAULT_LOCK_TIMEOUT));
     }
 
@@ -622,7 +622,7 @@ public final class RedisContextHolder {
      * @param name 锁名称
      * @return 锁标识
      */
-    public static String tryLock(String name) {
+    public static String tryLock(@NonNull String name) {
         return tryLock(name, Duration.ofSeconds(DEFAULT_LOCK_TIMEOUT));
     }
 
@@ -652,18 +652,16 @@ public final class RedisContextHolder {
      *
      * @param name  锁名称
      * @param token 锁标识
-     * @return true/false
      */
-    public static boolean unlock(@NonNull String name, @NonNull String token) {
+    public static void unlock(@NonNull String name, @NonNull String token) {
         byte[][] args = new byte[][]{name.getBytes(StandardCharsets.UTF_8), token.getBytes(StandardCharsets.UTF_8)};
-        return execute(getDefaultConnectionFactory(), connection -> {
-            Long result;
+        execute(getDefaultConnectionFactory(), connection -> {
             try {
-                result = connection.scriptingCommands().evalSha(UNLOCK_SCRIPT_SHA, ReturnType.INTEGER, 1, args);
+                connection.scriptingCommands().evalSha(UNLOCK_SCRIPT_SHA, ReturnType.INTEGER, 1, args);
             } catch (Exception e) {
-                result = connection.scriptingCommands().eval(UNLOCK_SCRIPT.getBytes(), ReturnType.INTEGER, 1, args);
+                connection.scriptingCommands().eval(UNLOCK_SCRIPT.getBytes(), ReturnType.INTEGER, 1, args);
             }
-            return result != null && result > 0;
+            return true;
         }, e -> {
             log.warn("Redis unlock failed: {}", e.getMessage());
             return false;
@@ -676,7 +674,7 @@ public final class RedisContextHolder {
      * @param name     锁名称
      * @param executor 回调方法
      */
-    public static void lockAround(String name, @NonNull Executor executor) {
+    public static void lockAround(@NonNull String name, @NonNull Executor executor) {
         lockAround(name, Duration.ofSeconds(DEFAULT_LOCK_TIMEOUT), executor);
     }
 
@@ -687,7 +685,7 @@ public final class RedisContextHolder {
      * @param duration 有效时间(毫秒)
      * @param executor 回调方法
      */
-    public static void lockAround(String name, Duration duration, @NonNull Executor executor) {
+    public static void lockAround(@NonNull String name, @NonNull Duration duration, @NonNull Executor executor) {
         String token = lock(name, duration);
         try {
             executor.execute();
@@ -704,7 +702,7 @@ public final class RedisContextHolder {
      * @param <T>      返回数据类型泛型
      * @return 执行结果
      */
-    public static <T> T lockAround(String name, Supplier<T> executor) {
+    public static <T> T lockAround(@NonNull String name, @NonNull Supplier<T> executor) {
         return lockAround(name, Duration.ofSeconds(DEFAULT_LOCK_TIMEOUT), executor);
     }
 
@@ -717,7 +715,7 @@ public final class RedisContextHolder {
      * @param <T>      返回数据类型泛型
      * @return 执行结果
      */
-    public static <T> T lockAround(String name, Duration duration, Supplier<T> executor) {
+    public static <T> T lockAround(@NonNull String name, @NonNull Duration duration, @NonNull Supplier<T> executor) {
         String token = lock(name, duration);
         try {
             return executor.get();
@@ -735,7 +733,7 @@ public final class RedisContextHolder {
      * @param <T>      返回数据类型泛型
      * @return 执行结果
      */
-    public static <T> T lockAround(String name, @NonNull Supplier<T> executor, @NonNull Supplier<T> failure) {
+    public static <T> T lockAround(@NonNull String name, @NonNull Supplier<T> executor, @NonNull Supplier<T> failure) {
         return lockAround(name, Duration.ofSeconds(DEFAULT_LOCK_TIMEOUT), executor, failure);
     }
 
@@ -749,7 +747,7 @@ public final class RedisContextHolder {
      * @param <T>      返回数据类型泛型
      * @return 执行结果
      */
-    public static <T> T lockAround(String name, Duration duration, @NonNull Supplier<T> executor,
+    public static <T> T lockAround(@NonNull String name, @NonNull Duration duration, @NonNull Supplier<T> executor,
                                    @NonNull Supplier<T> failure) {
         String token;
         try {
@@ -770,7 +768,7 @@ public final class RedisContextHolder {
      * @param name     锁名称
      * @param executor 回调方法
      */
-    public static void tryLockAround(String name, @NonNull Executor executor) {
+    public static void tryLockAround(@NonNull String name, @NonNull Executor executor) {
         tryLockAround(name, Duration.ofSeconds(DEFAULT_LOCK_TIMEOUT), executor);
     }
 
@@ -781,7 +779,7 @@ public final class RedisContextHolder {
      * @param duration 有效时间
      * @param executor 回调方法
      */
-    public static void tryLockAround(String name, Duration duration, @NonNull Executor executor) {
+    public static void tryLockAround(@NonNull String name, @NonNull Duration duration, @NonNull Executor executor) {
         String token = tryLock(name, duration);
         if (StringUtils.notEmpty(token)) {
             try {
@@ -800,7 +798,7 @@ public final class RedisContextHolder {
      * @param <T>      返回数据类型泛型
      * @return 执行结果
      */
-    public static <T> T tryLockAround(String name, Supplier<T> supplier) {
+    public static <T> T tryLockAround(@NonNull String name, @NonNull Supplier<T> supplier) {
         return tryLockAround(name, Duration.ofSeconds(DEFAULT_LOCK_TIMEOUT), supplier);
     }
 
@@ -813,7 +811,7 @@ public final class RedisContextHolder {
      * @param <T>      返回数据类型泛型
      * @return 执行结果
      */
-    public static <T> T tryLockAround(String name, Duration duration, Supplier<T> supplier) {
+    public static <T> T tryLockAround(@NonNull String name, @NonNull Duration duration, @NonNull Supplier<T> supplier) {
         return tryLockAround(name, duration, supplier, () -> null);
     }
 
@@ -826,7 +824,8 @@ public final class RedisContextHolder {
      * @param <T>      返回数据类型泛型
      * @return 执行结果
      */
-    public static <T> T tryLockAround(String name, @NonNull Supplier<T> supplier, @NonNull Supplier<T> failure) {
+    public static <T> T tryLockAround(@NonNull String name, @NonNull Supplier<T> supplier,
+                                      @NonNull Supplier<T> failure) {
         return tryLockAround(name, Duration.ofSeconds(DEFAULT_LOCK_TIMEOUT), supplier, failure);
     }
 
@@ -840,7 +839,7 @@ public final class RedisContextHolder {
      * @param <T>      返回数据类型泛型
      * @return 执行结果
      */
-    public static <T> T tryLockAround(String name, Duration duration, @NonNull Supplier<T> supplier,
+    public static <T> T tryLockAround(@NonNull String name, @NonNull Duration duration, @NonNull Supplier<T> supplier,
                                       @NonNull Supplier<T> failure) {
         String token = tryLock(name, duration);
         if (StringUtils.isEmpty(token)) {

+ 1 - 2
framework-sms/src/main/java/com/chelvc/framework/sms/support/DefaultCaptchaSmsHandler.java

@@ -82,8 +82,7 @@ public class DefaultCaptchaSmsHandler implements CaptchaSmsHandler {
         // 短信验证码发送频率限制
         String lock = "sms:captcha:interval:" + mobile;
         String secret = RedisContextHolder.tryLock(lock, Duration.ofSeconds(this.properties.getInterval()));
-        AssertUtils.available(StringUtils.notEmpty(secret),
-                () -> ApplicationContextHolder.getMessage("SMS.Frequency.Limit"));
+        AssertUtils.available(secret, () -> ApplicationContextHolder.getMessage("SMS.Frequency.Limit"));
 
         // 如果目标手机号属于白名单手机号则将手机号后{{length}}位作为验证码,否则获取真实的验证码
         try {

+ 130 - 0
framework-wechat/src/main/java/com/chelvc/framework/wechat/DefaultWechatAppletHandler.java

@@ -0,0 +1,130 @@
+package com.chelvc.framework.wechat;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import com.chelvc.framework.base.context.RestContextHolder;
+import com.chelvc.framework.base.context.SessionContextHolder;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.redis.context.RedisContextHolder;
+import com.chelvc.framework.wechat.config.WechatProperties;
+import com.chelvc.framework.wechat.context.WechatContextHolder;
+import com.google.common.collect.ImmutableMap;
+import lombok.NonNull;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+
+/**
+ * 微信小程序操作默认实现
+ *
+ * @author Woody
+ * @date 2024/6/19
+ */
+public class DefaultWechatAppletHandler implements WechatAppletHandler {
+    /**
+     * Scheme过期时间(天)
+     */
+    private static final int SCHEME_EXPIRE_INTERVAL = 30;
+
+    /**
+     * 获取用户信息接口地址
+     */
+    private static final String USER_URL =
+            "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";
+
+    /**
+     * 微信获取手机号接口地址
+     */
+    private static final String CODE2MOBILE_URL =
+            "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
+
+    /**
+     * 微信登录接口地址
+     */
+    private static final String JSCODE2SESSION_URL =
+            "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
+
+    /**
+     * 获取小程序schemeCode接口地址
+     */
+    private static final String GENERATE_SCHEME_URL = "https://api.weixin.qq.com/wxa/generatescheme?access_token=";
+
+    private final WechatProperties.Applet properties;
+
+    public DefaultWechatAppletHandler(@NonNull WechatProperties.Applet properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public String getId() {
+        return this.properties.getId();
+    }
+
+    @Override
+    public String getScheme(String path, String query, String environment) {
+        // 尝试从Redis中获取Scheme
+        path = StringUtils.ifEmpty(path, StringUtils.EMPTY);
+        query = StringUtils.ifEmpty(query, StringUtils.EMPTY);
+        environment = StringUtils.ifEmpty(environment, StringUtils.EMPTY);
+        HttpSession session = ObjectUtils.ifNull(SessionContextHolder.getRequest(), HttpServletRequest::getSession);
+        String id = ObjectUtils.ifNull(
+                ObjectUtils.ifNull(session, HttpSession::getId), () -> UUID.randomUUID().toString()
+        );
+        String key = "wechat:scheme:" + id + ":" + environment + ":" + path + ":" + query;
+        String scheme = (String) RedisContextHolder.getDefaultTemplate().opsForValue().get(key);
+        if (StringUtils.notEmpty(scheme)) {
+            return scheme;
+        }
+
+        // 重新获取Scheme
+        Map<String, Object> body = ImmutableMap.of(
+                "jump_wxa", ImmutableMap.of(
+                        "path", path,
+                        "query", query,
+                        "env_version", environment
+                ),
+                "expire_type", 1,
+                "expire_interval", SCHEME_EXPIRE_INTERVAL
+        );
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        HttpEntity<?> entity = new HttpEntity<>(body, headers);
+        String url = GENERATE_SCHEME_URL + this.getAccessToken();
+        scheme = WechatContextHolder.exchange(url, HttpMethod.POST, entity, WechatScheme.class).getOpenlink();
+        Duration duration = Duration.ofDays(SCHEME_EXPIRE_INTERVAL - 1);
+        RedisContextHolder.getDefaultTemplate().opsForValue().set(key, scheme, duration);
+        return scheme;
+    }
+
+    @Override
+    public String getAccessToken() {
+        return WechatContextHolder.getAccessToken(this.properties.getAppid());
+    }
+
+    @Override
+    public WechatUser getUser(@NonNull String openid) {
+        String url = String.format(USER_URL, this.getAccessToken(), openid);
+        return WechatContextHolder.get(url, WechatUser.class);
+    }
+
+    @Override
+    public String code2mobile(@NonNull String code) {
+        String url = CODE2MOBILE_URL + this.getAccessToken();
+        Map<String, String> request = ImmutableMap.of("code", code);
+        WechatMobile.Mobile mobile = WechatContextHolder.post(url, request, WechatMobile.class).getMobile();
+        return Objects.requireNonNull(mobile).getNumber();
+    }
+
+    @Override
+    public WechatSession code2session(@NonNull String code) {
+        String url = String.format(JSCODE2SESSION_URL, this.properties.getAppid(), this.properties.getSecret(), code);
+        return RestContextHolder.get(url, WechatSession.class);
+    }
+}

+ 0 - 324
framework-wechat/src/main/java/com/chelvc/framework/wechat/DefaultWechatHandler.java

@@ -1,324 +0,0 @@
-package com.chelvc.framework.wechat;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Supplier;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpSession;
-
-import com.chelvc.framework.base.context.ApplicationContextHolder;
-import com.chelvc.framework.base.context.RestContextHolder;
-import com.chelvc.framework.base.context.SessionContextHolder;
-import com.chelvc.framework.common.util.AssertUtils;
-import com.chelvc.framework.common.util.DecimalUtils;
-import com.chelvc.framework.common.util.HostUtils;
-import com.chelvc.framework.common.util.ObjectUtils;
-import com.chelvc.framework.common.util.StringUtils;
-import com.chelvc.framework.redis.context.RedisContextHolder;
-import com.chelvc.framework.wechat.config.WechatProperties;
-import com.chelvc.framework.wechat.context.WechatContextHolder;
-import com.github.wxpay.sdk.WXPayUtil;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Maps;
-import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.stereotype.Component;
-import org.springframework.util.CollectionUtils;
-
-/**
- * 微信处理默认实现
- *
- * @author Woody
- * @date 2024/1/30
- */
-@Component
-@RequiredArgsConstructor(onConstructor = @__(@Autowired))
-public class DefaultWechatHandler implements WechatHandler {
-    /**
-     * Scheme过期时间(天)
-     */
-    private static final int SCHEME_EXPIRE_INTERVAL = 30;
-
-    /**
-     * 访问令牌标识前缀
-     */
-    private static final String ACCESS_TOKEN_PREFIX = "wechat.token.";
-
-    /**
-     * 获取用户信息接口地址
-     */
-    private static final String USER_URL =
-            "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";
-
-    /**
-     * 微信下单接口地址
-     */
-    private static final String UNIFIEDORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
-
-    /**
-     * 微信获取手机号接口地址
-     */
-    private static final String CODE2MOBILE_URL =
-            "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
-
-    /**
-     * 微信登录接口地址
-     */
-    private static final String JSCODE2SESSION_URL =
-            "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
-
-    /**
-     * 获取访问令牌接口地址
-     */
-    private static final String ACCESS_TOKEN_URL =
-            "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
-
-    /**
-     * 获取小程序schemeCode接口地址
-     */
-    private static final String GENERATE_SCHEME_URL = "https://api.weixin.qq.com/wxa/generatescheme?access_token=";
-
-    private final WechatProperties properties;
-    private final Map<String, String> tokens = Maps.newConcurrentMap();
-
-    /**
-     * 查找支付配置
-     *
-     * @param type 支付类型
-     * @return 支付配置
-     */
-    private WechatProperties.Payment lookupPayment(String type) {
-        List<WechatProperties.Payment> payments = this.properties.getPayments();
-        WechatProperties.Payment payment = CollectionUtils.isEmpty(payments) ? null :
-                payments.stream().filter(config -> Objects.equals(config.getType(), type)).findFirst().orElse(null);
-        return AssertUtils.nonnull(payment, () -> "Not support payment type: " + type);
-    }
-
-    /**
-     * 参数签名
-     *
-     * @param payment    支付配置
-     * @param parameters 签名参数
-     * @return 签名信息
-     */
-    private String sign(WechatProperties.Payment payment, Map<String, String> parameters) {
-        return WechatContextHolder.sign(Objects.requireNonNull(payment.getMchkey()), parameters);
-    }
-
-    /**
-     * 构建支付参数
-     *
-     * @param payment 支付配置
-     * @param request 支付请求参数
-     * @return 支付参数键/值对
-     */
-    private Map<String, String> buildPaymentParameter(WechatProperties.Payment payment, WechatPayRequest request) {
-        String mchid = Objects.requireNonNull(payment.getMchid());
-        String callback = Objects.requireNonNull(payment.getCallback());
-        Map<String, String> parameters = Maps.newHashMap();
-        parameters.put("appid", payment.getAppid());
-        parameters.put("mch_id", mchid);
-        parameters.put("body", request.getComment());
-        parameters.put("out_trade_no", request.getOrder());
-        if (StringUtils.notEmpty(request.getContext())) {
-            parameters.put("attach", request.getContext());
-        }
-        parameters.put("fee_type", "CNY");
-        parameters.put("total_fee", String.valueOf(request.getAmount().multiply(DecimalUtils.HUNDRED).intValue()));
-        parameters.put("spbill_create_ip", HostUtils.LOCAL_ADDRESS);
-        parameters.put("notify_url", callback);
-        parameters.put("trade_type", payment.getMode().name());
-        parameters.put("nonce_str", WXPayUtil.generateNonceStr());
-        return parameters;
-    }
-
-    @Override
-    public String getAppid() {
-        return this.properties.getAppid();
-    }
-
-    @Override
-    public String getAccessToken() {
-        return this.getAccessToken(this.properties.getAppid(), this.properties.getSecret());
-    }
-
-    @Override
-    public String refreshAccessToken() {
-        return this.refreshAccessToken(this.properties.getAppid(), this.properties.getSecret());
-    }
-
-    @Override
-    public String getAccessToken(@NonNull String appid, @NonNull String secret) {
-        return this.tokens.computeIfAbsent(ACCESS_TOKEN_PREFIX + appid, k -> this.refreshAccessToken(appid, secret));
-    }
-
-    @Override
-    public String refreshAccessToken(@NonNull String appid, @NonNull String secret) {
-        String key = ACCESS_TOKEN_PREFIX + appid;
-        String token = ApplicationContextHolder.getProperty(key);
-        if (StringUtils.notEmpty(token)) {
-            return token;
-        }
-        return RedisContextHolder.lockAround(key + ":lock", () -> {
-            String value = (String) RedisContextHolder.getDefaultTemplate().opsForValue().get(key);
-            if (StringUtils.isEmpty(value) || Objects.equals(value, this.tokens.get(key))) {
-                String url = String.format(ACCESS_TOKEN_URL, appid, secret);
-                WechatAccessToken accessToken = RestContextHolder.get(url, WechatAccessToken.class);
-                value = accessToken.getToken();
-                long duration = accessToken.getDuration() - 60;
-                RedisContextHolder.getDefaultTemplate().opsForValue().set(key, value, duration, TimeUnit.SECONDS);
-                this.tokens.put(key, value);
-            }
-            return value;
-        }, () -> {
-            throw new IllegalStateException("Get wechat access token timeout");
-        });
-    }
-
-    @Override
-    public String getScheme() {
-        return this.getScheme(null, null, null);
-    }
-
-    @Override
-    public String getScheme(String path, String query, String environment) {
-        // 尝试从Redis中获取Scheme
-        path = StringUtils.ifEmpty(path, StringUtils.EMPTY);
-        query = StringUtils.ifEmpty(query, StringUtils.EMPTY);
-        environment = StringUtils.ifEmpty(environment, StringUtils.EMPTY);
-        HttpSession session = ObjectUtils.ifNull(SessionContextHolder.getRequest(), HttpServletRequest::getSession);
-        String id = ObjectUtils.ifNull(
-                ObjectUtils.ifNull(session, HttpSession::getId), () -> UUID.randomUUID().toString()
-        );
-        String key = "wechat:scheme:" + id + ":" + environment + ":" + path + ":" + query;
-        String scheme = (String) RedisContextHolder.getDefaultTemplate().opsForValue().get(key);
-        if (StringUtils.notEmpty(scheme)) {
-            return scheme;
-        }
-
-        // 重新获取Scheme
-        Map<String, Object> body = ImmutableMap.of(
-                "jump_wxa", ImmutableMap.of(
-                        "path", path,
-                        "query", query,
-                        "env_version", environment
-                ),
-                "expire_type", 1,
-                "expire_interval", SCHEME_EXPIRE_INTERVAL
-        );
-        HttpHeaders headers = new HttpHeaders();
-        headers.setContentType(MediaType.APPLICATION_JSON);
-        HttpEntity<?> entity = new HttpEntity<>(body, headers);
-        scheme = WechatContextHolder.exchange(
-                init -> GENERATE_SCHEME_URL + (init ? this.getAccessToken() : this.refreshAccessToken()),
-                HttpMethod.POST, entity, WechatScheme.class
-        ).getOpenlink();
-        RedisContextHolder.getDefaultTemplate().opsForValue()
-                .set(key, scheme, Duration.ofDays(SCHEME_EXPIRE_INTERVAL - 1));
-        return scheme;
-    }
-
-    @Override
-    public WechatUser getUser(@NonNull String accessToken, @NonNull String openid) {
-        return this.getUser(accessToken, openid, this::refreshAccessToken);
-    }
-
-    @Override
-    public WechatUser getUser(@NonNull String accessToken, @NonNull String openid,
-                              @NonNull Supplier<String> tokenRefresher) {
-        return WechatContextHolder.get(
-                init -> String.format(USER_URL, init ? accessToken : tokenRefresher.get(), openid),
-                WechatUser.class
-        );
-    }
-
-    @Override
-    public String code2mobile(@NonNull String code) {
-        WechatMobile.Mobile mobile = WechatContextHolder.post(
-                init -> CODE2MOBILE_URL + (init ? this.getAccessToken() : this.refreshAccessToken()),
-                ImmutableMap.of("code", code), WechatMobile.class
-        ).getMobile();
-        return Objects.requireNonNull(mobile).getNumber();
-    }
-
-    @Override
-    public WechatSession code2session(@NonNull String code) {
-        String url = String.format(JSCODE2SESSION_URL, this.properties.getAppid(), this.properties.getSecret(), code);
-        return RestContextHolder.get(url, WechatSession.class);
-    }
-
-    @Override
-    public String sign(@NonNull String type, @NonNull Map<String, String> parameters) {
-        return this.sign(this.lookupPayment(type), parameters);
-    }
-
-    @Override
-    public boolean validate(@NonNull String type, @NonNull Map<String, String> parameters) {
-        if (ObjectUtils.isEmpty(this.properties.getPayments())) {
-            return false;
-        }
-        String sign = parameters.get("sign");
-        if (StringUtils.isEmpty(sign)) {
-            return false;
-        }
-        for (WechatProperties.Payment payment : this.properties.getPayments()) {
-            if (Objects.equals(payment.getType(), type) && Objects.equals(this.sign(payment, parameters), sign)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public WechatUnifiedOrder unifiedorder(@NonNull String type, @NonNull WechatPayRequest request) {
-        // 获取支付配置
-        WechatProperties.Payment payment = this.lookupPayment(type);
-
-        // 构建请求参数
-        Map<String, String> parameters = this.buildPaymentParameter(payment, request);
-
-        if (payment.getMode() == PayMode.MWEB) {
-            // 如果是H5支付则需要指定scene_info,场景信息
-            parameters.put("scene_info", Objects.requireNonNull(request.getScene()));
-        } else if (payment.getMode() == PayMode.JSAPI) {
-            // 如果是小程序支付则需要指定openid
-            parameters.put("openid", Objects.requireNonNull(request.getOpenid()));
-        }
-
-        // 请求参数签名
-        parameters.put("sign", this.sign(payment, parameters));
-
-        // 将请求参数转换成xml请求体
-        String body = WechatContextHolder.map2xml(parameters);
-
-        // 发起下单请求
-        HttpHeaders headers = new HttpHeaders();
-        headers.setContentType(MediaType.TEXT_XML);
-        HttpEntity<?> entity = new HttpEntity<>(body, headers);
-        String response = RestContextHolder.execute(rest -> rest.exchange(
-                UNIFIEDORDER_URL, HttpMethod.POST, entity, String.class
-        )).getBody();
-
-        // 验证相应结果
-        Map<String, String> result = ObjectUtils.ifNull(response, WechatContextHolder::xml2map);
-        if (StringUtils.isEmpty(response) || !Objects.equals(result.get("return_code"), "SUCCESS")
-                || !Objects.equals(result.get("result_code"), "SUCCESS")
-                || !Objects.equals(result.remove("sign"), this.sign(payment, result))) {
-            throw new RuntimeException("Wechat unifiedorder failed");
-        }
-
-        // 构建微信统一下单信息
-        return WechatUnifiedOrder.builder().mode(payment.getMode()).appid(result.get("appid"))
-                .mchid(result.get("mch_id")).nonce(result.get("nonce_str")).prepayid(result.get("prepay_id"))
-                .qrcode(result.get("code_url")).redirect(result.get("mweb_url")).build();
-    }
-}

+ 173 - 0
framework-wechat/src/main/java/com/chelvc/framework/wechat/DefaultWechatPaymentHandler.java

@@ -0,0 +1,173 @@
+package com.chelvc.framework.wechat;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import com.chelvc.framework.base.context.ApplicationContextHolder;
+import com.chelvc.framework.base.context.RestContextHolder;
+import com.chelvc.framework.common.util.DecimalUtils;
+import com.chelvc.framework.common.util.HostUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.wechat.config.WechatProperties;
+import com.chelvc.framework.wechat.context.WechatContextHolder;
+import com.github.wxpay.sdk.WXPayUtil;
+import com.google.common.collect.Maps;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+
+/**
+ * 微信支付操作默认实现
+ *
+ * @author Woody
+ * @date 2024/6/19
+ */
+@Slf4j
+public class DefaultWechatPaymentHandler implements com.chelvc.framework.wechat.WechatPaymentHandler {
+    /**
+     * 微信下单接口地址
+     */
+    private static final String UNIFIEDORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
+
+    private final List<WechatProperties.Payment> payments;
+
+    public DefaultWechatPaymentHandler(@NonNull List<WechatProperties.Payment> payments) {
+        this.payments = payments;
+    }
+
+    /**
+     * 查找支付配置
+     *
+     * @param type 应用分类
+     * @return 支付配置
+     */
+    private WechatProperties.Payment lookupPayment(String type) {
+        if (ObjectUtils.notEmpty(this.payments)) {
+            WechatProperties.Payment optional = null;
+            String merchant = ApplicationContextHolder.getProperty("wechat.payment.merchant");
+            for (WechatProperties.Payment payment : this.payments) {
+                if (Objects.equals(payment.getType(), type)) {
+                    optional = payment;
+                }
+                // 优先使用指定商户号支付配置
+                if (StringUtils.isEmpty(merchant) || Objects.equals(payment.getMchid(), merchant)) {
+                    return payment;
+                }
+            }
+
+            // 使用同应用类型的其他支付配置
+            if (Objects.nonNull(optional)) {
+                return optional;
+            }
+        }
+        throw new IllegalArgumentException("Not support payment type: " + type);
+    }
+
+    /**
+     * 构建支付参数
+     *
+     * @param payment 支付配置
+     * @param request 支付请求参数
+     * @return 支付参数键/值对
+     */
+    private Map<String, String> buildPaymentParameter(WechatProperties.Payment payment, WechatPayRequest request) {
+        String mchid = Objects.requireNonNull(payment.getMchid());
+        String callback = Objects.requireNonNull(payment.getCallback());
+        Map<String, String> parameters = Maps.newHashMap();
+        parameters.put("appid", payment.getAppid());
+        parameters.put("mch_id", mchid);
+        parameters.put("body", request.getComment());
+        parameters.put("out_trade_no", request.getOrder());
+        if (StringUtils.notEmpty(request.getContext())) {
+            parameters.put("attach", request.getContext());
+        }
+        parameters.put("fee_type", "CNY");
+        parameters.put("total_fee", String.valueOf(request.getAmount().multiply(DecimalUtils.HUNDRED).intValue()));
+        parameters.put("spbill_create_ip", HostUtils.LOCAL_ADDRESS);
+        parameters.put("notify_url", callback);
+        parameters.put("trade_type", payment.getMode().name());
+        parameters.put("nonce_str", WXPayUtil.generateNonceStr());
+        return parameters;
+    }
+
+    @Override
+    public String sign(@NonNull String type, @NonNull Map<String, String> parameters) {
+        WechatProperties.Payment payment = this.lookupPayment(type);
+        return WechatContextHolder.sign(payment.getMchkey(), parameters);
+    }
+
+    @Override
+    public boolean validate(@NonNull String type, @NonNull Map<String, String> parameters) {
+        if (ObjectUtils.isEmpty(this.payments)) {
+            return false;
+        }
+        String sign = parameters.get("sign");
+        if (StringUtils.isEmpty(sign)) {
+            return false;
+        }
+        for (WechatProperties.Payment payment : this.payments) {
+            if (Objects.equals(payment.getType(), type)
+                    && Objects.equals(WechatContextHolder.sign(payment.getMchkey(), parameters), sign)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public WechatUnifiedOrder unifiedorder(@NonNull String type, @NonNull WechatPayRequest request) {
+        // 获取支付配置
+        WechatProperties.Payment payment = this.lookupPayment(type);
+
+        // 构建请求参数
+        Map<String, String> parameters = this.buildPaymentParameter(payment, request);
+
+        if (payment.getMode() == PayMode.MWEB) {
+            // 如果是H5支付则需要指定scene_info,场景信息
+            parameters.put("scene_info", Objects.requireNonNull(request.getScene()));
+        } else if (payment.getMode() == PayMode.JSAPI) {
+            // 如果是小程序支付则需要指定openid
+            parameters.put("openid", Objects.requireNonNull(request.getOpenid()));
+        }
+
+        // 请求参数签名
+        String mchkey = payment.getMchkey();
+        parameters.put("sign", WechatContextHolder.sign(mchkey, parameters));
+        boolean debug = log.isDebugEnabled();
+        if (debug) {
+            log.debug("Wechat unifiedorder request: {}", parameters);
+        }
+
+        // 将请求参数转换成xml请求体
+        String body = WechatContextHolder.map2xml(parameters);
+
+        // 发起下单请求
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.TEXT_XML);
+        HttpEntity<?> entity = new HttpEntity<>(body, headers);
+        String response = RestContextHolder.execute(rest -> rest.exchange(
+                UNIFIEDORDER_URL, HttpMethod.POST, entity, String.class
+        )).getBody();
+
+        // 验证相应结果
+        Map<String, String> result = ObjectUtils.ifNull(response, WechatContextHolder::xml2map);
+        if (debug) {
+            log.debug("Wechat unifiedorder response: {}", result);
+        }
+        if (StringUtils.isEmpty(response) || !Objects.equals(result.get("return_code"), "SUCCESS")
+                || !Objects.equals(result.get("result_code"), "SUCCESS")
+                || !Objects.equals(result.remove("sign"), WechatContextHolder.sign(mchkey, result))) {
+            throw new RuntimeException("Wechat unifiedorder failed");
+        }
+
+        // 构建微信统一下单信息
+        return WechatUnifiedOrder.builder().mode(payment.getMode()).appid(result.get("appid"))
+                .mchid(result.get("mch_id")).nonce(result.get("nonce_str")).prepayid(result.get("prepay_id"))
+                .qrcode(result.get("code_url")).redirect(result.get("mweb_url")).build();
+    }
+}

+ 42 - 67
framework-wechat/src/main/java/com/chelvc/framework/wechat/DefaultWechatPublicHandler.java

@@ -9,10 +9,8 @@ import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import javax.crypto.Cipher;
 
 import com.chelvc.framework.base.context.RestContextHolder;
-import com.chelvc.framework.common.util.AESUtils;
 import com.chelvc.framework.common.util.AssertUtils;
 import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
@@ -20,12 +18,9 @@ import com.chelvc.framework.wechat.config.WechatProperties;
 import com.chelvc.framework.wechat.context.WechatContextHolder;
 import com.google.common.collect.ImmutableMap;
 import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.codec.binary.Hex;
 import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.lang3.tuple.Pair;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
@@ -36,27 +31,26 @@ import org.springframework.util.CollectionUtils;
  * 微信公众号操作默认实现
  *
  * @author Woody
- * @date 2024/1/30
+ * @date 2024/4/3
  */
 @Slf4j
-@RequiredArgsConstructor(onConstructor = @__(@Autowired))
 public class DefaultWechatPublicHandler implements WechatPublicHandler {
     /**
-     * 发送微信服务号模板消息接口地址
+     * 获取用户信息接口地址
      */
-    private static final String SEND_TEMPLATE_MESSAGE_URL =
-            "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=";
+    private static final String USER_URL =
+            "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";
 
     /**
-     * 批量获取关注用户openid地址
+     * 获取公众号粉丝unionId信息地址
      */
-    private static final String OPENID_URL =
-            "https://api.weixin.qq.com/cgi-bin/user/get?access_token=%s&next_openid=%s";
+    private static final String FANS_URL = "https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=";
 
     /**
-     * 获取公众号粉丝unionId信息地址
+     * 批量获取关注用户openid地址
      */
-    private static final String USER_URL = "https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=";
+    private static final String OPENID_URL =
+            "https://api.weixin.qq.com/cgi-bin/user/get?access_token=%s&next_openid=%s";
 
     /**
      * 获取微信网页令牌接口地址
@@ -64,32 +58,32 @@ public class DefaultWechatPublicHandler implements WechatPublicHandler {
     private static final String CODE2TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token" +
             "?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
 
-    private final WechatHandler wechatHandler;
+    /**
+     * 发送微信服务号模板消息接口地址
+     */
+    private static final String SEND_TEMPLATE_MESSAGE_URL =
+            "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=";
+
     private final WechatProperties.Public properties;
 
-    @Override
-    public String getId() {
-        return StringUtils.ifEmpty(this.properties.getId(), this.properties.getAppid());
+    public DefaultWechatPublicHandler(@NonNull WechatProperties.Public properties) {
+        this.properties = properties;
     }
 
     @Override
-    public String getAppid() {
-        return this.properties.getAppid();
+    public String getId() {
+        return StringUtils.ifEmpty(this.properties.getId(), this.properties.getAppid());
     }
 
     @Override
     public String getAccessToken() {
-        return this.wechatHandler.getAccessToken(this.properties.getAppid(), this.properties.getSecret());
-    }
-
-    @Override
-    public String refreshAccessToken() {
-        return this.wechatHandler.refreshAccessToken(this.properties.getAppid(), this.properties.getSecret());
+        return WechatContextHolder.getAccessToken(this.properties.getAppid());
     }
 
     @Override
-    public WechatHandler getWechatHandler() {
-        return this.wechatHandler;
+    public String decrypt(@NonNull String ciphertext) {
+        String secret = AssertUtils.nonempty(this.properties.getKey(), () -> "Secret is missing");
+        return WechatContextHolder.decrypt(ciphertext, secret);
     }
 
     @Override
@@ -99,38 +93,32 @@ public class DefaultWechatPublicHandler implements WechatPublicHandler {
     }
 
     @Override
-    public String decrypt(String ciphertext) {
-        if (StringUtils.isEmpty(ciphertext)) {
-            return ciphertext;
-        }
-        String secret = AssertUtils.nonempty(this.properties.getKey(), () -> "Secret is missing");
-        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());
-        });
-        return WechatContextHolder.decrypt(cipher, ciphertext);
+    public WechatUser getUser(@NonNull String openid) {
+        String url = String.format(USER_URL, this.getAccessToken(), openid);
+        return WechatContextHolder.get(url, WechatUser.class);
     }
 
     @Override
-    public WechatUser getUser(@NonNull String openid) {
-        return this.wechatHandler.getUser(this.getAccessToken(), openid, this::refreshAccessToken);
+    public String sign(long timestamp, @NonNull String... elements) {
+        MessageDigest digest = DigestUtils.getDigest("SHA-1");
+        String original = Stream.concat(
+                Stream.of(this.properties.getToken(), String.valueOf(timestamp)), Stream.of(elements)
+        ).sorted().collect(Collectors.joining());
+        return Hex.encodeHexString(digest.digest(original.getBytes()));
     }
 
     @Override
-    public void iterateOpenids(@NonNull Consumer<List<String>> consumer) {
-        this.iterateOpenids((openids, total) -> consumer.accept(openids));
+    public void openids(@NonNull Consumer<List<String>> consumer) {
+        this.openids((openids, total) -> consumer.accept(openids));
     }
 
     @Override
-    public void iterateOpenids(@NonNull BiConsumer<List<String>, Integer> consumer) {
+    public void openids(@NonNull BiConsumer<List<String>, Integer> consumer) {
         int total = 0;
         String next = null;
         do {
-            final String _next = ObjectUtils.ifNull(next, StringUtils.EMPTY);
-            WechatPublicOpenids response = WechatContextHolder.get(
-                    init -> String.format(OPENID_URL, init ? this.getAccessToken() : this.refreshAccessToken(), _next),
-                    WechatPublicOpenids.class
-            );
+            String url = String.format(OPENID_URL, this.getAccessToken(), ObjectUtils.ifNull(next, StringUtils.EMPTY));
+            WechatPublicOpenids response = WechatContextHolder.get(url, WechatPublicOpenids.class);
             List<String> openids = ObjectUtils.ifNull(response.getData(), WechatPublicOpenids.Openids::getOpenid);
             if (CollectionUtils.isEmpty(openids)) {
                 break;
@@ -149,22 +137,11 @@ public class DefaultWechatPublicHandler implements WechatPublicHandler {
                 "user_list",
                 openids.stream().map(openid -> ImmutableMap.of("openid", openid)).collect(Collectors.toList())
         );
-        WechatPublicSubscribers response = WechatContextHolder.post(
-                init -> USER_URL + (init ? this.getAccessToken() : this.refreshAccessToken()),
-                body, WechatPublicSubscribers.class
-        );
+        String url = FANS_URL + this.getAccessToken();
+        WechatPublicSubscribers response = WechatContextHolder.post(url, body, WechatPublicSubscribers.class);
         return ObjectUtils.ifNull(response.getSubscribers(), Collections::emptyList);
     }
 
-    @Override
-    public String sign(long timestamp, @NonNull String... elements) {
-        MessageDigest digest = DigestUtils.getDigest("SHA-1");
-        String original = Stream.concat(
-                Stream.of(this.properties.getToken(), String.valueOf(timestamp)), Stream.of(elements)
-        ).sorted().collect(Collectors.joining());
-        return Hex.encodeHexString(digest.digest(original.getBytes()));
-    }
-
     @Override
     public long push(@NonNull String openid, @NonNull String template, @NonNull Map<?, ?> parameters) {
         return this.push(openid, template, StringUtils.EMPTY, parameters);
@@ -177,7 +154,7 @@ public class DefaultWechatPublicHandler implements WechatPublicHandler {
         if (debug) {
             log.debug("Wechat public push request: {}, {}, {}", openid, template, parameters);
         }
-        String appid = (redirect = redirect.trim()).isEmpty() ? StringUtils.EMPTY : this.wechatHandler.getAppid();
+        String appid = (redirect = redirect.trim()).isEmpty() ? StringUtils.EMPTY : this.properties.getAppid();
         Map<String, Object> body = ImmutableMap.of(
                 "touser", openid,
                 "template_id", template,
@@ -188,12 +165,10 @@ public class DefaultWechatPublicHandler implements WechatPublicHandler {
         HttpHeaders headers = new HttpHeaders();
         headers.setContentType(MediaType.APPLICATION_JSON);
         HttpEntity<?> entity = new HttpEntity<>(body, headers);
+        String url = SEND_TEMPLATE_MESSAGE_URL + this.getAccessToken();
         WechatPublicMessage response;
         try {
-            response = WechatContextHolder.exchange(
-                    init -> SEND_TEMPLATE_MESSAGE_URL + (init ? this.getAccessToken() : this.refreshAccessToken()),
-                    HttpMethod.POST, entity, WechatPublicMessage.class
-            );
+            response = WechatContextHolder.exchange(url, HttpMethod.POST, entity, WechatPublicMessage.class);
         } catch (Exception e) {
             log.warn("Wechat public push failed: {}, {}, {}", openid, template, e.getMessage());
             return 0;

+ 66 - 0
framework-wechat/src/main/java/com/chelvc/framework/wechat/WechatAppletHandler.java

@@ -0,0 +1,66 @@
+package com.chelvc.framework.wechat;
+
+/**
+ * 微信小程序操作接口
+ *
+ * @author Woody
+ * @date 2024/6/19
+ */
+public interface WechatAppletHandler {
+    /**
+     * 获取唯一标识
+     *
+     * @return 唯一标识
+     */
+    String getId();
+
+    /**
+     * 获取小程序Scheme码
+     *
+     * @return 小程序Scheme码
+     */
+    default String getScheme() {
+        return this.getScheme(null, null, null);
+    }
+
+    /**
+     * 获取小程序Scheme码
+     *
+     * @param path        进入的小程序页面路径
+     * @param query       进入小程序时的 query
+     * @param environment 环境标识(正式版为"release",体验版为"trial",开发版为"develop")
+     * @return 小程序Scheme码
+     */
+    String getScheme(String path, String query, String environment);
+
+    /**
+     * 获取访问令牌
+     *
+     * @return 访问令牌
+     */
+    String getAccessToken();
+
+    /**
+     * 获取微信用户信息
+     *
+     * @param openid openid
+     * @return 微信用户信息
+     */
+    WechatUser getUser(String openid);
+
+    /**
+     * 根据临时令牌获取手机号
+     *
+     * @param code 临时令牌
+     * @return 手机号
+     */
+    String code2mobile(String code);
+
+    /**
+     * 根据临时令牌获取会话信息
+     *
+     * @param code 临时令牌
+     * @return 会话信息
+     */
+    WechatSession code2session(String code);
+}

+ 0 - 130
framework-wechat/src/main/java/com/chelvc/framework/wechat/WechatHandler.java

@@ -1,130 +0,0 @@
-package com.chelvc.framework.wechat;
-
-import java.util.Map;
-import java.util.function.Supplier;
-
-/**
- * 微信操作接口
- *
- * @author Woody
- * @date 2024/1/30
- */
-public interface WechatHandler {
-    /**
-     * 获取AppID
-     *
-     * @return AppID
-     */
-    String getAppid();
-
-    /**
-     * 获取微信访问令牌
-     *
-     * @return 访问令牌
-     */
-    String getAccessToken();
-
-    /**
-     * 刷新微信访问令牌
-     *
-     * @return 访问令牌
-     */
-    String refreshAccessToken();
-
-    /**
-     * 获取微信访问令牌
-     *
-     * @param appid  AppID
-     * @param secret AppSecret
-     * @return 访问令牌
-     */
-    String getAccessToken(String appid, String secret);
-
-    /**
-     * 刷新微信访问令牌
-     *
-     * @param appid  AppID
-     * @param secret AppSecret
-     * @return 访问令牌
-     */
-    String refreshAccessToken(String appid, String secret);
-
-    /**
-     * 获取小程序Scheme码
-     *
-     * @return 小程序Scheme码
-     */
-    String getScheme();
-
-    /**
-     * 获取小程序Scheme码
-     *
-     * @param path        进入的小程序页面路径
-     * @param query       进入小程序时的 query
-     * @param environment 环境标识(正式版为"release",体验版为"trial",开发版为"develop")
-     * @return 小程序Scheme码
-     */
-    String getScheme(String path, String query, String environment);
-
-    /**
-     * 获取微信用户信息
-     *
-     * @param accessToken 访问令牌
-     * @param openid      openid
-     * @return 微信用户信息
-     */
-    WechatUser getUser(String accessToken, String openid);
-
-    /**
-     * 获取微信用户信息
-     *
-     * @param accessToken    访问令牌
-     * @param openid         openid
-     * @param tokenRefresher 令牌刷新方法
-     * @return 微信用户信息
-     */
-    WechatUser getUser(String accessToken, String openid, Supplier<String> tokenRefresher);
-
-    /**
-     * 根据临时令牌获取手机号
-     *
-     * @param code 临时令牌
-     * @return 手机号
-     */
-    String code2mobile(String code);
-
-    /**
-     * 根据临时令牌获取会话信息
-     *
-     * @param code 临时令牌
-     * @return 会话信息
-     */
-    WechatSession code2session(String code);
-
-    /**
-     * 参数签名
-     *
-     * @param type       支付类型
-     * @param parameters 签名参数
-     * @return 签名信息
-     */
-    String sign(String type, Map<String, String> parameters);
-
-    /**
-     * 校验参数有效性
-     *
-     * @param type       支付类型
-     * @param parameters 参数键/值对
-     * @return true/false
-     */
-    boolean validate(String type, Map<String, String> parameters);
-
-    /**
-     * 微信统一下单
-     *
-     * @param type    支付类型
-     * @param request 支付请求参数
-     * @return 微信统一下单信息
-     */
-    WechatUnifiedOrder unifiedorder(String type, WechatPayRequest request);
-}

+ 38 - 0
framework-wechat/src/main/java/com/chelvc/framework/wechat/WechatPaymentHandler.java

@@ -0,0 +1,38 @@
+package com.chelvc.framework.wechat;
+
+import java.util.Map;
+
+/**
+ * 微信支付操作接口
+ *
+ * @author Woody
+ * @date 2024/6/19
+ */
+public interface WechatPaymentHandler {
+    /**
+     * 参数签名
+     *
+     * @param type       应用分类
+     * @param parameters 签名参数
+     * @return 签名信息
+     */
+    String sign(String type, Map<String, String> parameters);
+
+    /**
+     * 校验参数有效性
+     *
+     * @param type       应用分类
+     * @param parameters 参数键/值对
+     * @return true/false
+     */
+    boolean validate(String type, Map<String, String> parameters);
+
+    /**
+     * 微信统一下单
+     *
+     * @param type    应用分类
+     * @param request 支付请求参数
+     * @return 微信统一下单信息
+     */
+    WechatUnifiedOrder unifiedorder(String type, WechatPayRequest request);
+}

+ 19 - 40
framework-wechat/src/main/java/com/chelvc/framework/wechat/WechatPublicHandler.java

@@ -10,43 +10,30 @@ import java.util.function.Consumer;
  * 微信公众号操作接口
  *
  * @author Woody
- * @date 2024/1/30
+ * @date 2024/4/3
  */
 public interface WechatPublicHandler {
     /**
-     * 获取配置ID
+     * 获取唯一标识
      *
-     * @return 配置ID
+     * @return 唯一标识
      */
     String getId();
 
     /**
-     * 获取AppID
-     *
-     * @return AppID
-     */
-    String getAppid();
-
-    /**
-     * 获取微信公众号访问令牌
+     * 获取访问令牌
      *
      * @return 访问令牌
      */
     String getAccessToken();
 
     /**
-     * 刷新微信访问令牌
-     *
-     * @return 访问令牌
-     */
-    String refreshAccessToken();
-
-    /**
-     * 获取微信操作处理器实例
+     * 数据解密
      *
-     * @return 微信操作处理器实例
+     * @param ciphertext 密文
+     * @return 明文
      */
-    WechatHandler getWechatHandler();
+    String decrypt(String ciphertext);
 
     /**
      * 根据临时令牌获取微信令牌
@@ -56,14 +43,6 @@ public interface WechatPublicHandler {
      */
     WechatWebToken code2token(String code);
 
-    /**
-     * 数据解密
-     *
-     * @param ciphertext 密文
-     * @return 明文
-     */
-    String decrypt(String ciphertext);
-
     /**
      * 获取微信公众号用户信息
      *
@@ -72,19 +51,28 @@ public interface WechatPublicHandler {
      */
     WechatUser getUser(String openid);
 
+    /**
+     * 数据签名
+     *
+     * @param timestamp 时间戳
+     * @param elements  签名要素
+     * @return 签名信息
+     */
+    String sign(long timestamp, String... elements);
+
     /**
      * 遍历所有关注用户openid
      *
      * @param consumer openid回调接口
      */
-    void iterateOpenids(Consumer<List<String>> consumer);
+    void openids(Consumer<List<String>> consumer);
 
     /**
      * 遍历所有关注用户openid
      *
      * @param consumer openid回调接口
      */
-    void iterateOpenids(BiConsumer<List<String>, Integer> consumer);
+    void openids(BiConsumer<List<String>, Integer> consumer);
 
     /**
      * 批量获取关注者信息
@@ -94,15 +82,6 @@ public interface WechatPublicHandler {
      */
     List<WechatPublicSubscriber> getSubscribers(Collection<String> openids);
 
-    /**
-     * 数据签名
-     *
-     * @param timestamp 时间戳
-     * @param elements  签名要素
-     * @return 签名信息
-     */
-    String sign(long timestamp, String... elements);
-
     /**
      * 推送消息
      *

+ 30 - 15
framework-wechat/src/main/java/com/chelvc/framework/wechat/config/WechatConfigurer.java

@@ -1,8 +1,11 @@
 package com.chelvc.framework.wechat.config;
 
+import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.wechat.DefaultWechatAppletHandler;
+import com.chelvc.framework.wechat.DefaultWechatPaymentHandler;
 import com.chelvc.framework.wechat.DefaultWechatPublicHandler;
-import com.chelvc.framework.wechat.WechatHandler;
+import com.chelvc.framework.wechat.WechatAppletHandler;
 import com.chelvc.framework.wechat.WechatPublicHandler;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.BeansException;
@@ -11,7 +14,6 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
 import org.springframework.beans.factory.support.RootBeanDefinition;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.support.GenericApplicationContext;
-import org.springframework.util.CollectionUtils;
 
 /**
  * 微信配置类
@@ -22,35 +24,48 @@ import org.springframework.util.CollectionUtils;
 @Configuration
 @RequiredArgsConstructor(onConstructor = @__(@Autowired))
 public class WechatConfigurer implements BeanPostProcessor {
-    private final WechatHandler wechatHandler;
     private final WechatProperties properties;
     private final GenericApplicationContext applicationContext;
     private volatile boolean initialized;
 
     /**
-     * 初始化微信公众号处理器
+     * 初始化微信操作处理器
      */
-    private void initializeWechatPublicHandler() {
-        if (!CollectionUtils.isEmpty(this.properties.getPublics())) {
+    private synchronized void initializeWechatHandler() {
+        if (this.initialized) {
+            return;
+        }
+
+        // 注册小程序操作Bean
+        if (ObjectUtils.notEmpty(this.properties.getApplets())) {
+            this.properties.getApplets().forEach(config -> {
+                WechatAppletHandler handler = new DefaultWechatAppletHandler(config);
+                String name = StringUtils.ifEmpty(config.getId(), config.getAppid());
+                RootBeanDefinition definition = new RootBeanDefinition(WechatAppletHandler.class, () -> handler);
+                this.applicationContext.registerBeanDefinition(name, definition);
+            });
+        }
+
+        // 注册公众号操作Bean
+        if (ObjectUtils.notEmpty(this.properties.getPublics())) {
             this.properties.getPublics().forEach(config -> {
-                WechatPublicHandler handler = new DefaultWechatPublicHandler(this.wechatHandler, config);
+                WechatPublicHandler handler = new DefaultWechatPublicHandler(config);
                 String name = StringUtils.ifEmpty(config.getId(), config.getAppid());
                 RootBeanDefinition definition = new RootBeanDefinition(WechatPublicHandler.class, () -> handler);
                 this.applicationContext.registerBeanDefinition(name, definition);
             });
         }
+
+        // 注册微信支付操作Bean
+        if (ObjectUtils.notEmpty(this.properties.getPayments())) {
+            this.applicationContext.registerBean(DefaultWechatPaymentHandler.class, this.properties.getPayments());
+        }
+        this.initialized = true;
     }
 
     @Override
     public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
-        if (!this.initialized) {
-            synchronized (this) {
-                if (!this.initialized) {
-                    this.initializeWechatPublicHandler();
-                    this.initialized = true;
-                }
-            }
-        }
+        this.initializeWechatHandler();
         return bean;
     }
 }

+ 38 - 6
framework-wechat/src/main/java/com/chelvc/framework/wechat/config/WechatProperties.java

@@ -19,14 +19,14 @@ import org.springframework.context.annotation.Configuration;
 @ConfigurationProperties("wechat")
 public class WechatProperties {
     /**
-     * AppID
+     * 令牌配置
      */
-    private String appid;
+    private final Token token = new Token();
 
     /**
-     * AppSecret
+     * 小程序配置列表
      */
-    private String secret;
+    private List<Applet> applets = Collections.emptyList();
 
     /**
      * 公众号配置列表
@@ -38,13 +38,45 @@ public class WechatProperties {
      */
     private List<Payment> payments = Collections.emptyList();
 
+    /**
+     * 令牌配置
+     */
+    @Data
+    public static class Token {
+        /**
+         * 是否可刷新
+         */
+        private boolean refreshable = true;
+    }
+
+    /**
+     * 小程序配置
+     */
+    @Data
+    public static class Applet {
+        /**
+         * 唯一标识
+         */
+        private String id;
+
+        /**
+         * AppID
+         */
+        private String appid;
+
+        /**
+         * AppSecret
+         */
+        private String secret;
+    }
+
     /**
      * 公众号配置
      */
     @Data
     public static class Public {
         /**
-         * 配置ID
+         * 唯一标识
          */
         private String id;
 
@@ -75,7 +107,7 @@ public class WechatProperties {
     @Data
     public static class Payment {
         /**
-         * 支付类型
+         * 应用分类
          */
         private String type;
 

+ 156 - 44
framework-wechat/src/main/java/com/chelvc/framework/wechat/context/WechatContextHolder.java

@@ -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());
     }
 }