feat: 字典实时同步和 websocket 重构优化
This commit is contained in:
@@ -173,7 +173,6 @@ INSERT INTO `sys_menu` VALUES (84, 6, '0,1,6', '字典删除', 4, NULL, '', NULL
|
|||||||
INSERT INTO `sys_menu` VALUES (88, 2, '0,1,2', '重置密码', 4, NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (88, 2, '0,1,2', '重置密码', 4, NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (89, 0, '0', '功能演示', 2, NULL, '/function', 'Layout', NULL, NULL, NULL, 1, 12, 'menu', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (89, 0, '0', '功能演示', 2, NULL, '/function', 'Layout', NULL, NULL, NULL, 1, 12, 'menu', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, NULL, '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, NULL, '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (91, 89, '0,89', '敬请期待...', 2, NULL, 'other/:id', 'demo/other', NULL, NULL, NULL, 1, 4, '', '', now(), now(), NULL);
|
|
||||||
INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, NULL, 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, NULL, 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, NULL, 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, NULL, 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (102, 26, '0,26', 'document', 3, '', 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (102, 26, '0,26', 'document', 3, '', 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
|
||||||
@@ -214,6 +213,7 @@ INSERT INTO `sys_menu` VALUES (144, 26, '0,26', '后端文档', 3, NULL, 'https:
|
|||||||
INSERT INTO `sys_menu` VALUES (145, 26, '0,26', '移动端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 4, 'document', '', '2024-10-05 23:36:03', '2024-10-05 23:36:03', NULL);
|
INSERT INTO `sys_menu` VALUES (145, 26, '0,26', '移动端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 4, 'document', '', '2024-10-05 23:36:03', '2024-10-05 23:36:03', NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (146, 36, '0,36', '拖拽组件', 1, NULL, 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', '2025-03-31 14:14:45', '2025-03-31 14:14:52', NULL);
|
INSERT INTO `sys_menu` VALUES (146, 36, '0,36', '拖拽组件', 1, NULL, 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', '2025-03-31 14:14:45', '2025-03-31 14:14:52', NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (147, 36, '0,36', '滚动文本', 1, NULL, 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', '2025-03-31 14:14:49', '2025-03-31 14:14:56', NULL);
|
INSERT INTO `sys_menu` VALUES (147, 36, '0,36', '滚动文本', 1, NULL, 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', '2025-03-31 14:14:49', '2025-03-31 14:14:56', NULL);
|
||||||
|
INSERT INTO `sys_menu` VALUES (148, 89, '0,89', '字典实时同步', 1, NULL, 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', '2025-03-31 14:14:49', '2025-03-31 14:14:56', NULL);
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for sys_role
|
-- Table structure for sys_role
|
||||||
@@ -351,6 +351,7 @@ INSERT INTO `sys_role_menu` VALUES (2, 144);
|
|||||||
INSERT INTO `sys_role_menu` VALUES (2, 145);
|
INSERT INTO `sys_role_menu` VALUES (2, 145);
|
||||||
INSERT INTO `sys_role_menu` VALUES (2, 146);
|
INSERT INTO `sys_role_menu` VALUES (2, 146);
|
||||||
INSERT INTO `sys_role_menu` VALUES (2, 147);
|
INSERT INTO `sys_role_menu` VALUES (2, 147);
|
||||||
|
INSERT INTO `sys_role_menu` VALUES (2, 148);
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for sys_user
|
-- Table structure for sys_user
|
||||||
@@ -559,4 +560,4 @@ INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0);
|
|||||||
INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0);
|
INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0);
|
||||||
INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
|
INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package com.youlai.boot.config;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.youlai.boot.core.security.model.SysUserDetails;
|
import com.youlai.boot.core.security.model.SysUserDetails;
|
||||||
import com.youlai.boot.core.security.token.TokenManager;
|
import com.youlai.boot.core.security.token.TokenManager;
|
||||||
import com.youlai.boot.system.event.UserConnectionEvent;
|
import com.youlai.boot.system.service.WebSocketService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.MessageChannel;
|
import org.springframework.messaging.MessageChannel;
|
||||||
@@ -27,144 +27,143 @@ import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
|||||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebSocket 配置
|
* WebSocket配置
|
||||||
*
|
*
|
||||||
* @author Ray.Hao
|
* @author Ray.Hao
|
||||||
* @since 2.4.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递
|
|
||||||
@EnableWebSocketMessageBroker
|
@EnableWebSocketMessageBroker
|
||||||
@Configuration
|
@Configuration
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final TokenManager tokenManager;
|
||||||
|
private final WebSocketService webSocketService;
|
||||||
|
|
||||||
private final TokenManager tokenManager;
|
public WebSocketConfig(TokenManager tokenManager, @Lazy WebSocketService webSocketService) {
|
||||||
|
this.tokenManager = tokenManager;
|
||||||
|
this.webSocketService = webSocketService;
|
||||||
|
}
|
||||||
|
|
||||||
public WebSocketConfig(ApplicationEventPublisher eventPublisher, TokenManager tokenManager) {
|
/**
|
||||||
this.eventPublisher = eventPublisher;
|
* 注册一个端点,客户端通过这个端点进行连接
|
||||||
this.tokenManager = tokenManager;
|
*/
|
||||||
}
|
@Override
|
||||||
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
/**
|
registry
|
||||||
* 注册一个端点,客户端通过这个端点进行连接
|
// 注册 /ws 的端点
|
||||||
*/
|
.addEndpoint("/ws")
|
||||||
@Override
|
// 允许跨域
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
.setAllowedOriginPatterns("*");
|
||||||
registry
|
}
|
||||||
// 注册 /ws 的端点
|
|
||||||
.addEndpoint("/ws")
|
|
||||||
// 允许跨域
|
|
||||||
.setAllowedOriginPatterns("*");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置消息代理
|
* 配置消息代理
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||||
// 客户端发送消息的请求前缀
|
// 客户端发送消息的请求前缀
|
||||||
registry.setApplicationDestinationPrefixes("/app");
|
registry.setApplicationDestinationPrefixes("/app");
|
||||||
|
|
||||||
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
|
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
|
||||||
registry.enableSimpleBroker("/topic", "/queue");
|
registry.enableSimpleBroker("/topic", "/queue");
|
||||||
|
|
||||||
// 服务端通知客户端的前缀,可以不设置,默认为user
|
// 服务端通知客户端的前缀,可以不设置,默认为user
|
||||||
registry.setUserDestinationPrefix("/user");
|
registry.setUserDestinationPrefix("/user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置客户端入站通道拦截器
|
* 配置客户端入站通道拦截器
|
||||||
* <p>
|
* <p>
|
||||||
* 核心功能:
|
* 核心功能:
|
||||||
* 1. 连接建立时解析令牌并绑定用户身份
|
* 1. 连接建立时解析令牌并绑定用户身份
|
||||||
* 2. 连接关闭时触发下线通知
|
* 2. 连接关闭时触发下线通知
|
||||||
* 3. 异常Token的防御性处理
|
* 3. 异常Token的防御性处理
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||||
registration.interceptors(new ChannelInterceptor() {
|
registration.interceptors(new ChannelInterceptor() {
|
||||||
@Override
|
@Override
|
||||||
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
|
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
|
||||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||||
if (accessor == null) {
|
if (accessor == null) {
|
||||||
return ChannelInterceptor.super.preSend(message, channel);
|
return ChannelInterceptor.super.preSend(message, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 处理客户端连接请求
|
// 处理客户端连接请求
|
||||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||||
/*
|
/*
|
||||||
* 安全校验流程:
|
* 安全校验流程:
|
||||||
* 1. 从HEADER中获取Authorization值
|
* 1. 从HEADER中获取Authorization值
|
||||||
* 2. 校验Bearer Token格式合法性
|
* 2. 校验Bearer Token格式合法性
|
||||||
* 3. 解析并验证JWT有效性
|
* 3. 解析并验证JWT有效性
|
||||||
* 4. 绑定用户身份到当前会话
|
* 4. 绑定用户身份到当前会话
|
||||||
*/
|
*/
|
||||||
String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
|
String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
// 防御性校验:确保Authorization头存在且格式正确
|
// 防御性校验:确保Authorization头存在且格式正确
|
||||||
if (StrUtil.isBlank(authorization) || !authorization.startsWith("Bearer ")) {
|
if (StrUtil.isBlank(authorization) || !authorization.startsWith("Bearer ")) {
|
||||||
log.warn("非法连接请求:缺少有效的Authorization头");
|
log.warn("非法连接请求:缺少有效的Authorization头");
|
||||||
throw new AuthenticationCredentialsNotFoundException("Missing authorization header");
|
throw new AuthenticationCredentialsNotFoundException("Missing authorization header");
|
||||||
}
|
|
||||||
|
|
||||||
// 提取并处理JWT令牌(移除Bearer前缀)
|
|
||||||
String token = authorization.substring(7);
|
|
||||||
Authentication authentication = tokenManager.parseToken(token);
|
|
||||||
|
|
||||||
// 令牌解析失败处理
|
|
||||||
if (authentication == null) {
|
|
||||||
log.error("令牌解析失败:{}", token);
|
|
||||||
throw new BadCredentialsException("Invalid token");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户详细信息
|
|
||||||
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
|
||||||
if (userDetails == null || StrUtil.isBlank(userDetails.getUsername())) {
|
|
||||||
log.error("无效的用户凭证:{}", token);
|
|
||||||
throw new BadCredentialsException("Invalid user credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
String username = userDetails.getUsername();
|
|
||||||
log.info("WebSocket连接建立:用户[{}]", username);
|
|
||||||
|
|
||||||
// 绑定用户身份到当前会话(重要:用于@SendToUser等注解)
|
|
||||||
accessor.setUser(authentication);
|
|
||||||
|
|
||||||
// 发布用户上线事件(示例:可用于更新在线用户列表)
|
|
||||||
eventPublisher.publishEvent(new UserConnectionEvent(this, username, true));
|
|
||||||
|
|
||||||
}
|
|
||||||
// 处理客户端断开请求
|
|
||||||
else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
|
|
||||||
/*
|
|
||||||
* 注意:只有成功建立过认证的连接才会触发下线事件
|
|
||||||
* 防止未认证成功的连接产生脏数据
|
|
||||||
*/
|
|
||||||
Authentication authentication = (Authentication) accessor.getUser();
|
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
|
||||||
String username = ((SysUserDetails) authentication.getPrincipal()).getUsername();
|
|
||||||
log.info("WebSocket连接关闭:用户[{}]", username);
|
|
||||||
eventPublisher.publishEvent(new UserConnectionEvent(this, username, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (AuthenticationException ex) {
|
|
||||||
// 认证失败时强制关闭连接
|
|
||||||
log.error("连接认证失败:{}", ex.getMessage());
|
|
||||||
throw ex;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
// 捕获其他未知异常
|
|
||||||
log.error("WebSocket连接处理异常:", ex);
|
|
||||||
throw new MessagingException("Connection processing failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChannelInterceptor.super.preSend(message, channel);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 提取并处理JWT令牌(移除Bearer前缀)
|
||||||
|
String token = authorization.substring(7);
|
||||||
|
Authentication authentication = tokenManager.parseToken(token);
|
||||||
|
|
||||||
|
// 令牌解析失败处理
|
||||||
|
if (authentication == null) {
|
||||||
|
log.error("令牌解析失败:{}", token);
|
||||||
|
throw new BadCredentialsException("Invalid token");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户详细信息
|
||||||
|
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||||
|
if (userDetails == null || StrUtil.isBlank(userDetails.getUsername())) {
|
||||||
|
log.error("无效的用户凭证:{}", token);
|
||||||
|
throw new BadCredentialsException("Invalid user credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = userDetails.getUsername();
|
||||||
|
log.info("WebSocket连接建立:用户[{}]", username);
|
||||||
|
|
||||||
|
// 绑定用户身份到当前会话(重要:用于@SendToUser等注解)
|
||||||
|
accessor.setUser(authentication);
|
||||||
|
|
||||||
|
// 记录用户上线状态
|
||||||
|
webSocketService.userConnected(username, accessor.getSessionId());
|
||||||
|
|
||||||
|
}
|
||||||
|
// 处理客户端断开请求
|
||||||
|
else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
|
||||||
|
/*
|
||||||
|
* 注意:只有成功建立过认证的连接才会触发下线事件
|
||||||
|
* 防止未认证成功的连接产生脏数据
|
||||||
|
*/
|
||||||
|
Authentication authentication = (Authentication) accessor.getUser();
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
String username = ((SysUserDetails) authentication.getPrincipal()).getUsername();
|
||||||
|
log.info("WebSocket连接关闭:用户[{}]", username);
|
||||||
|
|
||||||
|
// 记录用户下线状态
|
||||||
|
webSocketService.userDisconnected(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (AuthenticationException ex) {
|
||||||
|
// 认证失败时强制关闭连接
|
||||||
|
log.error("连接认证失败:{}", ex.getMessage());
|
||||||
|
throw ex;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// 捕获其他未知异常
|
||||||
|
log.error("WebSocket连接处理异常:", ex);
|
||||||
|
throw new MessagingException("Connection processing failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChannelInterceptor.super.preSend(message, channel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
/**
|
/**
|
||||||
* 邮件控制层
|
* 邮件控制层
|
||||||
*
|
*
|
||||||
* @author Ray
|
* @author Ray.Hao
|
||||||
* @since 2.10.0
|
* @since 2.10.0
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.youlai.boot.shared.websocket.listener;
|
|
||||||
|
|
||||||
import com.youlai.boot.shared.websocket.service.OnlineUserService;
|
|
||||||
import com.youlai.boot.system.event.UserConnectionEvent;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在线用户监听器
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/9/25
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class OnlineUserListener {
|
|
||||||
|
|
||||||
private final SimpMessagingTemplate messagingTemplate;
|
|
||||||
private final OnlineUserService onlineUserService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户连接事件处理
|
|
||||||
*
|
|
||||||
* @param event 用户连接事件
|
|
||||||
*/
|
|
||||||
@EventListener
|
|
||||||
public void handleUserConnectionEvent(UserConnectionEvent event) {
|
|
||||||
String username = event.getUsername();
|
|
||||||
if (event.isConnected()) {
|
|
||||||
onlineUserService.addOnlineUser(username);
|
|
||||||
log.info("User connected: {}", username);
|
|
||||||
} else {
|
|
||||||
onlineUserService.removeOnlineUser(username);
|
|
||||||
log.info("User disconnected: {}", username);
|
|
||||||
}
|
|
||||||
// 推送在线用户人数
|
|
||||||
messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package com.youlai.boot.shared.websocket.service;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在线用户服务
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/9/26
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class OnlineUserService {
|
|
||||||
|
|
||||||
private final Set<String> onlineUsers = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户到在线用户集合
|
|
||||||
*
|
|
||||||
* @param username 用户名
|
|
||||||
*/
|
|
||||||
public void addOnlineUser(String username) {
|
|
||||||
onlineUsers.add(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从在线用户集合移除用户
|
|
||||||
*
|
|
||||||
* @param username 用户名
|
|
||||||
*/
|
|
||||||
public void removeOnlineUser(String username) {
|
|
||||||
onlineUsers.remove(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有在线用户
|
|
||||||
*
|
|
||||||
* @return 在线用户集合
|
|
||||||
*/
|
|
||||||
public Set<String> getAllOnlineUsers() {
|
|
||||||
return Collections.unmodifiableSet(onlineUsers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取在线的接收者
|
|
||||||
* 从所有接收者中过滤出在线的接收者
|
|
||||||
*
|
|
||||||
* @param receivers 接收者
|
|
||||||
* @return 在线的接收者集合
|
|
||||||
*/
|
|
||||||
public Set<String> getOnlineReceivers(Set<String> receivers) {
|
|
||||||
return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取在线用户数量
|
|
||||||
*
|
|
||||||
* @return 在线用户数量
|
|
||||||
*/
|
|
||||||
public int getOnlineUserCount() {
|
|
||||||
return onlineUsers.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ import com.youlai.boot.system.model.form.DictForm;
|
|||||||
import com.youlai.boot.common.annotation.Log;
|
import com.youlai.boot.common.annotation.Log;
|
||||||
import com.youlai.boot.system.service.DictItemService;
|
import com.youlai.boot.system.service.DictItemService;
|
||||||
import com.youlai.boot.system.service.DictService;
|
import com.youlai.boot.system.service.DictService;
|
||||||
|
import com.youlai.boot.system.service.WebSocketService;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -42,7 +43,7 @@ public class DictController {
|
|||||||
|
|
||||||
private final DictService dictService;
|
private final DictService dictService;
|
||||||
private final DictItemService dictItemService;
|
private final DictItemService dictItemService;
|
||||||
|
private final WebSocketService webSocketService;
|
||||||
|
|
||||||
//---------------------------------------------------
|
//---------------------------------------------------
|
||||||
// 字典相关接口
|
// 字典相关接口
|
||||||
@@ -80,6 +81,10 @@ public class DictController {
|
|||||||
@RepeatSubmit
|
@RepeatSubmit
|
||||||
public Result<?> saveDict(@Valid @RequestBody DictForm formData) {
|
public Result<?> saveDict(@Valid @RequestBody DictForm formData) {
|
||||||
boolean result = dictService.saveDict(formData);
|
boolean result = dictService.saveDict(formData);
|
||||||
|
// 发送字典更新通知
|
||||||
|
if (result) {
|
||||||
|
webSocketService.broadcastDictChange(formData.getDictCode());
|
||||||
|
}
|
||||||
return Result.judge(result);
|
return Result.judge(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,9 +93,13 @@ public class DictController {
|
|||||||
@PreAuthorize("@ss.hasPerm('sys:dict:edit')")
|
@PreAuthorize("@ss.hasPerm('sys:dict:edit')")
|
||||||
public Result<?> updateDict(
|
public Result<?> updateDict(
|
||||||
@PathVariable Long id,
|
@PathVariable Long id,
|
||||||
@RequestBody DictForm DictForm
|
@RequestBody DictForm dictForm
|
||||||
) {
|
) {
|
||||||
boolean status = dictService.updateDict(id, DictForm);
|
boolean status = dictService.updateDict(id, dictForm);
|
||||||
|
// 发送字典更新通知
|
||||||
|
if (status && dictForm.getDictCode() != null) {
|
||||||
|
webSocketService.broadcastDictChange(dictForm.getDictCode());
|
||||||
|
}
|
||||||
return Result.judge(status);
|
return Result.judge(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +109,16 @@ public class DictController {
|
|||||||
public Result<?> deleteDictionaries(
|
public Result<?> deleteDictionaries(
|
||||||
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids
|
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids
|
||||||
) {
|
) {
|
||||||
|
// 获取字典编码列表,用于发送删除通知
|
||||||
|
List<String> dictCodes = dictService.getDictCodesByIds(Arrays.stream(ids.split(",")).toList());
|
||||||
|
|
||||||
dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList());
|
dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList());
|
||||||
|
|
||||||
|
// 发送字典删除通知
|
||||||
|
for (String dictCode : dictCodes) {
|
||||||
|
webSocketService.broadcastDictChange(dictCode);
|
||||||
|
}
|
||||||
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +156,12 @@ public class DictController {
|
|||||||
) {
|
) {
|
||||||
formData.setDictCode(dictCode);
|
formData.setDictCode(dictCode);
|
||||||
boolean result = dictItemService.saveDictItem(formData);
|
boolean result = dictItemService.saveDictItem(formData);
|
||||||
|
|
||||||
|
// 发送字典更新通知
|
||||||
|
if (result) {
|
||||||
|
webSocketService.broadcastDictChange(dictCode);
|
||||||
|
}
|
||||||
|
|
||||||
return Result.judge(result);
|
return Result.judge(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +187,12 @@ public class DictController {
|
|||||||
formData.setId(itemId);
|
formData.setId(itemId);
|
||||||
formData.setDictCode(dictCode);
|
formData.setDictCode(dictCode);
|
||||||
boolean status = dictItemService.updateDictItem(formData);
|
boolean status = dictItemService.updateDictItem(formData);
|
||||||
|
|
||||||
|
// 发送字典更新通知
|
||||||
|
if (status) {
|
||||||
|
webSocketService.broadcastDictChange(dictCode);
|
||||||
|
}
|
||||||
|
|
||||||
return Result.judge(status);
|
return Result.judge(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +200,14 @@ public class DictController {
|
|||||||
@DeleteMapping("/{dictCode}/items/{itemIds}")
|
@DeleteMapping("/{dictCode}/items/{itemIds}")
|
||||||
@PreAuthorize("@ss.hasPerm('sys:dict-item:delete')")
|
@PreAuthorize("@ss.hasPerm('sys:dict-item:delete')")
|
||||||
public Result<Void> deleteDictItems(
|
public Result<Void> deleteDictItems(
|
||||||
|
@PathVariable String dictCode,
|
||||||
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String itemIds
|
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String itemIds
|
||||||
) {
|
) {
|
||||||
dictItemService.deleteDictItemByIds(itemIds);
|
dictItemService.deleteDictItemByIds(itemIds);
|
||||||
|
|
||||||
|
// 发送字典更新通知
|
||||||
|
webSocketService.broadcastDictChange(dictCode);
|
||||||
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import org.mapstruct.Mapper;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典项 对象转换器
|
* 字典项对象转换器
|
||||||
*
|
*
|
||||||
* @author Ray
|
* @author Ray.Hao
|
||||||
* @since 2022/6/8
|
* @since 2022/6/8
|
||||||
*/
|
*/
|
||||||
@Mapper(componentModel = "spring")
|
@Mapper(componentModel = "spring")
|
||||||
public interface DictDataConverter {
|
public interface DictItemConverter {
|
||||||
|
|
||||||
Page<DictPageVO> toPageVo(Page<DictItem> page);
|
Page<DictPageVO> toPageVo(Page<DictItem> page);
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.youlai.boot.system.event;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.context.ApplicationEvent;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户连接事件
|
|
||||||
*
|
|
||||||
* @author Ray
|
|
||||||
* @since 2.3.0
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
public class UserConnectionEvent extends ApplicationEvent {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户名
|
|
||||||
*/
|
|
||||||
private final String username;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否连接
|
|
||||||
*/
|
|
||||||
private final boolean connected;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户连接事件
|
|
||||||
*
|
|
||||||
* @param source 事件源
|
|
||||||
* @param username 用户名
|
|
||||||
* @param connected 是否连接
|
|
||||||
*/
|
|
||||||
public UserConnectionEvent(Object source, String username, boolean connected) {
|
|
||||||
super(source);
|
|
||||||
this.username = username;
|
|
||||||
this.connected = connected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.youlai.boot.shared.websocket.handler;
|
package com.youlai.boot.system.handler;
|
||||||
|
|
||||||
|
|
||||||
import com.youlai.boot.shared.websocket.service.OnlineUserService;
|
import com.youlai.boot.system.service.UserOnlineService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
@@ -19,7 +19,7 @@ import org.springframework.stereotype.Component;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OnlineUserJobHandler {
|
public class OnlineUserJobHandler {
|
||||||
|
|
||||||
private final OnlineUserService onlineUserService;
|
private final UserOnlineService userOnlineService;
|
||||||
private final SimpMessagingTemplate messagingTemplate;
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
|
|
||||||
// 每分钟统计一次在线用户数
|
// 每分钟统计一次在线用户数
|
||||||
@@ -27,7 +27,7 @@ public class OnlineUserJobHandler {
|
|||||||
public void execute() {
|
public void execute() {
|
||||||
log.info("定时任务:统计在线用户数");
|
log.info("定时任务:统计在线用户数");
|
||||||
// 推送在线用户人数
|
// 推送在线用户人数
|
||||||
messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount());
|
messagingTemplate.convertAndSend("/topic/onlineUserCount", userOnlineService.getOnlineUserCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.youlai.boot.system.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户会话DTO
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserSessionDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户会话ID集合
|
||||||
|
*/
|
||||||
|
private Set<String> sessionIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后活动时间
|
||||||
|
*/
|
||||||
|
private long lastActiveTime;
|
||||||
|
|
||||||
|
public UserSessionDTO(String username) {
|
||||||
|
this.username = username;
|
||||||
|
this.sessionIds = new HashSet<>();
|
||||||
|
this.lastActiveTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.youlai.boot.system.model.event;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典更新事件
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DictEvent {
|
||||||
|
/**
|
||||||
|
* 字典编码
|
||||||
|
*/
|
||||||
|
private String dictCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间戳
|
||||||
|
*/
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
public DictEvent(String dictCode) {
|
||||||
|
this.dictCode = dictCode;
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.youlai.boot.system.model.form;
|
|||||||
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@@ -26,6 +27,7 @@ public class DictForm {
|
|||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Schema(description = "字典编码", example ="gender")
|
@Schema(description = "字典编码", example ="gender")
|
||||||
|
@NotBlank(message = "字典编码不能为空")
|
||||||
private String dictCode;
|
private String dictCode;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ public interface DictService extends IService<Dict> {
|
|||||||
*/
|
*/
|
||||||
void deleteDictByIds(List<String> ids);
|
void deleteDictByIds(List<String> ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典ID列表获取字典编码列表
|
||||||
|
*
|
||||||
|
* @param ids 字典ID列表
|
||||||
|
* @return 字典编码列表
|
||||||
|
*/
|
||||||
|
List<String> getDictCodesByIds(List<String> ids);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package com.youlai.boot.system.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.youlai.boot.core.security.model.SysUserDetails;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在线状态服务
|
||||||
|
* 负责维护用户的在线状态和相关统计
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class UserOnlineService {
|
||||||
|
|
||||||
|
// 在线用户映射表,key为用户名,value为用户在线信息
|
||||||
|
private final Map<String, UserOnlineInfo> onlineUsers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private SimpMessagingTemplate messagingTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UserOnlineService(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) {
|
||||||
|
this.messagingTemplate = messagingTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户上线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param sessionId WebSocket会话ID(可选)
|
||||||
|
*/
|
||||||
|
public void userConnected(String username, String sessionId) {
|
||||||
|
// 生成会话ID(如果未提供)
|
||||||
|
String actualSessionId = sessionId != null ? sessionId : "session-" + System.nanoTime();
|
||||||
|
UserOnlineInfo info = new UserOnlineInfo(username, actualSessionId, System.currentTimeMillis());
|
||||||
|
onlineUsers.put(username, info);
|
||||||
|
log.info("用户[{}]上线,当前在线用户数:{}", username, onlineUsers.size());
|
||||||
|
|
||||||
|
// 通知在线用户状态变更
|
||||||
|
notifyOnlineUsersChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户下线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
*/
|
||||||
|
public void userDisconnected(String username) {
|
||||||
|
onlineUsers.remove(username);
|
||||||
|
log.info("用户[{}]下线,当前在线用户数:{}", username, onlineUsers.size());
|
||||||
|
|
||||||
|
// 通知在线用户状态变更
|
||||||
|
notifyOnlineUsersChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户列表
|
||||||
|
*
|
||||||
|
* @return 在线用户名列表
|
||||||
|
*/
|
||||||
|
public List<UserOnlineDTO> getOnlineUsers() {
|
||||||
|
return onlineUsers.values().stream()
|
||||||
|
.map(info -> new UserOnlineDTO(info.getUsername(), info.getLoginTime()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户数量
|
||||||
|
*
|
||||||
|
* @return 在线用户数
|
||||||
|
*/
|
||||||
|
public int getOnlineUserCount() {
|
||||||
|
return onlineUsers.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否在线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @return 是否在线
|
||||||
|
*/
|
||||||
|
public boolean isUserOnline(String username) {
|
||||||
|
return onlineUsers.containsKey(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知所有客户端在线用户变更
|
||||||
|
*/
|
||||||
|
private void notifyOnlineUsersChange() {
|
||||||
|
if (messagingTemplate == null) {
|
||||||
|
log.warn("消息模板尚未初始化,无法发送在线用户变更通知");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
OnlineUsersChangeEvent event = new OnlineUsersChangeEvent();
|
||||||
|
event.setType("ONLINE_USERS_CHANGE");
|
||||||
|
event.setCount(onlineUsers.size());
|
||||||
|
event.setUsers(getOnlineUsers());
|
||||||
|
event.setTimestamp(System.currentTimeMillis());
|
||||||
|
|
||||||
|
String message = objectMapper.writeValueAsString(event);
|
||||||
|
messagingTemplate.convertAndSend("/topic/online-users", message);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Failed to send online users change event", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在线信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
private static class UserOnlineInfo {
|
||||||
|
private final String username;
|
||||||
|
private final String sessionId;
|
||||||
|
private final long loginTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在线DTO(用于返回给前端)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class UserOnlineDTO {
|
||||||
|
private final String username;
|
||||||
|
private final long loginTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户变更事件
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
private static class OnlineUsersChangeEvent {
|
||||||
|
private String type;
|
||||||
|
private int count;
|
||||||
|
private List<UserOnlineDTO> users;
|
||||||
|
private long timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.youlai.boot.system.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket服务接口
|
||||||
|
* <p>
|
||||||
|
* 提供与WebSocket连接管理相关的功能,包括:
|
||||||
|
* - 用户连接/断开事件处理
|
||||||
|
* - 字典数据变更通知
|
||||||
|
* - 系统消息推送
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
public interface WebSocketService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理用户连接事件
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param sessionId WebSocket会话ID
|
||||||
|
*/
|
||||||
|
void userConnected(String username, String sessionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理用户断开连接事件
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
*/
|
||||||
|
void userDisconnected(String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播字典数据变更通知
|
||||||
|
*
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
*/
|
||||||
|
void broadcastDictChange(String dictCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送系统通知给特定用户
|
||||||
|
*
|
||||||
|
* @param username 目标用户名
|
||||||
|
* @param message 通知消息内容
|
||||||
|
*/
|
||||||
|
void sendNotification(String username, Object message);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package com.youlai.boot.system.service.impl;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.youlai.boot.system.converter.DictDataConverter;
|
import com.youlai.boot.system.converter.DictItemConverter;
|
||||||
import com.youlai.boot.system.mapper.DictItemMapper;
|
import com.youlai.boot.system.mapper.DictItemMapper;
|
||||||
import com.youlai.boot.system.model.entity.DictItem;
|
import com.youlai.boot.system.model.entity.DictItem;
|
||||||
import com.youlai.boot.system.model.form.DictItemForm;
|
import com.youlai.boot.system.model.form.DictItemForm;
|
||||||
@@ -27,7 +27,7 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DictItemServiceImpl extends ServiceImpl<DictItemMapper, DictItem> implements DictItemService {
|
public class DictItemServiceImpl extends ServiceImpl<DictItemMapper, DictItem> implements DictItemService {
|
||||||
|
|
||||||
private final DictDataConverter dictDataConverter;
|
private final DictItemConverter dictItemConverter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取字典项分页列表
|
* 获取字典项分页列表
|
||||||
@@ -78,7 +78,7 @@ public class DictItemServiceImpl extends ServiceImpl<DictItemMapper, DictItem> i
|
|||||||
@Override
|
@Override
|
||||||
public DictItemForm getDictItemForm( Long itemId) {
|
public DictItemForm getDictItemForm( Long itemId) {
|
||||||
DictItem entity = this.getById(itemId);
|
DictItem entity = this.getById(itemId);
|
||||||
return dictDataConverter.toForm(entity);
|
return dictItemConverter.toForm(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +89,7 @@ public class DictItemServiceImpl extends ServiceImpl<DictItemMapper, DictItem> i
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean saveDictItem(DictItemForm formData) {
|
public boolean saveDictItem(DictItemForm formData) {
|
||||||
DictItem entity = dictDataConverter.toEntity(formData);
|
DictItem entity = dictItemConverter.toEntity(formData);
|
||||||
return this.save(entity);
|
return this.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ public class DictItemServiceImpl extends ServiceImpl<DictItemMapper, DictItem> i
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean updateDictItem(DictItemForm formData) {
|
public boolean updateDictItem(DictItemForm formData) {
|
||||||
DictItem entity = dictDataConverter.toEntity(formData);
|
DictItem entity = dictItemConverter.toEntity(formData);
|
||||||
return this.updateById(entity);
|
return this.updateById(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,9 @@ public class DictItemServiceImpl extends ServiceImpl<DictItemMapper, DictItem> i
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void deleteDictItemByIds(String ids) {
|
public void deleteDictItemByIds(String ids) {
|
||||||
List<Long> idList = Arrays.stream(ids.split(",")).map(Long::parseLong).toList();
|
List<Long> idList = Arrays.stream(ids.split(","))
|
||||||
|
.map(Long::parseLong)
|
||||||
|
.toList();
|
||||||
this.removeByIds(idList);
|
this.removeByIds(idList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import com.youlai.boot.system.model.entity.Dict;
|
|||||||
import com.youlai.boot.system.model.entity.DictItem;
|
import com.youlai.boot.system.model.entity.DictItem;
|
||||||
import com.youlai.boot.system.model.form.DictForm;
|
import com.youlai.boot.system.model.form.DictForm;
|
||||||
import com.youlai.boot.system.model.query.DictPageQuery;
|
import com.youlai.boot.system.model.query.DictPageQuery;
|
||||||
import com.youlai.boot.system.model.vo.DictItemOptionVO;
|
|
||||||
import com.youlai.boot.system.model.vo.DictPageVO;
|
import com.youlai.boot.system.model.vo.DictPageVO;
|
||||||
import com.youlai.boot.system.service.DictItemService;
|
import com.youlai.boot.system.service.DictItemService;
|
||||||
import com.youlai.boot.system.service.DictService;
|
import com.youlai.boot.system.service.DictService;
|
||||||
@@ -23,7 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据字典业务实现类
|
* 字典业务实现类
|
||||||
*
|
*
|
||||||
* @author haoxr
|
* @author haoxr
|
||||||
* @since 2022/10/12
|
* @since 2022/10/12
|
||||||
@@ -110,20 +109,23 @@ public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements Di
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean updateDict(Long id, DictForm dictForm) {
|
public boolean updateDict(Long id, DictForm dictForm) {
|
||||||
// 更新字典
|
// 获取字典
|
||||||
Dict entity = dictConverter.toEntity(dictForm);
|
Dict entity = this.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
// 校验 code 是否唯一
|
throw new BusinessException("字典不存在");
|
||||||
String dictCode = entity.getDictCode();
|
|
||||||
long count = this.count(new LambdaQueryWrapper<Dict>()
|
|
||||||
.eq(Dict::getDictCode, dictCode)
|
|
||||||
.ne(Dict::getId, id)
|
|
||||||
);
|
|
||||||
if (count > 0) {
|
|
||||||
throw new BusinessException("字典编码已存在");
|
|
||||||
}
|
}
|
||||||
|
// 校验 code 是否唯一
|
||||||
return this.updateById(entity);
|
String dictCode = dictForm.getDictCode();
|
||||||
|
if (!entity.getDictCode().equals(dictCode)) {
|
||||||
|
long count = this.count(new LambdaQueryWrapper<Dict>()
|
||||||
|
.eq(Dict::getDictCode, dictCode)
|
||||||
|
);
|
||||||
|
Assert.isTrue(count == 0, "字典编码已存在");
|
||||||
|
}
|
||||||
|
// 更新字典
|
||||||
|
Dict dict = dictConverter.toEntity(dictForm);
|
||||||
|
dict.setId(id);
|
||||||
|
return this.updateById(dict);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,25 +133,34 @@ public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements Di
|
|||||||
*
|
*
|
||||||
* @param ids 字典ID,多个以英文逗号(,)分割
|
* @param ids 字典ID,多个以英文逗号(,)分割
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@Override
|
||||||
public void deleteDictByIds(List<String> ids) {
|
public void deleteDictByIds(List<String> ids) {
|
||||||
for (String id : ids) {
|
// 删除字典
|
||||||
Dict dict = this.getById(id);
|
this.removeByIds(ids);
|
||||||
if (dict != null) {
|
|
||||||
boolean removeResult = this.removeById(id);
|
|
||||||
// 删除字典下的字典项
|
|
||||||
if (removeResult) {
|
|
||||||
dictItemService.remove(
|
|
||||||
new LambdaQueryWrapper<DictItem>()
|
|
||||||
.eq(DictItem::getDictCode, dict.getDictCode())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
// 删除字典项
|
||||||
|
List<Dict> list = this.listByIds(ids);
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
List<String> dictCodes = list.stream().map(Dict::getDictCode).toList();
|
||||||
|
dictItemService.remove(new LambdaQueryWrapper<DictItem>()
|
||||||
|
.in(DictItem::getDictCode, dictCodes)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典ID列表获取字典编码列表
|
||||||
|
*
|
||||||
|
* @param ids 字典ID列表
|
||||||
|
* @return 字典编码列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<String> getDictCodesByIds(List<String> ids) {
|
||||||
|
List<Dict> dictList = this.listByIds(ids);
|
||||||
|
return dictList.stream().map(Dict::getDictCode).toList();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.youlai.boot.common.exception.BusinessException;
|
import com.youlai.boot.common.exception.BusinessException;
|
||||||
import com.youlai.boot.core.security.util.SecurityUtils;
|
import com.youlai.boot.core.security.util.SecurityUtils;
|
||||||
import com.youlai.boot.shared.websocket.service.OnlineUserService;
|
|
||||||
import com.youlai.boot.system.converter.NoticeConverter;
|
import com.youlai.boot.system.converter.NoticeConverter;
|
||||||
import com.youlai.boot.system.enums.NoticePublishStatusEnum;
|
import com.youlai.boot.system.enums.NoticePublishStatusEnum;
|
||||||
import com.youlai.boot.system.enums.NoticeTargetEnum;
|
import com.youlai.boot.system.enums.NoticeTargetEnum;
|
||||||
@@ -26,6 +25,7 @@ import com.youlai.boot.system.model.vo.UserNoticePageVO;
|
|||||||
import com.youlai.boot.system.model.vo.NoticeDetailVO;
|
import com.youlai.boot.system.model.vo.NoticeDetailVO;
|
||||||
import com.youlai.boot.system.service.NoticeService;
|
import com.youlai.boot.system.service.NoticeService;
|
||||||
import com.youlai.boot.system.service.UserNoticeService;
|
import com.youlai.boot.system.service.UserNoticeService;
|
||||||
|
import com.youlai.boot.system.service.UserOnlineService;
|
||||||
import com.youlai.boot.system.service.UserService;
|
import com.youlai.boot.system.service.UserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
@@ -53,7 +53,7 @@ public class NoticeServiceImpl extends ServiceImpl<NoticeMapper, Notice> impleme
|
|||||||
private final UserNoticeService userNoticeService;
|
private final UserNoticeService userNoticeService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final SimpMessagingTemplate messagingTemplate;
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
private final OnlineUserService onlineUserService;
|
private final UserOnlineService userOnlineService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取通知公告分页列表
|
* 获取通知公告分页列表
|
||||||
@@ -213,7 +213,9 @@ public class NoticeServiceImpl extends ServiceImpl<NoticeMapper, Notice> impleme
|
|||||||
|
|
||||||
Set<String> receivers = targetUserList.stream().map(User::getUsername).collect(Collectors.toSet());
|
Set<String> receivers = targetUserList.stream().map(User::getUsername).collect(Collectors.toSet());
|
||||||
|
|
||||||
Set<String> allOnlineUsers = onlineUserService.getAllOnlineUsers();
|
Set<String> allOnlineUsers = userOnlineService.getOnlineUsers().stream()
|
||||||
|
.map(UserOnlineService.UserOnlineDTO::getUsername)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
// 找出在线用户的通知接收者
|
// 找出在线用户的通知接收者
|
||||||
Set<String> onlineReceivers = new HashSet<>(CollectionUtil.intersection(receivers, allOnlineUsers));
|
Set<String> onlineReceivers = new HashSet<>(CollectionUtil.intersection(receivers, allOnlineUsers));
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package com.youlai.boot.system.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.youlai.boot.system.model.event.DictEvent;
|
||||||
|
import com.youlai.boot.system.service.WebSocketService;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket服务实现类
|
||||||
|
* 统一处理WebSocket消息发送和用户在线状态管理
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class WebSocketServiceImpl implements WebSocketService {
|
||||||
|
|
||||||
|
// 在线用户映射表,key为用户名,value为用户在线信息
|
||||||
|
private final Map<String, UserOnlineInfo> onlineUsers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private SimpMessagingTemplate messagingTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public WebSocketServiceImpl(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) {
|
||||||
|
this.messagingTemplate = messagingTemplate;
|
||||||
|
log.info("WebSocket消息模板已初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
//==================================
|
||||||
|
// 用户在线状态管理功能
|
||||||
|
//==================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户上线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param sessionId WebSocket会话ID(可选)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void userConnected(String username, String sessionId) {
|
||||||
|
// 生成会话ID(如果未提供)
|
||||||
|
String actualSessionId = sessionId != null ? sessionId : "session-" + System.nanoTime();
|
||||||
|
UserOnlineInfo info = new UserOnlineInfo(username, actualSessionId, System.currentTimeMillis());
|
||||||
|
onlineUsers.put(username, info);
|
||||||
|
log.info("用户[{}]上线,当前在线用户数:{}", username, onlineUsers.size());
|
||||||
|
|
||||||
|
// 通知在线用户状态变更
|
||||||
|
notifyOnlineUsersChangeInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户下线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void userDisconnected(String username) {
|
||||||
|
onlineUsers.remove(username);
|
||||||
|
log.info("用户[{}]下线,当前在线用户数:{}", username, onlineUsers.size());
|
||||||
|
|
||||||
|
// 通知在线用户状态变更
|
||||||
|
notifyOnlineUsersChangeInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户列表
|
||||||
|
*
|
||||||
|
* @return 在线用户名列表
|
||||||
|
*/
|
||||||
|
public List<UserOnlineDTO> getOnlineUsers() {
|
||||||
|
return onlineUsers.values().stream()
|
||||||
|
.map(info -> new UserOnlineDTO(info.getUsername(), info.getLoginTime()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户数量
|
||||||
|
*
|
||||||
|
* @return 在线用户数
|
||||||
|
*/
|
||||||
|
public int getOnlineUserCount() {
|
||||||
|
return onlineUsers.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否在线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @return 是否在线
|
||||||
|
*/
|
||||||
|
public boolean isUserOnline(String username) {
|
||||||
|
return onlineUsers.containsKey(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发在线用户变更通知
|
||||||
|
* 供外部手动触发通知使用
|
||||||
|
*/
|
||||||
|
public void notifyOnlineUsersChange() {
|
||||||
|
log.info("手动触发在线用户变更通知,当前在线用户数:{}", onlineUsers.size());
|
||||||
|
notifyOnlineUsersChangeInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部通用通知方法
|
||||||
|
* 通知所有客户端在线用户变更
|
||||||
|
*/
|
||||||
|
private void notifyOnlineUsersChangeInternal() {
|
||||||
|
if (messagingTemplate == null) {
|
||||||
|
log.warn("消息模板尚未初始化,无法发送在线用户变更通知");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
OnlineUsersChangeEvent event = new OnlineUsersChangeEvent();
|
||||||
|
event.setType("ONLINE_USERS_CHANGE");
|
||||||
|
event.setCount(onlineUsers.size());
|
||||||
|
event.setUsers(getOnlineUsers());
|
||||||
|
event.setTimestamp(System.currentTimeMillis());
|
||||||
|
|
||||||
|
String message = objectMapper.writeValueAsString(event);
|
||||||
|
messagingTemplate.convertAndSend("/topic/online-users", message);
|
||||||
|
log.debug("已发送在线用户变更通知");
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("发送在线用户变更事件失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在线信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
private static class UserOnlineInfo {
|
||||||
|
private final String username;
|
||||||
|
private final String sessionId;
|
||||||
|
private final long loginTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在线DTO(用于返回给前端)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class UserOnlineDTO {
|
||||||
|
private final String username;
|
||||||
|
private final long loginTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户变更事件
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
private static class OnlineUsersChangeEvent {
|
||||||
|
private String type;
|
||||||
|
private int count;
|
||||||
|
private List<UserOnlineDTO> users;
|
||||||
|
private long timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
//==================================
|
||||||
|
// WebSocket消息发送功能
|
||||||
|
//==================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向所有客户端发送字典更新事件
|
||||||
|
*
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void broadcastDictChange(String dictCode) {
|
||||||
|
DictEvent event = new DictEvent(dictCode);
|
||||||
|
sendDictEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送字典事件消息
|
||||||
|
*
|
||||||
|
* @param event 字典事件
|
||||||
|
*/
|
||||||
|
private void sendDictEvent(DictEvent event) {
|
||||||
|
if (messagingTemplate == null) {
|
||||||
|
log.warn("消息模板尚未初始化,无法发送字典更新通知");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String message = objectMapper.writeValueAsString(event);
|
||||||
|
messagingTemplate.convertAndSend("/topic/dict", message);
|
||||||
|
log.info("已发送字典事件通知, dictCode: {}", event.getDictCode());
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("发送字典事件失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向特定用户发送系统消息
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param message 消息内容
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void sendNotification(String username, Object message) {
|
||||||
|
if (messagingTemplate == null) {
|
||||||
|
log.warn("消息模板尚未初始化,无法发送用户消息");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String messageJson = objectMapper.writeValueAsString(message);
|
||||||
|
messagingTemplate.convertAndSendToUser(username, "/queue/messages", messageJson);
|
||||||
|
log.info("已向用户[{}]发送消息", username);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("向用户[{}]发送消息失败", username, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送广播消息给所有用户
|
||||||
|
*
|
||||||
|
* @param message 消息内容
|
||||||
|
*/
|
||||||
|
public void broadcastMessage(String message) {
|
||||||
|
if (messagingTemplate == null) {
|
||||||
|
log.warn("消息模板尚未初始化,无法发送广播消息");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SystemMessage systemMessage = new SystemMessage("系统", message, System.currentTimeMillis());
|
||||||
|
String messageJson = objectMapper.writeValueAsString(systemMessage);
|
||||||
|
messagingTemplate.convertAndSend("/topic/public", messageJson);
|
||||||
|
log.info("已发送广播消息: {}", message);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("发送广播消息失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统消息对象
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class SystemMessage {
|
||||||
|
private String sender;
|
||||||
|
private String content;
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
public SystemMessage(String sender, String content, long timestamp) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.content = content;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
INNER JOIN sys_role t2 ON t1.role_id = t2.id AND t2.is_deleted = 0 AND t2.`status` = 1
|
INNER JOIN sys_role t2 ON t1.role_id = t2.id AND t2.is_deleted = 0 AND t2.`status` = 1
|
||||||
INNER JOIN sys_menu t3 ON t1.menu_id = t3.id
|
INNER JOIN sys_menu t3 ON t1.menu_id = t3.id
|
||||||
WHERE
|
WHERE
|
||||||
type = '${@com.youlai.boot.system.enums.MenuTypeEnum@BUTTON.getValue()}'
|
t3.type = '${@com.youlai.boot.system.enums.MenuTypeEnum@BUTTON.getValue()}'
|
||||||
<if test="roleCode!=null and roleCode.trim() neq ''">
|
<if test="roleCode!=null and roleCode.trim() neq ''">
|
||||||
AND t2.`code` = #{roleCode}
|
AND t2.`code` = #{roleCode}
|
||||||
</if>
|
</if>
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
<dict v-model="queryParams.$fieldConfig.fieldName" type="radio" code="$fieldConfig.dictType" />
|
<dict v-model="queryParams.$fieldConfig.fieldName" type="radio" code="$fieldConfig.dictType" />
|
||||||
#else
|
#else
|
||||||
<el-radio-group v-model="queryParams.$fieldConfig.fieldName">
|
<el-radio-group v-model="queryParams.$fieldConfig.fieldName">
|
||||||
<el-radio :key="1" :label="1">选项一</el-radio>
|
<el-radio :key="1" :value="1">选项一</el-radio>
|
||||||
<el-radio :key="2" :label="2">选项二</el-radio>
|
<el-radio :key="2" :value="2">选项二</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
#end
|
#end
|
||||||
#elseif($fieldConfig.formType == "CHECK_BOX")
|
#elseif($fieldConfig.formType == "CHECK_BOX")
|
||||||
|
|||||||
Reference in New Issue
Block a user