Commit e198d988b1243da85c349d9d43b0d1600d88d2f3

Authored by neilz
2 parents 52d4d9fd d6cc9e23

Merge branch 'master' of gitee.com:zhijiantianya/ruoyi-vue-pro into feature/ut-auth

... ... @@ -22,15 +22,15 @@
22 22 <maven.compiler.target>${java.version}</maven.compiler.target>
23 23 <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
24 24 <!-- 统一依赖管理 -->
25   - <spring.boot.version>2.4.2</spring.boot.version>
  25 + <spring.boot.version>2.4.4</spring.boot.version>
26 26 <!-- Web 相关 -->
27 27 <knife4j.version>3.0.2</knife4j.version>
28 28 <swagger-annotations.version>1.5.22</swagger-annotations.version>
29 29 <!-- DB 相关 -->
30 30 <mysql-connector-java.version>5.1.46</mysql-connector-java.version>
31 31 <druid.version>1.2.4</druid.version>
32   - <mybatis-plus.version>3.4.1</mybatis-plus.version>
33   - <redisson.version>3.14.1</redisson.version>
  32 + <mybatis-plus.version>3.4.2</mybatis-plus.version>
  33 + <redisson.version>3.15.1</redisson.version>
34 34 <!-- Config 配置中心相关 -->
35 35 <apollo.version>1.7.0</apollo.version>
36 36 <!-- 服务保障相关 -->
... ... @@ -42,7 +42,7 @@
42 42 <!-- 工具类相关 -->
43 43 <lombok.version>1.16.14</lombok.version>
44 44 <mapstruct.version>1.4.1.Final</mapstruct.version>
45   - <hutool.version>5.5.6</hutool.version>
  45 + <hutool.version>5.6.1</hutool.version>
46 46 <easyexcel.verion>2.2.7</easyexcel.verion>
47 47 <velocity.version>2.2</velocity.version>
48 48 <screw.version>1.0.5</screw.version>
... ... @@ -249,27 +249,7 @@
249 249  
250 250 <dependency>
251 251 <groupId>cn.hutool</groupId>
252   - <artifactId>hutool-core</artifactId>
253   - <version>${hutool.version}</version>
254   - </dependency>
255   - <dependency>
256   - <groupId>cn.hutool</groupId>
257   - <artifactId>hutool-extra</artifactId>
258   - <version>${hutool.version}</version>
259   - </dependency>
260   - <dependency>
261   - <groupId>cn.hutool</groupId>
262   - <artifactId>hutool-captcha</artifactId>
263   - <version>${hutool.version}</version>
264   - </dependency>
265   - <dependency>
266   - <groupId>cn.hutool</groupId>
267   - <artifactId>hutool-http</artifactId>
268   - <version>${hutool.version}</version>
269   - </dependency>
270   - <dependency>
271   - <groupId>cn.hutool</groupId>
272   - <artifactId>hutool-crypto</artifactId>
  252 + <artifactId>hutool-all</artifactId>
273 253 <version>${hutool.version}</version>
274 254 </dependency>
275 255  
... ...
src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java
1 1 package cn.iocoder.dashboard.framework.redis.config;
2 2  
  3 +import cn.hutool.system.SystemUtil;
3 4 import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
  5 +import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
4 6 import lombok.extern.slf4j.Slf4j;
5 7 import org.springframework.context.annotation.Bean;
6 8 import org.springframework.context.annotation.Configuration;
7 9 import org.springframework.data.redis.connection.RedisConnectionFactory;
  10 +import org.springframework.data.redis.connection.stream.Consumer;
  11 +import org.springframework.data.redis.connection.stream.ObjectRecord;
  12 +import org.springframework.data.redis.connection.stream.ReadOffset;
  13 +import org.springframework.data.redis.connection.stream.StreamOffset;
