diff --git a/sql/mysql/youlai_boot.sql b/sql/mysql/youlai_boot.sql index a6402555..d8a7927b 100644 --- a/sql/mysql/youlai_boot.sql +++ b/sql/mysql/youlai_boot.sql @@ -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 (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 (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 (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); @@ -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 (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 (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 @@ -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, 146); INSERT INTO `sys_role_menu` VALUES (2, 147); +INSERT INTO `sys_role_menu` VALUES (2, 148); -- ---------------------------- -- 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 (10, 10, 2, 1, NULL, now(), now(), 0); -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/youlai/boot/config/WebSocketConfig.java b/src/main/java/com/youlai/boot/config/WebSocketConfig.java index 2d9bbf48..a344010b 100644 --- a/src/main/java/com/youlai/boot/config/WebSocketConfig.java +++ b/src/main/java/com/youlai/boot/config/WebSocketConfig.java @@ -3,11 +3,11 @@ package com.youlai.boot.config; import cn.hutool.core.util.StrUtil; import com.youlai.boot.core.security.model.SysUserDetails; 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 org.jetbrains.annotations.NotNull; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpHeaders; import org.springframework.messaging.Message; 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; /** - * WebSocket 配置 + * WebSocket配置 * * @author Ray.Hao - * @since 2.4.0 + * @since 3.0.0 */ -// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 @EnableWebSocketMessageBroker @Configuration @Slf4j 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") - // 允许跨域 - .setAllowedOriginPatterns("*"); - } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } - /** - * 配置消息代理 - */ - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - // 客户端发送消息的请求前缀 - registry.setApplicationDestinationPrefixes("/app"); + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); - // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 - registry.enableSimpleBroker("/topic", "/queue"); + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); - // 服务端通知客户端的前缀,可以不设置,默认为user - registry.setUserDestinationPrefix("/user"); - } + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } - /** - * 配置客户端入站通道拦截器 - *

- * 核心功能: - * 1. 连接建立时解析令牌并绑定用户身份 - * 2. 连接关闭时触发下线通知 - * 3. 异常Token的防御性处理 - */ - @Override - public void configureClientInboundChannel(ChannelRegistration registration) { - registration.interceptors(new ChannelInterceptor() { - @Override - public Message preSend(@NotNull Message message, @NotNull MessageChannel channel) { - StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); - if (accessor == null) { - return ChannelInterceptor.super.preSend(message, channel); - } + /** + * 配置客户端入站通道拦截器 + *

+ * 核心功能: + * 1. 连接建立时解析令牌并绑定用户身份 + * 2. 连接关闭时触发下线通知 + * 3. 异常Token的防御性处理 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message preSend(@NotNull Message message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor == null) { + return ChannelInterceptor.super.preSend(message, channel); + } - try { - // 处理客户端连接请求 - if (StompCommand.CONNECT.equals(accessor.getCommand())) { - /* - * 安全校验流程: - * 1. 从HEADER中获取Authorization值 - * 2. 校验Bearer Token格式合法性 - * 3. 解析并验证JWT有效性 - * 4. 绑定用户身份到当前会话 - */ - String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + try { + // 处理客户端连接请求 + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + /* + * 安全校验流程: + * 1. 从HEADER中获取Authorization值 + * 2. 校验Bearer Token格式合法性 + * 3. 解析并验证JWT有效性 + * 4. 绑定用户身份到当前会话 + */ + String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); - // 防御性校验:确保Authorization头存在且格式正确 - if (StrUtil.isBlank(authorization) || !authorization.startsWith("Bearer ")) { - log.warn("非法连接请求:缺少有效的Authorization头"); - 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); + // 防御性校验:确保Authorization头存在且格式正确 + if (StrUtil.isBlank(authorization) || !authorization.startsWith("Bearer ")) { + log.warn("非法连接请求:缺少有效的Authorization头"); + 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); + + // 记录用户上线状态 + 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); + } + }); + } } diff --git a/src/main/java/com/youlai/boot/shared/mail/controller/MailController.java b/src/main/java/com/youlai/boot/shared/mail/controller/MailController.java index 7eb1acbc..d9e4c20d 100644 --- a/src/main/java/com/youlai/boot/shared/mail/controller/MailController.java +++ b/src/main/java/com/youlai/boot/shared/mail/controller/MailController.java @@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.*; /** * 邮件控制层 * - * @author Ray + * @author Ray.Hao * @since 2.10.0 */ @RestController diff --git a/src/main/java/com/youlai/boot/shared/websocket/listener/OnlineUserListener.java b/src/main/java/com/youlai/boot/shared/websocket/listener/OnlineUserListener.java deleted file mode 100644 index 54fd3481..00000000 --- a/src/main/java/com/youlai/boot/shared/websocket/listener/OnlineUserListener.java +++ /dev/null @@ -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()); - } - -} diff --git a/src/main/java/com/youlai/boot/shared/websocket/service/OnlineUserService.java b/src/main/java/com/youlai/boot/shared/websocket/service/OnlineUserService.java deleted file mode 100644 index 6ef06a57..00000000 --- a/src/main/java/com/youlai/boot/shared/websocket/service/OnlineUserService.java +++ /dev/null @@ -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 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 getAllOnlineUsers() { - return Collections.unmodifiableSet(onlineUsers); - } - - /** - * 获取在线的接收者 - * 从所有接收者中过滤出在线的接收者 - * - * @param receivers 接收者 - * @return 在线的接收者集合 - */ - public Set getOnlineReceivers(Set receivers) { - return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); - } - - /** - * 获取在线用户数量 - * - * @return 在线用户数量 - */ - public int getOnlineUserCount() { - return onlineUsers.size(); - } - - -} diff --git a/src/main/java/com/youlai/boot/system/controller/DictController.java b/src/main/java/com/youlai/boot/system/controller/DictController.java index 3ec31b3b..252af202 100644 --- a/src/main/java/com/youlai/boot/system/controller/DictController.java +++ b/src/main/java/com/youlai/boot/system/controller/DictController.java @@ -16,6 +16,7 @@ import com.youlai.boot.system.model.form.DictForm; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.system.service.DictItemService; 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.tags.Tag; import io.swagger.v3.oas.annotations.Operation; @@ -42,7 +43,7 @@ public class DictController { private final DictService dictService; private final DictItemService dictItemService; - + private final WebSocketService webSocketService; //--------------------------------------------------- // 字典相关接口 @@ -80,6 +81,10 @@ public class DictController { @RepeatSubmit public Result saveDict(@Valid @RequestBody DictForm formData) { boolean result = dictService.saveDict(formData); + // 发送字典更新通知 + if (result) { + webSocketService.broadcastDictChange(formData.getDictCode()); + } return Result.judge(result); } @@ -88,9 +93,13 @@ public class DictController { @PreAuthorize("@ss.hasPerm('sys:dict:edit')") public Result updateDict( @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); } @@ -100,7 +109,16 @@ public class DictController { public Result deleteDictionaries( @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids ) { + // 获取字典编码列表,用于发送删除通知 + List dictCodes = dictService.getDictCodesByIds(Arrays.stream(ids.split(",")).toList()); + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + + // 发送字典删除通知 + for (String dictCode : dictCodes) { + webSocketService.broadcastDictChange(dictCode); + } + return Result.success(); } @@ -138,6 +156,12 @@ public class DictController { ) { formData.setDictCode(dictCode); boolean result = dictItemService.saveDictItem(formData); + + // 发送字典更新通知 + if (result) { + webSocketService.broadcastDictChange(dictCode); + } + return Result.judge(result); } @@ -163,6 +187,12 @@ public class DictController { formData.setId(itemId); formData.setDictCode(dictCode); boolean status = dictItemService.updateDictItem(formData); + + // 发送字典更新通知 + if (status) { + webSocketService.broadcastDictChange(dictCode); + } + return Result.judge(status); } @@ -170,9 +200,14 @@ public class DictController { @DeleteMapping("/{dictCode}/items/{itemIds}") @PreAuthorize("@ss.hasPerm('sys:dict-item:delete')") public Result deleteDictItems( + @PathVariable String dictCode, @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String itemIds ) { dictItemService.deleteDictItemByIds(itemIds); + + // 发送字典更新通知 + webSocketService.broadcastDictChange(dictCode); + return Result.success(); } diff --git a/src/main/java/com/youlai/boot/system/converter/DictDataConverter.java b/src/main/java/com/youlai/boot/system/converter/DictItemConverter.java similarity index 89% rename from src/main/java/com/youlai/boot/system/converter/DictDataConverter.java rename to src/main/java/com/youlai/boot/system/converter/DictItemConverter.java index c2480f78..99a354b0 100644 --- a/src/main/java/com/youlai/boot/system/converter/DictDataConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/DictItemConverter.java @@ -10,13 +10,13 @@ import org.mapstruct.Mapper; import java.util.List; /** - * 字典项 对象转换器 + * 字典项对象转换器 * - * @author Ray + * @author Ray.Hao * @since 2022/6/8 */ @Mapper(componentModel = "spring") -public interface DictDataConverter { +public interface DictItemConverter { Page toPageVo(Page page); diff --git a/src/main/java/com/youlai/boot/system/event/UserConnectionEvent.java b/src/main/java/com/youlai/boot/system/event/UserConnectionEvent.java deleted file mode 100644 index dadea814..00000000 --- a/src/main/java/com/youlai/boot/system/event/UserConnectionEvent.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/youlai/boot/shared/websocket/handler/OnlineUserJobHandler.java b/src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java similarity index 77% rename from src/main/java/com/youlai/boot/shared/websocket/handler/OnlineUserJobHandler.java rename to src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java index 75b148e2..1bcc405d 100644 --- a/src/main/java/com/youlai/boot/shared/websocket/handler/OnlineUserJobHandler.java +++ b/src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java @@ -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.extern.slf4j.Slf4j; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class OnlineUserJobHandler { - private final OnlineUserService onlineUserService; + private final UserOnlineService userOnlineService; private final SimpMessagingTemplate messagingTemplate; // 每分钟统计一次在线用户数 @@ -27,7 +27,7 @@ public class OnlineUserJobHandler { public void execute() { log.info("定时任务:统计在线用户数"); // 推送在线用户人数 - messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + messagingTemplate.convertAndSend("/topic/onlineUserCount", userOnlineService.getOnlineUserCount()); } } diff --git a/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java b/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java new file mode 100644 index 00000000..80cb44c1 --- /dev/null +++ b/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java @@ -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 sessionIds; + + /** + * 最后活动时间 + */ + private long lastActiveTime; + + public UserSessionDTO(String username) { + this.username = username; + this.sessionIds = new HashSet<>(); + this.lastActiveTime = System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/youlai/boot/system/model/event/DictEvent.java b/src/main/java/com/youlai/boot/system/model/event/DictEvent.java new file mode 100644 index 00000000..7f30ee10 --- /dev/null +++ b/src/main/java/com/youlai/boot/system/model/event/DictEvent.java @@ -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(); + } +} diff --git a/src/main/java/com/youlai/boot/system/model/form/DictForm.java b/src/main/java/com/youlai/boot/system/model/form/DictForm.java index 31e5a996..b17d320a 100644 --- a/src/main/java/com/youlai/boot/system/model/form/DictForm.java +++ b/src/main/java/com/youlai/boot/system/model/form/DictForm.java @@ -2,6 +2,7 @@ package com.youlai.boot.system.model.form; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.Getter; import lombok.Setter; @@ -26,6 +27,7 @@ public class DictForm { private String name; @Schema(description = "字典编码", example ="gender") + @NotBlank(message = "字典编码不能为空") private String dictCode; @Schema(description = "备注") diff --git a/src/main/java/com/youlai/boot/system/service/DictService.java b/src/main/java/com/youlai/boot/system/service/DictService.java index 6c9b9b68..5ab06ab7 100644 --- a/src/main/java/com/youlai/boot/system/service/DictService.java +++ b/src/main/java/com/youlai/boot/system/service/DictService.java @@ -66,6 +66,11 @@ public interface DictService extends IService { */ void deleteDictByIds(List ids); - - + /** + * 根据字典ID列表获取字典编码列表 + * + * @param ids 字典ID列表 + * @return 字典编码列表 + */ + List getDictCodesByIds(List ids); } diff --git a/src/main/java/com/youlai/boot/system/service/UserOnlineService.java b/src/main/java/com/youlai/boot/system/service/UserOnlineService.java new file mode 100644 index 00000000..0632a76c --- /dev/null +++ b/src/main/java/com/youlai/boot/system/service/UserOnlineService.java @@ -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 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 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 users; + private long timestamp; + } +} diff --git a/src/main/java/com/youlai/boot/system/service/WebSocketService.java b/src/main/java/com/youlai/boot/system/service/WebSocketService.java new file mode 100644 index 00000000..487412a8 --- /dev/null +++ b/src/main/java/com/youlai/boot/system/service/WebSocketService.java @@ -0,0 +1,46 @@ +package com.youlai.boot.system.service; + +/** + * WebSocket服务接口 + *

+ * 提供与WebSocket连接管理相关的功能,包括: + * - 用户连接/断开事件处理 + * - 字典数据变更通知 + * - 系统消息推送 + *

+ * + * @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); +} diff --git a/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java index 620a53cf..7c1d8058 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java @@ -3,7 +3,7 @@ package com.youlai.boot.system.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.model.entity.DictItem; import com.youlai.boot.system.model.form.DictItemForm; @@ -27,7 +27,7 @@ import java.util.List; @RequiredArgsConstructor public class DictItemServiceImpl extends ServiceImpl implements DictItemService { - private final DictDataConverter dictDataConverter; + private final DictItemConverter dictItemConverter; /** * 获取字典项分页列表 @@ -78,7 +78,7 @@ public class DictItemServiceImpl extends ServiceImpl i @Override public DictItemForm getDictItemForm( Long itemId) { DictItem entity = this.getById(itemId); - return dictDataConverter.toForm(entity); + return dictItemConverter.toForm(entity); } /** @@ -89,7 +89,7 @@ public class DictItemServiceImpl extends ServiceImpl i */ @Override public boolean saveDictItem(DictItemForm formData) { - DictItem entity = dictDataConverter.toEntity(formData); + DictItem entity = dictItemConverter.toEntity(formData); return this.save(entity); } @@ -101,7 +101,7 @@ public class DictItemServiceImpl extends ServiceImpl i */ @Override public boolean updateDictItem(DictItemForm formData) { - DictItem entity = dictDataConverter.toEntity(formData); + DictItem entity = dictItemConverter.toEntity(formData); return this.updateById(entity); } @@ -112,7 +112,9 @@ public class DictItemServiceImpl extends ServiceImpl i */ @Override public void deleteDictItemByIds(String ids) { - List idList = Arrays.stream(ids.split(",")).map(Long::parseLong).toList(); + List idList = Arrays.stream(ids.split(",")) + .map(Long::parseLong) + .toList(); this.removeByIds(idList); } diff --git a/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java index 8bd1d11f..256f76bb 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java @@ -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.form.DictForm; 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.service.DictItemService; import com.youlai.boot.system.service.DictService; @@ -23,7 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; /** - * 数据字典业务实现类 + * 字典业务实现类 * * @author haoxr * @since 2022/10/12 @@ -110,20 +109,23 @@ public class DictServiceImpl extends ServiceImpl implements Di */ @Override public boolean updateDict(Long id, DictForm dictForm) { - // 更新字典 - Dict entity = dictConverter.toEntity(dictForm); - - // 校验 code 是否唯一 - String dictCode = entity.getDictCode(); - long count = this.count(new LambdaQueryWrapper() - .eq(Dict::getDictCode, dictCode) - .ne(Dict::getId, id) - ); - if (count > 0) { - throw new BusinessException("字典编码已存在"); + // 获取字典 + Dict entity = this.getById(id); + if (entity == null) { + throw new BusinessException("字典不存在"); } - - return this.updateById(entity); + // 校验 code 是否唯一 + String dictCode = dictForm.getDictCode(); + if (!entity.getDictCode().equals(dictCode)) { + long count = this.count(new LambdaQueryWrapper() + .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 implements Di * * @param ids 字典ID,多个以英文逗号(,)分割 */ - @Override @Transactional + @Override public void deleteDictByIds(List ids) { - for (String id : ids) { - Dict dict = this.getById(id); - if (dict != null) { - boolean removeResult = this.removeById(id); - // 删除字典下的字典项 - if (removeResult) { - dictItemService.remove( - new LambdaQueryWrapper() - .eq(DictItem::getDictCode, dict.getDictCode()) - ); - } + // 删除字典 + this.removeByIds(ids); - } + // 删除字典项 + List list = this.listByIds(ids); + if (!list.isEmpty()) { + List dictCodes = list.stream().map(Dict::getDictCode).toList(); + dictItemService.remove(new LambdaQueryWrapper() + .in(DictItem::getDictCode, dictCodes) + ); } } + /** + * 根据字典ID列表获取字典编码列表 + * + * @param ids 字典ID列表 + * @return 字典编码列表 + */ + @Override + public List getDictCodesByIds(List ids) { + List dictList = this.listByIds(ids); + return dictList.stream().map(Dict::getDictCode).toList(); + } + } diff --git a/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java index a05f3051..e78c876e 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java @@ -9,7 +9,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.common.exception.BusinessException; 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.enums.NoticePublishStatusEnum; 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.service.NoticeService; import com.youlai.boot.system.service.UserNoticeService; +import com.youlai.boot.system.service.UserOnlineService; import com.youlai.boot.system.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -53,7 +53,7 @@ public class NoticeServiceImpl extends ServiceImpl impleme private final UserNoticeService userNoticeService; private final UserService userService; private final SimpMessagingTemplate messagingTemplate; - private final OnlineUserService onlineUserService; + private final UserOnlineService userOnlineService; /** * 获取通知公告分页列表 @@ -213,7 +213,9 @@ public class NoticeServiceImpl extends ServiceImpl impleme Set receivers = targetUserList.stream().map(User::getUsername).collect(Collectors.toSet()); - Set allOnlineUsers = onlineUserService.getAllOnlineUsers(); + Set allOnlineUsers = userOnlineService.getOnlineUsers().stream() + .map(UserOnlineService.UserOnlineDTO::getUsername) + .collect(Collectors.toSet()); // 找出在线用户的通知接收者 Set onlineReceivers = new HashSet<>(CollectionUtil.intersection(receivers, allOnlineUsers)); diff --git a/src/main/java/com/youlai/boot/system/service/impl/WebSocketServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/WebSocketServiceImpl.java new file mode 100644 index 00000000..db714dab --- /dev/null +++ b/src/main/java/com/youlai/boot/system/service/impl/WebSocketServiceImpl.java @@ -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 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 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 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; + } + } +} diff --git a/src/main/resources/mapper/system/RoleMenuMapper.xml b/src/main/resources/mapper/system/RoleMenuMapper.xml index c6654581..11fc527d 100644 --- a/src/main/resources/mapper/system/RoleMenuMapper.xml +++ b/src/main/resources/mapper/system/RoleMenuMapper.xml @@ -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_menu t3 ON t1.menu_id = t3.id WHERE - type = '${@com.youlai.boot.system.enums.MenuTypeEnum@BUTTON.getValue()}' + t3.type = '${@com.youlai.boot.system.enums.MenuTypeEnum@BUTTON.getValue()}' AND t2.`code` = #{roleCode} diff --git a/src/main/resources/templates/codegen/index.vue.vm b/src/main/resources/templates/codegen/index.vue.vm index b7c4021a..67ce5bd2 100644 --- a/src/main/resources/templates/codegen/index.vue.vm +++ b/src/main/resources/templates/codegen/index.vue.vm @@ -26,8 +26,8 @@ #else - 选项一 - 选项二 + 选项一 + 选项二 #end #elseif($fieldConfig.formType == "CHECK_BOX")