Commit e198d988b1243da85c349d9d43b0d1600d88d2f3
Merge branch 'master' of gitee.com:zhijiantianya/ruoyi-vue-pro into feature/ut-auth
Showing
17 changed files
with
498 additions
and
51 deletions
pom.xml
| ... | ... | @@ -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<T extends ChannelMessage> 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<SysRoleMenuDO> { |
| 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<SysRoleMenuDO> { |
| 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 | ... | ... |