8 14 import org.springframework.data.redis.core.RedisTemplate;
9 15 import org.springframework.data.redis.listener.ChannelTopic;
10 16 import org.springframework.data.redis.listener.RedisMessageListenerContainer;
11 17 import org.springframework.data.redis.serializer.RedisSerializer;
  18 +import org.springframework.data.redis.stream.StreamMessageListenerContainer;
12 19  
13 20 import java.util.List;
14 21  
... ... @@ -19,6 +26,9 @@ import java.util.List;
19 26 @Slf4j
20 27 public class RedisConfig {
21 28  
  29 + /**
  30 + * 创建 RedisTemplate Bean,使用 JSON 序列化方式
  31 + */
22 32 @Bean
23 33 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
24 34 // 创建 RedisTemplate 对象
... ... @@ -27,11 +37,16 @@ public class RedisConfig {
27 37 template.setConnectionFactory(factory);
28 38 // 使用 String 序列化方式,序列化 KEY 。
29 39 template.setKeySerializer(RedisSerializer.string());
  40 + template.setHashKeySerializer(RedisSerializer.string());
30 41 // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
31 42 template.setValueSerializer(RedisSerializer.json());
  43 + template.setHashValueSerializer(RedisSerializer.json());
32 44 return template;
33 45 }
34 46  
  47 + /**
  48 + * 创建 Redis Pub/Sub 广播消费的容器
  49 + */
35 50 @Bean
36 51 public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory,
37 52 List<AbstractChannelMessageListener<?>> listeners) {
... ... @@ -48,4 +63,51 @@ public class RedisConfig {
48 63 return container;
49 64 }
50 65  
  66 + /**
  67 + * 创建 Redis Stream 集群消费的容器
  68 + *
  69 + * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
  70 + */
  71 + @Bean(initMethod = "start", destroyMethod = "stop")
  72 + public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(RedisTemplate<String, Object> redisTemplate,
  73 + List<AbstractStreamMessageListener<?>> listeners) {
  74 + // 第一步,创建 StreamMessageListenerContainer 容器
  75 + // 创建 options 配置
  76 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
  77 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
  78 + .batchSize(10) // 一次性最多拉取多少条消息
  79 + .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化
  80 + .build();
  81 + // 创建 container 对象
  82 + StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer.create(
  83 + redisTemplate.getRequiredConnectionFactory(), containerOptions);
  84 +
  85 + // 第二步,注册监听器,消费对应的 Stream 主题
  86 +// String consumerName = buildConsumerName();
  87 + String consumerName = "110";
  88 + listeners.forEach(listener -> {
  89 + // 创建 listener 对应的消费者分组
  90 + try {
  91 + redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
  92 + } catch (Exception ignore) {}
  93 + // 设置 listener 对应的 redisTemplate
  94 + listener.setRedisTemplate(redisTemplate);
  95 + // 创建 Consumer 对象
  96 + Consumer consumer = Consumer.from(listener.getGroup(), consumerName);
  97 + // 设置 Consumer 消费进度,以最小消费进度为准
  98 + StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed());
  99 + // 设置 Consumer 监听
  100 + StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
  101 + .builder(streamOffset).consumer(consumer)
  102 + .autoAcknowledge(false) // 不自动 ack
  103 + .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
  104 + container.register(builder.build(), listener);
  105 + });
  106 + return container;
  107 + }
  108 +
  109 + private static String buildConsumerName() {
  110 + return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
  111 + }
  112 +
51 113 }
... ...
src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java
1 1 package cn.iocoder.dashboard.framework.redis.core.pubsub;
2 2  
3   -import cn.hutool.core.util.ArrayUtil;
  3 +import cn.hutool.core.util.TypeUtil;
