Browse Source

优化OAuth认证逻辑

woody 1 year ago
parent
commit
7a1031013f
27 changed files with 1899 additions and 803 deletions
  1. 19 14
      framework-base/src/main/java/com/chelvc/framework/base/context/DefaultSessionFactory.java
  2. 0 5
      framework-base/src/main/java/com/chelvc/framework/base/context/LoggingContextHolder.java
  3. 9 7
      framework-base/src/main/java/com/chelvc/framework/base/context/Session.java
  4. 79 25
      framework-base/src/main/java/com/chelvc/framework/base/context/SessionContextHolder.java
  5. 37 0
      framework-base/src/main/java/com/chelvc/framework/base/context/Using.java
  6. 1 1
      framework-boot/pom.xml
  7. 20 0
      framework-common/src/main/java/com/chelvc/framework/common/util/DateUtils.java
  8. 58 33
      framework-common/src/main/java/com/chelvc/framework/common/util/ObjectUtils.java
  9. 0 12
      framework-common/src/main/java/com/chelvc/framework/common/util/StringUtils.java
  10. 4 4
      framework-database/src/main/java/com/chelvc/framework/database/config/TypeHandlerConfigurer.java
  11. 3 1
      framework-feign/src/main/java/com/chelvc/framework/feign/interceptor/FeignInvokeInterceptor.java
  12. 15 12
      framework-oauth/src/main/java/com/chelvc/framework/oauth/config/OAuthConfigurer.java
  13. 73 35
      framework-oauth/src/main/java/com/chelvc/framework/oauth/context/OAuthContextHolder.java
  14. 27 0
      framework-oauth/src/main/java/com/chelvc/framework/oauth/token/DefaultSessionValidator.java
  15. 38 24
      framework-oauth/src/main/java/com/chelvc/framework/oauth/token/RedisSessionValidator.java
  16. 0 71
      framework-oauth/src/main/java/com/chelvc/framework/oauth/token/RedisTokenStore.java
  17. 2 2
      framework-oauth/src/main/java/com/chelvc/framework/oauth/token/SessionValidator.java
  18. 2 1
      framework-oauth/src/main/java/com/chelvc/framework/oauth/token/TimestampTokenValidator.java
  19. 5 5
      framework-redis/src/main/java/com/chelvc/framework/redis/config/RedisProperties.java
  20. 69 521
      framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java
  21. 286 0
      framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisDailyHashHolder.java
  22. 382 0
      framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisHashHolder.java
  23. 497 0
      framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisStreamHolder.java
  24. 243 0
      framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisUserDailyHashHolder.java
  25. 3 2
      framework-redis/src/main/java/com/chelvc/framework/redis/stream/ConsumerPendingCleaner.java
  26. 13 14
      framework-redis/src/main/java/com/chelvc/framework/redis/stream/DefaultRedisMQListenerContainer.java
  27. 14 14
      framework-redis/src/main/java/com/chelvc/framework/redis/stream/MessageStreamListener.java

+ 19 - 14
framework-base/src/main/java/com/chelvc/framework/base/context/DefaultSessionFactory.java

@@ -37,6 +37,21 @@ public class DefaultSessionFactory implements SessionFactory {
         return null;
     }
 
+    /**
+     * 获取使用类别
+     *
+     * @param request Http请求对象
+     * @return 使用类别
+     */
+    protected Using getUsing(@NonNull HttpServletRequest request) {
+        try {
+            return StringUtils.ifEmpty(request.getHeader(SessionContextHolder.HEADER_USING), Using::valueOf);
+        } catch (Exception e) {
+            log.warn("Using convert failed: {}", e.getMessage());
+        }
+        return null;
+    }
+
     /**
      * 获取客户端主机地址
      *
@@ -161,22 +176,12 @@ public class DefaultSessionFactory implements SessionFactory {
         return null;
     }
 
-    /**
-     * 获取设备指纹
-     *
-     * @param request Http请求对象
-     * @return 设备指纹
-     */
-    protected String getFingerprint(@NonNull HttpServletRequest request) {
-        return StringUtils.ifEmpty(request.getHeader(SessionContextHolder.HEADER_FINGERPRINT), (String) null);
-    }
-
     @Override
     public Session build(@NonNull HttpServletRequest request) {
-        return Session.builder().id(this.getId(request)).host(this.getHost(request)).scope(this.getScope(request))
-                .device(this.getDevice(request)).channel(this.getChannel(request)).platform(this.getPlatform(request))
-                .terminal(this.getTerminal(request)).version(this.getVersion(request))
-                .fingerprint(this.getFingerprint(request)).anonymous(this.isAnonymous(request))
+        return Session.builder().id(this.getId(request)).using(this.getUsing(request)).host(this.getHost(request))
+                .scope(this.getScope(request)).device(this.getDevice(request)).channel(this.getChannel(request))
+                .platform(this.getPlatform(request)).terminal(this.getTerminal(request))
+                .version(this.getVersion(request)).anonymous(this.isAnonymous(request))
                 .authorities(Collections.unmodifiableSet(this.getAuthorities(request)))
                 .timestamp(this.getTimestamp(request)).build();
     }

+ 0 - 5
framework-base/src/main/java/com/chelvc/framework/base/context/LoggingContextHolder.java

