diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml
index 1b7ba6515..e4d010fb5 100644
--- a/yudao-dependencies/pom.xml
+++ b/yudao-dependencies/pom.xml
@@ -203,6 +203,12 @@
${revision}
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-websocket
+ ${revision}
+
+
org.springdoc
springdoc-openapi-ui
diff --git a/yudao-framework/pom.xml b/yudao-framework/pom.xml
index c2e7d6123..577ff56fd 100644
--- a/yudao-framework/pom.xml
+++ b/yudao-framework/pom.xml
@@ -43,6 +43,7 @@
yudao-spring-boot-starter-flowable
yudao-spring-boot-starter-captcha
+ yudao-spring-boot-starter-websocket
yudao-spring-boot-starter-desensitize
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
index 919173da6..2d3232978 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
@@ -280,6 +280,15 @@ public class CollectionUtils {
return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
}
+ public static List convertListByFlatMap(Collection from,
+ Function super T, ? extends U> mapper,
+ Function> func) {
+ if (CollUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return from.stream().map(mapper).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
public static Set convertSetByFlatMap(Collection from,
Function> func) {
if (CollUtil.isEmpty(from)) {
@@ -288,4 +297,13 @@ public class CollectionUtils {
return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
}
+ public static Set convertSetByFlatMap(Collection from,
+ Function super T, ? extends U> mapper,
+ Function> func) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashSet<>();
+ }
+ return from.stream().map(mapper).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+ }
+
}
diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
similarity index 92%
rename from yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java
rename to yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
index bbc63b719..d02e84b14 100644
--- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java
+++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
@@ -5,7 +5,6 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
-import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
@@ -23,7 +22,6 @@ import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
@@ -33,30 +31,19 @@ import java.util.List;
import java.util.Properties;
/**
- * 消息队列配置类
+ * Redis 消息队列 Consumer 配置类
*
* @author 芋道源码
*/
@Slf4j
@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
-public class YudaoRedisMQAutoConfiguration {
-
- @Bean
- public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
- List interceptors) {
- RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
- // 添加拦截器
- interceptors.forEach(redisMQTemplate::addInterceptor);
- return redisMQTemplate;
- }
-
- // ========== 消费者相关 ==========
+public class YudaoRedisMQConsumerAutoConfiguration {
/**
* 创建 Redis Pub/Sub 广播消费的容器
*/
- @Bean(initMethod = "start", destroyMethod = "stop")
+ @Bean
@ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisMQTemplate redisMQTemplate, List> listeners) {
diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java
new file mode 100644
index 000000000..c1950c489
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.mq.redis.config;
+
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.List;
+
+/**
+ * Redis 消息队列 Producer 配置类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
+public class YudaoRedisMQProducerAutoConfiguration {
+
+ @Bean
+ public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
+ List interceptors) {
+ RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
+ // 添加拦截器
+ interceptors.forEach(redisMQTemplate::addInterceptor);
+ return redisMQTemplate;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index f0f201707..4b98f6110 100644
--- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,2 +1,3 @@
-cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQAutoConfiguration
+cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQProducerAutoConfiguration
+cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration
cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration
diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index 0bc54d532..6a76024d5 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.github.yulichang.base.MPJBaseMapper;
+import com.github.yulichang.interfaces.MPJBaseJoin;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
@@ -39,6 +40,13 @@ public interface BaseMapperX extends MPJBaseMapper {
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
+ default PageResult selectJoinPage(PageParam pageParam, Class resultTypeClass, MPJBaseJoin joinQueryWrapper) {
+ IPage mpPage = MyBatisUtils.buildPage(pageParam);
+ selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
+ // 转换返回
+ return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+ }
+
default T selectOne(String field, Object value) {
return selectOne(new QueryWrapper().eq(field, value));
}
diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
index 2560dcc0b..3d19f32a6 100644
--- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
+++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
@@ -19,6 +19,13 @@ public class SecurityProperties {
*/
@NotEmpty(message = "Token Header 不能为空")
private String tokenHeader = "Authorization";
+ /**
+ * HTTP 请求时,访问令牌的请求参数
+ *
+ * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
+ */
+ @NotEmpty(message = "Token Parameter 不能为空")
+ private String tokenParameter = "token";
/**
* mock 模式的开关
@@ -41,5 +48,4 @@ public class SecurityProperties {
* PasswordEncoder 加密复杂度,越高开销越大
*/
private Integer passwordEncoderLength = 4;
-
}
diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
index 6efd48340..1bc13cd3e 100644
--- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
+++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
@@ -48,7 +48,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
// 情况二,基于 Token 获得用户
// 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。
if (loginUser == null) {
- String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
+ String token = SecurityFrameworkUtils.obtainAuthorization(request,
+ securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotEmpty(token)) {
Integer userType = WebFrameworkUtils.getLoginUserType(request);
try {
@@ -82,7 +83,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
return null;
}
// 用户类型不匹配,无权限
- if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
+ // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
+ // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
+ if (userType != null
+ && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
throw new AccessDeniedException("错误的用户类型");
}
// 构建登录用户
diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
index 3fa9c0004..8607c31aa 100644
--- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
+++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.security.core.util;
+import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import org.springframework.lang.Nullable;
@@ -20,6 +21,9 @@ import java.util.Collections;
*/
public class SecurityFrameworkUtils {
+ /**
+ * HEADER 认证头 value 的前缀
+ */
public static final String AUTHORIZATION_BEARER = "Bearer";
public static final String LOGIN_USER_HEADER = "login-user";
@@ -30,19 +34,23 @@ public class SecurityFrameworkUtils {
* 从请求中,获得认证 Token
*
* @param request 请求
- * @param header 认证 Token 对应的 Header 名字
+ * @param headerName 认证 Token 对应的 Header 名字
+ * @param parameterName 认证 Token 对应的 Parameter 名字
* @return 认证 Token
*/
- public static String obtainAuthorization(HttpServletRequest request, String header) {
- String authorization = request.getHeader(header);
- if (!StringUtils.hasText(authorization)) {
+ public static String obtainAuthorization(HttpServletRequest request,
+ String headerName, String parameterName) {
+ // 1. 获得 Token。优先级:Header > Parameter
+ String token = request.getHeader(headerName);
+ if (StrUtil.isEmpty(token)) {
+ token = request.getParameter(parameterName);
+ }
+ if (!StringUtils.hasText(token)) {
return null;
}
- int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
- if (index == -1) { // 未找到
- return null;
- }
- return authorization.substring(index + 7).trim();
+ // 2. 去除 Token 中带的 Bearer
+ int index = token.indexOf(AUTHORIZATION_BEARER + " ");
+ return index >= 0 ? token.substring(index + 7).trim() : token;
}
/**
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml b/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
new file mode 100644
index 000000000..8fb9cc875
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
@@ -0,0 +1,84 @@
+
+
+
+ cn.iocoder.cloud
+ yudao-framework
+ ${revision}
+
+ 4.0.0
+ yudao-spring-boot-starter-websocket
+ jar
+
+ ${project.artifactId}
+ WebSocket 框架,支持多节点的广播
+ https://github.com/YunaiV/ruoyi-vue-pro
+
+
+
+
+ cn.iocoder.cloud
+ yudao-common
+
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-security
+ provided
+
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-security
+ provided
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-mq
+
+
+ org.springframework.kafka
+ spring-kafka
+ true
+
+
+ org.springframework.amqp
+ spring-rabbit
+ true
+
+
+ org.apache.rocketmq
+ rocketmq-spring-boot-starter
+ true
+
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-biz-tenant
+ provided
+
+
+
+
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
new file mode 100644
index 000000000..aa618fb04
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.websocket.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * WebSocket 配置项
+ *
+ * @author xingyu4j
+ */
+@ConfigurationProperties("yudao.websocket")
+@Data
+@Validated
+public class WebSocketProperties {
+
+ /**
+ * WebSocket 的连接路径
+ */
+ @NotEmpty(message = "WebSocket 的连接路径不能为空")
+ private String path = "/ws";
+
+ /**
+ * 消息发送器的类型
+ *
+ * 可选值:local、redis、rocketmq、kafka、rabbitmq
+ */
+ @NotNull(message = "WebSocket 的消息发送者不能为空")
+ private String senderType = "local";
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
new file mode 100644
index 000000000..0f08b7cf5
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
@@ -0,0 +1,177 @@
+package cn.iocoder.yudao.framework.websocket.config;
+
+import cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration;
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
+import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.List;
+
+/**
+ * WebSocket 自动配置
+ *
+ * @author xingyu4j
+ */
+@AutoConfiguration(before = YudaoRedisMQConsumerAutoConfiguration.class) // before YudaoRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer
+@EnableWebSocket // 开启 websocket
+@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) // 允许使用 yudao.websocket.enable=false 禁用 websocket
+@EnableConfigurationProperties(WebSocketProperties.class)
+public class YudaoWebSocketAutoConfiguration {
+
+ @Bean
+ public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
+ WebSocketHandler webSocketHandler,
+ WebSocketProperties webSocketProperties) {
+ return registry -> registry
+ // 添加 WebSocketHandler
+ .addHandler(webSocketHandler, webSocketProperties.getPath())
+ .addInterceptors(handshakeInterceptors)
+ // 允许跨域,否则前端连接会直接断开
+ .setAllowedOriginPatterns("*");
+ }
+
+ @Bean
+ public HandshakeInterceptor handshakeInterceptor() {
+ return new LoginUserHandshakeInterceptor();
+ }
+
+ @Bean
+ public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
+ List extends WebSocketMessageListener>> messageListeners) {
+ // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
+ JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
+ // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
+ return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
+ }
+
+ @Bean
+ public WebSocketSessionManager webSocketSessionManager() {
+ return new WebSocketSessionManagerImpl();
+ }
+
+ // ==================== Sender 相关 ====================
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
+ public class LocalWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
+ return new LocalWebSocketMessageSender(sessionManager);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
+ public class RedisWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
+ RedisMQTemplate redisMQTemplate) {
+ return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
+ }
+
+ @Bean
+ public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
+ RedisWebSocketMessageSender redisWebSocketMessageSender) {
+ return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true)
+ public class RocketMQWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
+ WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
+ @Value("${yudao.websocket.sender-rocketmq.topic}") String topic) {
+ return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
+ }
+
+ @Bean
+ public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
+ RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
+ return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true)
+ public class RabbitMQWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
+ WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
+ TopicExchange websocketTopicExchange) {
+ return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
+ }
+
+ @Bean
+ public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
+ RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
+ return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
+ }
+
+ /**
+ * 创建 Topic Exchange
+ */
+ @Bean
+ public TopicExchange websocketTopicExchange(@Value("${yudao.websocket.sender-rabbitmq.exchange}") String exchange) {
+ return new TopicExchange(exchange,
+ true, // durable: 是否持久化
+ false); // exclusive: 是否排它
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true)
+ public class KafkaWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
+ WebSocketSessionManager sessionManager, KafkaTemplate
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-websocket
+
+
cn.iocoder.cloud
@@ -101,6 +106,10 @@
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-mq
+
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java
new file mode 100644
index 000000000..53c98dd09
--- /dev/null
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.infra.api.websocket;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.module.infra.api.websocket.dto.WebSocketSendReqDTO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@RestController // 提供 RESTful API 接口,给 Feign 调用
+@Validated
+public class WebSocketSenderApiImpl implements WebSocketSenderApi {
+
+ @Resource
+ private WebSocketMessageSender webSocketMessageSender;
+
+ @Override
+ public CommonResult send(WebSocketSendReqDTO message) {
+ if (StrUtil.isNotEmpty(message.getSessionId())) {
+ webSocketMessageSender.send(message.getSessionId(),
+ message.getMessageType(), message.getMessageContent());
+ } else if (message.getUserType() != null && message.getUserId() != null) {
+ webSocketMessageSender.send(message.getUserType(), message.getUserId(),
+ message.getMessageType(), message.getMessageContent());
+ } else if (message.getUserType() != null) {
+ webSocketMessageSender.send(message.getUserType(),
+ message.getMessageType(), message.getMessageContent());
+ }
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java
index 13402c7d6..b529c4981 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java
@@ -64,7 +64,7 @@ public class CodegenBuilder {
*/
public static final String TENANT_ID_FIELD = "tenantId";
/**
- * {@link BaseDO} 的字段
+ * {@link cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO} 的字段
*/
public static final Set BASE_DO_FIELDS = new HashSet<>();
/**
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java
new file mode 100644
index 000000000..9ccf6070e
--- /dev/null
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.infra.websocket;
+
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
+import cn.iocoder.yudao.module.infra.websocket.message.DemoReceiveMessage;
+import cn.iocoder.yudao.module.infra.websocket.message.DemoSendMessage;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketSession;
+
+import javax.annotation.Resource;
+
+/**
+ * WebSocket 示例:单发消息
+ *
+ * @author 芋道源码
+ */
+@Component
+public class DemoWebSocketMessageListener implements WebSocketMessageListener {
+
+ @Resource
+ private WebSocketMessageSender webSocketMessageSender;
+
+ @Override
+ public void onMessage(WebSocketSession session, DemoSendMessage message) {
+ Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session);
+ // 情况一:单发
+ if (message.getToUserId() != null) {
+ DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId)
+ .setText(message.getText()).setSingle(true);
+ webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getToUserId(), // 给指定用户
+ "demo-message-receive", toMessage);
+ return;
+ }
+ // 情况二:群发
+ DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId)
+ .setText(message.getText()).setSingle(false);
+ webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), // 给所有用户
+ "demo-message-receive", toMessage);
+ }
+
+ @Override
+ public String getType() {
+ return "demo-message-send";
+ }
+
+}
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoReceiveMessage.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoReceiveMessage.java
new file mode 100644
index 000000000..03a246cf9
--- /dev/null
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoReceiveMessage.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.infra.websocket.message;
+
+import lombok.Data;
+
+/**
+ * 示例:server -> client 同步消息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class DemoReceiveMessage {
+
+ /**
+ * 接收人的编号
+ */
+ private Long fromUserId;
+ /**
+ * 内容
+ */
+ private String text;
+
+ /**
+ * 是否单聊
+ */
+ private Boolean single;
+
+}
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoSendMessage.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoSendMessage.java
new file mode 100644
index 000000000..f0c14f5d3
--- /dev/null
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoSendMessage.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.infra.websocket.message;
+
+import lombok.Data;
+
+/**
+ * 示例:client -> server 发送消息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class DemoSendMessage {
+
+ /**
+ * 发送给谁
+ *
+ * 如果为空,说明发送给所有人
+ */
+ private Long toUserId;
+ /**
+ * 内容
+ */
+ private String text;
+
+}
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-dev.yaml b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-dev.yaml
index 357acc316..3e6c2130e 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-dev.yaml
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-dev.yaml
@@ -60,6 +60,21 @@ spring:
--- #################### MQ 消息队列相关配置 ####################
+# rocketmq 配置项,对应 RocketMQProperties 配置类
+rocketmq:
+ name-server: 127.0.0.1:9876 # RocketMQ Namesrv
+
+spring:
+ # RabbitMQ 配置项,对应 RabbitProperties 配置类
+ rabbitmq:
+ host: 127.0.0.1 # RabbitMQ 服务的地址
+ port: 5672 # RabbitMQ 服务的端口
+ username: guest # RabbitMQ 服务的账号
+ password: guest # RabbitMQ 服务的密码
+ # Kafka 配置项,对应 KafkaProperties 配置类
+ kafka:
+ bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
+
--- #################### 定时任务相关配置 ####################
xxl:
job:
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-local.yaml b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-local.yaml
index 896a7b3d2..af3b2cfa5 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-local.yaml
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-local.yaml
@@ -75,6 +75,21 @@ spring:
--- #################### MQ 消息队列相关配置 ####################
+# rocketmq 配置项,对应 RocketMQProperties 配置类
+rocketmq:
+ name-server: 127.0.0.1:9876 # RocketMQ Namesrv
+
+spring:
+ # RabbitMQ 配置项,对应 RabbitProperties 配置类
+ rabbitmq:
+ host: 127.0.0.1 # RabbitMQ 服务的地址
+ port: 5672 # RabbitMQ 服务的端口
+ username: guest # RabbitMQ 服务的账号
+ password: guest # RabbitMQ 服务的密码
+ # Kafka 配置项,对应 KafkaProperties 配置类
+ kafka:
+ bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
+
--- #################### 定时任务相关配置 ####################
xxl:
job:
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application.yaml b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application.yaml
index b6aa6fe2e..cd9000a40 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application.yaml
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application.yaml
@@ -72,7 +72,31 @@ spring:
--- #################### RPC 远程调用相关配置 ####################
---- #################### MQ 消息队列相关配置 ####################
+--- #################### 消息队列相关 ####################
+
+# rocketmq 配置项,对应 RocketMQProperties 配置类
+rocketmq:
+ # Producer 配置项
+ producer:
+ group: ${spring.application.name}_PRODUCER # 生产者分组
+
+spring:
+ # Kafka 配置项,对应 KafkaProperties 配置类
+ kafka:
+ # Kafka Producer 配置项
+ producer:
+ acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
+ retries: 3 # 发送失败时,重试发送的次数
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
+ # Kafka Consumer 配置项
+ consumer:
+ auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解
+ value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
+ properties:
+ spring.json.trusted.packages: '*'
+ # Kafka Consumer Listener 监听器配置
+ listener:
+ missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错
--- #################### 定时任务相关配置 ####################
@@ -92,6 +116,19 @@ yudao:
web:
admin-ui:
url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址
+ websocket:
+ enable: true # websocket的开关
+ path: /infra/ws # 路径
+ sender-type: redis # 消息发送的类型,可选值为 local、redis、rocketmq、kafka、rabbitmq
+ sender-rocketmq:
+ topic: ${spring.application.name}-websocket # 消息发送的 RocketMQ Topic
+ consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 RocketMQ Consumer Group
+ sender-rabbitmq:
+ exchange: ${spring.application.name}-websocket-exchange # 消息发送的 RabbitMQ Exchange
+ queue: ${spring.application.name}-websocket-queue # 消息发送的 RabbitMQ Queue
+ sender-kafka:
+ topic: ${spring.application.name}-websocket # 消息发送的 Kafka Topic
+ consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 Kafka Consumer Group
swagger:
title: 管理后台
description: 提供管理员管理的所有功能
diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java
index 0eaa0763d..96cafaa2b 100644
--- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java
+++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java
@@ -53,7 +53,8 @@ public class AppAuthController {
@PermitAll
@Operation(summary = "登出系统")
public CommonResult logout(HttpServletRequest request) {
- String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
+ String token = SecurityFrameworkUtils.obtainAuthorization(request,
+ securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotBlank(token)) {
authService.logout(token);
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
index c2eae8d38..061539494 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
@@ -38,7 +39,6 @@ import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization;
@Tag(name = "管理后台 - 认证")
@RestController
@@ -76,7 +76,8 @@ public class AuthController {
@Operation(summary = "登出系统")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult logout(HttpServletRequest request) {
- String token = obtainAuthorization(request, securityProperties.getTokenHeader());
+ String token = SecurityFrameworkUtils.obtainAuthorization(request,
+ securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotBlank(token)) {
authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType());
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java
index 0e1957785..5a566702f 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java
@@ -1,12 +1,16 @@
package cn.iocoder.yudao.module.system.controller.admin.notice;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeRespVO;
import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO;
import cn.iocoder.yudao.module.system.convert.notice.NoticeConvert;
+import cn.iocoder.yudao.module.system.dal.dataobject.notice.NoticeDO;
import cn.iocoder.yudao.module.system.service.notice.NoticeService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
@@ -29,6 +33,9 @@ public class NoticeController {
@Resource
private NoticeService noticeService;
+ @Resource
+ private WebSocketSenderApi webSocketSenderApi;
+
@PostMapping("/create")
@Operation(summary = "创建通知公告")
@PreAuthorize("@ss.hasPermission('system:notice:create')")
@@ -69,4 +76,16 @@ public class NoticeController {
return success(NoticeConvert.INSTANCE.convert(noticeService.getNotice(id)));
}
+ @PostMapping("/push")
+ @Operation(summary = "推送通知公告", description = "只发送给 websocket 连接在线的用户")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('system:notice:update')")
+ public CommonResult push(@RequestParam("id") Long id) {
+ NoticeDO notice = noticeService.getNotice(id);
+ Assert.notNull(notice, "公告不能为空");
+ // 通过 websocket 推送给在线的用户
+ webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), "notice-push", notice);
+ return success(true);
+ }
+
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/rpc/config/RpcConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/rpc/config/RpcConfiguration.java
index 73b7f8ccd..57ba6740a 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/rpc/config/RpcConfiguration.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/rpc/config/RpcConfiguration.java
@@ -1,10 +1,11 @@
package cn.iocoder.yudao.module.system.framework.rpc.config;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
+import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
-@EnableFeignClients(clients = FileApi.class)
+@EnableFeignClients(clients = {FileApi.class, WebSocketSenderApi.class})
public class RpcConfiguration {
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/resources/application.yaml b/yudao-module-system/yudao-module-system-biz/src/main/resources/application.yaml
index 81c7f4d45..ad27d8c6a 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/resources/application.yaml
+++ b/yudao-module-system/yudao-module-system-biz/src/main/resources/application.yaml
@@ -72,8 +72,6 @@ spring:
--- #################### RPC 远程调用相关配置 ####################
---- #################### MQ 消息队列相关配置 ####################
-
--- #################### 消息队列相关 ####################
# rocketmq 配置项,对应 RocketMQProperties 配置类