4 4 import cn.iocoder.dashboard.util.json.JsonUtils;
5 5 import lombok.SneakyThrows;
6 6 import org.springframework.data.redis.connection.Message;
7 7 import org.springframework.data.redis.connection.MessageListener;
8   -import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
9 8  
10 9 import java.lang.reflect.Type;
11 10  
... ... @@ -62,21 +61,11 @@ public abstract class AbstractChannelMessageListener&lt;T extends ChannelMessage&gt; i
62 61 */
63 62 @SuppressWarnings("unchecked")
64 63 private Class<T> getMessageClass() {
65   - Class<?> targetClass = getClass();
66   - while (targetClass.getSuperclass() != null) {
67   - // 如果不是 AbstractMessageListener 父类,继续向上查找
68   - if (targetClass.getSuperclass() != AbstractChannelMessageListener.class) {
69   - targetClass = targetClass.getSuperclass();
70   - continue;
71   - }
72   - // 如果是 AbstractMessageListener 父类,则解析泛型
73   - Type[] types = ((ParameterizedTypeImpl) targetClass.getGenericSuperclass()).getActualTypeArguments();
74   - if (ArrayUtil.isEmpty(types)) {
75   - throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
76   - }
77   - return (Class<T>) types[0];
  64 + Type type = TypeUtil.getTypeArgument(getClass(), 0);
  65 + if (type == null) {
  66 + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
78 67 }
79   - throw new IllegalStateException(String.format("类型(%s) 找不到 AbstractMessageListener 父类", getClass().getName()));
  68 + return (Class<T>) type;
80 69 }
81 70  
82 71 }
... ...
src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java
... ... @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
4 4  
5 5 /**
6 6 * Redis Channel Message 接口
  7 + *
  8 + * @author 芋道源码
7 9 */
8 10 public interface ChannelMessage {
9 11  
... ... @@ -12,7 +14,7 @@ public interface ChannelMessage {
12 14 *
13 15 * @return Channel
14 16 */
15   - @JsonIgnore // 必须序列化
  17 + @JsonIgnore // 避免序列化
16 18 String getChannel();
17 19  
18 20 }
... ...
src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.redis.core.stream;
  2 +
  3 +import cn.hutool.core.util.TypeUtil;
  4 +import cn.iocoder.dashboard.util.json.JsonUtils;
  5 +import lombok.Getter;
  6 +import lombok.Setter;
  7 +import lombok.SneakyThrows;
  8 +import org.springframework.beans.factory.annotation.Value;
  9 +import org.springframework.data.redis.connection.stream.ObjectRecord;
  10 +import org.springframework.data.redis.core.RedisTemplate;
  11 +import org.springframework.data.redis.stream.StreamListener;
  12 +
  13 +import java.lang.reflect.Type;
  14 +
  15 +/**
  16 + * Redis Stream 监听器抽象类,用于实现集群消费
  17 + *
  18 + * @param <T> 消息类型。一定要填写噢,不然会报错
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +public abstract class AbstractStreamMessageListener<T extends StreamMessage>
  23 + implements StreamListener<String, ObjectRecord<String, String>> {
  24 +
  25 + /**
  26 + * 消息类型
  27 + */
  28 + private final Class<T> messageType;
  29 + /**
  30 + * Redis Channel
  31 + */
  32 + @Getter
  33 + private final String streamKey;
  34 +
  35 + /**
  36 + * Redis 消费者分组,默认使用 spring.application.name 名字
  37 + */
  38 + @Value("${spring.application.name}")
  39 + @Getter
  40 + private String group;
  41 + /**
  42 + *
  43 + */
  44 + @Setter
  45 + private RedisTemplate<String, ?> redisTemplate;
  46 +
  47 + @SneakyThrows
  48 + protected AbstractStreamMessageListener() {
  49 + this.messageType = getMessageClass();
  50 + this.streamKey = messageType.newInstance().getStreamKey();
  51 + }
  52 +
  53 + @Override
  54 + public void onMessage(ObjectRecord<String, String> message) {
  55 + // 消费消息
  56 + T messageObj = JsonUtils.parseObject(message.getValue(), messageType);
  57 + this.onMessage(messageObj);
  58 + // ack 消息消费完成
  59 + redisTemplate.opsForStream().acknowledge(group, message);
  60 + // TODO 芋艿:需要额外考虑以下几个点:
  61 + // 1. 处理异常的情况
  62 + // 2. 发送日志;以及事务的结合
  63 + // 3. 消费日志;以及通用的幂等性
  64 + // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638
  65 + }
  66 +
  67 + /**
  68 + * 处理消息
  69 + *
  70 + * @param message 消息
  71 + */
  72 + public abstract void onMessage(T message);
  73 +
  74 + /**
  75 + * 通过解析类上的泛型,获得消息类型
  76 + *
  77 + * @return 消息类型
  78 + */
  79 + @SuppressWarnings("unchecked")
  80 + private Class<T> getMessageClass() {
  81 + Type type = TypeUtil.getTypeArgument(getClass(), 0);
  82 + if (type == null) {
  83 + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
  84 + }
  85 + return (Class<T>) type;
  86 + }
  87 +
  88 +}