@@ -81,7 +81,6 @@ public final class LoggingContextHolder {
         Long id = ObjectUtils.ifNull(session, Session::getId);
         String host = ObjectUtils.ifNull(session, Session::getHost);
         String device = ObjectUtils.ifNull(session, Session::getDevice);
-        String fingerprint = ObjectUtils.ifNull(session, Session::getFingerprint);
         Platform platform = ObjectUtils.ifNull(session, Session::getPlatform);
         Terminal terminal = ObjectUtils.ifNull(session, Session::getTerminal);
         String version = ObjectUtils.ifNull(session, Session::getVersion);
@@ -99,10 +98,6 @@ public final class LoggingContextHolder {
             buffer.append(device);
         }
         buffer.append("] [");
-        if (StringUtils.notEmpty(fingerprint)) {
-            buffer.append(fingerprint);
-        }
-        buffer.append("] [");
         if (platform != null) {
             buffer.append(platform);
         }

+ 9 - 7
framework-base/src/main/java/com/chelvc/framework/base/context/Session.java

@@ -35,6 +35,11 @@ public class Session implements Serializable {
      */
     private Long id;
 
+    /**
+     * 使用类别
+     */
+    private Using using;
+
     /**
      * 请求地址
      */
@@ -66,15 +71,10 @@ public class Session implements Serializable {
     private Terminal terminal;
 
     /**
-     * 客户端版本号
+     * 终端版本
      */
     private String version;
 
-    /**
-     * 设备指纹
-     */
-    private String fingerprint;
-
     /**
      * 是否匿名
      */
@@ -94,13 +94,15 @@ public class Session implements Serializable {
      * 初始化会话主体信息
      *
      * @param id          主体标识
+     * @param using       使用类别
      * @param scope       应用范围
      * @param anonymous   是否匿名
      * @param authorities 权限标识集合
      */
-    void initializePrincipal(@NonNull Long id, @NonNull String scope, boolean anonymous,
+    void initializePrincipal(@NonNull Long id, @NonNull Using using, @NonNull String scope, boolean anonymous,
                              @NonNull Set<String> authorities) {
         this.id = id;
+        this.using = using;
         this.scope = scope;
         this.anonymous = anonymous;
         this.authorities = Collections.unmodifiableSet(authorities);

+ 79 - 25
framework-base/src/main/java/com/chelvc/framework/base/context/SessionContextHolder.java

@@ -41,6 +41,11 @@ public class SessionContextHolder implements ServletRequestListener {
      */
     public static final String HEADER_ID = "id";
 
+    /**
+     * 使用类别请求头
+     */
+    public static final String HEADER_USING = "using";
+
     /**
      * 应用范围请求头
      */
@@ -86,11 +91,6 @@ public class SessionContextHolder implements ServletRequestListener {
      */
     public static final String HEADER_TIMESTAMP = "timestamp";
 
-    /**
-     * 设备指纹请求头
-     */
-    public static final String HEADER_FINGERPRINT = "fingerprint";
-
     /**
      * 空会话对象实例
      */
@@ -146,7 +146,19 @@ public class SessionContextHolder implements ServletRequestListener {
      * @return 会话信息
      */
     public static Session setSession(@NonNull Long id, @NonNull String scope) {
-        return setSession(id, scope, false);
+        return setSession(id, Using.NORMAL, scope);
+    }
+
+    /**
+     * 设置会话信息,如果当前已存在会话则更新会话主体信息
+     *
+     * @param id    主体标识
+     * @param using 使用类别
+     * @param scope 应用范围
+     * @return 会话信息
+     */
+    public static Session setSession(@NonNull Long id, @NonNull Using using, @NonNull String scope) {
+        return setSession(id, using, scope, true);
     }
 
     /**
@@ -158,7 +170,20 @@ public class SessionContextHolder implements ServletRequestListener {
      * @return 会话信息
      */
     public static Session setSession(@NonNull Long id, @NonNull String scope, boolean anonymous) {
-        return setSession(id, scope, anonymous, Collections.emptySet());
+        return setSession(id, Using.NORMAL, scope, anonymous);
+    }
+
+    /**
+     * 设置会话信息,如果当前已存在会话则更新会话主体信息
+     *
+     * @param id        主体标识
+     * @param using     使用类别
+     * @param scope     应用范围
+     * @param anonymous 是否匿名
+     * @return 会话信息
+     */
+    public static Session setSession(@NonNull Long id, @NonNull Using using, @NonNull String scope, boolean anonymous) {
+        return setSession(id, using, scope, anonymous, Collections.emptySet());
     }
 
     /**
@@ -170,7 +195,21 @@ public class SessionContextHolder implements ServletRequestListener {
      * @return 会话信息
      */
     public static Session setSession(@NonNull Long id, @NonNull String scope, @NonNull Set<String> authorities) {
-        return setSession(id, scope, false, authorities);
+        return setSession(id, Using.NORMAL, scope, authorities);
+    }
+
+    /**
+     * 设置会话信息,如果当前已存在会话则更新会话主体信息
+     *
+     * @param id          主体标识
+     * @param using       使用类别
+     * @param scope       应用范围
+     * @param authorities 权限标识集合
+     * @return 会话信息
+     */
+    public static Session setSession(@NonNull Long id, @NonNull Using using, @NonNull String scope,
+                                     @NonNull Set<String> authorities) {
+        return setSession(id, using, scope, true, authorities);
     }
 
     /**
@@ -184,6 +223,21 @@ public class SessionContextHolder implements ServletRequestListener {
      */
     public static Session setSession(@NonNull Long id, @NonNull String scope, boolean anonymous,
                                      @NonNull Set<String> authorities) {
+        return setSession(id, Using.NORMAL, scope, anonymous, authorities);
+    }
+
+    /**
+     * 设置会话信息,如果当前已存在会话则更新会话主体信息
+     *
+     * @param id          主体标识
+     * @param using       使用类别
+     * @param scope       应用范围
+     * @param anonymous   是否匿名
+     * @param authorities 权限标识集合
+     * @return 会话信息
+     */
+    public static Session setSession(@NonNull Long id, @NonNull Using using, @NonNull String scope, boolean anonymous,
+                                     @NonNull Set<String> authorities) {
         Deque<Session> deque = SESSION_CONTEXT.get();
         Session session = deque.peek();
         if (session == null || session == EMPTY_SESSION) {
@@ -195,13 +249,13 @@ public class SessionContextHolder implements ServletRequestListener {
                     .timestamp(System.currentTimeMillis()).build();
             deque.push(session);
         } else {
-            session.initializePrincipal(id, scope, anonymous, authorities);
+            session.initializePrincipal(id, using, scope, anonymous, authorities);
         }
         return session;
     }
 
     /**
-     * 获取当前用户ID
+     * 获取用户ID
      *
      * @return 用户ID
      */
@@ -210,7 +264,16 @@ public class SessionContextHolder implements ServletRequestListener {
     }
 
     /**
-     * 获取当前会话请求地址
+     * 获取使用类别
+     *
+     * @return 使用类别
+     */
+    public static Using getUsing() {
+        return ObjectUtils.ifNull(getSession(false), Session::getUsing);
+    }
+
+    /**
+     * 获取请求地址
      *
      * @return 请求地址
      */
@@ -228,7 +291,7 @@ public class SessionContextHolder implements ServletRequestListener {
     }
 
     /**
-     * 获取当前会话设备标识
+     * 获取设备标识
      *
      * @return 设备标识
      */
@@ -237,7 +300,7 @@ public class SessionContextHolder implements ServletRequestListener {
     }
 
     /**
-     * 获取当前会话渠道来源
+     * 获取渠道来源
      *
      * @return 渠道来源
      */
@@ -246,7 +309,7 @@ public class SessionContextHolder implements ServletRequestListener {
     }
 
     /**
-     * 获取当前会话平台信息
+     * 获取平台信息
      *
      * @return 平台信息
      */
@@ -255,7 +318,7 @@ public class SessionContextHolder implements ServletRequestListener {
     }
 
     /**
-     * 获取当前会话终端信息
+     * 获取终端信息
      *
      * @return 终端信息
      */
@@ -264,7 +327,7 @@ public class SessionContextHolder implements ServletRequestListener {
     }
 
     /**
-     * 获取当前会话版本信息
+     * 获取版本信息
      *
      * @return 版本信息
      */
@@ -291,15 +354,6 @@ public class SessionContextHolder implements ServletRequestListener {
         return ObjectUtils.ifNull(getSession(false), Session::getTimestamp);
     }
 
-    /**
-     * 获取设备指纹
-     *
-     * @return 设备指纹
-     */
-    public static String getFingerprint() {
-        return ObjectUtils.ifNull(getSession(false), Session::getFingerprint);
-    }
-
     /**
      * 获取当前请求对象
      *

+ 37 - 0
framework-base/src/main/java/com/chelvc/framework/base/context/Using.java

@@ -0,0 +1,37 @@
+package com.chelvc.framework.base.context;
+
+import com.chelvc.framework.common.model.Enumerable;
+import lombok.Getter;
+
+/**
+ * 使用类别枚举
+ *
+ * @author Woody
+ * @date 2023/9/10
+ */
+@Getter
+public enum Using implements Enumerable {
+    /**
+     * 历史首次使用
+     */
+    NEWLY("历史首次使用"),
+
+    /**
+     * 当日首次使用
+     */
+    DAILY("当日首次使用"),
+
+    /**
+     * 常规使用
+     */
+    NORMAL("常规使用");
+
+    /**
+     * 类别描述
+     */
+    private final String description;
+
+    Using(String description) {
+        this.description = description;
+    }
+}

+ 1 - 1
framework-boot/pom.xml

@@ -69,7 +69,7 @@
                 <artifactId>apidoc-maven-plugin</artifactId>
                 <version>${apidoc-maven-plugin.version}</version>
                 <configuration>
-                    <includeHeaders>{String} device 设备标识,{String} channel 渠道来源,{String} platform 平台标识: PC(PC)、IOS(苹果)、ANDROID(安卓),{String} terminal 终端标识: WEB(Web)、APP(App)、APPLET(小程序),{String} version 终端版本,{Long} timestamp 请求时间戳,{String} signature 签名信息,{String} fingerprint 设备指纹,{String} authorization 认证信息</includeHeaders>
+                    <includeHeaders>{String} device 设备标识,{String} channel 渠道来源,{String} platform 平台标识: PC(PC)、IOS(苹果)、ANDROID(安卓),{String} terminal 终端标识: WEB(Web)、APP(App)、APPLET(小程序),{String} version 终端版本,{Long} timestamp 请求时间戳,{String} signature 签名信息,{String} authorization 认证信息</includeHeaders>
                     <includeGroupIdentities>com.chelvc.framework</includeGroupIdentities>
                     <excludeClasses>com.chelvc.framework.base.interceptor.GlobalExceptionInterceptor</excludeClasses>
                     <analyserFactoryClass>com.chelvc.framework.base.apidoc.MethodAnalyserFactory</analyserFactoryClass>

+ 20 - 0
framework-common/src/main/java/com/chelvc/framework/common/util/DateUtils.java

@@ -1322,4 +1322,24 @@ public final class DateUtils {
     public static long timestamp(long timestamp, boolean millis) {
         return millis ? timestamp : seconds(timestamp) * 1000;
     }
+
+    /**
+     * 获取当前时间距过期时间的时长
+     *
+     * @param expiration 过期时间
+     * @return 持续时长
+     */
+    public static Duration duration(@NonNull Date expiration) {
+        return duration(expiration.getTime());
+    }
+
+    /**
+     * 获取当前时间距过期时间的时长
+     *
+     * @param expiration 过期时间戳
+     * @return 持续时长
+     */
+    public static Duration duration(@NonNull long expiration) {
+        return Duration.ofMillis(Math.max(expiration - System.currentTimeMillis(), 1000));
+    }
 }

+ 58 - 33
framework-common/src/main/java/com/chelvc/framework/common/util/ObjectUtils.java

@@ -12,6 +12,7 @@ import java.lang.reflect.GenericArrayType;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Collection;
@@ -934,13 +935,13 @@ public final class ObjectUtils {
      * @param type 对象类型
      * @return 对象类型
      */
-    public static Class<?> type2class(java.lang.reflect.Type type) {
+    public static Class<?> type2class(Type type) {
         if (type instanceof Class) {
             return (Class<?>) type;
         } else if (type instanceof ParameterizedType) {
             return (Class<?>) ((ParameterizedType) type).getRawType();
         } else if (type instanceof GenericArrayType) {
-            java.lang.reflect.Type component = ((GenericArrayType) type).getGenericComponentType();
+            Type component = ((GenericArrayType) type).getGenericComponentType();
             if (component instanceof Class) {
                 return Array.newInstance((Class<?>) component, 0).getClass();
             }
@@ -1003,14 +1004,14 @@ public final class ObjectUtils {
      * @param type 对象类型
      * @return true/false
      */
-    public static boolean isOnlyMap(java.lang.reflect.Type type) {
+    public static boolean isOnlyMap(Type type) {
         if (type == Map.class) {
             return true;
         } else if (type instanceof ParameterizedType) {
             ParameterizedType parameterized = (ParameterizedType) type;
             if (parameterized.getRawType() == Map.class) {
-                java.lang.reflect.Type[] args = parameterized.getActualTypeArguments();
-                java.lang.reflect.Type arg1 = args[0], arg2 = args[1];
+                Type[] args = parameterized.getActualTypeArguments();
+                Type arg1 = args[0], arg2 = args[1];
                 return !(arg1 instanceof Class || arg1 instanceof ParameterizedType)
                         && !(arg2 instanceof Class || arg2 instanceof ParameterizedType);
             }
@@ -1024,13 +1025,13 @@ public final class ObjectUtils {
      * @param type 对象类型
      * @return true/false
      */
-    public static boolean isOnlySet(java.lang.reflect.Type type) {
+    public static boolean isOnlySet(Type type) {
         if (type == Set.class) {
             return true;
         } else if (type instanceof ParameterizedType) {
             ParameterizedType parameterized = (ParameterizedType) type;
             if (parameterized.getRawType() == Set.class) {
-                java.lang.reflect.Type arg = parameterized.getActualTypeArguments()[0];
+                Type arg = parameterized.getActualTypeArguments()[0];
                 return !(arg instanceof Class || arg instanceof ParameterizedType);
             }
         }
@@ -1043,13 +1044,13 @@ public final class ObjectUtils {
      * @param type 对象类型
      * @return true/false
      */
-    public static boolean isOnlyList(java.lang.reflect.Type type) {
+    public static boolean isOnlyList(Type type) {
         if (type == List.class) {
             return true;
         } else if (type instanceof ParameterizedType) {
             ParameterizedType parameterized = (ParameterizedType) type;
             if (parameterized.getRawType() == List.class) {
-                java.lang.reflect.Type arg = parameterized.getActualTypeArguments()[0];
+                Type arg = parameterized.getActualTypeArguments()[0];
                 return !(arg instanceof Class || arg instanceof ParameterizedType);
             }
         }
@@ -1062,13 +1063,13 @@ public final class ObjectUtils {
      * @param type 对象类型
      * @return true/false
      */
-    public static boolean isOnlyCollection(java.lang.reflect.Type type) {
+    public static boolean isOnlyCollection(Type type) {
         if (type == Collection.class) {
             return true;
         } else if (type instanceof ParameterizedType) {
             ParameterizedType parameterized = (ParameterizedType) type;
             if (parameterized.getRawType() == Collection.class) {
-                java.lang.reflect.Type arg = parameterized.getActualTypeArguments()[0];
+                Type arg = parameterized.getActualTypeArguments()[0];
                 return !(arg instanceof Class || arg instanceof ParameterizedType);
             }
         }
@@ -1081,14 +1082,14 @@ public final class ObjectUtils {
      * @param type 对象类型实例
      * @return 对象类型解析字符串
      */
-    public static String analyse(@NonNull java.lang.reflect.Type type) {
+    public static String analyse(@NonNull Type type) {
         if (type instanceof ParameterizedType) {
             ParameterizedType parameterized = (ParameterizedType) type;
             StringBuilder buffer = new StringBuilder();
             buffer.append(TypeUtils.class.getName()).append(".parameterize(");
             buffer.append(analyse(parameterized.getRawType()));
-            java.lang.reflect.Type[] args = parameterized.getActualTypeArguments();
-            buffer.append(", new ").append(java.lang.reflect.Type.class.getName()).append("[]{");
+            Type[] args = parameterized.getActualTypeArguments();
+            buffer.append(", new ").append(Type.class.getName()).append("[]{");
             if (notEmpty(args)) {
                 for (int i = 0; i < args.length; i++) {
                     if (i > 0) {
@@ -1108,9 +1109,9 @@ public final class ObjectUtils {
      * @param clazz 对象类型
      * @return 父类/接口类型列表
      */
-    public static List<java.lang.reflect.Type> getGenericSuperclasses(@NonNull Class<?> clazz) {
-        java.lang.reflect.Type superclass = clazz.getGenericSuperclass();
-        java.lang.reflect.Type[] interfaces = clazz.getGenericInterfaces();
+    public static List<Type> getGenericSuperclasses(@NonNull Class<?> clazz) {
+        Type superclass = clazz.getGenericSuperclass();
+        Type[] interfaces = clazz.getGenericInterfaces();
         if (superclass == null && isEmpty(interfaces)) {
             return Collections.emptyList();
         } else if (superclass == null) {
@@ -1118,7 +1119,7 @@ public final class ObjectUtils {
         } else if (isEmpty(interfaces)) {
             return Collections.singletonList(superclass);
         }
-        List<java.lang.reflect.Type> superclasses = Lists.newArrayList(interfaces);
+        List<Type> superclasses = Lists.newArrayList(interfaces);
         superclasses.add(superclass);
         return superclasses;
     }
@@ -1130,19 +1131,18 @@ public final class ObjectUtils {
      * @param superclass 父类/接口类型
      * @return 父类/接口类型
      */
-    public static java.lang.reflect.Type lookupGenericSuperclass(@NonNull Class<?> clazz,
-                                                                 @NonNull Class<?> superclass) {
+    public static Type lookupGenericSuperclass(@NonNull Class<?> clazz, @NonNull Class<?> superclass) {
         if (clazz == superclass) {
             return clazz;
         }
-        for (java.lang.reflect.Type type : getGenericSuperclasses(clazz)) {
+        for (Type type : getGenericSuperclasses(clazz)) {
             Class<?> classed = type2class(type);
             if (classed == null) {
                 continue;
             } else if (Objects.equals(classed, superclass)) {
                 return type;
             }
-            java.lang.reflect.Type found = lookupGenericSuperclass(classed, superclass);
+            Type found = lookupGenericSuperclass(classed, superclass);
             if (found != null) {
                 return found;
             }
@@ -1157,8 +1157,7 @@ public final class ObjectUtils {
      * @param superclass 父类/接口类型
      * @return 泛型类型
      */
-    public static java.lang.reflect.Type lookupSuperclassParameterized(@NonNull Class<?> clazz,
-                                                                       @NonNull Class<?> superclass) {
+    public static Type lookupSuperclassParameterized(@NonNull Class<?> clazz, @NonNull Class<?> superclass) {
         return lookupSuperclassParameterized(clazz, superclass, null);
     }
 
@@ -1170,10 +1169,9 @@ public final class ObjectUtils {
      * @param defaultType 默认类型
      * @return 泛型类型
      */
-    public static java.lang.reflect.Type lookupSuperclassParameterized(@NonNull Class<?> clazz,
-                                                                       @NonNull Class<?> superclass,
-                                                                       java.lang.reflect.Type defaultType) {
-        java.lang.reflect.Type[] types = lookupSuperclassParameterizes(clazz, superclass);
+    public static Type lookupSuperclassParameterized(@NonNull Class<?> clazz, @NonNull Class<?> superclass,
+                                                     Type defaultType) {
+        Type[] types = lookupSuperclassParameterizes(clazz, superclass);
         return isEmpty(types) ? defaultType : types[0];
     }
 
@@ -1184,17 +1182,16 @@ public final class ObjectUtils {
      * @param superclass 父类/接口类型
      * @return 泛型类型数组
      */
-    public static java.lang.reflect.Type[] lookupSuperclassParameterizes(@NonNull Class<?> clazz,
-                                                                         @NonNull Class<?> superclass) {
-        for (java.lang.reflect.Type type : getGenericSuperclasses(clazz)) {
+    public static Type[] lookupSuperclassParameterizes(@NonNull Class<?> clazz, @NonNull Class<?> superclass) {
+        for (Type type : getGenericSuperclasses(clazz)) {
             if (Objects.equals(type2class(type), superclass)) {
                 if (type instanceof ParameterizedType) {
                     return ((ParameterizedType) type).getActualTypeArguments();
                 }
-                return new java.lang.reflect.Type[0];
+                return new Type[0];
             }
         }
-        return new java.lang.reflect.Type[0];
+        return new Type[0];
     }
 
     /**
@@ -1499,4 +1496,32 @@ public final class ObjectUtils {
     public static int size(Collection<?> collection) {
         return collection == null ? 0 : collection.size();
     }
+
+    /**
+     * 获取列表指定下标元素
+     *
+     * @param list  元素列表
+     * @param index 元素下标
+     * @param <T>   元素类型
+     * @return 元素
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T get(List<?> list, int index) {
+        return (T) get(list, index, value -> value);
+    }
+
+    /**
+     * 获取列表指定下标元素
+     *
+     * @param list     元素列表
+     * @param index    元素下标
+     * @param function 对象转换回调函数
+     * @param <T>      元素类型
+     * @param <R>      返回类型
+     * @return 元素
+     */
+    public static <T, R> R get(List<T> list, int index, @NonNull Function<T, R> function) {
+        AssertUtils.check(index > -1, () -> "index must be greater than -1");
+        return size(list) > index ? ifNull(list.get(index), function) : null;
+    }
 }

+ 0 - 12
framework-common/src/main/java/com/chelvc/framework/common/util/StringUtils.java

@@ -228,18 +228,6 @@ public final class StringUtils {
         return isEmpty(first) ? second : first;
     }
 
-    /**
-     * 比较两个字符串中第一个是否为空,如果是则返回第二个字符串,否则返回第一个字符串
-     *
-     * @param first  第一个字符串
-     * @param second 第二个字符串提供者
-     * @param <T>    字符串类型
-     * @return 非空字符串
-     */
-    public static <T> T ifEmpty(T first, @NonNull Supplier<T> second) {
-        return isEmpty(first) ? second.get() : first;
-    }
-
     /**
      * 如果原始字符串不为空,则对原始字符串做对象适配
      *

+ 4 - 4
framework-database/src/main/java/com/chelvc/framework/database/config/TypeHandlerConfigurer.java

@@ -474,10 +474,10 @@ public class TypeHandlerConfigurer extends MybatisConfigurer {
         if (ObjectUtils.notEmpty(fields)) {
             fields.forEach((name, field) -> {
                 String tag = field.isAnnotationPresent(TableId.class) ? "id" : "result";
-                String column = StringUtils.ifEmpty(
-                        ObjectUtils.ifNull(field.getAnnotation(TableField.class), TableField::value),
-                        () -> StringUtils.hump2underscore(field.getName())
-                );
+                String column = ObjectUtils.ifNull(field.getAnnotation(TableField.class), TableField::value);
+                if (StringUtils.isEmpty(column)) {
+                    column = StringUtils.hump2underscore(field.getName());
+                }
                 Element child = document.createElement(tag);
                 child.setAttribute("column", column);
                 child.setAttribute("property", field.getName());

+ 3 - 1
framework-feign/src/main/java/com/chelvc/framework/feign/interceptor/FeignInvokeInterceptor.java

@@ -3,6 +3,7 @@ package com.chelvc.framework.feign.interceptor;
 import com.chelvc.framework.base.context.ApplicationContextHolder;
 import com.chelvc.framework.base.context.Session;
 import com.chelvc.framework.base.context.SessionContextHolder;
+import com.chelvc.framework.base.context.Using;
 import com.chelvc.framework.base.util.HttpUtils;
 import com.chelvc.framework.common.model.Platform;
 import com.chelvc.framework.common.model.Terminal;
@@ -41,6 +42,8 @@ public class FeignInvokeInterceptor implements RequestInterceptor {
             template.header("x-real-ip", session.getHost());
             template.header(SessionContextHolder.HEADER_ID,
                     (String) ObjectUtils.ifNull(session.getId(), String::valueOf));
+            template.header(SessionContextHolder.HEADER_USING,
+                    ObjectUtils.ifNull(session.getUsing(), Using::name));
             template.header(SessionContextHolder.HEADER_SCOPE, session.getScope());
             template.header(SessionContextHolder.HEADER_DEVICE, session.getDevice());
             template.header(SessionContextHolder.HEADER_CHANNEL, session.getChannel());
@@ -49,7 +52,6 @@ public class FeignInvokeInterceptor implements RequestInterceptor {
             template.header(SessionContextHolder.HEADER_TERMINAL,
                     ObjectUtils.ifNull(session.getTerminal(), Terminal::name));
             template.header(SessionContextHolder.HEADER_VERSION, session.getVersion());
-            template.header(SessionContextHolder.HEADER_FINGERPRINT, session.getFingerprint());
             template.header(SessionContextHolder.HEADER_ANONYMOUS, String.valueOf(session.isAnonymous()));
             template.header(SessionContextHolder.HEADER_AUTHORITIES, session.getAuthorities());
             template.header(SessionContextHolder.HEADER_TIMESTAMP,

+ 15 - 12
framework-oauth/src/main/java/com/chelvc/framework/oauth/config/OAuthConfigurer.java

@@ -1,6 +1,7 @@
 package com.chelvc.framework.oauth.config;
 
 import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -20,14 +21,15 @@ import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.oauth.annotation.Authorize;
 import com.chelvc.framework.oauth.context.OAuthContextHolder;
+import com.chelvc.framework.oauth.token.DefaultSessionValidator;
+import com.chelvc.framework.oauth.token.SessionValidator;
 import com.chelvc.framework.oauth.token.TimestampTokenValidator;
-import com.chelvc.framework.oauth.token.TokenValidator;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.aop.framework.AopProxyUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.core.io.Resource;
@@ -44,7 +46,6 @@ import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
-import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
@@ -71,6 +72,12 @@ public class OAuthConfigurer extends WebSecurityConfigurerAdapter {
     @Autowired(required = false)
     private LogoutHandler logoutHandler;
 
+    @Bean
+    @ConditionalOnMissingBean(SessionValidator.class)
+    public SessionValidator sessionValidator() {
+        return new DefaultSessionValidator();
+    }
+
     @Bean
     public JwtDecoder jwtDecoder() {
         // 构建JWT解码器实例
@@ -79,14 +86,10 @@ public class OAuthConfigurer extends WebSecurityConfigurerAdapter {
         NimbusJwtDecoder decoder = NimbusJwtDecoder.withSecretKey(key).build();
 
         // 添加自定义令牌验证器
-        Collection<OAuth2TokenValidator<Jwt>> validators = Lists.newLinkedList();
-        validators.add(new TimestampTokenValidator());
-        validators.addAll(ApplicationContextHolder.getOrderBeans(this.applicationContext, TokenValidator.class));
-        validators.add(jwt -> {
-            // 初始化会话主体信息
-            OAuthContextHolder.initializeSessionPrincipal(jwt);
-            return OAuth2TokenValidatorResult.success();
-        });
+        Collection<OAuth2TokenValidator<Jwt>> validators = Arrays.asList(
+                new TimestampTokenValidator(),
+                this.applicationContext.getBean(SessionValidator.class)
+        );
         decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));
         return decoder;
     }
@@ -123,7 +126,7 @@ public class OAuthConfigurer extends WebSecurityConfigurerAdapter {
         JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
         JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
         jwtGrantedAuthoritiesConverter.setAuthorityPrefix(StringUtils.EMPTY);
-        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(OAuthContextHolder.JWT_CLAIM_AUTHORITIES);
+        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(OAuthContextHolder.AUTHORITIES);
         jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
         return jwtAuthenticationConverter;
     }

+ 73 - 35
framework-oauth/src/main/java/com/chelvc/framework/oauth/context/OAuthContextHolder.java

@@ -8,8 +8,6 @@ import java.util.Objects;
 import java.util.Set;
 import javax.servlet.http.HttpServletRequest;
 
-import com.chelvc.framework.base.context.Session;
-import com.chelvc.framework.base.context.SessionContextHolder;
 import com.chelvc.framework.common.function.Adapter;
 import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
@@ -31,29 +29,39 @@ import org.springframework.security.oauth2.server.resource.web.DefaultBearerToke
  */
 public final class OAuthContextHolder {
     /**
-     * Jwt唯一标识
+     * 令牌唯一标识
      */
-    public static final String JWT_CLAIM_JTI = "jti";
+    public static final String JTI = "jti";
 
     /**
-     * Jwt应用范围
+     * 应用范围标识
      */
-    public static final String JWT_CLAIM_SCOPE = "scope";
+    public static final String SCOPE = "scope";
 
     /**
-     * Jwt用户账号标识
+     * 客户端登陆标识
      */
-    public static final String JWT_CLAIM_USERNAME = "user_name";
+    public static final String CLIENT = "client";
 
     /**
-     * Jwt是否匿名
+     * 用户账号标识
      */
-    public static final String JWT_CLAIM_ANONYMOUS = "anonymous";
+    public static final String USERNAME = "user_name";
 
     /**
-     * Jwt用户权限标识
+     * 是否匿名标识
      */
-    public static final String JWT_CLAIM_AUTHORITIES = "authorities";
+    public static final String ANONYMOUS = "anonymous";
+
+    /**
+     * 用户权限标识
+     */
+    public static final String AUTHORITIES = "authorities";
+
+    /**
+     * 主体注册时间戳
+     */
+    public static final String REGISTERING = "registering";
 
     /**
      * Bearer令牌解析器
@@ -147,7 +155,7 @@ public final class OAuthContextHolder {
      * @return 主体身份标识
      */
     public static Long getId(Jwt jwt) {
-        return jwt == null ? null : StringUtils.ifEmpty(jwt.getClaimAsString(JWT_CLAIM_USERNAME), Long::parseLong);
+        return jwt == null ? null : StringUtils.ifEmpty(jwt.getClaimAsString(USERNAME), Long::parseLong);
     }
 
     /**
@@ -157,9 +165,7 @@ public final class OAuthContextHolder {
      * @return 主体身份标识
      */
     public static Long getId(JWT jwt) {
-        return getJwtClaim(jwt, claims -> StringUtils.ifEmpty(
-                claims.getStringClaim(JWT_CLAIM_USERNAME), Long::parseLong
-        ));
+        return getJwtClaim(jwt, claims -> StringUtils.ifEmpty(claims.getStringClaim(USERNAME), Long::parseLong));
     }
 
     /**
@@ -169,7 +175,7 @@ public final class OAuthContextHolder {
      * @return JWT身份标识
      */
     public static String getJti(Jwt jwt) {
-        return jwt == null ? null : StringUtils.ifEmpty(jwt.getClaimAsString(JWT_CLAIM_JTI), (String) null);
+        return jwt == null ? null : StringUtils.ifEmpty(jwt.getClaimAsString(JTI), (String) null);
     }
 
     /**
@@ -179,7 +185,7 @@ public final class OAuthContextHolder {
      * @return JWT身份标识
      */
     public static String getJti(JWT jwt) {
-        return StringUtils.ifEmpty(getJwtClaim(jwt, claims -> claims.getStringClaim(JWT_CLAIM_JTI)), (String) null);
+        return StringUtils.ifEmpty(getJwtClaim(jwt, claims -> claims.getStringClaim(JTI)), (String) null);
     }
 
     /**
@@ -189,7 +195,7 @@ public final class OAuthContextHolder {
      * @return 应用范围
      */
     public static String getScope(Jwt jwt) {
-        return jwt == null ? null : StringUtils.ifEmpty(jwt.getClaimAsString(JWT_CLAIM_SCOPE), (String) null);
+        return jwt == null ? null : StringUtils.ifEmpty(jwt.getClaimAsString(SCOPE), (String) null);
     }
 
     /**
@@ -199,7 +205,7 @@ public final class OAuthContextHolder {
      * @return 应用范围
      */
     public static String getScope(JWT jwt) {
-        return StringUtils.ifEmpty(getJwtClaim(jwt, claims -> claims.getStringClaim(JWT_CLAIM_SCOPE)), (String) null);
+        return StringUtils.ifEmpty(getJwtClaim(jwt, claims -> claims.getStringClaim(SCOPE)), (String) null);
     }
 
     /**
@@ -210,7 +216,38 @@ public final class OAuthContextHolder {
      */
     public static String getScope(OAuth2AccessToken token) {
         Map<String, Object> additions = ObjectUtils.ifNull(token, OAuth2AccessToken::getAdditionalInformation);
-        return additions == null ? null : StringUtils.ifEmpty(additions.get(JWT_CLAIM_SCOPE), String::valueOf);
+        return additions == null ? null : StringUtils.ifEmpty(additions.get(SCOPE), String::valueOf);
+    }
+
+    /**
+     * 是否是客户端登陆
+     *
+     * @param jwt Jwt对象
+     * @return true/false
+     */
+    public static boolean isClient(Jwt jwt) {
+        return jwt != null && Boolean.TRUE.equals(jwt.getClaimAsBoolean(CLIENT));
+    }
+
+    /**
+     * 是否是客户端登陆
+     *
+     * @param jwt JWT对象
+     * @return true/false
+     */
+    public static boolean isClient(JWT jwt) {
+        return Boolean.TRUE.equals(getJwtClaim(jwt, claims -> claims.getBooleanClaim(CLIENT)));
+    }
+
+    /**
+     * 是否是客户端登陆
+     *
+     * @param token OAuth认证令牌对象
+     * @return true/false
+     */
+    public static boolean isClient(OAuth2AccessToken token) {
+        Map<String, Object> additions = ObjectUtils.ifNull(token, OAuth2AccessToken::getAdditionalInformation);
+        return additions != null && Boolean.TRUE.equals(additions.get(CLIENT));
     }
 
     /**
@@ -220,7 +257,7 @@ public final class OAuthContextHolder {
      * @return true/false
      */
     public static boolean isAnonymous(Jwt jwt) {
-        Boolean anonymous = jwt == null ? null : jwt.getClaimAsBoolean(JWT_CLAIM_ANONYMOUS);
+        Boolean anonymous = jwt == null ? null : jwt.getClaimAsBoolean(ANONYMOUS);
         return anonymous == null || Boolean.TRUE.equals(anonymous);
     }
 
@@ -231,10 +268,21 @@ public final class OAuthContextHolder {
      * @return true/false
      */
     public static boolean isAnonymous(JWT jwt) {
-        Boolean anonymous = getJwtClaim(jwt, claims -> claims.getBooleanClaim(JWT_CLAIM_ANONYMOUS));
+        Boolean anonymous = getJwtClaim(jwt, claims -> claims.getBooleanClaim(ANONYMOUS));
         return anonymous == null || Boolean.TRUE.equals(anonymous);
     }
 
+    /**
+     * 是否是匿名用户
+     *
+     * @param token OAuth认证令牌对象
+     * @return true/false
+     */
+    public static boolean isAnonymous(OAuth2AccessToken token) {
+        Map<String, Object> additions = ObjectUtils.ifNull(token, OAuth2AccessToken::getAdditionalInformation);
+        return additions == null || Boolean.TRUE.equals(additions.get(ANONYMOUS));
+    }
+
     /**
      * 获取用户授权信息
      *
@@ -245,7 +293,7 @@ public final class OAuthContextHolder {
         if (jwt == null) {
             return Collections.emptySet();
         }
-        List<String> authorities = jwt.getClaimAsStringList(JWT_CLAIM_AUTHORITIES);
+        List<String> authorities = jwt.getClaimAsStringList(AUTHORITIES);
         return ObjectUtils.isEmpty(authorities) ? Collections.emptySet() : Sets.newHashSet(authorities);
     }
 
@@ -256,17 +304,7 @@ public final class OAuthContextHolder {
      * @return 授权信息集合
      */
     public static Set<String> getAuthorities(JWT jwt) {
-        String[] authorities = getJwtClaim(jwt, claims -> claims.getStringArrayClaim(JWT_CLAIM_AUTHORITIES));
+        String[] authorities = getJwtClaim(jwt, claims -> claims.getStringArrayClaim(AUTHORITIES));
         return ObjectUtils.isEmpty(authorities) ? Collections.emptySet() : Sets.newHashSet(authorities);
     }
-
-    /**
-     * 初始化会话主体信息
-     *
-     * @param jwt JWT对象
-     * @return 会话信息
-     */
-    public static Session initializeSessionPrincipal(@NonNull Jwt jwt) {
-        return SessionContextHolder.setSession(getId(jwt), getScope(jwt), isAnonymous(jwt), getAuthorities(jwt));
-    }
 }

+ 27 - 0
framework-oauth/src/main/java/com/chelvc/framework/oauth/token/DefaultSessionValidator.java

@@ -0,0 +1,27 @@
+package com.chelvc.framework.oauth.token;
+
+import com.chelvc.framework.base.context.SessionContextHolder;
+import com.chelvc.framework.oauth.context.OAuthContextHolder;
+import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+/**
+ * 会话验证器默认实现
+ *
+ * @author Woody
+ * @date 2024/6/29
+ */
+public class DefaultSessionValidator implements SessionValidator {
+    @Override
+    public OAuth2TokenValidatorResult validate(Jwt jwt) {
+        if (!OAuthContextHolder.isClient(jwt)) {
+            SessionContextHolder.setSession(
+                    OAuthContextHolder.getId(jwt),
+                    OAuthContextHolder.getScope(jwt),
+                    OAuthContextHolder.isAnonymous(jwt),
+                    OAuthContextHolder.getAuthorities(jwt)
+            );
+        }
+        return OAuth2TokenValidatorResult.success();
+    }
+}

+ 38 - 24
framework-oauth/src/main/java/com/chelvc/framework/oauth/token/RedisTokenValidator.java → framework-oauth/src/main/java/com/chelvc/framework/oauth/token/RedisSessionValidator.java

@@ -1,22 +1,24 @@
 package com.chelvc.framework.oauth.token;
 
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 
 import com.chelvc.framework.base.context.ApplicationContextHolder;
 import com.chelvc.framework.base.context.SessionContextHolder;
+import com.chelvc.framework.base.context.Using;
+import com.chelvc.framework.common.util.DateUtils;
 import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.oauth.config.OAuthProperties;
 import com.chelvc.framework.oauth.context.OAuthContextHolder;
 import com.chelvc.framework.redis.context.RedisContextHolder;
+import com.chelvc.framework.redis.context.RedisHashHolder;
+import com.chelvc.framework.redis.context.RedisUserDailyHashHolder;
 import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
@@ -29,31 +31,31 @@ import org.springframework.stereotype.Component;
  * @author Woody
  * @date 2024/1/30
  */
-@Slf4j
 @Component
 @ConditionalOnClass(RedisContextHolder.class)
-@ConditionalOnProperty(value = "oauth.redis.enabled", havingValue = "true")
+@ConditionalOnProperty(value = "oauth.redis.enabled", havingValue = "true", matchIfMissing = true)
 @RequiredArgsConstructor(onConstructor = @__(@Autowired))
-public class RedisTokenValidator implements TokenValidator {
+public class RedisSessionValidator implements SessionValidator {
     private final OAuthProperties properties;
 
     @Override
     public OAuth2TokenValidatorResult validate(Jwt jwt) {
-        // 基于Redis令牌有效性校验
-        String key = OAuthContextHolder.key(OAuthContextHolder.getId(jwt));
-        Collection<Object> fields = Arrays.asList(
-                SessionContextHolder.HEADER_SCOPE,
-                String.valueOf(SessionContextHolder.getTerminal())
-        );
-        List<?> values;
-        try {
-            values = RedisContextHolder.getDefaultTemplate().opsForHash().multiGet(key, fields);
-        } catch (Exception e) {
-            log.warn("Get token from redis failed: {}", e.getMessage());
+        if (OAuthContextHolder.isClient(jwt)) {
             return OAuth2TokenValidatorResult.success();
         }
-        String scope = ObjectUtils.size(values) > 0 ? (String) StringUtils.ifEmpty(values.get(0), (String) null) : null;
-        String token = ObjectUtils.size(values) > 1 ? (String) values.get(1) : null;
+
+        // 从Redis获取令牌信息
+        Long id = OAuthContextHolder.getId(jwt);
+        String terminal = String.valueOf(SessionContextHolder.getTerminal());
+        RedisTemplate<String, Object> template = RedisContextHolder.getDefaultTemplate();
+        List<?> values = RedisHashHolder.get(
+                template, OAuthContextHolder.key(id),
+                terminal, OAuthContextHolder.SCOPE, OAuthContextHolder.REGISTERING
+        );
+
+        // 校验令牌有效性及应用范围
+        String token = ObjectUtils.get(values, 0);
+        String scope = OAuthContextHolder.getScope(jwt);
         if (StringUtils.isEmpty(token)) {
             throw new OAuth2AuthenticationException(new OAuth2Error(
                     "TOKEN_EXPIRED", ApplicationContextHolder.getMessage("Token.Expired"), null
@@ -63,13 +65,25 @@ public class RedisTokenValidator implements TokenValidator {
             throw new OAuth2AuthenticationException(new OAuth2Error(
                     "TOKEN_CHANGED", ApplicationContextHolder.getMessage("Token.Changed"), null
             ));
-        } else if (this.properties.isScoped() && !Objects.equals(scope, OAuthContextHolder.getScope(jwt))) {
+        } else if (this.properties.isScoped()) {
             // 判断应用范围是否相同,如果不同则表示应用范围已被重置,需要刷新令牌
-            String arg = StringUtils.isEmpty(scope) ? StringUtils.EMPTY :
-                    ApplicationContextHolder.getMessage(scope);
-            String message = ApplicationContextHolder.getMessage("Scope.Changed", new Object[]{arg});
-            throw new OAuth2AuthenticationException(new OAuth2Error("SCOPE_CHANGED", message, null));
+            String real = ObjectUtils.get(values, 1);
+            if (!Objects.equals(scope, real)) {
+                String arg = StringUtils.ifEmpty(real, ApplicationContextHolder::getMessage);
+                String message = ApplicationContextHolder.getMessage("Scope.Changed", new Object[]{arg});
+                throw new OAuth2AuthenticationException(new OAuth2Error("SCOPE_CHANGED", message, null));
+            }
         }
+
+        // 更新会话主体信息
+        long usage = RedisUserDailyHashHolder.increment(template, id, "usage");
+        long registering = ObjectUtils.ifNull(ObjectUtils.get(values, 2), 0L);
+        Using using = usage > 1 ? Using.NORMAL : registering >= DateUtils.today().getTime() ? Using.NEWLY : Using.DAILY;
+        SessionContextHolder.setSession(
+                id, using, scope,
+                OAuthContextHolder.isAnonymous(jwt),
+                OAuthContextHolder.getAuthorities(jwt)
+        );
         return OAuth2TokenValidatorResult.success();
     }
 }

+ 0 - 71
framework-oauth/src/main/java/com/chelvc/framework/oauth/token/RedisTokenStore.java

@@ -1,71 +0,0 @@
-package com.chelvc.framework.oauth.token;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import com.chelvc.framework.base.context.SessionContextHolder;
-import com.chelvc.framework.common.model.Terminal;
-import com.chelvc.framework.common.util.ObjectUtils;
-import com.chelvc.framework.common.util.StringUtils;
-import com.chelvc.framework.oauth.context.OAuthContextHolder;
-import com.chelvc.framework.redis.context.RedisContextHolder;
-import lombok.NonNull;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.data.redis.core.script.DefaultRedisScript;
-import org.springframework.data.redis.core.script.RedisScript;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.oauth2.common.OAuth2AccessToken;
-import org.springframework.security.oauth2.provider.OAuth2Authentication;
-import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
-import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
-
-/**
- * 基于Redis令牌管理器实现
- *
- * @author Woody
- * @date 2024/1/30
- */
-@Slf4j
-public class RedisTokenStore extends JwtTokenStore {
-    /**
-     * 终端/令牌更新脚本映射表
-     */
-    private final Map<Terminal, RedisScript<Boolean>> scripts;
-
-    public RedisTokenStore(@NonNull JwtAccessTokenConverter jwtAccessTokenConverter) {
-        super(jwtAccessTokenConverter);
-
-        // 初始化各终端令牌更新脚本映射表
-        this.scripts = Stream.of(Terminal.values()).collect(Collectors.toMap(terminal -> terminal,
-                terminal -> new DefaultRedisScript<>(
-                        String.format("return redis.call('HSET', KEYS[1], 'scope', ARGV[1], '%s', ARGV[2]) " +
-                                "and redis.call('EXPIRE', KEYS[1], ARGV[3])", terminal), Boolean.class
-                )));
-
-        // 初始化无终端令牌更新脚本
-        this.scripts.put(null, new DefaultRedisScript<>(
-                "return redis.call('HSET', KEYS[1], 'scope', ARGV[1], 'null', ARGV[2]) " +
-                        "and redis.call('EXPIRE', KEYS[1], ARGV[3])", Boolean.class
-        ));
-    }
-
-    @Override
-    public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
-        Object principal = authentication.getPrincipal();
-        if (principal instanceof UserDetails) {
-            principal = ((UserDetails) principal).getUsername();
-        }
-        List<String> keys = Collections.singletonList(OAuthContextHolder.key(principal));
-        String scope = ObjectUtils.ifNull(OAuthContextHolder.getScope(token), StringUtils.EMPTY);
-        long duration = RedisContextHolder.duration(token.getExpiration()).getSeconds();
-        RedisScript<Boolean> script = this.scripts.get(SessionContextHolder.getTerminal());
-        try {
-            RedisContextHolder.getDefaultTemplate().execute(script, keys, scope, token.getValue(), duration);
-        } catch (Exception e) {
-            log.warn("Redis token save failed: {}", e.getMessage());
-        }
-    }
-}

+ 2 - 2
framework-oauth/src/main/java/com/chelvc/framework/oauth/token/TokenValidator.java → framework-oauth/src/main/java/com/chelvc/framework/oauth/token/SessionValidator.java

@@ -4,10 +4,10 @@ import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.jwt.Jwt;
 
 /**
- * 令牌验证器接口
+ * 会话验证器接口
  *
  * @author Woody
  * @date 2024/6/23
  */
-public interface TokenValidator extends OAuth2TokenValidator<Jwt> {
+public interface SessionValidator extends OAuth2TokenValidator<Jwt> {
 }

+ 2 - 1
framework-oauth/src/main/java/com/chelvc/framework/oauth/token/TimestampTokenValidator.java

@@ -3,6 +3,7 @@ package com.chelvc.framework.oauth.token;
 import com.chelvc.framework.base.context.ApplicationContextHolder;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
@@ -13,7 +14,7 @@ import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
  * @author Woody
  * @date 2024/6/23
  */
-public class TimestampTokenValidator implements TokenValidator {
+public class TimestampTokenValidator implements OAuth2TokenValidator<Jwt> {
     private final JwtTimestampValidator delegate = new JwtTimestampValidator();
 
     @Override

+ 5 - 5
framework-redis/src/main/java/com/chelvc/framework/redis/config/RedisProperties.java

@@ -14,11 +14,6 @@ import org.springframework.context.annotation.Configuration;
 @Configuration
 @ConfigurationProperties("spring.redis")
 public class RedisProperties {
-    /**
-     * 命名空间
-     */
-    private String namespace;
-
     /**
      * 消息流配置
      */
@@ -34,6 +29,11 @@ public class RedisProperties {
      */
     @Data
     public static class Stream {
+        /**
+         * 命名空间
+         */
+        private String namespace;
+
         /**
          * 消费者空闲时间(秒)
          */

+ 69 - 521
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java

@@ -1,62 +1,43 @@
 package com.chelvc.framework.redis.context;
 
-import java.lang.reflect.Type;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
-import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
 import com.chelvc.framework.base.context.ApplicationContextHolder;
-import com.chelvc.framework.base.context.Session;
-import com.chelvc.framework.base.context.SessionContextHolder;
 import com.chelvc.framework.common.function.Executor;
 import com.chelvc.framework.common.util.IdentityUtils;
-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.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
-import org.springframework.data.redis.RedisSystemException;
 import org.springframework.data.redis.connection.RedisConnection;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
-import org.springframework.data.redis.connection.RedisStreamCommands;
 import org.springframework.data.redis.connection.RedisStringCommands;
 import org.springframework.data.redis.connection.ReturnType;
 import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
-import org.springframework.data.redis.connection.stream.ByteRecord;
-import org.springframework.data.redis.connection.stream.MapRecord;
-import org.springframework.data.redis.connection.stream.RecordId;
-import org.springframework.data.redis.connection.stream.StreamInfo;
-import org.springframework.data.redis.connection.stream.StreamOffset;
 import org.springframework.data.redis.core.RedisConnectionUtils;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.data.redis.core.script.DigestUtils;
 import org.springframework.data.redis.core.script.RedisScript;
 import org.springframework.data.redis.core.types.Expiration;
-import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.RedisSerializer;
-import org.springframework.data.redis.stream.StreamListener;
-import org.springframework.data.redis.stream.StreamMessageListenerContainer;
 import org.springframework.util.CollectionUtils;
-import org.springframework.util.ErrorHandler;
 
 /**
  * Redis上下文工具类
@@ -66,26 +47,6 @@ import org.springframework.util.ErrorHandler;
  */
 @Slf4j
 public final class RedisContextHolder {
-    /**
-     * 消息ID标识
-     */
-    private static final String ID = "id";
-
-    /**
-     * 消息主题标识
-     */
-    private static final String TOPIC = "topic";
-
-    /**
-     * 消息载体标识
-     */
-    private static final String PAYLOAD = "payload";
-
-    /**
-     * 延时时间标识
-     */
-    private static final String DELAYING = "delaying";
-
     /**
      * 默认锁超时时间(秒)
      */
@@ -224,11 +185,6 @@ public final class RedisContextHolder {
                     "else redis.call('SET', KEYS[1], 0) return 0 end", Long.class
     );
 
-    /**
-     * 新增Stream消息流脚本
-     */
-    private static RedisScript<String> STREAM_ADD_SCRIPT;
-
     /**
      * Redis连接工厂
      */
@@ -249,21 +205,11 @@ public final class RedisContextHolder {
      */
     private static RedisTemplate<String, Object> REDIS_TEMPLATE;
 
-    /**
-     * 字符串RedisTemplate实例
-     */
-    private static RedisTemplate<String, Object> STRING_TEMPLATE;
-
     /**
      * 默认RedisTemplate实例
      */
     private static RedisTemplate<String, Object> DEFAULT_TEMPLATE;
 
-    /**
-     * 配置属性
-     */
-    private static com.chelvc.framework.redis.config.RedisProperties PROPERTIES;
-
     private RedisContextHolder() {
     }
 
@@ -343,49 +289,6 @@ public final class RedisContextHolder {
         return REDIS_TEMPLATE;
     }
 
-    /**
-     * 获取当前RedisTemplate实例
-     *
-     * @param database 数据库下标
-     * @return RedisTemplate实例
-     */
-    public static RedisTemplate<String, Object> getRedisTemplate(int database) {
-        RedisSerializer<?> valueSerializer =
-                ApplicationContextHolder.getBean(Jackson2JsonRedisSerializer.class);
-        RedisTemplate<String, Object> template = new RedisTemplate<>();
-        template.setConnectionFactory(getConnectionFactory(getConfiguration(database)));
-        template.setKeySerializer(RedisSerializer.string());
-        template.setValueSerializer(valueSerializer);
-        template.setHashKeySerializer(RedisSerializer.string());
-        template.setHashValueSerializer(valueSerializer);
-        template.afterPropertiesSet();
-        return template;
-    }
-
-    /**
-     * 获取基于字符串配置的0号库RedisTemplate实例
-     *
-     * @return RedisTemplate实例
-     */
-    public static RedisTemplate<String, Object> getStringTemplate() {
-        if (STRING_TEMPLATE == null) {
-            synchronized (RedisTemplate.class) {
-                if (STRING_TEMPLATE == null) {
-                    RedisTemplate<String, Object> template = new RedisTemplate<>();
-                    template.setConnectionFactory(getDefaultConnectionFactory());
-                    template.setKeySerializer(RedisSerializer.string());
-                    template.setValueSerializer(RedisSerializer.string());
-                    template.setHashKeySerializer(RedisSerializer.string());
-                    template.setHashValueSerializer(RedisSerializer.string());
-                    template.afterPropertiesSet();
-
-                    STRING_TEMPLATE = template;
-                }
-            }
-        }
-        return STRING_TEMPLATE;
-    }
-
     /**
      * 获取基于默认配置的0号库RedisTemplate实例
      *
@@ -403,39 +306,21 @@ public final class RedisContextHolder {
     }
 
     /**
-     * 获取新增Stream消息流脚本
-     *
-     * @return Lua脚本
-     */
-    private static RedisScript<String> getStreamAddScript() {
-        if (STREAM_ADD_SCRIPT == null) {
-            synchronized (RedisContextHolder.class) {
-                if (STREAM_ADD_SCRIPT == null) {
-                    int capacity = getProperties().getStream().getCapacity();
-                    if (capacity > 0) {
-                        STREAM_ADD_SCRIPT = new DefaultRedisScript<>(String.format(
-                                "return redis.call('XADD', KEYS[1], 'MAXLEN', '~', %d, '*', unpack(ARGV))", capacity
-                        ), String.class);
-                    } else {
-                        STREAM_ADD_SCRIPT = new DefaultRedisScript<>(
-                                "return redis.call('XADD', KEYS[1], '*', unpack(ARGV))", String.class
-                        );
-                    }
-                }
-            }
-        }
-        return STREAM_ADD_SCRIPT;
-    }
-
-    /**
-     * 环境隔离
+     * 获取当前RedisTemplate实例
      *
-     * @param original 原始标记
-     * @return 环境隔离标记
+     * @param database 数据库下标
+     * @return RedisTemplate实例
      */
-    public static String isolate(@NonNull String original) {
-        String namespace = getProperties().getNamespace();
-        return StringUtils.isEmpty(namespace) ? original : (namespace + original);
+    public static RedisTemplate<String, Object> getRedisTemplate(int database) {
+        RedisTemplate<?, ?> reference = getRedisTemplate();
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(getConnectionFactory(getConfiguration(database)));
+        template.setKeySerializer(reference.getKeySerializer());
+        template.setValueSerializer(reference.getValueSerializer());
+        template.setHashKeySerializer(reference.getHashKeySerializer());
+        template.setHashValueSerializer(reference.getHashValueSerializer());
+        template.afterPropertiesSet();
+        return template;
     }
 
     /**
@@ -479,33 +364,6 @@ public final class RedisContextHolder {
         return IDENTITY_GENERATOR;
     }
 
-    /**
-     * 获取消息流配置属性
-     *
-     * @return 配置属性
-     */
-    private static com.chelvc.framework.redis.config.RedisProperties getProperties() {
-        if (PROPERTIES == null) {
-            synchronized (com.chelvc.framework.redis.config.RedisProperties.class) {
-                if (PROPERTIES == null) {
-                    PROPERTIES = ApplicationContextHolder.getBean(
-                            com.chelvc.framework.redis.config.RedisProperties.class
-                    );
-                }
-            }
-        }
-        return PROPERTIES;
-    }
-
-    /**
-     * 获取MQ消费者空闲时间(秒)
-     *
-     * @return 空闲时间
-     */
-    public static int getStreamIdle() {
-        return getProperties().getStream().getIdle();
-    }
-
     /**
      * 执行Redis操作
      *
@@ -570,6 +428,21 @@ public final class RedisContextHolder {
         }
     }
 
+    /**
+     * 对象序列化
+     *
+     * @param serializer 序列化处理器
+     * @param value      对象实例
+     * @return 字节数组
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public static byte[] serialize(RedisSerializer serializer, Object value) {
+        if (serializer == null && value instanceof byte[]) {
+            return (byte[]) value;
+        }
+        return Objects.requireNonNull(serializer).serialize(value);
+    }
+
     /**
      * 加锁(阻塞),加锁成功则返回锁标识
      *
@@ -881,7 +754,7 @@ public final class RedisContextHolder {
     public static long sequence(@NonNull Duration duration) {
         ThreadLocalRandom random = ThreadLocalRandom.current();
         RedisConnectionFactory factory = getDefaultConnectionFactory();
-        for (int i = 0; i < 10; i++) {
+        for (int i = 0; i < 3; i++) {
             String time = IDENTITY_DATETIME_FORMATTER.format(LocalDateTime.now());
             String value = StringUtils.rjust(String.valueOf(random.nextInt(100000)), 5, '0');
             long identity = Long.parseLong(time + value);
@@ -1080,7 +953,7 @@ public final class RedisContextHolder {
      * @param initialValue 初始化值
      * @return 自增后的值
      */
-    public static Long increment(@NonNull String key, long initialValue) {
+    public static long increment(@NonNull String key, long initialValue) {
         return increment(getRedisTemplate(), key, initialValue);
     }
 
@@ -1093,8 +966,9 @@ public final class RedisContextHolder {
      * @param <K>          键类型
      * @return 自增后的值
      */
-    public static <K> Long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long initialValue) {
-        return template.execute(INCR_WITH_INITIAL_SCRIPT, Collections.singletonList(key), initialValue);
+    public static <K> long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long initialValue) {
+        List<K> keys = Collections.singletonList(key);
+        return Objects.requireNonNull(template.execute(INCR_WITH_INITIAL_SCRIPT, keys, initialValue));
     }
 
     /**
@@ -1105,7 +979,7 @@ public final class RedisContextHolder {
      * @param initialValue 初始化值
      * @return 自增后的值
      */
-    public static Long increment(@NonNull String key, long delta, long initialValue) {
+    public static long increment(@NonNull String key, long delta, long initialValue) {
         return increment(getRedisTemplate(), key, delta, initialValue);
     }
 
@@ -1119,9 +993,10 @@ public final class RedisContextHolder {
      * @param <K>          键类型
      * @return 自增后的值
      */
-    public static <K> Long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long delta,
+    public static <K> long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long delta,
                                      long initialValue) {
-        return template.execute(INCRBY_WITH_INITIAL_SCRIPT, Collections.singletonList(key), delta, initialValue);
+        List<K> keys = Collections.singletonList(key);
+        return Objects.requireNonNull(template.execute(INCRBY_WITH_INITIAL_SCRIPT, keys, delta, initialValue));
     }
 
     /**
@@ -1132,7 +1007,7 @@ public final class RedisContextHolder {
      * @param duration     有效时间
      * @return 自增后的值
      */
-    public static Long increment(@NonNull String key, long initialValue, @NonNull Duration duration) {
+    public static long increment(@NonNull String key, long initialValue, @NonNull Duration duration) {
         return increment(getRedisTemplate(), key, initialValue, duration);
     }
 
@@ -1146,10 +1021,12 @@ public final class RedisContextHolder {
      * @param <K>          键类型
      * @return 自增后的值
      */
-    public static <K> Long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long initialValue,
+    public static <K> long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long initialValue,
                                      @NonNull Duration duration) {
-        return template.execute(INCR_WITH_INITIAL_DURATION_SCRIPT, Collections.singletonList(key), initialValue,
-                duration.getSeconds());
+        List<K> keys = Collections.singletonList(key);
+        return Objects.requireNonNull(template.execute(
+                INCR_WITH_INITIAL_DURATION_SCRIPT, keys, initialValue, duration.getSeconds()
+        ));
     }
 
     /**
@@ -1161,7 +1038,7 @@ public final class RedisContextHolder {
      * @param duration     有效时间
      * @return 自增后的值
      */
-    public static Long increment(@NonNull String key, long delta, long initialValue, @NonNull Duration duration) {
+    public static long increment(@NonNull String key, long delta, long initialValue, @NonNull Duration duration) {
         return increment(getRedisTemplate(), key, delta, initialValue, duration);
     }
 
@@ -1176,10 +1053,12 @@ public final class RedisContextHolder {
      * @param <K>          键类型
      * @return 自增后的值
      */
-    public static <K> Long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long delta,
+    public static <K> long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long delta,
                                      long initialValue, @NonNull Duration duration) {
-        return template.execute(INCRBY_WITH_INITIAL_DURATION_SCRIPT, Collections.singletonList(key), delta,
-                initialValue, duration.getSeconds());
+        List<K> keys = Collections.singletonList(key);
+        return Objects.requireNonNull(template.execute(
+                INCRBY_WITH_INITIAL_DURATION_SCRIPT, keys, delta, initialValue, duration.getSeconds()
+        ));
     }
 
     /**
@@ -1189,7 +1068,7 @@ public final class RedisContextHolder {
      * @param initialValue 初始化值
      * @return 自减后的值
      */
-    public static Long decrement(@NonNull String key, long initialValue) {
+    public static long decrement(@NonNull String key, long initialValue) {
         return decrement(getRedisTemplate(), key, initialValue);
     }
 
@@ -1202,8 +1081,9 @@ public final class RedisContextHolder {
      * @param <K>          键类型
      * @return 自减后的值
      */
-    public static <K> Long decrement(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long initialValue) {
-        return template.execute(DECR_WITH_INITIAL_SCRIPT, Collections.singletonList(key), initialValue);
+    public static <K> long decrement(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long initialValue) {
+        List<K> keys = Collections.singletonList(key);
+        return Objects.requireNonNull(template.execute(DECR_WITH_INITIAL_SCRIPT, keys, initialValue));
     }
 
     /**
@@ -1214,7 +1094,7 @@ public final class RedisContextHolder {
      * @param initialValue 初始化值
      * @return 自减后的值
      */
-    public static Long decrement(@NonNull String key, long delta, long initialValue) {
+    public static long decrement(@NonNull String key, long delta, long initialValue) {
         return decrement(getRedisTemplate(), key, delta, initialValue);
     }
 
@@ -1228,9 +1108,10 @@ public final class RedisContextHolder {
      * @param <K>          键类型
      * @return 自减后的值
      */
-    public static <K> Long decrement(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long delta,
+    public static <K> long decrement(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long delta,
                                      long initialValue) {
-        return template.execute(DECRBY_WITH_INITIAL_SCRIPT, Collections.singletonList(key), delta, initialValue);
+        List<K> keys = Collections.singletonList(key);
+        return Objects.requireNonNull(template.execute(DECRBY_WITH_INITIAL_SCRIPT, keys, delta, initialValue));
     }
 
     /**
@@ -1241,7 +1122,7 @@ public final class RedisContextHolder {
      * @param duration     有效时间
      * @return 自减后的值
      */
-    public static Long decrement(@NonNull String key, long initialValue, @NonNull Duration duration) {
+    public static long decrement(@NonNull String key, long initialValue, @NonNull Duration duration) {
         return decrement(getRedisTemplate(), key, initialValue, duration);
     }
 
@@ -1255,10 +1136,12 @@ public final class RedisContextHolder {
      * @param <K>          键类型
      * @return 自减后的值
      */
-    public static <K> Long decrement(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long initialValue,
+    public static <K> long decrement(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long initialValue,
                                      @NonNull Duration duration) {
-        return template.execute(DECR_WITH_INITIAL_DURATION_SCRIPT, Collections.singletonList(key), initialValue,
-                duration.getSeconds());
+        List<K> keys = Collections.singletonList(key);
+        return Objects.requireNonNull(template.execute(
+                DECR_WITH_INITIAL_DURATION_SCRIPT, keys, initialValue, duration.getSeconds()
+        ));
     }
 
     /**
@@ -1270,7 +1153,7 @@ public final class RedisContextHolder {
      * @param duration     有效时间
      * @return 自减后的值
      */
-    public static Long decrement(@NonNull String key, long delta, long initialValue, @NonNull Duration duration) {
+    public static long decrement(@NonNull String key, long delta, long initialValue, @NonNull Duration duration) {
         return decrement(getRedisTemplate(), key, delta, initialValue, duration);
     }
 
@@ -1285,10 +1168,12 @@ public final class RedisContextHolder {
      * @param <K>          键类型
      * @return 自减后的值
      */
-    public static <K> Long decrement(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long delta,
+    public static <K> long decrement(@NonNull RedisTemplate<K, ?> template, @NonNull K key, long delta,
                                      long initialValue, @NonNull Duration duration) {
-        return template.execute(DECRBY_WITH_INITIAL_DURATION_SCRIPT, Collections.singletonList(key), delta,
-                initialValue, duration.getSeconds());
+        List<K> keys = Collections.singletonList(key);
+        return Objects.requireNonNull(template.execute(
+                DECRBY_WITH_INITIAL_DURATION_SCRIPT, keys, delta, initialValue, duration.getSeconds()
+        ));
     }
 
     /**
@@ -1373,341 +1258,4 @@ public final class RedisContextHolder {
         }
         throw new RuntimeException("Generate random attempts limit exceeded");
     }
-
-    /**
-     * 获取当前时间距过期时间的时长
-     *
-     * @param expiration 过期时间
-     * @return 持续时长
-     */
-    public static Duration duration(@NonNull Date expiration) {
-        return Duration.ofMillis(Math.max(expiration.getTime() - System.currentTimeMillis(), 1000));
-    }
-
-    /**
-     * 发送消息
-     *
-     * @param topic   消息主题
-     * @param payload 消息内容
-     * @return 消息记录ID
-     */
-    public static RecordId send(@NonNull String topic, @NonNull Object payload) {
-        return send(topic, payload, (Long) null);
-    }
-
-    /**
-     * 发送消息
-     *
-     * @param topic    消息主题
-     * @param payload  消息内容
-     * @param delaying 延时消息时间
-     * @return 消息记录ID
-     */
-    public static RecordId send(@NonNull String topic, @NonNull Object payload, Duration delaying) {
-        return send(topic, payload, delaying == null ? null : System.currentTimeMillis() + delaying.toMillis());
-    }
-
-    /**
-     * 发送消息
-     *
-     * @param topic    消息主题
-     * @param payload  消息内容
-     * @param delaying 延时消息时间
-     * @return 消息记录ID
-     */
-    public static RecordId send(@NonNull String topic, @NonNull Object payload, Date delaying) {
-        return send(topic, payload, ObjectUtils.ifNull(delaying, Date::getTime));
-    }
-
-    /**
-     * 发送消息
-     *
-     * @param topic    消息主题
-     * @param payload  消息内容
-     * @param delaying 延时消息时间
-     * @return 消息记录ID
-     */
-    public static RecordId send(@NonNull String topic, @NonNull Object payload, Long delaying) {
-        List<String> args = Lists.newLinkedList();
-        args.add(PAYLOAD);
-        args.add(JacksonUtils.serialize(payload));
-        if (delaying != null) {
-            args.add(DELAYING);
-            args.add(String.valueOf(delaying));
-        }
-        Session session = SessionContextHolder.getSession(false);
-        if (session != null) {
-            args.add(Session.NAMING);
-            args.add(JacksonUtils.serialize(session));
-        }
-        List<String> keys = Collections.singletonList(isolate(topic));
-        String id = getStringTemplate().execute(getStreamAddScript(), keys, args.toArray());
-        return StringUtils.ifEmpty(id, RecordId::of);
-    }
-
-    /**
-     * 转移消息
-     *
-     * @param topic    消息主题
-     * @param consumer 目标消费者
-     * @param ids      目标记录ID数组
-     * @return 转移成功消息记录列表
-     */
-    public static List<ByteRecord> claim(@NonNull String topic,
-                                         @NonNull org.springframework.data.redis.connection.stream.Consumer consumer,
-                                         @NonNull RecordId... ids) {
-        return claim(topic, consumer.getGroup(), consumer.getName(), ids);
-    }
-
-    /**
-     * 转移消息
-     *
-     * @param topic    消息主题
-     * @param group    消费者组
-     * @param consumer 目标消费者名称
-     * @param ids      目标记录ID数组
-     * @return 转移成功消息记录列表
-     */
-    public static List<ByteRecord> claim(@NonNull String topic, @NonNull String group, @NonNull String consumer,
-                                         @NonNull RecordId... ids) {
-        return claim(topic, group, consumer, Duration.ZERO, ids);
-    }
-
-    /**
-     * 转移消息
-     *
-     * @param topic    消息主题
-     * @param consumer 目标消费者
-     * @param idle     消息空闲时间
-     * @param ids      目标ID数组
-     * @return 转移成功消息记录列表
-     */
-    public static List<ByteRecord> claim(@NonNull String topic,
-                                         @NonNull org.springframework.data.redis.connection.stream.Consumer consumer,
-                                         @NonNull Duration idle, @NonNull RecordId... ids) {
-        return claim(topic, consumer.getGroup(), consumer.getName(), idle, ids);
-    }
-
-    /**
-     * 转移消息
-     *
-     * @param topic    消息主题
-     * @param group    消费者组
-     * @param consumer 目标消费者名称
-     * @param idle     消息空闲时间
-     * @param ids      目标ID数组
-     * @return 转移成功消息记录列表
-     */
-    public static List<ByteRecord> claim(@NonNull String topic, @NonNull String group, @NonNull String consumer,
-                                         @NonNull Duration idle, @NonNull RecordId... ids) {
-        if (ObjectUtils.isEmpty(ids)) {
-            return Collections.emptyList();
-        }
-        RedisStreamCommands.XClaimOptions options = RedisStreamCommands.XClaimOptions.minIdle(idle).ids(ids);
-        return execute(getDefaultConnectionFactory(),
-                connection -> connection.streamCommands().xClaim(topic.getBytes(), group, consumer, options));
-    }
-
-    /**
-     * 消息消费确认
-     *
-     * @param topic 消息主题
-     * @param group 消费者组
-     * @param ids   消息记录ID数组
-     */
-    public static void ack(@NonNull String topic, @NonNull String group, @NonNull RecordId... ids) {
-        if (ObjectUtils.notEmpty(ids)) {
-            getStringTemplate().opsForStream().acknowledge(topic, group, ids);
-        }
-    }
-
-    /**
-     * 删除消费者
-     *
-     * @param topic 消息主题
-     * @param ids   消息记录ID数组
-     */
-    public static void delete(@NonNull String topic, @NonNull RecordId... ids) {
-        if (ObjectUtils.notEmpty(ids)) {
-            getStringTemplate().opsForStream().delete(topic, ids);
-        }
-    }
-
-    /**
-     * 删除消费者
-     *
-     * @param topic    消息主题
-     * @param consumer 消费者信息
-     */
-    public static void delete(@NonNull String topic,
-                              @NonNull org.springframework.data.redis.connection.stream.Consumer consumer) {
-        getStringTemplate().opsForStream().deleteConsumer(topic, consumer);
-    }
-
-    /**
-     * 发送心跳包
-     *
-     * @param topic 消息主题
-     */
-    public static void heartbeat(@NonNull String topic) {
-        List<String> keys = Collections.singletonList(topic);
-        getStringTemplate().execute(getStreamAddScript(), keys, PAYLOAD, StringUtils.EMPTY);
-    }
-
-    /**
-     * 判断消息是否是心跳包
-     *
-     * @param record 消息记录
-     * @return true/false
-     */
-    public static boolean isHeartbeat(MapRecord<String, String, String> record) {
-        Map<String, String> value = ObjectUtils.ifNull(record, MapRecord::getValue);
-        return ObjectUtils.isEmpty(value) || StringUtils.isEmpty(value.get(PAYLOAD));
-    }
-
-    /**
-     * 序列化消息记录
-     *
-     * @param record 消息记录
-     * @return 消息内容
-     */
-    public static String serialize(@NonNull MapRecord<String, String, String> record) {
-        Map<String, String> value = record.getValue();
-        ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
-        builder.put(ID, record.getId().getValue());
-        builder.put(TOPIC, Objects.requireNonNull(record.getStream()));
-        String payload = value.get(PAYLOAD);
-        if (StringUtils.notEmpty(payload)) {
-            builder.put(PAYLOAD, payload);
-        }
-        String session = value.get(Session.NAMING);
-        if (StringUtils.notEmpty(session)) {
-            builder.put(Session.NAMING, session);
-        }
-        String delaying = value.get(DELAYING);
-        if (StringUtils.notEmpty(delaying)) {
-            builder.put(DELAYING, delaying);
-        }
-        return JacksonUtils.serialize(builder.build());
-    }
-
-    /**
-     * 反序列化消息记录
-     *
-     * @param message 消息内容
-     * @return 消息记录实例
-     */
-    @SuppressWarnings("unchecked")
-    public static MapRecord<String, String, String> deserialize(@NonNull String message) {
-        Map<String, String> content = JacksonUtils.deserialize(message, Map.class);
-        String id = content.remove(ID);
-        String topic = content.remove(TOPIC);
-        return MapRecord.create(topic, content).withId(RecordId.of(id));
-    }
-
-    /**
-     * 消费消息
-     *
-     * @param record   消息记录
-     * @param type     消息类型
-     * @param consumer 消息消费者
-     */
-    @SuppressWarnings("unchecked")
-    public static <T> void consume(@NonNull MapRecord<String, String, String> record, @NonNull Type type,
-                                   @NonNull Consumer<T> consumer) {
-        Map<String, String> value = record.getValue();
-        T payload = (T) JacksonUtils.deserialize(value.get(PAYLOAD), type);
-        Session session = JacksonUtils.deserialize(value.get(Session.NAMING), Session.class);
-        SessionContextHolder.setSession(session);
-        try {
-            consumer.accept(payload);
-        } finally {
-            SessionContextHolder.clearSessionContext();
-        }
-    }
-
-    /**
-     * 获取消费者信息
-     *
-     * @param topic 消息主题
-     * @param group 消费者组
-     * @return 消费者信息
-     */
-    public static StreamInfo.XInfoConsumers consumers(@NonNull String topic, @NonNull String group) {
-        return getStringTemplate().opsForStream().consumers(topic, group);
-    }
-
-    /**
-     * 获取消息延时时间戳
-     *
-     * @param record 消息记录
-     * @return 时间戳
-     */
-    public static Long getMessageDelaying(@NonNull MapRecord<String, String, String> record) {
-        String delaying = ObjectUtils.ifNull(record.getValue(), value -> value.get(DELAYING));
-        return StringUtils.ifEmpty(delaying, Long::parseLong);
-    }
-
-    /**
-     * 初始化消费者组
-     *
-     * @param topic 消息主题
-     * @param group 消费者组
-     */
-    public static void initializeConsumerGroup(@NonNull String topic, @NonNull String group) {
-        try {
-            getStringTemplate().opsForStream().createGroup(topic, group);
-        } catch (RedisSystemException e) {
-            if (StringUtils.isEmpty(e.getMessage()) || !e.getMessage().contains("BUSYGROUP")) {
-                throw e;
-            }
-        }
-    }
-
-    /**
-     * 初始化消息监听器容器
-     *
-     * @param listener 消息监听器
-     * @param consumer 消费者信息
-     * @param offset   消息偏移信息
-     * @param batch    批量获取消息数量
-     * @param executor 消息获取执行器
-     * @return 消息监听器容器
-     */
-    public static StreamMessageListenerContainer<String, MapRecord<String, String, String>>
-    initializeMessageListenerContainer(@NonNull StreamListener<String, MapRecord<String, String, String>> listener,
-                                       @NonNull org.springframework.data.redis.connection.stream.Consumer consumer,
-                                       @NonNull StreamOffset<String> offset, int batch,
-                                       @NonNull java.util.concurrent.Executor executor) {
-        return initializeMessageListenerContainer(listener, consumer, offset, batch, executor, null);
-    }
-
-    /**
-     * 初始化消息监听器容器
-     *
-     * @param listener     消息监听器
-     * @param consumer     消费者信息
-     * @param offset       消息偏移信息
-     * @param batch        批量获取消息数量
-     * @param executor     消息获取执行器
-     * @param errorHandler 异常处理器
-     * @return 消息监听器容器
-     */
-    public static StreamMessageListenerContainer<String, MapRecord<String, String, String>>
-    initializeMessageListenerContainer(@NonNull StreamListener<String, MapRecord<String, String, String>> listener,
-                                       @NonNull org.springframework.data.redis.connection.stream.Consumer consumer,
-                                       @NonNull StreamOffset<String> offset, int batch,
-                                       @NonNull java.util.concurrent.Executor executor, ErrorHandler errorHandler) {
-        StreamMessageListenerContainer.
-                StreamMessageListenerContainerOptionsBuilder<String, MapRecord<String, String, String>>
-                builder = StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
-                .batchSize(batch).pollTimeout(Duration.ZERO).executor(executor).serializer(RedisSerializer.string());
-        if (errorHandler != null) {
-            builder.errorHandler(errorHandler);
-        }
-        StreamMessageListenerContainer<String, MapRecord<String, String, String>> container =
-                StreamMessageListenerContainer.create(getDefaultConnectionFactory(), builder.build());
-        container.receive(consumer, offset, listener);
-        return container;
-    }
 }

+ 286 - 0
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisDailyHashHolder.java

@@ -0,0 +1,286 @@
+package com.chelvc.framework.redis.context;
+
+import java.io.Serializable;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import com.chelvc.framework.common.util.DateUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import lombok.NonNull;
+import org.springframework.data.redis.core.RedisTemplate;
+
+/**
+ * Redis Hash结构当日数据操作工具类
+ *
+ * @author Woody
+ * @date 2024/6/29
+ */
+public final class RedisDailyHashHolder {
+    private RedisDailyHashHolder() {
+    }
+
+    /**
+     * 获取当日时间周期
+     *
+     * @return 时间周期
+     */
+    public static Duration duration() {
+        return DateUtils.duration(DateUtils.tomorrow());
+    }
+
+    /**
+     * 获取Redis Hash结构名称
+     *
+     * @param type 数据类型
+     * @param id   数据标识
+     * @return Hash结构名称
+     */
+    public static String name(@NonNull String type, @NonNull Serializable id) {
+        return type + ":" + id + ":" + DateUtils.date2number(DateUtils.today());
+    }
+
+    /**
+     * 获取Redis Hash结构当日数据
+     *
+     * @param type 数据类型
+     * @param id   数据标识
+     * @param key  Hash键
+     * @return Hash值
+     */
+    public static Object get(@NonNull String type, @NonNull Serializable id, @NonNull String key) {
+        return RedisHashHolder.get(name(type, id), key);
+    }
+
+    /**
+     * 获取Redis Hash结构当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param key      Hash键
+     * @return Hash值
+     */
+    public static Object get(@NonNull RedisTemplate<String, ?> template, @NonNull String type, @NonNull Serializable id,
+                             @NonNull String key) {
+        return RedisHashHolder.get(template, name(type, id), key);
+    }
+
+    /**
+     * 获取Redis Hash结构当日数据
+     *
+     * @param type 数据类型
+     * @param id   数据标识
+     * @param keys Hash键数组
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull String type, @NonNull Serializable id, @NonNull String... keys) {
+        return ObjectUtils.isEmpty(keys) ? Collections.emptyList() : RedisHashHolder.get(name(type, id), keys);
+    }
+
+    /**
+     * 获取Redis Hash结构当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param keys     Hash键数组
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull RedisTemplate<String, ?> template, @NonNull String type,
+                              @NonNull Serializable id, @NonNull String... keys) {
+        if (ObjectUtils.isEmpty(keys)) {
+            return Collections.emptyList();
+        }
+        return RedisHashHolder.get(template, name(type, id), keys);
+    }
+
+    /**
+     * 获取Redis Hash结构当日数据
+     *
+     * @param type 数据类型
+     * @param id   数据标识
+     * @param keys Hash键集合
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull String type, @NonNull Serializable id, @NonNull Collection<String> keys) {
+        return ObjectUtils.isEmpty(keys) ? Collections.emptyList() : RedisHashHolder.get(name(type, id), keys);
+    }
+
+    /**
+     * 获取Redis Hash结构当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param keys     Hash键集合
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull RedisTemplate<String, ?> template, @NonNull String type,
+                              @NonNull Serializable id, @NonNull Collection<String> keys) {
+        if (ObjectUtils.isEmpty(keys)) {
+            return Collections.emptyList();
+        }
+        return RedisHashHolder.get(template, name(type, id), keys);
+    }
+
+    /**
+     * 设置Redis Hash结构当日数据
+     *
+     * @param type  数据类型
+     * @param id    数据标识
+     * @param array Hash键/值数组
+     * @return true/false
+     */
+    public static boolean set(@NonNull String type, @NonNull Serializable id, @NonNull Object... array) {
+        return ObjectUtils.notEmpty(array) && RedisHashHolder.set(name(type, id), array, duration());
+    }
+
+    /**
+     * 设置Redis Hash结构当日数据
+     *
+     * @param type 数据类型
+     * @param id   数据标识
+     * @param map  Hash键/值映射表
+     * @return true/false
+     */
+    public static boolean set(@NonNull String type, @NonNull Serializable id, @NonNull Map<String, ?> map) {
+        return ObjectUtils.notEmpty(map) && RedisHashHolder.set(name(type, id), map, duration());
+    }
+
+    /**
+     * 设置Redis Hash结构当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param array    Hash键/值数组
+     * @return true/false
+     */
+    public static boolean set(@NonNull RedisTemplate<String, ?> template, @NonNull String type,
+                              @NonNull Serializable id, @NonNull Object... array) {
+        return ObjectUtils.notEmpty(array) && RedisHashHolder.set(template, name(type, id), array, duration());
+    }
+
+    /**
+     * 设置Redis Hash结构当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param map      Hash键/值映射表
+     * @return true/false
+     */
+    public static boolean set(@NonNull RedisTemplate<String, ?> template, @NonNull String type,
+                              @NonNull Serializable id, @NonNull Map<String, ?> map) {
+        return ObjectUtils.notEmpty(map) && RedisHashHolder.set(template, name(type, id), map, duration());
+    }
+
+    /**
+     * Redis Hash结构当日数字自增1
+     *
+     * @param type 数据类型
+     * @param id   数据标识
+     * @param key  Hash键
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull String type, @NonNull Serializable id, @NonNull String key) {
+        return RedisHashHolder.increment(name(type, id), key, duration());
+    }
+
+    /**
+     * Redis Hash结构当日数字自增/减,delta < 0 时自减
+     *
+     * @param type  数据类型
+     * @param id    数据标识
+     * @param key   Hash键
+     * @param delta 增/减值
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull String type, @NonNull Serializable id, @NonNull String key, long delta) {
+        return RedisHashHolder.increment(name(type, id), key, delta, duration());
+    }
+
+    /**
+     * Redis Hash结构当日数字自增1
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param key      Hash键
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull RedisTemplate<String, ?> template, @NonNull String type,
+                                 @NonNull Serializable id, @NonNull String key) {
+        return RedisHashHolder.increment(template, name(type, id), key, duration());
+    }
+
+    /**
+     * Redis Hash结构当日数字自增/减,delta < 0 时自减
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param key      Hash键
+     * @param delta    增/减值
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull RedisTemplate<String, ?> template, @NonNull String type,
+                                 @NonNull Serializable id, @NonNull String key, long delta) {
+        return RedisHashHolder.increment(template, name(type, id), key, delta, duration());
+    }
+
+    /**
+     * Redis Hash结构删除当日数据
+     *
+     * @param type 数据类型
+     * @param id   数据标识
+     * @param keys Hash键数组
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull String type, @NonNull Serializable id, @NonNull String... keys) {
+        return ObjectUtils.isEmpty(keys) ? 0 : RedisHashHolder.delete(name(type, id), keys);
+    }
+
+    /**
+     * Redis Hash结构删除当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param keys     Hash键数组
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull RedisTemplate<String, ?> template, @NonNull String type,
+                              @NonNull Serializable id, @NonNull String... keys) {
+        return ObjectUtils.isEmpty(keys) ? 0 : RedisHashHolder.delete(template, name(type, id), keys);
+    }
+
+    /**
+     * Redis Hash结构删除当日数据
+     *
+     * @param type 数据类型
+     * @param id   数据标识
+     * @param keys Hash键集合
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull String type, @NonNull Serializable id, @NonNull Collection<String> keys) {
+        return ObjectUtils.isEmpty(keys) ? 0 : RedisHashHolder.delete(name(type, id), keys);
+    }
+
+    /**
+     * Redis Hash结构删除当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param type     数据类型
+     * @param id       数据标识
+     * @param keys     Hash键集合
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull RedisTemplate<String, ?> template, @NonNull String type,
+                              @NonNull Serializable id, @NonNull Collection<String> keys) {
+        return ObjectUtils.isEmpty(keys) ? 0 : RedisHashHolder.delete(template, name(type, id), keys);
+    }
+}

+ 382 - 0
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisHashHolder.java

@@ -0,0 +1,382 @@
+package com.chelvc.framework.redis.context;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import com.chelvc.framework.common.util.AssertUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import lombok.NonNull;
+import org.springframework.data.redis.connection.RedisScriptingCommands;
+import org.springframework.data.redis.connection.ReturnType;
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DigestUtils;
+
+/**
+ * Redis Hash操作工具类
+ *
+ * @author Woody
+ * @date 2024/6/28
+ */
+public final class RedisHashHolder {
+    /**
+     * Redis带过期时间HMSET脚本
+     */
+    private static final String SET_DURATION_SCRIPT =
+            "return redis.call('HMSET', KEYS[1], unpack(ARGV, 2)) and redis.call('EXPIRE', KEYS[1], ARGV[1])";
+
+    /**
+     * Redis带过期时间HINCRBY脚本
+     */
+    private static final String HINCRBY_DURATION_SCRIPT =
+            "return redis.call('HINCRBY', KEYS[1], ARGV[1], ARGV[2]) and redis.call('EXPIRE', KEYS[1], ARGV[3])";
+
+    /**
+     * Redis带过期时间HMSET脚本SHA
+     */
+    private static final String SET_DURATION_SCRIPT_SHA = DigestUtils.sha1DigestAsHex(SET_DURATION_SCRIPT);
+
+    /**
+     * Redis带过期时间HINCRBY脚本SHA
+     */
+    private static final String HINCRBY_DURATION_SCRIPT_SHA = DigestUtils.sha1DigestAsHex(HINCRBY_DURATION_SCRIPT);
+
+    private RedisHashHolder() {
+    }
+
+    /**
+     * 设置Redis Hash结构数据
+     *
+     * @param template Redis操作模版实例
+     * @param args     参数字节二维数组
+     * @param <K>      Hash结构名称类型
+     * @return true/false
+     */
+    private static <K> boolean set(@NonNull RedisTemplate<K, ?> template, @NonNull byte[][] args) {
+        return RedisContextHolder.execute(template.getConnectionFactory(), connection -> {
+            Boolean success;
+            RedisScriptingCommands commands = connection.scriptingCommands();
+            try {
+                success = commands.evalSha(SET_DURATION_SCRIPT_SHA, ReturnType.BOOLEAN, 1, args);
+            } catch (Exception e) {
+                success = commands.eval(SET_DURATION_SCRIPT.getBytes(), ReturnType.BOOLEAN, 1, args);
+            }
+            return Boolean.TRUE.equals(success);
+        });
+    }
+
+    /**
+     * 获取Redis Hash结构数据
+     *
+     * @param name Hash结构名称
+     * @param key  Hash键
+     * @return Hash值
+     */
+    public static Object get(@NonNull String name, @NonNull String key) {
+        return get(RedisContextHolder.getRedisTemplate(), name, key);
+    }
+
+    /**
+     * 获取Redis Hash结构数据
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param key      Hash键
+     * @param <K>      Hash结构名称类型
+     * @return Hash值
+     */
+    public static <K> Object get(@NonNull RedisTemplate<K, ?> template, @NonNull K name, @NonNull String key) {
+        return template.opsForHash().get(name, key);
+    }
+
+    /**
+     * 获取Redis Hash结构数据
+     *
+     * @param name Hash结构名称
+     * @param keys Hash键数组
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull String name, @NonNull String... keys) {
+        return get(RedisContextHolder.getRedisTemplate(), name, keys);
+    }
+
+    /**
+     * 获取Redis Hash结构数据
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param keys     Hash键数组
+     * @param <K>      Hash结构名称类型
+     * @return Hash值列表
+     */
+    public static <K> List<?> get(@NonNull RedisTemplate<K, ?> template, @NonNull K name, @NonNull String... keys) {
+        return ObjectUtils.isEmpty(keys) ? Collections.emptyList() : get(template, name, Arrays.asList(keys));
+    }
+
+    /**
+     * 获取Redis Hash结构数据
+     *
+     * @param name Hash结构名称
+     * @param keys Hash键集合
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull String name, @NonNull Collection<String> keys) {
+        return get(RedisContextHolder.getRedisTemplate(), name, keys);
+    }
+
+    /**
+     * 获取Redis Hash结构数据
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param keys     Hash键集合
+     * @param <K>      Hash结构名称类型
+     * @return Hash值列表
+     */
+    public static <K> List<?> get(@NonNull RedisTemplate<K, ?> template, @NonNull K name,
+                                  @NonNull Collection<String> keys) {
+        if (ObjectUtils.isEmpty(keys)) {
+            return Collections.emptyList();
+        }
+        HashOperations<K, String, ?> operations = template.opsForHash();
+        return operations.multiGet(name, keys);
+    }
+
+    /**
+     * 设置Redis Hash结构数据
+     *
+     * @param name     Hash结构名称
+     * @param array    Hash键/值数组
+     * @param duration 有效周期
+     * @return true/false
+     */
+    public static boolean set(@NonNull String name, @NonNull Object[] array, @NonNull Duration duration) {
+        return set(RedisContextHolder.getRedisTemplate(), name, array, duration);
+    }
+
+    /**
+     * 设置Redis Hash结构数据
+     *
+     * @param name     Hash结构名称
+     * @param map      Hash键/值映射表
+     * @param duration 有效周期
+     * @return true/false
+     */
+    public static boolean set(@NonNull String name, @NonNull Map<String, ?> map, @NonNull Duration duration) {
+        return set(RedisContextHolder.getRedisTemplate(), name, map, duration);
+    }
+
+    /**
+     * 设置Redis Hash结构数据
+     *
+     * @param name     Hash结构名称
+     * @param key      Hash键
+     * @param value    Hash值
+     * @param duration 有效周期
+     * @return true/false
+     */
+    public static boolean set(@NonNull String name, @NonNull String key, Object value, @NonNull Duration duration) {
+        return set(RedisContextHolder.getRedisTemplate(), name, key, value, duration);
+    }
+
+    /**
+     * 设置Redis Hash结构数据
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param array    Hash键/值数组
+     * @param duration 有效周期
+     * @param <K>      Hash结构名称类型
+     * @return true/false
+     */
+    public static <K> boolean set(@NonNull RedisTemplate<K, ?> template, @NonNull K name, @NonNull Object[] array,
+                                  @NonNull Duration duration) {
+        if (ObjectUtils.isEmpty(array)) {
+            return false;
+        }
+
+        AssertUtils.check(array.length % 2 == 0, () -> "Invalid key/value array");
+        byte[][] args = new byte[array.length + 2][];
+        args[0] = RedisContextHolder.serialize(template.getKeySerializer(), name);
+        args[1] = RedisContextHolder.serialize(template.getValueSerializer(), duration.getSeconds());
+        for (int i = 0, n = 2; i < array.length; i += 2) {
+            args[n++] = RedisContextHolder.serialize(template.getHashKeySerializer(), array[i]);
+            args[n++] = RedisContextHolder.serialize(template.getHashValueSerializer(), array[i + 1]);
+        }
+        return set(template, args);
+    }
+
+    /**
+     * 设置Redis Hash结构数据
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param map      Hash键/值映射表
+     * @param duration 有效周期
+     * @param <K>      Hash结构名称类型
+     * @return true/false
+     */
+    public static <K> boolean set(@NonNull RedisTemplate<K, ?> template, @NonNull K name, @NonNull Map<String, ?> map,
+                                  @NonNull Duration duration) {
+        if (ObjectUtils.isEmpty(map)) {
+            return false;
+        }
+
+        int i = 0;
+        byte[][] args = new byte[map.size() * 2 + 2][];
+        args[i++] = RedisContextHolder.serialize(template.getKeySerializer(), name);
+        args[i++] = RedisContextHolder.serialize(template.getValueSerializer(), duration.getSeconds());
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            args[i++] = RedisContextHolder.serialize(template.getHashKeySerializer(), entry.getKey());
+            args[i++] = RedisContextHolder.serialize(template.getHashValueSerializer(), entry.getValue());
+        }
+        return set(template, args);
+    }
+
+    /**
+     * 设置Redis Hash结构数据
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param key      Hash键
+     * @param value    Hash值
+     * @param duration 有效周期
+     * @param <K>      Hash结构名称类型
+     * @return true/false
+     */
+    public static <K> boolean set(@NonNull RedisTemplate<K, ?> template, @NonNull K name, @NonNull String key,
+                                  Object value, @NonNull Duration duration) {
+        byte[][] args = new byte[][]{
+                RedisContextHolder.serialize(template.getKeySerializer(), name),
+                RedisContextHolder.serialize(template.getValueSerializer(), duration.getSeconds()),
+                RedisContextHolder.serialize(template.getHashKeySerializer(), key),
+                RedisContextHolder.serialize(template.getHashValueSerializer(), value)
+        };
+        return set(template, args);
+    }
+
+    /**
+     * Redis Hash结构数字自增1
+     *
+     * @param name     Hash结构名称
+     * @param key      Hash键
+     * @param duration 有效周期
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull String name, @NonNull String key, @NonNull Duration duration) {
+        return increment(name, key, 1, duration);
+    }
+
+    /**
+     * Redis Hash结构数字自增/减,delta < 0 时自减
+     *
+     * @param name     Hash结构名称
+     * @param key      Hash键
+     * @param delta    增/减值
+     * @param duration 有效周期
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull String name, @NonNull String key, long delta, @NonNull Duration duration) {
+        return increment(RedisContextHolder.getRedisTemplate(), name, key, delta, duration);
+    }
+
+    /**
+     * Redis Hash结构数字自增1
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param key      Hash键
+     * @param duration 有效周期
+     * @param <K>      Hash结构名称类型
+     * @return 增/减后数值
+     */
+    public static <K> long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K name, @NonNull String key,
+                                     @NonNull Duration duration) {
+        return increment(template, name, key, 1, duration);
+    }
+
+    /**
+     * Redis Hash结构数字自增/减,delta < 0 时自减
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param key      Hash键
+     * @param delta    增/减值
+     * @param duration 有效周期
+     * @param <K>      Hash结构名称类型
+     * @return 增/减后数值
+     */
+    public static <K> long increment(@NonNull RedisTemplate<K, ?> template, @NonNull K name, @NonNull String key,
+                                     long delta, @NonNull Duration duration) {
+        byte[][] args = new byte[][]{
+                RedisContextHolder.serialize(template.getKeySerializer(), name),
+                RedisContextHolder.serialize(template.getHashKeySerializer(), key),
+                RedisContextHolder.serialize(template.getHashValueSerializer(), delta),
+                RedisContextHolder.serialize(template.getValueSerializer(), duration.getSeconds())
+        };
+        return RedisContextHolder.execute(template.getConnectionFactory(), connection -> {
+            Number value;
+            RedisScriptingCommands commands = connection.scriptingCommands();
+            try {
+                value = commands.evalSha(HINCRBY_DURATION_SCRIPT_SHA, ReturnType.INTEGER, 1, args);
+            } catch (Exception e) {
+                value = commands.eval(HINCRBY_DURATION_SCRIPT.getBytes(), ReturnType.INTEGER, 1, args);
+            }
+            return Objects.requireNonNull(value).longValue();
+        });
+    }
+
+    /**
+     * Redis Hash结构删除数据
+     *
+     * @param name Hash结构名称
+     * @param keys Hash键数组
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull String name, @NonNull String... keys) {
+        return delete(RedisContextHolder.getRedisTemplate(), name, keys);
+    }
+
+    /**
+     * Redis Hash结构删除数据
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param keys     Hash键数组
+     * @param <K>      Hash结构名称类型
+     * @return 成功删除数量
+     */
+    public static <K> long delete(@NonNull RedisTemplate<K, ?> template, @NonNull K name, @NonNull String... keys) {
+        return ObjectUtils.isEmpty(keys) ? 0 : template.opsForHash().delete(name, (Object[]) keys);
+    }
+
+    /**
+     * Redis Hash结构删除数据
+     *
+     * @param name Hash结构名称
+     * @param keys Hash键集合
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull String name, @NonNull Collection<String> keys) {
+        return delete(RedisContextHolder.getRedisTemplate(), name, keys);
+    }
+
+    /**
+     * Redis Hash结构删除数据
+     *
+     * @param template Redis操作模版实例
+     * @param name     Hash结构名称
+     * @param keys     Hash键集合
+     * @param <K>      Hash结构名称类型
+     * @return 成功删除数量
+     */
+    public static <K> long delete(@NonNull RedisTemplate<K, ?> template, @NonNull K name,
+                                  @NonNull Collection<String> keys) {
+        return ObjectUtils.isEmpty(keys) ? 0 : template.opsForHash().delete(name, keys.toArray());
+    }
+}

+ 497 - 0
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisStreamHolder.java

@@ -0,0 +1,497 @@
+package com.chelvc.framework.redis.context;
+
+import java.lang.reflect.Type;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import com.chelvc.framework.base.context.ApplicationContextHolder;
+import com.chelvc.framework.base.context.Session;
+import com.chelvc.framework.base.context.SessionContextHolder;
+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.redis.config.RedisProperties;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import lombok.NonNull;
+import org.springframework.data.redis.RedisSystemException;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStreamCommands;
+import org.springframework.data.redis.connection.stream.ByteRecord;
+import org.springframework.data.redis.connection.stream.MapRecord;
+import org.springframework.data.redis.connection.stream.RecordId;
+import org.springframework.data.redis.connection.stream.StreamInfo;
+import org.springframework.data.redis.connection.stream.StreamOffset;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.stream.StreamListener;
+import org.springframework.data.redis.stream.StreamMessageListenerContainer;
+import org.springframework.util.ErrorHandler;
+
+/**
+ * Redis Stream操作工具类
+ *
+ * @author Woody
+ * @date 2024/6/28
+ */
+public final class RedisStreamHolder {
+    /**
+     * 消息ID标识
+     */
+    private static final String ID = "id";
+
+    /**
+     * 消息主题标识
+     */
+    private static final String TOPIC = "topic";
+
+    /**
+     * 消息载体标识
+     */
+    private static final String PAYLOAD = "payload";
+
+    /**
+     * 延时时间标识
+     */
+    private static final String DELAYING = "delaying";
+
+    /**
+     * 新增消息脚本
+     */
+    private static RedisScript<String> ADD_SCRIPT;
+
+    /**
+     * 消息流配置属性
+     */
+    private static RedisProperties.Stream PROPERTIES;
+
+    /**
+     * 字符串RedisTemplate实例
+     */
+    private static RedisTemplate<String, Object> STRING_TEMPLATE;
+
+    private RedisStreamHolder() {
+    }
+
+    /**
+     * 获取新增Stream消息流脚本
+     *
+     * @return Lua脚本
+     */
+    private static RedisScript<String> getAddScript() {
+        if (ADD_SCRIPT == null) {
+            synchronized (RedisContextHolder.class) {
+                if (ADD_SCRIPT == null) {
+                    int capacity = getProperties().getCapacity();
+                    if (capacity > 0) {
+                        ADD_SCRIPT = new DefaultRedisScript<>(String.format(
+                                "return redis.call('XADD', KEYS[1], 'MAXLEN', '~', %d, '*', unpack(ARGV))", capacity
+                        ), String.class);
+                    } else {
+                        ADD_SCRIPT = new DefaultRedisScript<>(
+                                "return redis.call('XADD', KEYS[1], '*', unpack(ARGV))", String.class
+                        );
+                    }
+                }
+            }
+        }
+        return ADD_SCRIPT;
+    }
+
+    /**
+     * 获取消息流配置属性
+     *
+     * @return 配置属性
+     */
+    private static RedisProperties.Stream getProperties() {
+        if (PROPERTIES == null) {
+            synchronized (RedisProperties.Stream.class) {
+                if (PROPERTIES == null) {
+                    PROPERTIES = ApplicationContextHolder.getBean(RedisProperties.class).getStream();
+                }
+            }
+        }
+        return PROPERTIES;
+    }
+
+    /**
+     * 获取消息流处理RedisTemplate实例
+     *
+     * @return RedisTemplate实例
+     */
+    public static RedisTemplate<String, Object> getStreamTemplate() {
+        if (STRING_TEMPLATE == null) {
+            synchronized (RedisTemplate.class) {
+                if (STRING_TEMPLATE == null) {
+                    RedisTemplate<String, Object> template = new RedisTemplate<>();
+                    template.setConnectionFactory(RedisContextHolder.getDefaultConnectionFactory());
+                    template.setKeySerializer(RedisSerializer.string());
+                    template.setValueSerializer(RedisSerializer.string());
+                    template.setHashKeySerializer(RedisSerializer.string());
+                    template.setHashValueSerializer(RedisSerializer.string());
+                    template.afterPropertiesSet();
+
+                    STRING_TEMPLATE = template;
+                }
+            }
+        }
+        return STRING_TEMPLATE;
+    }
+
+    /**
+     * 获取MQ消费者空闲时间(秒)
+     *
+     * @return 空闲时间
+     */
+    public static int getIdle() {
+        return getProperties().getIdle();
+    }
+
+    /**
+     * 环境隔离
+     *
+     * @param original 原始标记
+     * @return 环境隔离标记
+     */
+    public static String isolate(@NonNull String original) {
+        String namespace = getProperties().getNamespace();
+        return StringUtils.isEmpty(namespace) ? original : (namespace + original);
+    }
+
+    /**
+     * 序列化消息记录
+     *
+     * @param record 消息记录
+     * @return 消息内容
+     */
+    public static String serialize(@NonNull MapRecord<String, String, String> record) {
+        Map<String, String> value = record.getValue();
+        ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+        builder.put(ID, record.getId().getValue());
+        builder.put(TOPIC, Objects.requireNonNull(record.getStream()));
+        String payload = value.get(PAYLOAD);
+        if (StringUtils.notEmpty(payload)) {
+            builder.put(PAYLOAD, payload);
+        }
+        String session = value.get(Session.NAMING);
+        if (StringUtils.notEmpty(session)) {
+            builder.put(Session.NAMING, session);
+        }
+        String delaying = value.get(DELAYING);
+        if (StringUtils.notEmpty(delaying)) {
+            builder.put(DELAYING, delaying);
+        }
+        return JacksonUtils.serialize(builder.build());
+    }
+
+    /**
+     * 反序列化消息记录
+     *
+     * @param message 消息内容
+     * @return 消息记录实例
+     */
+    @SuppressWarnings("unchecked")
+    public static MapRecord<String, String, String> deserialize(@NonNull String message) {
+        Map<String, String> content = JacksonUtils.deserialize(message, Map.class);
+        String id = content.remove(ID);
+        String topic = content.remove(TOPIC);
+        return MapRecord.create(topic, content).withId(RecordId.of(id));
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param topic   消息主题
+     * @param payload 消息内容
+     * @return 消息记录ID
+     */
+    public static RecordId send(@NonNull String topic, @NonNull Object payload) {
+        return send(topic, payload, (Long) null);
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param topic    消息主题
+     * @param payload  消息内容
+     * @param delaying 延时消息时间
+     * @return 消息记录ID
+     */
+    public static RecordId send(@NonNull String topic, @NonNull Object payload, Duration delaying) {
+        return send(topic, payload, delaying == null ? null : System.currentTimeMillis() + delaying.toMillis());
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param topic    消息主题
+     * @param payload  消息内容
+     * @param delaying 延时消息时间
+     * @return 消息记录ID
+     */
+    public static RecordId send(@NonNull String topic, @NonNull Object payload, Date delaying) {
+        return send(topic, payload, ObjectUtils.ifNull(delaying, Date::getTime));
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param topic    消息主题
+     * @param payload  消息内容
+     * @param delaying 延时消息时间
+     * @return 消息记录ID
+     */
+    public static RecordId send(@NonNull String topic, @NonNull Object payload, Long delaying) {
+        List<String> args = Lists.newLinkedList();
+        args.add(PAYLOAD);
+        args.add(JacksonUtils.serialize(payload));
+        if (delaying != null) {
+            args.add(DELAYING);
+            args.add(String.valueOf(delaying));
+        }
+        Session session = SessionContextHolder.getSession(false);
+        if (session != null) {
+            args.add(Session.NAMING);
+            args.add(JacksonUtils.serialize(session));
+        }
+        List<String> keys = Collections.singletonList(isolate(topic));
+        String id = getStreamTemplate().execute(getAddScript(), keys, args.toArray());
+        return StringUtils.ifEmpty(id, RecordId::of);
+    }
+
+    /**
+     * 转移消息
+     *
+     * @param topic    消息主题
+     * @param consumer 目标消费者
+     * @param ids      目标记录ID数组
+     * @return 转移成功消息记录列表
+     */
+    public static List<ByteRecord> claim(@NonNull String topic,
+                                         @NonNull org.springframework.data.redis.connection.stream.Consumer consumer,
+                                         @NonNull RecordId... ids) {
+        return claim(topic, consumer.getGroup(), consumer.getName(), ids);
+    }
+
+    /**
+     * 转移消息
+     *
+     * @param topic    消息主题
+     * @param group    消费者组
+     * @param consumer 目标消费者名称
+     * @param ids      目标记录ID数组
+     * @return 转移成功消息记录列表
+     */
+    public static List<ByteRecord> claim(@NonNull String topic, @NonNull String group, @NonNull String consumer,
+                                         @NonNull RecordId... ids) {
+        return claim(topic, group, consumer, Duration.ZERO, ids);
+    }
+
+    /**
+     * 转移消息
+     *
+     * @param topic    消息主题
+     * @param consumer 目标消费者
+     * @param idle     消息空闲时间
+     * @param ids      目标ID数组
+     * @return 转移成功消息记录列表
+     */
+    public static List<ByteRecord> claim(@NonNull String topic,
+                                         @NonNull org.springframework.data.redis.connection.stream.Consumer consumer,
+                                         @NonNull Duration idle, @NonNull RecordId... ids) {
+        return claim(topic, consumer.getGroup(), consumer.getName(), idle, ids);
+    }
+
+    /**
+     * 转移消息
+     *
+     * @param topic    消息主题
+     * @param group    消费者组
+     * @param consumer 目标消费者名称
+     * @param idle     消息空闲时间
+     * @param ids      目标ID数组
+     * @return 转移成功消息记录列表
+     */
+    public static List<ByteRecord> claim(@NonNull String topic, @NonNull String group, @NonNull String consumer,
+                                         @NonNull Duration idle, @NonNull RecordId... ids) {
+        if (ObjectUtils.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
+        RedisStreamCommands.XClaimOptions options = RedisStreamCommands.XClaimOptions.minIdle(idle).ids(ids);
+        return RedisContextHolder.execute(
+                RedisContextHolder.getDefaultConnectionFactory(),
+                connection -> connection.streamCommands().xClaim(topic.getBytes(), group, consumer, options)
+        );
+    }
+
+    /**
+     * 消息消费确认
+     *
+     * @param topic 消息主题
+     * @param group 消费者组
+     * @param ids   消息记录ID数组
+     */
+    public static void ack(@NonNull String topic, @NonNull String group, @NonNull RecordId... ids) {
+        if (ObjectUtils.notEmpty(ids)) {
+            getStreamTemplate().opsForStream().acknowledge(topic, group, ids);
+        }
+    }
+
+    /**
+     * 删除消费者
+     *
+     * @param topic 消息主题
+     * @param ids   消息记录ID数组
+     */
+    public static void delete(@NonNull String topic, @NonNull RecordId... ids) {
+        if (ObjectUtils.notEmpty(ids)) {
+            getStreamTemplate().opsForStream().delete(topic, ids);
+        }
+    }
+
+    /**
+     * 删除消费者
+     *
+     * @param topic    消息主题
+     * @param consumer 消费者信息
+     */
+    public static void delete(@NonNull String topic,
+                              @NonNull org.springframework.data.redis.connection.stream.Consumer consumer) {
+        getStreamTemplate().opsForStream().deleteConsumer(topic, consumer);
+    }
+
+    /**
+     * 发送心跳包
+     *
+     * @param topic 消息主题
+     */
+    public static void heartbeat(@NonNull String topic) {
+        List<String> keys = Collections.singletonList(topic);
+        getStreamTemplate().execute(getAddScript(), keys, PAYLOAD, StringUtils.EMPTY);
+    }
+
+    /**
+     * 判断消息是否是心跳包
+     *
+     * @param record 消息记录
+     * @return true/false
+     */
+    public static boolean isHeartbeat(MapRecord<String, String, String> record) {
+        Map<String, String> value = ObjectUtils.ifNull(record, MapRecord::getValue);
+        return ObjectUtils.isEmpty(value) || StringUtils.isEmpty(value.get(PAYLOAD));
+    }
+
+    /**
+     * 消费消息
+     *
+     * @param record   消息记录
+     * @param type     消息类型
+     * @param consumer 消息消费者
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> void consume(@NonNull MapRecord<String, String, String> record, @NonNull Type type,
+                                   @NonNull Consumer<T> consumer) {
+        Map<String, String> value = record.getValue();
+        T payload = (T) JacksonUtils.deserialize(value.get(PAYLOAD), type);
+        Session session = JacksonUtils.deserialize(value.get(Session.NAMING), Session.class);
+        SessionContextHolder.setSession(session);
+        try {
+            consumer.accept(payload);
+        } finally {
+            SessionContextHolder.clearSessionContext();
+        }
+    }
+
+    /**
+     * 获取消费者信息
+     *
+     * @param topic 消息主题
+     * @param group 消费者组
+     * @return 消费者信息
+     */
+    public static StreamInfo.XInfoConsumers consumers(@NonNull String topic, @NonNull String group) {
+        return getStreamTemplate().opsForStream().consumers(topic, group);
+    }
+
+    /**
+     * 获取消息延时时间戳
+     *
+     * @param record 消息记录
+     * @return 时间戳
+     */
+    public static Long getMessageDelaying(@NonNull MapRecord<String, String, String> record) {
+        String delaying = ObjectUtils.ifNull(record.getValue(), value -> value.get(DELAYING));
+        return StringUtils.ifEmpty(delaying, Long::parseLong);
+    }
+
+    /**
+     * 初始化消费者组
+     *
+     * @param topic 消息主题
+     * @param group 消费者组
+     */
+    public static void initializeConsumerGroup(@NonNull String topic, @NonNull String group) {
+        try {
+            getStreamTemplate().opsForStream().createGroup(topic, group);
+        } catch (RedisSystemException e) {
+            if (StringUtils.isEmpty(e.getMessage()) || !e.getMessage().contains("BUSYGROUP")) {
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * 初始化消息监听器容器
+     *
+     * @param listener 消息监听器
+     * @param consumer 消费者信息
+     * @param offset   消息偏移信息
+     * @param batch    批量获取消息数量
+     * @param executor 消息获取执行器
+     * @return 消息监听器容器
+     */
+    public static StreamMessageListenerContainer<String, MapRecord<String, String, String>>
+    initializeMessageListenerContainer(@NonNull StreamListener<String, MapRecord<String, String, String>> listener,
+                                       @NonNull org.springframework.data.redis.connection.stream.Consumer consumer,
+                                       @NonNull StreamOffset<String> offset, int batch,
+                                       @NonNull java.util.concurrent.Executor executor) {
+        return initializeMessageListenerContainer(listener, consumer, offset, batch, executor, null);
+    }
+
+    /**
+     * 初始化消息监听器容器
+     *
+     * @param listener     消息监听器
+     * @param consumer     消费者信息
+     * @param offset       消息偏移信息
+     * @param batch        批量获取消息数量
+     * @param executor     消息获取执行器
+     * @param errorHandler 异常处理器
+     * @return 消息监听器容器
+     */
+    public static StreamMessageListenerContainer<String, MapRecord<String, String, String>>
+    initializeMessageListenerContainer(@NonNull StreamListener<String, MapRecord<String, String, String>> listener,
+                                       @NonNull org.springframework.data.redis.connection.stream.Consumer consumer,
+                                       @NonNull StreamOffset<String> offset, int batch,
+                                       @NonNull java.util.concurrent.Executor executor, ErrorHandler errorHandler) {
+        StreamMessageListenerContainer.
+                StreamMessageListenerContainerOptionsBuilder<String, MapRecord<String, String, String>>
+                builder = StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
+                .batchSize(batch).pollTimeout(Duration.ZERO).executor(executor).serializer(RedisSerializer.string());
+        if (errorHandler != null) {
+            builder.errorHandler(errorHandler);
+        }
+        RedisConnectionFactory connectionFactory = RedisContextHolder.getDefaultConnectionFactory();
+        StreamMessageListenerContainer<String, MapRecord<String, String, String>> container =
+                StreamMessageListenerContainer.create(connectionFactory, builder.build());
+        container.receive(consumer, offset, listener);
+        return container;
+    }
+}

+ 243 - 0
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisUserDailyHashHolder.java

@@ -0,0 +1,243 @@
+package com.chelvc.framework.redis.context;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import lombok.NonNull;
+import org.springframework.data.redis.core.RedisTemplate;
+
+/**
+ * Redis Hash结构当日数据操作工具类
+ *
+ * @author Woody
+ * @date 2024/6/29
+ */
+public final class RedisUserDailyHashHolder {
+    /**
+     * 数据类型
+     */
+    public static final String TYPE = "user";
+
+    private RedisUserDailyHashHolder() {
+    }
+
+    /**
+     * 获取Redis Hash结构用户当日数据
+     *
+     * @param id  数据标识
+     * @param key Hash键
+     * @return Hash值
+     */
+    public static Object get(@NonNull Serializable id, @NonNull String key) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.get(TYPE, id, key);
+    }
+
+    /**
+     * 获取Redis Hash结构用户当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param key      Hash键
+     * @return Hash值
+     */
+    public static Object get(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                             @NonNull String key) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.get(template, TYPE, id, key);
+    }
+
+    /**
+     * 获取Redis Hash结构用户当日数据
+     *
+     * @param id   数据标识
+     * @param keys Hash键数组
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull Serializable id, @NonNull String... keys) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.get(TYPE, id, keys);
+    }
+
+    /**
+     * 获取Redis Hash结构用户当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param keys     Hash键数组
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                              @NonNull String... keys) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.get(template, TYPE, id, keys);
+    }
+
+    /**
+     * 获取Redis Hash结构用户当日数据
+     *
+     * @param id   数据标识
+     * @param keys Hash键集合
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull Serializable id, @NonNull Collection<String> keys) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.get(TYPE, id, keys);
+    }
+
+    /**
+     * 获取Redis Hash结构用户当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param keys     Hash键集合
+     * @return Hash值列表
+     */
+    public static List<?> get(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                              @NonNull Collection<String> keys) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.get(template, TYPE, id, keys);
+    }
+
+    /**
+     * 设置Redis Hash结构用户当日数据
+     *
+     * @param id    数据标识
+     * @param array Hash键/值数组
+     * @return true/false
+     */
+    public static boolean set(@NonNull Serializable id, @NonNull Object... array) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.set(TYPE, id, array);
+    }
+
+    /**
+     * 设置Redis Hash结构用户当日数据
+     *
+     * @param id  数据标识
+     * @param map Hash键/值映射表
+     * @return true/false
+     */
+    public static boolean set(@NonNull Serializable id, @NonNull Map<String, ?> map) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.set(TYPE, id, map);
+    }
+
+    /**
+     * 设置Redis Hash结构用户当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param array    Hash键/值数组
+     * @return true/false
+     */
+    public static boolean set(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                              @NonNull Object... array) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.set(template, TYPE, id, array);
+    }
+
+    /**
+     * 设置Redis Hash结构用户当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param map      Hash键/值映射表
+     * @return true/false
+     */
+    public static boolean set(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                              @NonNull Map<String, ?> map) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.set(template, TYPE, id, map);
+    }
+
+    /**
+     * Redis Hash结构用户当日数字自增1
+     *
+     * @param id  数据标识
+     * @param key Hash键
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull Serializable id, @NonNull String key) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.increment(TYPE, id, key);
+    }
+
+    /**
+     * Redis Hash结构用户当日数字自增/减,delta < 0 时自减
+     *
+     * @param id    数据标识
+     * @param key   Hash键
+     * @param delta 增/减值
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull Serializable id, @NonNull String key, long delta) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.increment(TYPE, id, key, delta);
+    }
+
+    /**
+     * Redis Hash结构用户当日数字自增1
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param key      Hash键
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                                 @NonNull String key) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.increment(template, TYPE, id, key);
+    }
+
+    /**
+     * Redis Hash结构用户当日数字自增/减,delta < 0 时自减
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param key      Hash键
+     * @param delta    增/减值
+     * @return 增/减后数值
+     */
+    public static long increment(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                                 @NonNull String key, long delta) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.increment(template, TYPE, id, key, delta);
+    }
+
+    /**
+     * Redis Hash结构删除用户当日数据
+     *
+     * @param id   数据标识
+     * @param keys Hash键数组
+     * @return 成功删除数量
+     */
+    public static long delete(Serializable id, @NonNull String... keys) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.delete(TYPE, id, keys);
+    }
+
+    /**
+     * Redis Hash结构删除用户当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param keys     Hash键数组
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                              @NonNull String... keys) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.delete(template, TYPE, id, keys);
+    }
+
+    /**
+     * Redis Hash结构删除用户当日数据
+     *
+     * @param id   数据标识
+     * @param keys Hash键集合
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull Serializable id, @NonNull Collection<String> keys) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.delete(TYPE, id, keys);
+    }
+
+    /**
+     * Redis Hash结构删除用户当日数据
+     *
+     * @param template Redis操作模版实例
+     * @param id       数据标识
+     * @param keys     Hash键集合
+     * @return 成功删除数量
+     */
+    public static long delete(@NonNull RedisTemplate<String, ?> template, @NonNull Serializable id,
+                              @NonNull Collection<String> keys) {
+        return com.chelvc.framework.redis.context.RedisDailyHashHolder.delete(template, TYPE, id, keys);
+    }
+}

+ 3 - 2
framework-redis/src/main/java/com/chelvc/framework/redis/stream/ConsumerPendingCleaner.java

@@ -7,6 +7,7 @@ import java.util.concurrent.TimeUnit;
 
 import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.redis.context.RedisContextHolder;
+import com.chelvc.framework.redis.context.RedisStreamHolder;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.connection.stream.Consumer;
 import org.springframework.data.redis.connection.stream.MapRecord;
@@ -52,9 +53,9 @@ public class ConsumerPendingCleaner<T> {
 
         // 初始化并启动PEL数据转移消息监听器容器
         this.timestamp = System.currentTimeMillis();
-        this.container = RedisContextHolder.initializeMessageListenerContainer(
+        this.container = RedisStreamHolder.initializeMessageListenerContainer(
                 record -> {
-                    RedisContextHolder.claim(this.topic, this.target, record.getId());
+                    RedisStreamHolder.claim(this.topic, this.target, record.getId());
                     listener.onMessage(record);
                     timestamp = System.currentTimeMillis();
                 },

+ 13 - 14
framework-redis/src/main/java/com/chelvc/framework/redis/stream/DefaultRedisMQListenerContainer.java

@@ -14,7 +14,7 @@ import com.chelvc.framework.common.function.Executor;
 import com.chelvc.framework.common.util.AssertUtils;
 import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.redis.annotation.RedisMQConsumer;
-import com.chelvc.framework.redis.context.RedisContextHolder;
+import com.chelvc.framework.redis.context.RedisStreamHolder;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.core.env.Environment;
@@ -38,6 +38,7 @@ public class DefaultRedisMQListenerContainer<T> implements RedisMQListenerContai
     private final String name = StringUtils.uuid();
     private String topic;
     private String group;
+    private int idle;
     private int batch;
     private Consumer consumer;
     private ExecutorService executor;
@@ -53,14 +54,14 @@ public class DefaultRedisMQListenerContainer<T> implements RedisMQListenerContai
     private void clear() {
         // 发送心跳包
         try {
-            RedisContextHolder.heartbeat(this.topic);
+            RedisStreamHolder.heartbeat(this.topic);
         } catch (Throwable t) {
             log.warn("Consumer heartbeat message send failed: {}, {}", this.consumer, t.getMessage());
         }
 
         // 执行消费者清理逻辑
-        int idle = RedisContextHolder.getStreamIdle() * 2 * 1000;
-        StreamInfo.XInfoConsumers consumers = RedisContextHolder.consumers(this.topic, this.group);
+        int idle = this.idle * 2 * 1000;
+        StreamInfo.XInfoConsumers consumers = RedisStreamHolder.consumers(this.topic, this.group);
         for (int i = 0, length = consumers.size(); i < length; i++) {
             StreamInfo.XInfoConsumer consumer = consumers.get(i);
             if (Objects.equals(consumer.consumerName(), this.name) || consumer.idleTimeMs() < idle) {
@@ -68,7 +69,7 @@ public class DefaultRedisMQListenerContainer<T> implements RedisMQListenerContai
             }
             Consumer source = Consumer.from(consumer.groupName(), consumer.consumerName());
             if (consumer.pendingCount() == 0) {
-                RedisContextHolder.delete(this.topic, source);
+                RedisStreamHolder.delete(this.topic, source);
                 log.info("Consumer deleted: {}", source);
                 continue;
             }
@@ -92,13 +93,11 @@ public class DefaultRedisMQListenerContainer<T> implements RedisMQListenerContai
                            @NonNull RedisMQListener<T> listener, @NonNull ExecutorService executor) {
         AssertUtils.nonempty(topic, () -> "Consumer topic must not be empty");
         AssertUtils.nonempty(group, () -> "Consumer group must not be empty");
-        AssertUtils.check(batch > 0, () -> "Consumer batch must be greater than 0");
-        int idle = RedisContextHolder.getStreamIdle();
-        AssertUtils.check(idle > 0, () -> "Consumer idle must be greater than 0");
+        AssertUtils.check((this.batch = batch) > 0, () -> "Consumer batch must be greater than 0");
+        AssertUtils.check((this.idle = RedisStreamHolder.getIdle()) > 0, () -> "Consumer idle must be greater than 0");
         Environment environment = ApplicationContextHolder.getEnvironment();
-        this.topic = RedisContextHolder.isolate(environment.resolvePlaceholders(topic));
-        this.group = RedisContextHolder.isolate(environment.resolvePlaceholders(group));
-        this.batch = batch;
+        this.topic = RedisStreamHolder.isolate(environment.resolvePlaceholders(topic));
+        this.group = RedisStreamHolder.isolate(environment.resolvePlaceholders(group));
         this.consumer = Consumer.from(this.group, this.name);
         this.offset = StreamOffset.create(this.topic, ReadOffset.lastConsumed());
         this.listener = new MessageStreamListener<>(
@@ -117,10 +116,10 @@ public class DefaultRedisMQListenerContainer<T> implements RedisMQListenerContai
             }
 
             // 初始化消费者组
-            RedisContextHolder.initializeConsumerGroup(this.topic, this.group);
+            RedisStreamHolder.initializeConsumerGroup(this.topic, this.group);
 
             // 初始化消息监听器容器
-            this.container = RedisContextHolder.initializeMessageListenerContainer(
+            this.container = RedisStreamHolder.initializeMessageListenerContainer(
                     this.listener, this.consumer, this.offset, this.batch, this.executor,
                     t -> {
                         if (!(t instanceof RedisSystemException) || StringUtils.isEmpty(t.getMessage())
@@ -140,7 +139,7 @@ public class DefaultRedisMQListenerContainer<T> implements RedisMQListenerContai
 
         // 初始化消费者清理定时器
         this.cleaner = Executors.newScheduledThreadPool(1);
-        this.cleaner.scheduleAtFixedRate(this::clear, 0, idle, TimeUnit.SECONDS);
+        this.cleaner.scheduleAtFixedRate(this::clear, 0, this.idle, TimeUnit.SECONDS);
     }
 
     @Override

+ 14 - 14
framework-redis/src/main/java/com/chelvc/framework/redis/stream/MessageStreamListener.java

@@ -7,7 +7,7 @@ import java.util.concurrent.Executor;
 
 import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.ThreadUtils;
-import com.chelvc.framework.redis.context.RedisContextHolder;
+import com.chelvc.framework.redis.context.RedisStreamHolder;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.connection.stream.Consumer;
 import org.springframework.data.redis.connection.stream.MapRecord;
@@ -56,7 +56,7 @@ public class MessageStreamListener<T> implements StreamListener<String, MapRecor
             while (!Thread.currentThread().isInterrupted()) {
                 // 批量获取延时消息(100条),为避免重复获取将消息延时时间增加1分钟
                 String timestamp = String.valueOf(System.currentTimeMillis());
-                List<String> messages = RedisContextHolder.getStringTemplate().execute(
+                List<String> messages = RedisStreamHolder.getStreamTemplate().execute(
                         RANGE_AND_INCREMENT_SCRIPT, keys, "-1", timestamp, "60000", "100"
                 );
 
@@ -80,13 +80,13 @@ public class MessageStreamListener<T> implements StreamListener<String, MapRecor
     private void processing(String message) {
         this.executor.execute(() -> {
             try {
-                MapRecord<String, String, String> record = RedisContextHolder.deserialize(message);
-                if (RedisContextHolder.isHeartbeat(record)) {
+                MapRecord<String, String, String> record = RedisStreamHolder.deserialize(message);
+                if (RedisStreamHolder.isHeartbeat(record)) {
                     // 如果是心跳消息则删除相关数据
-                    RedisContextHolder.delete(this.topic, record.getId());
-                    RedisContextHolder.getStringTemplate().opsForZSet().remove(this.delayed, message);
+                    RedisStreamHolder.delete(this.topic, record.getId());
+                    RedisStreamHolder.getStreamTemplate().opsForZSet().remove(this.delayed, message);
                 } else if (this.processing(record)) {
-                    RedisContextHolder.getStringTemplate().opsForZSet().remove(this.delayed, message);
+                    RedisStreamHolder.getStreamTemplate().opsForZSet().remove(this.delayed, message);
                 }
             } catch (Throwable t) {
                 log.error("Message consume failed: {}, {}", this.consumer, message, t);
@@ -103,7 +103,7 @@ public class MessageStreamListener<T> implements StreamListener<String, MapRecor
     private boolean processing(MapRecord<String, String, String> record) {
         // 消费消息
         try {
-            RedisContextHolder.consume(record, this.type, this.listener::consume);
+            RedisStreamHolder.consume(record, this.type, this.listener::consume);
         } catch (Throwable t) {
             log.error("Message consume failed: {}, {}", this.consumer, record, t);
             return false;
@@ -120,8 +120,8 @@ public class MessageStreamListener<T> implements StreamListener<String, MapRecor
      */
     private boolean delaying(MapRecord<String, String, String> record, long timestamp) {
         try {
-            String message = RedisContextHolder.serialize(record);
-            RedisContextHolder.getStringTemplate().opsForZSet().add(this.delayed, message, timestamp);
+            String message = RedisStreamHolder.serialize(record);
+            RedisStreamHolder.getStreamTemplate().opsForZSet().add(this.delayed, message, timestamp);
         } catch (Throwable t) {
             log.error("Message delaying failed: {}, {}", this.consumer, record, t);
             return false;
@@ -133,14 +133,14 @@ public class MessageStreamListener<T> implements StreamListener<String, MapRecor
     public void onMessage(MapRecord<String, String, String> record) {
         this.executor.execute(() -> {
             // 如果是心跳消息则延时3秒后删除该消息,以防止同一topic的心跳消息立即删除后其他消费组无法消费到心跳消息的问题
-            if (RedisContextHolder.isHeartbeat(record)) {
+            if (RedisStreamHolder.isHeartbeat(record)) {
                 this.delaying(record, System.currentTimeMillis() + 3000);
-                RedisContextHolder.ack(this.topic, this.consumer.getGroup(), record.getId());
+                RedisStreamHolder.ack(this.topic, this.consumer.getGroup(), record.getId());
                 return;
             }
 
             boolean success;
-            Long delaying = RedisContextHolder.getMessageDelaying(record);
+            Long delaying = RedisStreamHolder.getMessageDelaying(record);
             if (delaying != null && delaying - System.currentTimeMillis() >= 1000) {
                 // 消息延时处理
                 success = this.delaying(record, delaying);
@@ -150,7 +150,7 @@ public class MessageStreamListener<T> implements StreamListener<String, MapRecor
             }
             if (success) {
                 // 消息消费确认
-                RedisContextHolder.ack(this.topic, this.consumer.getGroup(), record.getId());
+                RedisStreamHolder.ack(this.topic, this.consumer.getGroup(), record.getId());
             }
         });
     }