... ...
src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.redis.core.stream;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +
  5 +/**
  6 + * Redis Stream Message 接口
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +public interface StreamMessage {
  11 +
  12 + /**
  13 + * 获得 Redis Stream Key
  14 + *
  15 + * @return Channel
  16 + */
  17 + @JsonIgnore // 避免序列化
  18 + String getStreamKey();
  19 +
  20 +}
... ...
src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java
1 1 package cn.iocoder.dashboard.framework.redis.core.util;
2 2  
3 3 import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
  4 +import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage;
4 5 import cn.iocoder.dashboard.util.json.JsonUtils;
  6 +import org.springframework.data.redis.connection.stream.RecordId;
  7 +import org.springframework.data.redis.connection.stream.StreamRecords;
5 8 import org.springframework.data.redis.core.RedisTemplate;
6 9  
7 10 /**
... ... @@ -17,8 +20,21 @@ public class RedisMessageUtils {
17 20 * @param redisTemplate Redis 操作模板
18 21 * @param message 消息
19 22 */
20   - public static <T extends ChannelMessage> void sendChannelMessage(RedisTemplate<?, ?> redisTemplate, T message) {
  23 + public static <T extends ChannelMessage> void sendChannelMessage(RedisTemplate<?, ?> redisTemplate, T message) {
21 24 redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
22 25 }
23 26  
  27 + /**
  28 + * 发送 Redis 消息,基于 Redis Stream 实现
  29 + *
  30 + * @param redisTemplate Redis 操作模板
  31 + * @param message 消息
  32 + * @return 消息记录的编号对象
  33 + */
  34 + public static <T extends StreamMessage> RecordId sendStreamMessage(RedisTemplate<String, ?> redisTemplate, T message) {
  35 + return redisTemplate.opsForStream().add(StreamRecords.newRecord()
  36 + .ofObject(JsonUtils.toJsonString(message)) // 设置内容
  37 + .withStreamKey(message.getStreamKey())); // 设置 stream key
  38 + }
  39 +
24 40 }
... ...
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/permission/SysRoleMenuMapper.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
4 4 import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleMenuDO;
5 5 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
6 6 import org.apache.ibatis.annotations.Mapper;
  7 +import org.apache.ibatis.annotations.Select;
7 8  
8 9 import java.util.Collection;
9 10 import java.util.Date;
... ... @@ -32,7 +33,7 @@ public interface SysRoleMenuMapper extends BaseMapperX&lt;SysRoleMenuDO&gt; {
32 33 delete(new QueryWrapper<SysRoleMenuDO>().eq("role_id", roleId)
33 34 .in("menu_id", menuIds));
34 35 }
35   -
  36 +
36 37 default void deleteListByMenuId(Long menuId) {
37 38 delete(new QueryWrapper<SysRoleMenuDO>().eq("menu_id", menuId));
38 39 }
... ... @@ -41,9 +42,7 @@ public interface SysRoleMenuMapper extends BaseMapperX&lt;SysRoleMenuDO&gt; {
41 42 delete(new QueryWrapper<SysRoleMenuDO>().eq("role_id", roleId));
42 43 }
43 44  
44   - default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) {
45   - return selectOne(new QueryWrapper<SysRoleMenuDO>().select("id")
46   - .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null;
47   - }
  45 + @Select("SELECT id FROM sys_role_menu WHERE update_time > #{maxUpdateTime} LIMIT 1")
  46 + Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
48 47  
49 48 }
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/mail/SysMailSendConsumer.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.consumer.mail;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
  4 +import cn.iocoder.dashboard.modules.system.mq.message.mail.SysMailSendMessage;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.stereotype.Component;
  7 +
  8 +@Component
  9 +@Slf4j
  10 +public class SysMailSendConsumer extends AbstractStreamMessageListener<SysMailSendMessage> {
  11 +
  12 + @Override
  13 + public void onMessage(SysMailSendMessage message) {
  14 + log.info("[onMessage][消息内容({})]", message);
  15 + }
  16 +
  17 +}
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsSendConsumer.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
  4 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.stereotype.Component;
  7 +
  8 +@Component
  9 +@Slf4j
  10 +public class SysSmsSendConsumer extends AbstractStreamMessageListener<SysSmsSendMessage> {
  11 +
  12 + @Override
  13 + public void onMessage(SysSmsSendMessage message) {
  14 + log.info("[onMessage][消息内容({})]", message);
  15 + }
  16 +
  17 +}
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/mail/SysMailSendMessage.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.message.mail;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage;
  4 +import lombok.Data;
  5 +
  6 +import javax.validation.constraints.NotNull;
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * 邮箱发送消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Data
  15 +public class SysMailSendMessage implements StreamMessage {
  16 +
  17 + /**
  18 + * 邮箱地址
  19 + */
  20 + @NotNull(message = "邮箱地址不能为空")
  21 + private String address;
  22 + /**
  23 + * 短信模板编号
  24 + */
  25 + @NotNull(message = "短信模板编号不能为空")
  26 + private String templateCode;
  27 + /**
  28 + * 短信模板参数
  29 + */
  30 + private Map<String, Object> templateParams;
  31 +
  32 + /**
  33 + * 用户编号,允许空
  34 + */
  35 + private Integer userId;
  36 + /**
  37 + * 用户类型,允许空
  38 + */
  39 + private Integer userType;
  40 +
  41 + @Override
  42 + public String getStreamKey() {
  43 + return "system.mail.send";
  44 + }
  45 +
  46 +}
... ...
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsSendMessage.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.message.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage;
  4 +import lombok.Data;
  5 +
  6 +import javax.validation.constraints.NotNull;
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * 短信发送消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Data
  15 +public class SysSmsSendMessage implements StreamMessage {
  16 +
  17 + /**
  18 + * 手机号
  19 + */
  20 + @NotNull(message = "手机号不能为空")
  21 + private String mobile;
  22 + /**
  23 + * 短信模板编号
  24 + */
  25 + @NotNull(message = "短信模板编号不能为空")
  26 + private String templateCode;
  27 + /**
  28 + * 短信模板参数
  29 + */
  30 + private Map<String, Object> templateParams;
  31 +
  32 + /**
  33 + * 用户编号,允许空
  34 + */
  35 + private Integer userId;
  36 + /**
  37 + * 用户类型,允许空
  38 + */
  39 + private Integer userType;
  40 +
  41 + @Override
  42 + public String getStreamKey() {
  43 + return "system.sms.send";
  44 + }
  45 +
  46 +}
... ...
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java
... ... @@ -86,6 +86,7 @@ public class SysPermissionServiceImpl implements SysPermissionService {
86 86 @Override
87 87 @PostConstruct
88 88 public void initLocalCache() {
  89 + Date now = new Date();
89 90 // 获取角色与菜单的关联列表,如果有更新
90 91 List<SysRoleMenuDO> roleMenuList = this.loadRoleMenuIfUpdate(maxUpdateTime);
91 92 if (CollUtil.isEmpty(roleMenuList)) {
... ... @@ -102,7 +103,7 @@ public class SysPermissionServiceImpl implements SysPermissionService {
102 103 roleMenuCache = roleMenuCacheBuilder.build();
103 104 menuRoleCache = menuRoleCacheBuilder.build();
104 105 assert roleMenuList.size() > 0; // 断言,避免告警
105   - maxUpdateTime = roleMenuList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
  106 + maxUpdateTime = now;
106 107 log.info("[initLocalCache][初始化角色与菜单的关联数量为 {}]", roleMenuList.size());
107 108 }
108 109  
... ... @@ -123,7 +124,7 @@ public class SysPermissionServiceImpl implements SysPermissionService {
123 124 if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
124 125 log.info("[loadRoleMenuIfUpdate][首次加载全量角色与菜单的关联]");
125 126 } else { // 判断数据库中是否有更新的角色与菜单的关联
126   - if (!roleMenuMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) {
  127 + if (Objects.isNull(roleMenuMapper.selectExistsByUpdateTimeAfter(maxUpdateTime))) {
127 128 return null;
128 129 }
129 130 log.info("[loadRoleMenuIfUpdate][增量加载全量角色与菜单的关联]");
... ...
src/test-integration/java/cn/iocoder/dashboard/BaseRedisIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.dashboard;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.config.RedisConfig;
  4 +import org.redisson.spring.starter.RedissonAutoConfiguration;
  5 +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
  6 +import org.springframework.boot.test.context.SpringBootTest;
  7 +import org.springframework.context.annotation.Import;
  8 +import org.springframework.test.context.ActiveProfiles;
  9 +
  10 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class)
  11 +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
  12 +public class BaseRedisIntegrationTest {
  13 +
  14 + @Import({
  15 + // Redis 配置类
  16 + RedisAutoConfiguration.class, // Spring Redis 自动配置类
  17 + RedisConfig.class, // 自己的 Redis 配置类
  18 + RedissonAutoConfiguration.class, // Redisson 自动高配置类
  19 + })
  20 + public static class Application {
  21 + }
  22 +
  23 +}
... ...
src/test-integration/java/cn/iocoder/dashboard/framework/redis/core/stream/RedisStreamTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.redis.core.stream;
  2 +
  3 +import cn.hutool.core.thread.ThreadUtil;
  4 +import cn.iocoder.dashboard.BaseRedisIntegrationTest;
  5 +import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils;
  6 +import cn.iocoder.dashboard.modules.system.mq.consumer.mail.SysMailSendConsumer;
  7 +import cn.iocoder.dashboard.modules.system.mq.consumer.sms.SysSmsSendConsumer;
  8 +import cn.iocoder.dashboard.modules.system.mq.message.mail.SysMailSendMessage;
  9 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
  10 +import org.junit.jupiter.api.Test;
  11 +import org.springframework.context.annotation.Import;
  12 +import org.springframework.data.redis.core.RedisTemplate;
  13 +import org.springframework.data.redis.core.StringRedisTemplate;
  14 +
  15 +import javax.annotation.Resource;
  16 +import java.util.concurrent.TimeUnit;
  17 +
  18 +public class RedisStreamTest {
  19 +
  20 + @Import({SysSmsSendConsumer.class, SysMailSendConsumer.class})
  21 + public static class ConsumerTest extends BaseRedisIntegrationTest {
  22 +
  23 + @Test
  24 + public void testConsumer() {
  25 + ThreadUtil.sleep(1, TimeUnit.DAYS);
  26 + }
  27 +
  28 + }
  29 +
  30 + public static class ProducerTest extends BaseRedisIntegrationTest {
  31 +
  32 + @Resource
  33 + private StringRedisTemplate stringRedisTemplate;
  34 +
  35 + @Resource
  36 + private RedisTemplate<String, Object> redisTemplate;
  37 +
  38 + @Test
  39 + public void testProducer01() {
  40 + for (int i = 0; i < 100; i++) {
  41 + // 创建消息
  42 + SysSmsSendMessage message = new SysSmsSendMessage();
  43 + message.setMobile("15601691300").setTemplateCode("test:" + i);
  44 + // 发送消息
  45 + RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message);
  46 + }
  47 + }
  48 +
  49 + @Test
  50 + public void testProducer02() {
  51 + // 创建消息
  52 + SysMailSendMessage message = new SysMailSendMessage();
  53 + message.setAddress("fangfang@mihayou.com").setTemplateCode("test");
  54 + // 发送消息
  55 + RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message);
  56 + }
  57 +
  58 + }
  59 +
  60 +}
... ...
src/test-integration/resources/application-integration-test.yaml 0 → 100644
  1 +spring:
  2 + main:
  3 + lazy-initialization: true # 开启懒加载,加快速度
  4 + banner-mode: off # 单元测试,禁用 Banner
  5 +
  6 +--- #################### 数据库相关配置 ####################
  7 +
  8 +spring:
  9 + # 数据源配置项
  10 + datasource:
  11 + name: ruoyi-vue-pro
  12 + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
  13 + driver-class-name: org.h2.Driver
  14 + username: sa
  15 + password:
  16 + schema: classpath:sql/create_tables.sql # MySQL 转 H2 的语句,使用 https://www.jooq.org/translate/ 工具
  17 + druid:
  18 + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
  19 + initial-size: 1 # 单元测试,配置为 1,提升启动速度
  20 +
  21 + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
  22 + redis:
  23 + host: 127.0.0.1 # 地址
  24 + port: 6379 # 端口(单元测试,使用 16379 端口)
  25 + database: 0 # 数据库索引
  26 +
  27 +mybatis:
  28 + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
  29 +
  30 +--- #################### 定时任务相关配置 ####################
  31 +
  32 +--- #################### 配置中心相关配置 ####################
  33 +
  34 +--- #################### 服务保障相关配置 ####################
  35 +
  36 +# Lock4j 配置项(单元测试,禁用 Lock4j)
  37 +
  38 +# Resilience4j 配置项
  39 +resilience4j:
  40 + ratelimiter:
  41 + instances:
  42 + backendA:
  43 + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50
  44 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500
  45 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s
  46 + register-health-indicator: true # 是否注册到健康监测
  47 +
  48 +--- #################### 监控相关配置 ####################
  49 +
  50 +--- #################### 芋道相关配置 ####################
  51 +
  52 +# 芋道配置项,设置当前项目所有自定义的配置
  53 +yudao:
  54 + info:
  55 + version: 1.0.0
  56 + base-package: cn.iocoder.dashboard
  57 + web:
  58 + api-prefix: /api
  59 + controller-package: ${yudao.info.base-package}
  60 + security:
  61 + token-header: Authorization
  62 + token-secret: abcdefghijklmnopqrstuvwxyz
  63 + token-timeout: 1d
  64 + session-timeout: 30m
  65 + mock-enable: true
  66 + mock-secret: test
  67 + swagger:
  68 + enable: false # 单元测试,禁用 Swagger
  69 + captcha:
  70 + timeout: 5m
  71 + width: 160
  72 + height: 60
  73 + file:
  74 + base-path: http://127.0.0.1:${server.port}/${yudao.web.api-prefix}/file/get/
  75 + codegen:
  76 + base-package: ${yudao.info.base-package}.modules
  77 + db-schemas: ${spring.datasource.name}
  78 + xss:
  79 + enable: false
  80 + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系
  81 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
  82 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
... ...
src/test/java/cn/iocoder/dashboard/modules/system/service/dept/SysDeptServiceTest.java
... ... @@ -72,7 +72,6 @@ class SysDeptServiceTest extends BaseDbUnitTest {
72 72 // 断言 maxUpdateTime 缓存
73 73 Date maxUpdateTime = (Date) getFieldValue(deptService, "maxUpdateTime");
74 74 assertEquals(ObjectUtils.max(deptDO1.getUpdateTime(), deptDO2.getUpdateTime()), maxUpdateTime);
75   -
76 75 }
77 76  
78 77 @Test
... ...