From 070b4bd9bbba52caa09fdaaa9c752bfba66fa934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Mon, 30 Mar 2026 19:44:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5vue=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9=20update=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=B8=B8=E9=87=8F=E6=9B=BF=E4=BB=A3=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E7=9A=84=E5=88=A0=E9=99=A4=E6=A0=87=E5=BF=97=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9F=A5=E8=AF=A2=E6=9D=A1=E4=BB=B6=20update?= =?UTF-8?q?=20=E5=A2=9E=E5=8A=A0=E6=B6=88=E6=81=AF=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=B3=A8=E9=87=8A=20update=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20=E5=B0=86=E5=85=A8=E5=B1=80=E7=BB=A7=E6=89=BFMPJ?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=8C=89=E9=9C=80=E6=B1=82=E7=BB=A7=E6=89=BF?= =?UTF-8?q?=20fix=20=E4=BF=AE=E5=A4=8D=20distinct=20=E5=9C=A8=20sqlserver?= =?UTF-8?q?=20=E4=B8=AD=E7=9A=84=E9=99=90=E5=88=B6=20=E8=A1=A5=E7=BC=BA?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E5=AD=97=E6=AE=B5=20=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E8=AF=AD=E6=B3=95=E6=AD=A3=E7=A1=AE=20update=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=83=A8=E9=97=A8Excel=E8=BD=AC=E6=8D=A2=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=92=8C=E4=B8=8B=E6=8B=89=E9=80=89=E9=A1=B9=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/core/utils/TreeBuildUtils.java | 32 ++++ .../mybatis/core/mapper/BaseMapperPlus.java | 4 +- .../push/config/MessageSseConfiguration.java | 20 ++ .../config/MessageWebSocketConfiguration.java | 22 ++- .../push/constant/MessageConstants.java | 15 ++ .../common/push/core/PushSessionManager.java | 27 +++ .../push/core/SseEmitterSessionManager.java | 6 +- .../push/core/WebSocketSessionManager.java | 98 +++++++++- .../push/enums/MessageTransportEnum.java | 25 +++ .../push/handler/PlusWebSocketHandler.java | 58 +++++- .../common/push/helper/PushHelper.java | 61 ++++++ .../interceptor/PlusWebSocketInterceptor.java | 23 ++- .../push/listener/MessageTopicListener.java | 17 ++ .../dubbo/RemoteMessageServiceImpl.java | 8 +- .../resource/service/ISysMessageService.java | 20 ++ .../service/impl/SysMessageServiceImpl.java | 103 ++++++++++ .../system/domain/vo/SysRoleMenuPermVo.java | 20 ++ .../system/domain/vo/SysUserExportVo.java | 13 +- .../system/domain/vo/SysUserImportVo.java | 6 +- .../system/listener/DeptExcelConverter.java | 86 +++++++++ .../system/listener/DeptExcelOptions.java | 30 +++ .../dromara/system/mapper/SysDeptMapper.java | 70 +++---- .../system/mapper/SysDictDataMapper.java | 6 + .../system/mapper/SysDictTypeMapper.java | 2 +- .../dromara/system/mapper/SysMenuMapper.java | 176 +++++++++--------- .../dromara/system/mapper/SysPostMapper.java | 14 +- .../dromara/system/mapper/SysRoleMapper.java | 25 +-- .../system/mapper/SysRoleMenuMapper.java | 4 +- .../dromara/system/mapper/SysUserMapper.java | 15 +- .../common/constant/FlowConstant.java | 10 + .../workflow/mapper/FlwHisTaskMapper.java | 41 ++-- .../workflow/mapper/FlwTaskMapper.java | 39 ++-- .../workflow/mapper/FlwUserMapper.java | 41 ++-- 33 files changed, 876 insertions(+), 261 deletions(-) create mode 100644 ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysRoleMenuPermVo.java create mode 100644 ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/listener/DeptExcelConverter.java create mode 100644 ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/listener/DeptExcelOptions.java diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java index 92ae0be41..189a2b695 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java @@ -5,10 +5,12 @@ import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.TreeNodeConfig; import cn.hutool.core.lang.tree.TreeUtil; import cn.hutool.core.lang.tree.parser.NodeParser; +import cn.hutool.core.util.StrUtil; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.dromara.common.core.utils.reflect.ReflectUtils; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -120,6 +122,20 @@ public class TreeBuildUtils extends TreeUtil { .collect(Collectors.toList()); } + /** + * 构建树节点路径 Map: 路径为 key, 节点为 value + * + * @param trees 树结构 + * @param joiner 拼接符 + * @param fieldGetter 路径拼接字段 + * @return Map<拼接路径, 原始Tree节点> + */ + public static Map> buildTreeNodeMap(List> trees, String joiner, Function, CharSequence> fieldGetter) { + Map> nodeMap = new LinkedHashMap<>(); + doBuildTreeNodeMap(trees, "", joiner, fieldGetter, nodeMap); + return nodeMap; + } + /** * 获取指定节点下的所有叶子节点 * @@ -137,4 +153,20 @@ public class TreeBuildUtils extends TreeUtil { } } + private static void doBuildTreeNodeMap(List> trees, String parentPath, String joiner, + Function, CharSequence> fieldGetter, Map> nodeMap) { + if (CollUtil.isEmpty(trees)) { + return; + } + for (Tree tree : trees) { + CharSequence field = fieldGetter.apply(tree); + if (StrUtil.isEmpty(field)) { + continue; + } + String currentPath = StrUtil.isEmpty(parentPath) ? field.toString() : parentPath + joiner + field; + nodeMap.put(currentPath, tree); + doBuildTreeNodeMap(tree.getChildren(), currentPath, joiner, fieldGetter, nodeMap); + } + } + } diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java index c8bd8548f..a5213fda4 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java @@ -4,11 +4,11 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.toolkit.Db; -import com.github.yulichang.base.MPJBaseMapper; import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.LogFactory; import org.dromara.common.core.utils.MapstructUtils; @@ -29,7 +29,7 @@ import java.util.function.Function; * @since 2021-05-13 */ @SuppressWarnings("unchecked") -public interface BaseMapperPlus extends MPJBaseMapper { +public interface BaseMapperPlus extends BaseMapper { Log log = LogFactory.getLog(BaseMapperPlus.class); diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageSseConfiguration.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageSseConfiguration.java index 02b9d78ff..2f14eb893 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageSseConfiguration.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageSseConfiguration.java @@ -16,16 +16,36 @@ import org.springframework.context.annotation.Bean; @ConditionalOnProperty(prefix = "message", name = "transport", havingValue = "sse", matchIfMissing = true) public class MessageSseConfiguration { + /** + * 注册 SSE 会话管理器 + * 负责管理用户 SSE 连接、消息发送、会话清理 + * + * @return SseEmitterSessionManager 实例 + */ @Bean public SseEmitterSessionManager sseEmitterManager() { return new SseEmitterSessionManager(); } + /** + * 注册消息主题监听器 + * 监听 Redis 全局消息,用于集群环境下的消息分发 + * + * @param manager SSE 会话管理器 + * @return MessageTopicListener 实例 + */ @Bean public MessageTopicListener messageTopicListener(SseEmitterSessionManager manager) { return new MessageTopicListener(manager); } + /** + * 注册 SSE 控制器 + * 提供前端建立 SSE 连接的接口 + * + * @param manager SSE 会话管理器 + * @return SseController 实例 + */ @Bean public SseController sseController(SseEmitterSessionManager manager) { return new SseController(manager); diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageWebSocketConfiguration.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageWebSocketConfiguration.java index b1bb6ec2e..174fa4b4c 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageWebSocketConfiguration.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageWebSocketConfiguration.java @@ -1,9 +1,9 @@ package org.dromara.common.push.config; -import org.dromara.common.push.listener.MessageTopicListener; import org.dromara.common.push.core.WebSocketSessionManager; import org.dromara.common.push.handler.PlusWebSocketHandler; import org.dromara.common.push.interceptor.PlusWebSocketInterceptor; +import org.dromara.common.push.listener.MessageTopicListener; import org.dromara.common.push.properties.MessageProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -23,6 +23,10 @@ import org.springframework.web.socket.server.HandshakeInterceptor; @ConditionalOnProperty(prefix = "message", name = "transport", havingValue = "websocket") public class MessageWebSocketConfiguration { + /** + * WebSocket 配置注册 + * 配置连接路径、拦截器、跨域 + */ @Bean public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor handshakeInterceptor, WebSocketHandler webSocketHandler, @@ -33,21 +37,37 @@ public class MessageWebSocketConfiguration { .setAllowedOrigins(messageProperties.getAllowedOrigins()); } + /** + * WebSocket 会话管理器 + * 负责连接管理、消息发送、定时清理失效会话 + */ @Bean public WebSocketSessionManager webSocketSessionManager() { return new WebSocketSessionManager(); } + /** + * WebSocket 握手拦截器 + * 建立连接前做登录校验、客户端ID校验 + */ @Bean public HandshakeInterceptor handshakeInterceptor() { return new PlusWebSocketInterceptor(); } + /** + * WebSocket 消息处理器 + * 处理连接、消息、心跳、断开、异常等事件 + */ @Bean public WebSocketHandler webSocketHandler(WebSocketSessionManager webSocketSessionManager) { return new PlusWebSocketHandler(webSocketSessionManager); } + /** + * 消息主题监听器 + * 订阅 Redis 消息,实现集群环境下的消息分发 + */ @Bean public MessageTopicListener messageTopicListener(WebSocketSessionManager webSocketSessionManager) { return new MessageTopicListener(webSocketSessionManager); diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/constant/MessageConstants.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/constant/MessageConstants.java index 3f66957af..cefec0238 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/constant/MessageConstants.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/constant/MessageConstants.java @@ -7,13 +7,28 @@ package org.dromara.common.push.constant; */ public interface MessageConstants { + /** + * 登录用户信息 + */ String LOGIN_USER_KEY = "loginUser"; + /** + * 登录令牌 + */ String LOGIN_TOKEN_KEY = "token"; + /** + * 全局消息订阅主题 + */ String MESSAGE_TOPIC = "global:message"; + /** + * 心跳请求标识 + */ String PING = "ping"; + /** + * 心跳响应标识 + */ String PONG = "pong"; } diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/PushSessionManager.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/PushSessionManager.java index 898167d38..dff15e115 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/PushSessionManager.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/PushSessionManager.java @@ -12,13 +12,40 @@ import java.util.function.Consumer; */ public interface PushSessionManager { + /** + * 订阅消息通道 + * 注册消息消费者,用于监听并处理消息推送事件 + * + * @param consumer 消息消费逻辑 + */ void subscribeMessage(Consumer consumer); + /** + * 发送消息给指定用户 + * + * @param userId 目标用户ID + * @param payload 消息体 + */ void sendMessage(Long userId, PushPayloadDTO payload); + /** + * 全局广播消息(所有在线用户) + * + * @param payload 消息体 + */ void sendMessage(PushPayloadDTO payload); + /** + * 批量发布消息给指定用户列表 + * + * @param pushDTO 推送参数封装对象 + */ void publishMessage(PushDTO pushDTO); + /** + * 全局广播消息(所有用户) + * + * @param payload 消息体 + */ void publishAll(PushPayloadDTO payload); } diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/SseEmitterSessionManager.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/SseEmitterSessionManager.java index 8cef1a75e..71bf5b3b0 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/SseEmitterSessionManager.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/SseEmitterSessionManager.java @@ -3,11 +3,11 @@ package org.dromara.common.push.core; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import lombok.extern.slf4j.Slf4j; -import org.dromara.common.push.constant.MessageConstants; -import org.dromara.common.push.dto.PushPayloadDTO; -import org.dromara.common.push.dto.PushDTO; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.json.utils.JsonUtils; +import org.dromara.common.push.constant.MessageConstants; +import org.dromara.common.push.dto.PushDTO; +import org.dromara.common.push.dto.PushPayloadDTO; import org.dromara.common.redis.utils.RedisUtils; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/WebSocketSessionManager.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/WebSocketSessionManager.java index 39a6c2162..7cb877967 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/WebSocketSessionManager.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/WebSocketSessionManager.java @@ -5,14 +5,10 @@ import cn.hutool.core.map.MapUtil; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.json.utils.JsonUtils; -import org.dromara.common.push.dto.PushPayloadDTO; import org.dromara.common.push.dto.PushDTO; +import org.dromara.common.push.dto.PushPayloadDTO; import org.dromara.common.redis.utils.RedisUtils; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.PongMessage; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketMessage; -import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.*; import java.io.IOException; import java.util.ArrayList; @@ -33,20 +29,44 @@ import static org.dromara.common.push.constant.MessageConstants.MESSAGE_TOPIC; @Slf4j public class WebSocketSessionManager implements PushSessionManager { + /** + * 用户会话存储集合 + * 结构:userId -> (token -> WebSocketSession) + * 支持同一用户多终端、多设备同时在线 + */ private static final Map> USER_TOKEN_SESSIONS = new ConcurrentHashMap<>(); + /** + * 构造函数 + * 初始化定时任务:每60秒执行一次会话监控,自动清理无效连接 + */ public WebSocketSessionManager() { SpringUtils.getBean(ScheduledExecutorService.class) .scheduleWithFixedDelay(this::sessionMonitor, 60L, 60L, TimeUnit.SECONDS); } + /** + * 用户建立WebSocket连接 + * + * @param userId 用户ID + * @param token 客户端唯一标识(区分不同设备/终端) + * @param session WebSocket会话对象 + */ public void connect(Long userId, String token, WebSocketSession session) { Map sessions = USER_TOKEN_SESSIONS.computeIfAbsent(userId, key -> new ConcurrentHashMap<>()); + // 移除并关闭旧的同token会话,避免重复连接 WebSocketSession oldSession = sessions.remove(token); closeSession(oldSession, CloseStatus.NORMAL); + // 存储新会话 sessions.put(token, session); } + /** + * 用户断开WebSocket连接 + * + * @param userId 用户ID + * @param token 客户端唯一标识 + */ public void disconnect(Long userId, String token) { if (userId == null || token == null) { return; @@ -56,12 +76,18 @@ public class WebSocketSessionManager implements PushSessionManager { USER_TOKEN_SESSIONS.remove(userId); return; } + // 移除指定token会话并关闭 closeSession(sessions.remove(token), CloseStatus.NORMAL); + // 该用户无任何会话时,从缓存中移除 if (sessions.isEmpty()) { USER_TOKEN_SESSIONS.remove(userId); } } + /** + * 会话监控定时任务 + * 定期清理已关闭、失效的WebSocket会话,防止内存泄漏 + */ public void sessionMonitor() { List toRemoveUsers = new ArrayList<>(); USER_TOKEN_SESSIONS.forEach((userId, sessionMap) -> { @@ -69,6 +95,7 @@ public class WebSocketSessionManager implements PushSessionManager { toRemoveUsers.add(userId); return; } + // 移除已关闭的无效会话 sessionMap.entrySet().removeIf(entry -> { WebSocketSession session = entry.getValue(); if (session == null || !session.isOpen()) { @@ -77,18 +104,32 @@ public class WebSocketSessionManager implements PushSessionManager { } return false; }); + // 无有效会话,标记用户待删除 if (sessionMap.isEmpty()) { toRemoveUsers.add(userId); } }); + // 批量清理无会话用户 toRemoveUsers.forEach(USER_TOKEN_SESSIONS::remove); } + /** + * 订阅消息通道 + * 注册消息消费者,监听Redis消息推送 + * + * @param consumer 消息消费逻辑 + */ @Override public void subscribeMessage(Consumer consumer) { RedisUtils.subscribe(MESSAGE_TOPIC, PushDTO.class, consumer); } + /** + * 向指定用户发送消息 + * + * @param userId 目标用户ID + * @param payload 消息体 + */ @Override public void sendMessage(Long userId, PushPayloadDTO payload) { if (payload == null) { @@ -99,24 +140,38 @@ public class WebSocketSessionManager implements PushSessionManager { USER_TOKEN_SESSIONS.remove(userId); return; } + // 发送消息并自动清理失效会话 sessions.entrySet().removeIf(entry -> { WebSocketSession session = entry.getValue(); if (session == null || !session.isOpen()) { closeSession(session, CloseStatus.NORMAL); return true; } + // 发送失败的会话也会被移除 return !sendMessage(session, new TextMessage(JsonUtils.toJsonString(payload))); }); + // 无有效会话则移除用户 if (sessions.isEmpty()) { USER_TOKEN_SESSIONS.remove(userId); } } + /** + * 向所有在线用户广播消息 + * + * @param payload 消息体 + */ @Override public void sendMessage(PushPayloadDTO payload) { USER_TOKEN_SESSIONS.keySet().forEach(userId -> sendMessage(userId, payload)); } + /** + * 发布消息到Redis订阅通道 + * 支持集群环境下的分布式消息推送 + * + * @param pushDTO 推送消息封装对象 + */ @Override public void publishMessage(PushDTO pushDTO) { RedisUtils.publish(MESSAGE_TOPIC, pushDTO, consumer -> log.info( @@ -127,6 +182,11 @@ public class WebSocketSessionManager implements PushSessionManager { )); } + /** + * 全局广播消息(所有用户) + * + * @param payload 消息体 + */ @Override public void publishAll(PushPayloadDTO payload) { PushDTO dto = new PushDTO(); @@ -134,14 +194,33 @@ public class WebSocketSessionManager implements PushSessionManager { publishMessage(dto); } + /** + * 发送心跳Pong消息 + * 用于维持WebSocket长连接存活 + * + * @param session WebSocket会话 + */ public void sendPongMessage(WebSocketSession session) { sendMessage(session, new PongMessage()); } + /** + * 发送文本消息 + * + * @param session WebSocket会话 + * @param message 文本内容 + */ public void sendMessage(WebSocketSession session, String message) { sendMessage(session, new TextMessage(message)); } + /** + * 底层消息发送方法 + * + * @param session 会话对象 + * @param message WebSocket消息对象 + * @return 发送是否成功 + */ private boolean sendMessage(WebSocketSession session, WebSocketMessage message) { if (session == null || !session.isOpen()) { log.warn("[send] session会话已经关闭"); @@ -156,6 +235,12 @@ public class WebSocketSessionManager implements PushSessionManager { } } + /** + * 安全关闭WebSocket会话 + * + * @param session 待关闭的会话 + * @param status 关闭状态码 + */ private void closeSession(WebSocketSession session, CloseStatus status) { if (session == null) { return; @@ -163,6 +248,7 @@ public class WebSocketSessionManager implements PushSessionManager { try { session.close(status); } catch (Exception ignored) { + // 关闭异常忽略,防止影响主流程 } } } diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/enums/MessageTransportEnum.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/enums/MessageTransportEnum.java index ee5660e1b..362d26645 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/enums/MessageTransportEnum.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/enums/MessageTransportEnum.java @@ -14,15 +14,40 @@ import java.util.Arrays; @AllArgsConstructor public enum MessageTransportEnum { + /** + * SSE 传输方式 + * 服务端推送事件,单向轻量传输 + */ SSE("sse"), + + /** + * WebSocket 传输方式 + * 全双工长连接,支持双向实时通信 + */ WEBSOCKET("websocket"); + /** + * 传输类型编码 + */ private final String code; + /** + * 判断传输方式是否匹配 + * + * @param transport 传输方式字符串 + * @return 是否匹配 + */ public boolean matches(String transport) { return code.equalsIgnoreCase(transport); } + /** + * 根据传输类型字符串获取枚举 + * 找不到则默认返回 SSE + * + * @param transport 传输方式字符串 + * @return 对应的消息传输枚举 + */ public static MessageTransportEnum of(String transport) { return Arrays.stream(values()) .filter(item -> item.matches(transport)) diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/handler/PlusWebSocketHandler.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/handler/PlusWebSocketHandler.java index 03863d452..d4928caac 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/handler/PlusWebSocketHandler.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/handler/PlusWebSocketHandler.java @@ -7,14 +7,10 @@ import org.dromara.common.core.enums.PushSourceEnum; import org.dromara.common.core.enums.PushTypeEnum; import org.dromara.common.push.constant.MessageConstants; import org.dromara.common.push.core.WebSocketSessionManager; -import org.dromara.common.push.dto.PushPayloadDTO; import org.dromara.common.push.dto.PushDTO; +import org.dromara.common.push.dto.PushPayloadDTO; import org.dromara.system.api.model.LoginUser; -import org.springframework.web.socket.BinaryMessage; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.PongMessage; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.*; import org.springframework.web.socket.handler.AbstractWebSocketHandler; import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; @@ -22,7 +18,8 @@ import java.io.IOException; import java.util.List; /** - * WebSocket Handler。 + * WebSocket 请求处理器 + * 处理WebSocket连接建立、消息接收、异常、断开等全生命周期事件 * * @author Lion Li */ @@ -30,17 +27,31 @@ import java.util.List; @Slf4j public class PlusWebSocketHandler extends AbstractWebSocketHandler { + /** + * WebSocket 会话管理器 + */ private final WebSocketSessionManager webSocketSessionManager; + /** + * 建立WebSocket连接后触发 + * 校验用户登录信息,注册会话 + * + * @param session WebSocket会话 + */ @Override public void afterConnectionEstablished(WebSocketSession session) throws IOException { + // 从会话属性中获取登录用户信息和Token LoginUser loginUser = (LoginUser) session.getAttributes().get(MessageConstants.LOGIN_USER_KEY); String token = (String) session.getAttributes().get(MessageConstants.LOGIN_TOKEN_KEY); + + // 校验用户信息是否为空,无效则直接关闭连接 if (ObjectUtil.hasNull(loginUser, token)) { session.close(CloseStatus.BAD_DATA); log.info("[connect] invalid token received. sessionId: {}", session.getId()); return; } + + // 并发安全包装会话,并注册到会话管理器 webSocketSessionManager.connect( loginUser.getUserId(), token, @@ -49,16 +60,27 @@ public class PlusWebSocketHandler extends AbstractWebSocketHandler { log.info("[connect] sessionId: {}, userId:{}, token:{}", session.getId(), loginUser.getUserId(), token); } + /** + * 处理客户端发送的文本消息 + * 支持心跳ping/pong,以及自定义消息转发 + * + * @param session WebSocket会话 + * @param message 文本消息 + */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { LoginUser loginUser = (LoginUser) session.getAttributes().get(MessageConstants.LOGIN_USER_KEY); if (ObjectUtil.isNull(loginUser)) { return; } + + // 心跳处理:客户端发送ping,服务端回复pong if (MessageConstants.PING.equalsIgnoreCase(message.getPayload())) { webSocketSessionManager.sendMessage(session, MessageConstants.PONG); return; } + + // 构建客户端自定义消息并发布 PushDTO dto = new PushDTO(); dto.setUserIds(List.of(loginUser.getUserId())); dto.setPayload(PushPayloadDTO.of( @@ -70,33 +92,55 @@ public class PlusWebSocketHandler extends AbstractWebSocketHandler { webSocketSessionManager.publishMessage(dto); } + /** + * 处理二进制消息(默认实现) + */ @Override protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception { super.handleBinaryMessage(session, message); } + /** + * 处理Pong心跳响应 + * 维持长连接存活 + */ @Override protected void handlePongMessage(WebSocketSession session, PongMessage message) { webSocketSessionManager.sendPongMessage(session); } + /** + * 传输异常处理 + * 记录异常日志 + */ @Override public void handleTransportError(WebSocketSession session, Throwable exception) { log.error("[transport error] sessionId: {}, exception:{}", session.getId(), exception.getMessage()); } + /** + * 连接关闭后触发 + * 注销用户会话 + */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { LoginUser loginUser = (LoginUser) session.getAttributes().get(MessageConstants.LOGIN_USER_KEY); String token = (String) session.getAttributes().get(MessageConstants.LOGIN_TOKEN_KEY); + if (ObjectUtil.hasNull(loginUser, token)) { log.info("[disconnect] invalid token received. sessionId: {}", session.getId()); return; } + + // 从会话管理器中移除连接 webSocketSessionManager.disconnect(loginUser.getUserId(), token); log.info("[disconnect] sessionId: {}, userId:{}, token:{}", session.getId(), loginUser.getUserId(), token); } + /** + * 是否支持分片消息 + * 关闭:不支持分片传输 + */ @Override public boolean supportsPartialMessages() { return false; diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/helper/PushHelper.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/helper/PushHelper.java index 0cf4f3420..dd38545e7 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/helper/PushHelper.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/helper/PushHelper.java @@ -19,14 +19,31 @@ import java.util.List; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class PushHelper { + /** + * 发送指定用户文本消息 + * + * @param userId 目标用户ID + * @param message 文本消息内容 + */ public static void sendMessage(Long userId, String message) { sendMessage(userId, buildMessage(message)); } + /** + * 全局广播文本消息 + * + * @param message 文本消息内容 + */ public static void sendMessage(String message) { sendMessage(buildMessage(message)); } + /** + * 发送指定用户自定义消息体 + * + * @param userId 目标用户ID + * @param payload 消息推送体 + */ public static void sendMessage(Long userId, PushPayloadDTO payload) { if (!isEnabled()) { return; @@ -34,6 +51,11 @@ public class PushHelper { getSessionManager().sendMessage(userId, payload); } + /** + * 全局广播自定义消息体 + * + * @param payload 消息推送体 + */ public static void sendMessage(PushPayloadDTO payload) { if (!isEnabled()) { return; @@ -41,6 +63,12 @@ public class PushHelper { getSessionManager().sendMessage(payload); } + /** + * 批量发布消息给指定用户列表 + * + * @param userIds 用户ID集合 + * @param payload 消息推送体 + */ public static void publishMessage(List userIds, PushPayloadDTO payload) { PushDTO dto = new PushDTO(); dto.setUserIds(userIds); @@ -48,6 +76,11 @@ public class PushHelper { publishMessage(dto); } + /** + * 批量发布消息(使用完整推送DTO) + * + * @param dto 推送参数封装对象 + */ public static void publishMessage(PushDTO dto) { if (!isEnabled() || dto == null || dto.getPayload() == null) { return; @@ -55,10 +88,20 @@ public class PushHelper { getSessionManager().publishMessage(dto); } + /** + * 发布全局广播文本消息 + * + * @param message 文本消息内容 + */ public static void publishAll(String message) { publishAll(buildMessage(message)); } + /** + * 发布全局广播自定义消息体 + * + * @param payload 消息推送体 + */ public static void publishAll(PushPayloadDTO payload) { if (!isEnabled()) { return; @@ -66,15 +109,33 @@ public class PushHelper { getSessionManager().publishAll(payload); } + /** + * 判断消息推送功能是否开启 + * 读取配置:message.enabled + * + * @return 是否开启推送 + */ public static boolean isEnabled() { return Boolean.TRUE.equals(SpringUtils.getProperty("message.enabled", Boolean.class, Boolean.TRUE)); } + /** + * 获取推送会话管理器Bean + * + * @return PushSessionManager 实例 + */ private static PushSessionManager getSessionManager() { return SpringUtils.getBean(PushSessionManager.class); } + /** + * 构建默认格式的消息推送体 + * + * @param message 消息内容 + * @return 封装好的 PushPayloadDTO + */ private static PushPayloadDTO buildMessage(String message) { return PushPayloadDTO.of(PushTypeEnum.MESSAGE, PushSourceEnum.BACKEND, message, null); } + } diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/interceptor/PlusWebSocketInterceptor.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/interceptor/PlusWebSocketInterceptor.java index 799130cff..b53a537c0 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/interceptor/PlusWebSocketInterceptor.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/interceptor/PlusWebSocketInterceptor.java @@ -14,6 +14,7 @@ import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import java.util.Map; + /** * WebSocket 握手拦截器。 * @@ -22,36 +23,54 @@ import java.util.Map; @Slf4j public class PlusWebSocketInterceptor implements HandshakeInterceptor { + /** + * 握手前拦截(核心认证逻辑) + * 校验登录状态、Token、客户端ID,认证通过才允许建立 WebSocket 连接 + * + * @param attributes 用于传递到 WebSocketSession 的属性集合 + * @return 是否允许握手(true=允许,false=拒绝) + */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { try { + // 1. 获取当前登录用户与 Token LoginUser loginUser = LoginHelper.getLoginUser(); String tokenValue = StpUtil.getTokenValue(); + + // 2. 未登录直接拒绝握手 if (loginUser == null || StringUtils.isBlank(tokenValue)) { return false; } + // 3. 校验客户端ID(防止多端冒用) String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY); String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY); String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString(); + + // 客户端ID必须与请求头/参数中的一致,否则拒绝连接 if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) { throw NotLoginException.newInstance(StpUtil.getLoginType(), "-100", "客户端ID与Token不匹配", StpUtil.getTokenValue()); } + // 4. 认证通过,将用户信息存入会话属性,供后续 WebSocketHandler 使用 attributes.put(MessageConstants.LOGIN_USER_KEY, loginUser); attributes.put(MessageConstants.LOGIN_TOKEN_KEY, tokenValue); return true; } catch (NotLoginException e) { + // 认证失败,记录日志并拒绝连接 log.error("WebSocket 认证失败'{}',无法访问系统资源", e.getMessage()); return false; } } + /** + * 握手完成后触发 + * 此处无需处理,留空即可 + */ @Override - public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, - Exception exception) { + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { } } diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/listener/MessageTopicListener.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/listener/MessageTopicListener.java index 4f1bbad7a..a0cb451a4 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/listener/MessageTopicListener.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/listener/MessageTopicListener.java @@ -17,10 +17,20 @@ import org.springframework.core.Ordered; @RequiredArgsConstructor public class MessageTopicListener implements ApplicationRunner, Ordered { + /** + * 推送会话管理器 + */ private final PushSessionManager pushSessionManager; + /** + * 项目启动后执行 + * 注册消息订阅,监听消息并分发给对应用户/全局广播 + * + * @param args 启动参数 + */ @Override public void run(ApplicationArguments args) { + // 订阅消息主题,处理消息分发 pushSessionManager.subscribeMessage(message -> { log.info("消息主题订阅收到消息userIds={} message={}", message.getUserIds(), @@ -28,15 +38,22 @@ public class MessageTopicListener implements ApplicationRunner, Ordered { if (message.getPayload() == null) { return; } + // 有指定用户 -> 单发 if (CollUtil.isNotEmpty(message.getUserIds())) { message.getUserIds().forEach(userId -> pushSessionManager.sendMessage(userId, message.getPayload())); } else { + // 无指定用户 -> 全局广播 pushSessionManager.sendMessage(message.getPayload()); } }); log.info("初始化消息主题订阅监听器成功"); } + /** + * 执行顺序,优先级设为最高,确保消息订阅最先初始化 + * + * @return 优先级,值越小越先执行 + */ @Override public int getOrder() { return -1; diff --git a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/dubbo/RemoteMessageServiceImpl.java b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/dubbo/RemoteMessageServiceImpl.java index 83292f6b8..310f50ba4 100644 --- a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/dubbo/RemoteMessageServiceImpl.java +++ b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/dubbo/RemoteMessageServiceImpl.java @@ -31,12 +31,12 @@ public class RemoteMessageServiceImpl implements RemoteMessageService { /** * 发送消息 * - * @param sessionKey session主键 一般为用户id - * @param message 消息文本 + * @param userIds 用户ID列表 + * @param message 消息文本 */ @Override - public void publishMessage(List sessionKey, String message) { - publishMessagePayload(sessionKey, RemotePushPayLoad.of(PushTypeEnum.MESSAGE, PushSourceEnum.BACKEND, message, null)); + public void publishMessage(List userIds, String message) { + publishMessagePayload(userIds, RemotePushPayLoad.of(PushTypeEnum.MESSAGE, PushSourceEnum.BACKEND, message, null)); } @Override diff --git a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/ISysMessageService.java b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/ISysMessageService.java index a7df177ee..b86ff89eb 100644 --- a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/ISysMessageService.java +++ b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/ISysMessageService.java @@ -12,9 +12,29 @@ import java.util.List; */ public interface ISysMessageService { + /** + * 查询当前用户消息盒子数据 + * 按系统消息、通知公告、工作流消息分类返回 + * + * @param userId 用户ID + * @return 消息盒子数据 + */ SysMessageBoxVo queryMessageBox(Long userId); + /** + * 存储全局广播消息到数据库 + * + * @param payload 消息推送体 + * @return 回填消息ID后的消息体 + */ PushPayloadDTO storeAll(PushPayloadDTO payload); + /** + * 存储指定用户消息到数据库 + * + * @param userIds 用户ID集合 + * @param payload 消息推送体 + * @return 回填消息ID后的消息体 + */ PushPayloadDTO storeUsers(List userIds, PushPayloadDTO payload); } diff --git a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/impl/SysMessageServiceImpl.java b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/impl/SysMessageServiceImpl.java index 5ab1e3ed8..b63ce9763 100644 --- a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/impl/SysMessageServiceImpl.java +++ b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/impl/SysMessageServiceImpl.java @@ -34,15 +34,45 @@ import java.util.concurrent.TimeUnit; @Service public class SysMessageServiceImpl implements ISysMessageService { + /** + * 全局广播用户标识(所有用户可见) + */ private static final String GLOBAL_USER_IDS = "0"; + + /** + * 消息分类:系统消息 + */ private static final String CATEGORY_SYSTEM = "system"; + + /** + * 消息分类:通知公告 + */ private static final String CATEGORY_NOTICE = "notice"; + + /** + * 消息分类:工作流 + */ private static final String CATEGORY_WORKFLOW = "workflow"; + + /** + * 消息盒子每页展示最大条数 + */ private static final int BOX_LIMIT = 100; + + /** + * 消息盒子展示消息天数(仅展示30天内) + */ private static final long BOX_DAYS = 30L; private final SysMessageMapper baseMapper; + /** + * 查询当前用户消息盒子数据 + * 按系统消息、通知公告、工作流消息分类返回 + * + * @param userId 用户ID + * @return 分类消息盒子数据 + */ @Override public SysMessageBoxVo queryMessageBox(Long userId) { SysMessageBoxVo box = new SysMessageBoxVo(); @@ -52,16 +82,37 @@ public class SysMessageServiceImpl implements ISysMessageService { return box; } + /** + * 存储全局广播消息到数据库 + * + * @param payload 消息推送体 + * @return 回填消息ID后的消息体 + */ @Override public PushPayloadDTO storeAll(PushPayloadDTO payload) { return storeMessage(null, payload); } + /** + * 存储指定用户消息到数据库 + * + * @param userIds 用户ID集合 + * @param payload 消息推送体 + * @return 回填消息ID后的消息体 + */ @Override public PushPayloadDTO storeUsers(List userIds, PushPayloadDTO payload) { return storeMessage(userIds, payload); } + /** + * 统一消息存储逻辑 + * 判断是否需要存入消息盒子,需要则插入数据库 + * + * @param userIds 用户ID集合(为null则全局广播) + * @param payload 消息推送体 + * @return 回填消息ID后的消息体 + */ private PushPayloadDTO storeMessage(List userIds, PushPayloadDTO payload) { if (!supportsMessageBox(payload)) { return payload; @@ -72,6 +123,14 @@ public class SysMessageServiceImpl implements ISysMessageService { return payload; } + /** + * 根据分类和用户ID查询消息列表 + * 仅查询30天内、最多100条、按时间倒序 + * + * @param category 消息分类 + * @param userId 用户ID + * @return 消息VO列表 + */ private List selectMessageList(String category, Long userId) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(SysMessage::getCategory, category); @@ -84,6 +143,13 @@ public class SysMessageServiceImpl implements ISysMessageService { return list.stream().map(this::buildVo).toList(); } + /** + * 构建消息实体(用于数据库存储) + * + * @param userIds 接收用户ID集合 + * @param payload 消息推送体 + * @return 系统消息实体 + */ private SysMessage buildMessage(List userIds, PushPayloadDTO payload) { SysMessage message = new SysMessage(); message.setMessageId(payload.getMessageId() == null ? IdGeneratorUtil.nextLongId() : payload.getMessageId()); @@ -99,6 +165,12 @@ public class SysMessageServiceImpl implements ISysMessageService { return message; } + /** + * 消息实体转换为展示VO + * + * @param entity 消息实体 + * @return 消息展示VO + */ private SysMessageVo buildVo(SysMessage entity) { SysMessageVo vo = new SysMessageVo(); vo.setMessageId(entity.getMessageId()); @@ -114,6 +186,13 @@ public class SysMessageServiceImpl implements ISysMessageService { return vo; } + /** + * 判断消息是否需要存入消息盒子 + * 仅系统消息、通知消息需要存入 + * + * @param payload 消息推送体 + * @return 是否支持存入消息盒子 + */ private boolean supportsMessageBox(PushPayloadDTO payload) { if (payload == null) { return false; @@ -125,6 +204,12 @@ public class SysMessageServiceImpl implements ISysMessageService { return false; } + /** + * 根据消息类型/来源自动解析消息分类 + * + * @param payload 消息推送体 + * @return 消息分类(system/notice/workflow) + */ private String resolveCategory(PushPayloadDTO payload) { if (StringUtils.equalsAny(payload.getType(), PushTypeEnum.NOTICE.getType()) || StringUtils.equalsAny(payload.getSource(), PushSourceEnum.NOTICE.getSource())) { @@ -136,6 +221,12 @@ public class SysMessageServiceImpl implements ISysMessageService { return CATEGORY_SYSTEM; } + /** + * 根据消息分类自动生成消息标题 + * + * @param payload 消息推送体 + * @return 消息标题 + */ private String resolveTitle(PushPayloadDTO payload) { return switch (resolveCategory(payload)) { case CATEGORY_NOTICE -> "通知公告消息"; @@ -144,6 +235,12 @@ public class SysMessageServiceImpl implements ISysMessageService { }; } + /** + * 解析消息内容(从data中提取noticeContent) + * + * @param payload 消息推送体 + * @return 消息内容 + */ private String resolveContent(PushPayloadDTO payload) { Object data = payload.getData(); if (data instanceof Map map) { @@ -152,6 +249,12 @@ public class SysMessageServiceImpl implements ISysMessageService { return null; } + /** + * 解析JSON数据字符串为对象 + * + * @param dataJson JSON字符串 + * @return 解析后对象 + */ private Object parseData(String dataJson) { if (StringUtils.isBlank(dataJson)) { return null; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysRoleMenuPermVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysRoleMenuPermVo.java new file mode 100644 index 000000000..0254e1e9a --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysRoleMenuPermVo.java @@ -0,0 +1,20 @@ +package org.dromara.system.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 角色菜单权限视图 + */ +@Data +public class SysRoleMenuPermVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long roleId; + + private String perms; +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java index 580d407ab..e25a6d583 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java @@ -5,6 +5,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.dromara.common.excel.annotation.ExcelDictFormat; import org.dromara.common.excel.convert.ExcelDictConvert; +import org.dromara.system.listener.DeptExcelConverter; import java.io.Serial; import java.io.Serializable; @@ -35,6 +36,12 @@ public class SysUserExportVo implements Serializable { @ExcelProperty(value = "用户账号") private String userName; + /** + * 部门ID + */ + @ExcelProperty(value = "部门名称", converter = DeptExcelConverter.class) + private Long deptId; + /** * 用户昵称 */ @@ -79,12 +86,6 @@ public class SysUserExportVo implements Serializable { @ExcelProperty(value = "最后登录时间") private Date loginDate; - /** - * 部门名称 - */ - @ExcelProperty(value = "部门名称") - private String deptName; - /** * 负责人 */ diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java index 8247581d2..bb0c890fe 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java @@ -4,7 +4,10 @@ import org.apache.fesod.sheet.annotation.ExcelProperty; import lombok.Data; import lombok.NoArgsConstructor; import org.dromara.common.excel.annotation.ExcelDictFormat; +import org.dromara.common.excel.annotation.ExcelDynamicOptions; import org.dromara.common.excel.convert.ExcelDictConvert; +import org.dromara.system.listener.DeptExcelConverter; +import org.dromara.system.listener.DeptExcelOptions; import java.io.Serial; import java.io.Serializable; @@ -33,7 +36,8 @@ public class SysUserImportVo implements Serializable { /** * 部门ID */ - @ExcelProperty(value = "部门编号") + @ExcelProperty(value = "部门名称", converter = DeptExcelConverter.class) + @ExcelDynamicOptions(providerClass = DeptExcelOptions.class) private Long deptId; /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/listener/DeptExcelConverter.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/listener/DeptExcelConverter.java new file mode 100644 index 000000000..2225adf39 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/listener/DeptExcelConverter.java @@ -0,0 +1,86 @@ +package org.dromara.system.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.tree.Tree; +import lombok.RequiredArgsConstructor; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.dromara.common.core.utils.SpringUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.core.utils.TreeBuildUtils; +import org.dromara.system.domain.bo.SysDeptBo; +import org.dromara.system.service.ISysDeptService; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * Excel 部门转换处理 + */ +@RequiredArgsConstructor +@Component +public class DeptExcelConverter implements Converter { + + private static final ThreadLocal> TL_ID_TO_NAME = new ThreadLocal<>(); + + private static final ThreadLocal> TL_NAME_TO_ID = new ThreadLocal<>(); + + private void initThreadCache() { + Map idMap = TL_ID_TO_NAME.get(); + if (CollUtil.isNotEmpty(idMap)) { + return; + } + + Map> deptPathToTreeMap = TreeBuildUtils.buildTreeNodeMap( + SpringUtils.getBean(ISysDeptService.class).selectDeptTreeList(new SysDeptBo()), + "/", + Tree::getName + ); + + Map idToName = new HashMap<>(); + Map nameToId = new HashMap<>(); + deptPathToTreeMap.forEach((name, treeNode) -> { + Long deptId = treeNode.getId(); + idToName.put(deptId, name); + nameToId.put(name, deptId); + }); + + TL_ID_TO_NAME.set(idToName); + TL_NAME_TO_ID.set(nameToId); + } + + @Override + public Class supportJavaTypeKey() { + return Long.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public Long convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + String deptName = cellData.getStringValue(); + if (StringUtils.isBlank(deptName)) { + return null; + } + initThreadCache(); + return TL_NAME_TO_ID.get().get(deptName); + } + + @Override + public WriteCellData convertToExcelData(Long value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + if (value == null) { + return new WriteCellData<>(""); + } + initThreadCache(); + String deptName = TL_ID_TO_NAME.get().getOrDefault(value, ""); + return new WriteCellData<>(deptName); + } +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/listener/DeptExcelOptions.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/listener/DeptExcelOptions.java new file mode 100644 index 000000000..993518a22 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/listener/DeptExcelOptions.java @@ -0,0 +1,30 @@ +package org.dromara.system.listener; + +import cn.hutool.core.lang.tree.Tree; +import lombok.RequiredArgsConstructor; +import org.dromara.common.core.utils.TreeBuildUtils; +import org.dromara.common.excel.core.ExcelOptionsProvider; +import org.dromara.system.domain.bo.SysDeptBo; +import org.dromara.system.service.ISysDeptService; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Excel 部门下拉选项数据源 + */ +@RequiredArgsConstructor +@Component +public class DeptExcelOptions implements ExcelOptionsProvider { + + private final ISysDeptService deptService; + + @Override + public Set getOptions() { + List> trees = deptService.selectDeptTreeList(new SysDeptBo()); + Map> treeMap = TreeBuildUtils.buildTreeNodeMap(trees, "/", Tree::getName); + return treeMap.keySet(); + } +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDeptMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDeptMapper.java index 80a99cf34..5777e6a90 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDeptMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDeptMapper.java @@ -3,58 +3,31 @@ package org.dromara.system.mapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; +import com.github.yulichang.toolkit.JoinWrappers; import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.mybatis.annotation.DataColumn; import org.dromara.common.mybatis.annotation.DataPermission; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.common.mybatis.helper.DataBaseHelper; import org.dromara.system.domain.SysDept; +import org.dromara.system.domain.SysRole; +import org.dromara.system.domain.SysRoleDept; import org.dromara.system.domain.vo.SysDeptVo; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; + +import static org.dromara.common.core.constant.SystemConstants.NORMAL; /** * 部门管理 数据层 * * @author Lion Li */ -public interface SysDeptMapper extends BaseMapperPlus { - - /** - * 构建角色对应的部门 SQL 查询语句 - * - *

该 SQL 用于查询某个角色关联的所有部门 ID,常用于数据权限控制

- * - * @param roleId 角色ID - * @return 查询部门ID的 SQL 语句字符串 - */ - default String buildDeptByRoleSql(Long roleId) { - return """ - select srd.dept_id from sys_role_dept srd - left join sys_role sr on sr.role_id = srd.role_id - where srd.role_id = %d and sr.status = '0' - """.formatted(roleId); - } - - /** - * 构建 SQL 查询,用于获取当前角色拥有的部门中所有的父部门ID - * - *

- * 该 SQL 用于 deptCheckStrictly 场景下,排除非叶子节点(父节点)用。 - *

- * - * @param roleId 角色ID - * @return SQL 语句字符串,查询角色下部门的所有父部门ID - */ - default String buildParentDeptByRoleSql(Long roleId) { - return """ - select parent_id from sys_dept where dept_id in ( - select srd.dept_id from sys_role_dept srd - left join sys_role sr on sr.role_id = srd.role_id - where srd.role_id = %d and sr.status = '0' - ) - """.formatted(roleId); - } +public interface SysDeptMapper extends BaseMapperPlus, MPJBaseMapper { /** * 查询部门管理数据 @@ -129,15 +102,20 @@ public interface SysDeptMapper extends BaseMapperPlus { * @return 选中部门列表 */ default List selectDeptListByRoleId(Long roleId, boolean deptCheckStrictly) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.select(SysDept::getDeptId) - .inSql(SysDept::getDeptId, this.buildDeptByRoleSql(roleId)) - .orderByAsc(SysDept::getParentId) - .orderByAsc(SysDept::getOrderNum); - if (deptCheckStrictly) { - wrapper.notInSql(SysDept::getDeptId, this.buildParentDeptByRoleSql(roleId)); - } - return this.selectObjs(wrapper); + List depts = this.selectJoinList(SysDept.class, JoinWrappers.lambda("d", SysDept.class) + .distinct() + .select(SysDept::getDeptId, SysDept::getParentId, SysDept::getOrderNum) + .leftJoin(SysRoleDept.class, "srd", SysRoleDept::getDeptId, SysDept::getDeptId) + .leftJoin(SysRole.class, "sr", SysRole::getRoleId, SysRoleDept::getRoleId) + .eq("srd", SysRoleDept::getRoleId, roleId) + .eq("sr", SysRole::getStatus, NORMAL) + .orderByAsc("d", SysDept::getParentId) + .orderByAsc("d", SysDept::getOrderNum)); + Set parentIds = deptCheckStrictly ? new HashSet<>(StreamUtils.toList(depts, SysDept::getParentId)) : Collections.emptySet(); + return depts.stream() + .map(SysDept::getDeptId) + .filter(deptId -> !parentIds.contains(deptId)) + .toList(); } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDictDataMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDictDataMapper.java index c2f1a7cbe..7298db3d0 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDictDataMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDictDataMapper.java @@ -14,6 +14,12 @@ import java.util.List; */ public interface SysDictDataMapper extends BaseMapperPlus { + /** + * 根据字典类型查询字典数据列表 + * + * @param dictType 字典类型 + * @return 符合条件的字典数据列表 + */ default List selectDictDataByType(String dictType) { return selectVoList( new LambdaQueryWrapper() diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDictTypeMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDictTypeMapper.java index 9a9bdd52d..ae1563927 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDictTypeMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysDictTypeMapper.java @@ -1,7 +1,7 @@ package org.dromara.system.mapper; -import org.dromara.system.domain.SysDictType; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.dromara.system.domain.SysDictType; import org.dromara.system.domain.vo.SysDictTypeVo; /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysMenuMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysMenuMapper.java index d4f989494..a51b1352a 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysMenuMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysMenuMapper.java @@ -2,12 +2,19 @@ package org.dromara.system.mapper; import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.yulichang.base.MPJBaseMapper; +import com.github.yulichang.toolkit.JoinWrappers; import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.system.domain.SysMenu; +import org.dromara.system.domain.SysRole; +import org.dromara.system.domain.SysRoleMenu; +import org.dromara.system.domain.SysUserRole; +import org.dromara.system.domain.bo.SysMenuBo; import org.dromara.system.domain.vo.SysMenuVo; +import org.dromara.system.domain.vo.SysRoleMenuPermVo; import java.util.*; @@ -16,67 +23,7 @@ import java.util.*; * * @author Lion Li */ -public interface SysMenuMapper extends BaseMapperPlus { - - /** - * 构建用户权限菜单 SQL - * - *

- * 查询用户所属角色所拥有的菜单权限,用于权限判断、菜单加载等场景 - *

- * - * @param userId 用户ID - * @return SQL 字符串,用于 inSql 条件 - */ - default String buildMenuByUserSql(Long userId) { - return """ - select menu_id from sys_role_menu where role_id in ( - select sur.role_id from sys_user_role sur - left join sys_role sr on sr.role_id = sur.role_id - where sur.user_id = %d and sr.status = '0' - ) - """.formatted(userId); - } - - /** - * 构建角色对应的菜单ID SQL 子查询 - * - *

- * 用于根据角色ID查询其所拥有的菜单权限(用于权限标识、菜单显示等场景) - * 通常配合 inSql 使用 - *

- * - * @param roleId 角色ID - * @return 查询菜单ID的 SQL 子查询字符串 - */ - default String buildMenuByRoleSql(Long roleId) { - return """ - select srm.menu_id from sys_role_menu srm - left join sys_role sr on sr.role_id = srm.role_id - where srm.role_id = %d and sr.status = '0' - """.formatted(roleId); - } - - /** - * 构建角色所关联菜单的父菜单ID查询 SQL - * - *

- * 用于配合菜单勾选树结构的 {@code menuCheckStrictly} 模式,过滤掉非叶子节点(父菜单), - * 只返回角色实际勾选的末级菜单 - *

- * - * @param roleId 角色ID - * @return SQL 语句字符串(查询菜单的父菜单ID) - */ - default String buildParentMenuByRoleSql(Long roleId) { - return """ - select parent_id from sys_menu where menu_id in ( - select srm.menu_id from sys_role_menu srm - left join sys_role sr on sr.role_id = srm.role_id - where srm.role_id = %d and sr.status = '0' - ) - """.formatted(roleId); - } +public interface SysMenuMapper extends BaseMapperPlus, MPJBaseMapper { /** * 根据用户ID查询权限 @@ -85,13 +32,16 @@ public interface SysMenuMapper extends BaseMapperPlus { * @return 权限列表 */ default Set selectMenuPermsByUserId(Long userId) { - List list = this.selectObjs( - new LambdaQueryWrapper() - .select(SysMenu::getPerms) - .inSql(SysMenu::getMenuId, this.buildMenuByUserSql(userId)) - .isNotNull(SysMenu::getPerms) - ); - return new HashSet<>(StreamUtils.filter(list, StringUtils::isNotBlank)); + List list = this.selectJoinList(SysMenu.class, JoinWrappers.lambda("m", SysMenu.class) + .distinct() + .select(SysMenu::getPerms) + .leftJoin(SysRoleMenu.class, "srm", SysRoleMenu::getMenuId, SysMenu::getMenuId) + .leftJoin(SysUserRole.class, "sur", SysUserRole::getRoleId, SysRoleMenu::getRoleId) + .leftJoin(SysRole.class, "sr", SysRole::getRoleId, SysRoleMenu::getRoleId) + .eq("sur", SysUserRole::getUserId, userId) + .eq("sr", SysRole::getStatus, SystemConstants.NORMAL) + .isNotNull("m", SysMenu::getPerms)); + return new HashSet<>(StreamUtils.filter(StreamUtils.toList(list, SysMenu::getPerms), StringUtils::isNotBlank)); } /** @@ -101,13 +51,15 @@ public interface SysMenuMapper extends BaseMapperPlus { * @return 权限列表 */ default Set selectMenuPermsByRoleId(Long roleId) { - List list = this.selectObjs( - new LambdaQueryWrapper() - .select(SysMenu::getPerms) - .inSql(SysMenu::getMenuId, this.buildMenuByRoleSql(roleId)) - .isNotNull(SysMenu::getPerms) - ); - return new HashSet<>(StreamUtils.filter(list, StringUtils::isNotBlank)); + List list = this.selectJoinList(SysMenu.class, JoinWrappers.lambda("m", SysMenu.class) + .distinct() + .select(SysMenu::getPerms) + .leftJoin(SysRoleMenu.class, "srm", SysRoleMenu::getMenuId, SysMenu::getMenuId) + .leftJoin(SysRole.class, "sr", SysRole::getRoleId, SysRoleMenu::getRoleId) + .eq("srm", SysRoleMenu::getRoleId, roleId) + .eq("sr", SysRole::getStatus, SystemConstants.NORMAL) + .isNotNull("m", SysMenu::getPerms)); + return new HashSet<>(StreamUtils.filter(StreamUtils.toList(list, SysMenu::getPerms), StringUtils::isNotBlank)); } /** @@ -116,12 +68,26 @@ public interface SysMenuMapper extends BaseMapperPlus { * @param roleIds 角色ID列表 * @return 角色权限映射 */ - default Map> selectMenuPermsByRoleIds(List roleIds) { + default Map> selectMenuPermsByRoleIds(Collection roleIds) { if (CollUtil.isEmpty(roleIds)) { return Map.of(); } + List list = this.selectJoinList(SysRoleMenuPermVo.class, JoinWrappers.lambda("m", SysMenu.class) + .distinct() + .selectAs("srm", SysRoleMenu::getRoleId, SysRoleMenuPermVo::getRoleId) + .selectAs(SysMenu::getPerms, SysRoleMenuPermVo::getPerms) + .leftJoin(SysRoleMenu.class, "srm", SysRoleMenu::getMenuId, SysMenu::getMenuId) + .leftJoin(SysRole.class, "sr", SysRole::getRoleId, SysRoleMenu::getRoleId) + .in("srm", SysRoleMenu::getRoleId, roleIds) + .eq("sr", SysRole::getStatus, SystemConstants.NORMAL) + .isNotNull("m", SysMenu::getPerms)); Map> result = new LinkedHashMap<>(); - roleIds.forEach(roleId -> result.put(roleId, this.selectMenuPermsByRoleId(roleId))); + for (SysRoleMenuPermVo item : list) { + if (StringUtils.isBlank(item.getPerms())) { + continue; + } + result.computeIfAbsent(item.getRoleId(), key -> new LinkedHashSet<>()).add(item.getPerms()); + } return result; } @@ -147,15 +113,53 @@ public interface SysMenuMapper extends BaseMapperPlus { * @return 选中菜单列表 */ default List selectMenuListByRoleId(Long roleId, boolean menuCheckStrictly) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.select(SysMenu::getMenuId) - .inSql(SysMenu::getMenuId, buildMenuByRoleSql(roleId)) - .orderByAsc(SysMenu::getParentId) - .orderByAsc(SysMenu::getOrderNum); - if (menuCheckStrictly) { - wrapper.notInSql(SysMenu::getMenuId, this.buildParentMenuByRoleSql(roleId)); - } - return this.selectObjs(wrapper); + List menus = this.selectJoinList(SysMenu.class, JoinWrappers.lambda("m", SysMenu.class) + .distinct() + .select(SysMenu::getMenuId, SysMenu::getParentId, SysMenu::getOrderNum) + .leftJoin(SysRoleMenu.class, "srm", SysRoleMenu::getMenuId, SysMenu::getMenuId) + .leftJoin(SysRole.class, "sr", SysRole::getRoleId, SysRoleMenu::getRoleId) + .eq("srm", SysRoleMenu::getRoleId, roleId) + .eq("sr", SysRole::getStatus, SystemConstants.NORMAL) + .orderByAsc("m", SysMenu::getParentId) + .orderByAsc("m", SysMenu::getOrderNum)); + Set parentIds = menuCheckStrictly ? new HashSet<>(StreamUtils.toList(menus, SysMenu::getParentId)) : Collections.emptySet(); + return menus.stream() + .map(SysMenu::getMenuId) + .filter(menuId -> !parentIds.contains(menuId)) + .toList(); + } + + default List selectMenuListByUserId(SysMenuBo menu, Long userId) { + return this.selectJoinList(SysMenuVo.class, JoinWrappers.lambda("m", SysMenu.class) + .distinct() + .selectAll(SysMenu.class) + .leftJoin(SysRoleMenu.class, "srm", SysRoleMenu::getMenuId, SysMenu::getMenuId) + .leftJoin(SysUserRole.class, "sur", SysUserRole::getRoleId, SysRoleMenu::getRoleId) + .leftJoin(SysRole.class, "sr", SysRole::getRoleId, SysRoleMenu::getRoleId) + .eq("sur", SysUserRole::getUserId, userId) + .eq("sr", SysRole::getStatus, SystemConstants.NORMAL) + .like(StringUtils.isNotBlank(menu.getMenuName()), "m", SysMenu::getMenuName, menu.getMenuName()) + .eq(StringUtils.isNotBlank(menu.getVisible()), "m", SysMenu::getVisible, menu.getVisible()) + .eq(StringUtils.isNotBlank(menu.getStatus()), "m", SysMenu::getStatus, menu.getStatus()) + .eq(StringUtils.isNotBlank(menu.getMenuType()), "m", SysMenu::getMenuType, menu.getMenuType()) + .eq(Objects.nonNull(menu.getParentId()), "m", SysMenu::getParentId, menu.getParentId()) + .orderByAsc("m", SysMenu::getParentId) + .orderByAsc("m", SysMenu::getOrderNum)); + } + + default List selectMenuTreeByUserId(Long userId) { + return this.selectJoinList(SysMenu.class, JoinWrappers.lambda("m", SysMenu.class) + .distinct() + .selectAll(SysMenu.class) + .leftJoin(SysRoleMenu.class, "srm", SysRoleMenu::getMenuId, SysMenu::getMenuId) + .leftJoin(SysUserRole.class, "sur", SysUserRole::getRoleId, SysRoleMenu::getRoleId) + .leftJoin(SysRole.class, "sr", SysRole::getRoleId, SysRoleMenu::getRoleId) + .eq("sur", SysUserRole::getUserId, userId) + .eq("sr", SysRole::getStatus, SystemConstants.NORMAL) + .in("m", SysMenu::getMenuType, SystemConstants.TYPE_DIR, SystemConstants.TYPE_MENU) + .eq("m", SysMenu::getStatus, SystemConstants.NORMAL) + .orderByAsc("m", SysMenu::getParentId) + .orderByAsc("m", SysMenu::getOrderNum)); } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysPostMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysPostMapper.java index d8d03157a..8e740e0a4 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysPostMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysPostMapper.java @@ -3,12 +3,16 @@ package org.dromara.system.mapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; +import com.github.yulichang.toolkit.JoinWrappers; import org.dromara.common.mybatis.annotation.DataColumn; import org.dromara.common.mybatis.annotation.DataPermission; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.system.domain.SysPost; +import org.dromara.system.domain.SysUserPost; import org.dromara.system.domain.vo.SysPostVo; +import java.util.Collection; import java.util.List; /** @@ -16,7 +20,7 @@ import java.util.List; * * @author Lion Li */ -public interface SysPostMapper extends BaseMapperPlus { +public interface SysPostMapper extends BaseMapperPlus, MPJBaseMapper { /** * 分页查询岗位列表 @@ -57,7 +61,7 @@ public interface SysPostMapper extends BaseMapperPlus { @DataColumn(key = "deptName", value = "dept_id"), @DataColumn(key = "userName", value = "create_by") }) - default long selectPostCount(List postIds) { + default long selectPostCount(Collection postIds) { return this.selectCount(new LambdaQueryWrapper().in(SysPost::getPostId, postIds)); } @@ -68,8 +72,10 @@ public interface SysPostMapper extends BaseMapperPlus { * @return 岗位信息列表 */ default List selectPostsByUserId(Long userId) { - return this.selectVoList(new LambdaQueryWrapper() - .inSql(SysPost::getPostId, "select post_id from sys_user_post where user_id = " + userId)); + return this.selectJoinList(SysPostVo.class, JoinWrappers.lambda("p", SysPost.class) + .selectAll(SysPost.class) + .leftJoin(SysUserPost.class, "sup", SysUserPost::getPostId, SysPost::getPostId) + .eq("sup", SysUserPost::getUserId, userId)); } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysRoleMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysRoleMapper.java index 920780563..96d127a7a 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysRoleMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysRoleMapper.java @@ -4,13 +4,17 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; +import com.github.yulichang.toolkit.JoinWrappers; import org.apache.ibatis.annotations.Param; import org.dromara.common.mybatis.annotation.DataColumn; import org.dromara.common.mybatis.annotation.DataPermission; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.system.domain.SysRole; +import org.dromara.system.domain.SysUserRole; import org.dromara.system.domain.vo.SysRoleVo; +import java.util.Collection; import java.util.List; /** @@ -18,19 +22,7 @@ import java.util.List; * * @author Lion Li */ -public interface SysRoleMapper extends BaseMapperPlus { - - /** - * 构建根据用户ID查询角色ID的SQL子查询 - * - * @param userId 用户ID - * @return 查询用户对应角色ID的SQL语句字符串 - */ - default String buildRoleByUserSql(Long userId) { - return """ - select role_id from sys_user_role where user_id = %d - """.formatted(userId); - } +public interface SysRoleMapper extends BaseMapperPlus, MPJBaseMapper { /** * 分页查询角色列表 @@ -71,7 +63,7 @@ public interface SysRoleMapper extends BaseMapperPlus { @DataColumn(key = "deptName", value = "create_dept"), @DataColumn(key = "userName", value = "create_by") }) - default long selectRoleCount(List roleIds) { + default long selectRoleCount(Collection roleIds) { return this.selectCount(new LambdaQueryWrapper().in(SysRole::getRoleId, roleIds)); } @@ -96,10 +88,11 @@ public interface SysRoleMapper extends BaseMapperPlus { * @return 角色列表 */ default List selectRolesByUserId(Long userId) { - return this.selectVoList(new LambdaQueryWrapper() + return this.selectJoinList(SysRoleVo.class, JoinWrappers.lambda("r", SysRole.class) .select(SysRole::getRoleId, SysRole::getRoleName, SysRole::getRoleKey, SysRole::getRoleSort, SysRole::getDataScope, SysRole::getStatus) - .inSql(SysRole::getRoleId, this.buildRoleByUserSql(userId))); + .leftJoin(SysUserRole.class, "sur", SysUserRole::getRoleId, SysRole::getRoleId) + .eq("sur", SysUserRole::getUserId, userId)); } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysRoleMenuMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysRoleMenuMapper.java index 8aa9dd3ea..6b552965a 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysRoleMenuMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysRoleMenuMapper.java @@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.system.domain.SysRoleMenu; -import java.util.List; +import java.util.Collection; /** * 角色与菜单关联表 数据层 @@ -19,7 +19,7 @@ public interface SysRoleMenuMapper extends BaseMapperPlus menuIds) { + default int deleteByMenuIds(Collection menuIds) { return this.delete(new LambdaUpdateWrapper().in(SysRoleMenu::getMenuId, menuIds)); } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysUserMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysUserMapper.java index 3a9a04df7..5cd3b7b22 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysUserMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysUserMapper.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.apache.ibatis.annotations.Param; @@ -27,7 +28,7 @@ import java.util.List; * * @author Lion Li */ -public interface SysUserMapper extends BaseMapperPlus { +public interface SysUserMapper extends BaseMapperPlus, MPJBaseMapper { /** * 分页查询用户列表,并进行数据权限控制 @@ -61,7 +62,8 @@ public interface SysUserMapper extends BaseMapperPlus { /** * 根据条件分页查询用户列表 * - * @param queryWrapper 查询条件 + * @param user 查询条件 + * @param deptIds 部门ID集合 * @return 用户信息集合信息 */ @DataPermission({ @@ -71,7 +73,6 @@ public interface SysUserMapper extends BaseMapperPlus { default List selectUserExportList(SysUserBo user, List deptIds) { MPJLambdaWrapper wrapper = JoinWrappers.lambda("u", SysUser.class) .selectAll(SysUser.class) - .selectAs(SysDept::getDeptName, SysUserExportVo::getDeptName) .selectAs("u1", SysUser::getUserName, SysUserExportVo::getLeaderName) .leftJoin(SysDept.class, "d", SysDept::getDeptId, SysUser::getDeptId) .leftJoin(SysUser.class, "u1", SysUser::getUserId, SysDept::getLeader) @@ -91,7 +92,7 @@ public interface SysUserMapper extends BaseMapperPlus { * 根据条件分页查询已配用户角色列表 * * @param page 分页信息 - * @param queryWrapper 查询条件 + * @param user 查询条件 * @return 用户信息集合信息 */ @DataPermission({ @@ -108,7 +109,9 @@ public interface SysUserMapper extends BaseMapperPlus { /** * 根据条件分页查询未分配用户角色列表 * - * @param queryWrapper 查询条件 + * @param page 分页信息 + * @param user 查询条件 + * @param userIds 未分配用户角色的用户ID列表 * @return 用户信息集合信息 */ @DataPermission({ @@ -163,7 +166,7 @@ public interface SysUserMapper extends BaseMapperPlus { }) int updateById(@Param(Constants.ENTITY) SysUser user); - private MPJLambdaWrapper buildUserRoleJoinWrapper(SysUserBo user) { + default MPJLambdaWrapper buildUserRoleJoinWrapper(SysUserBo user) { return JoinWrappers.lambda("u", SysUser.class) .distinct() .selectAll(SysUser.class) diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java index 00d9b70ab..1f0221d8c 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java @@ -108,4 +108,14 @@ public interface FlowConstant { */ String VAR_IGNORE_COOPERATE = "ignoreCooperate"; + /** + * 未删除(正常数据) + */ + Integer NOT_DELETED = 0; + + /** + * 已删除(逻辑删除) + */ + Integer DELETED = 1; + } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskMapper.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskMapper.java index a66ff5240..0f1dde0a6 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskMapper.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskMapper.java @@ -1,6 +1,8 @@ package org.dromara.workflow.mapper; +import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.dromara.common.core.utils.StringUtils; @@ -14,16 +16,20 @@ import org.dromara.workflow.domain.bo.FlowTaskBo; import org.dromara.workflow.domain.vo.FlowHisTaskVo; import java.util.List; +import java.util.Map; + +import static org.dromara.workflow.common.constant.FlowConstant.NOT_DELETED; /** * 历史任务查询 Mapper */ -public interface FlwHisTaskMapper extends BaseMapperPlus { +public interface FlwHisTaskMapper extends BaseMapperPlus, MPJBaseMapper { default Page getListFinishTask(Page page, FlowTaskBo bo, List categoryIds, String userId) { + Map params = bo.getParams(); MPJLambdaWrapper wrapper = JoinWrappers.lambda("a", FlowHisTask.class) .selectAs(FlowHisTask::getId, FlowHisTaskVo::getId) .selectAs(FlowHisTask::getNodeCode, FlowHisTaskVo::getNodeCode) @@ -55,17 +61,18 @@ public interface FlwHisTaskMapper extends BaseMapperPlus values) { - return values != null && !values.isEmpty(); - } - - default boolean hasBetween(FlowTaskBo bo) { - return bo != null && bo.getParams() != null && bo.getParams().get("beginTime") != null && bo.getParams().get("endTime") != null; - } - } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwTaskMapper.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwTaskMapper.java index 162071ca8..0b946056e 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwTaskMapper.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwTaskMapper.java @@ -1,6 +1,8 @@ package org.dromara.workflow.mapper; +import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.dromara.common.core.enums.BusinessStatusEnum; @@ -16,6 +18,9 @@ import org.dromara.workflow.domain.bo.FlowTaskBo; import org.dromara.workflow.domain.vo.FlowTaskVo; import java.util.List; +import java.util.Map; + +import static org.dromara.workflow.common.constant.FlowConstant.NOT_DELETED; /** * 任务信息Mapper接口 @@ -23,12 +28,13 @@ import java.util.List; * @author may * @date 2024-03-02 */ -public interface FlwTaskMapper extends BaseMapperPlus { +public interface FlwTaskMapper extends BaseMapperPlus, MPJBaseMapper { default Page getListRunTask(Page page, FlowTaskBo bo, List categoryIds, String userId) { + Map params = bo.getParams(); MPJLambdaWrapper wrapper = JoinWrappers.lambda("t", FlowTask.class) .distinct() .selectAs(FlowTask::getId, FlowTaskVo::getId) @@ -57,16 +63,17 @@ public interface FlwTaskMapper extends BaseMapperPlus { .leftJoin(FlowInstance.class, "i", FlowInstance::getId, FlowTask::getInstanceId) .leftJoin(FlowInstanceBizExt.class, "biz", FlowInstanceBizExt::getInstanceId, FlowInstance::getId) .eq("t", FlowTask::getNodeType, NodeType.BETWEEN.getKey()) - .eq("t", FlowTask::getDelFlag, "0") - .eq("uu", FlowUser::getDelFlag, "0") + .eq("t", FlowTask::getDelFlag, NOT_DELETED) + .eq("uu", FlowUser::getDelFlag, NOT_DELETED) .in("uu", FlowUser::getType, List.of("1", "2", "3")) - .like(hasText(bo.getNodeName()), "t", FlowTask::getNodeName, bo.getNodeName()) - .like(hasText(bo.getFlowName()), "d", FlowDefinition::getFlowName, bo.getFlowName()) - .like(hasText(bo.getFlowCode()), "d", FlowDefinition::getFlowCode, bo.getFlowCode()) - .like(hasText(bo.getFlowStatus()), "i", FlowInstance::getFlowStatus, bo.getFlowStatus()) - .in(hasItems(bo.getCreateByIds()), "i", FlowInstance::getCreateBy, bo.getCreateByIds()) - .in(hasItems(categoryIds), "d", FlowDefinition::getCategory, categoryIds) - .between(hasBetween(bo), "t", FlowTask::getCreateTime, bo.getParams().get("beginTime"), bo.getParams().get("endTime")) + .like(StringUtils.isNotBlank(bo.getNodeName()), "t", FlowTask::getNodeName, bo.getNodeName()) + .like(StringUtils.isNotBlank(bo.getFlowName()), "d", FlowDefinition::getFlowName, bo.getFlowName()) + .like(StringUtils.isNotBlank(bo.getFlowCode()), "d", FlowDefinition::getFlowCode, bo.getFlowCode()) + .like(StringUtils.isNotBlank(bo.getFlowStatus()), "i", FlowInstance::getFlowStatus, bo.getFlowStatus()) + .in(CollUtil.isNotEmpty(bo.getCreateByIds()), "i", FlowInstance::getCreateBy, bo.getCreateByIds()) + .in(CollUtil.isNotEmpty(categoryIds), "d", FlowDefinition::getCategory, categoryIds) + .between(params.get("beginTime") != null && params.get("endTime") != null, + "t", FlowTask::getCreateTime, params.get("beginTime"), params.get("endTime")) .eq(StringUtils.isNotBlank(userId), "uu", FlowUser::getProcessedBy, userId) .eq(StringUtils.isNotBlank(userId), "i", FlowInstance::getFlowStatus, BusinessStatusEnum.WAITING.getStatus()) .orderByDesc("t", FlowTask::getCreateTime) @@ -74,16 +81,4 @@ public interface FlwTaskMapper extends BaseMapperPlus { return wrapper.page(page, FlowTaskVo.class); } - default boolean hasText(String value) { - return StringUtils.isNotBlank(value); - } - - default boolean hasItems(List values) { - return values != null && !values.isEmpty(); - } - - default boolean hasBetween(FlowTaskBo bo) { - return bo != null && bo.getParams() != null && bo.getParams().get("beginTime") != null && bo.getParams().get("endTime") != null; - } - } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwUserMapper.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwUserMapper.java index 44053eeb6..61458c7f9 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwUserMapper.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwUserMapper.java @@ -1,6 +1,8 @@ package org.dromara.workflow.mapper; +import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.dromara.common.core.utils.StringUtils; @@ -14,16 +16,20 @@ import org.dromara.workflow.domain.bo.FlowTaskBo; import org.dromara.workflow.domain.vo.FlowTaskVo; import java.util.List; +import java.util.Map; + +import static org.dromara.workflow.common.constant.FlowConstant.NOT_DELETED; /** * 流程用户查询 Mapper */ -public interface FlwUserMapper extends BaseMapperPlus { +public interface FlwUserMapper extends BaseMapperPlus, MPJBaseMapper { default Page getTaskCopyByPage(Page page, FlowTaskBo bo, List categoryIds, String userId) { + Map params = bo.getParams(); MPJLambdaWrapper wrapper = JoinWrappers.lambda("a", FlowUser.class) .selectAs("b", FlowHisTask::getId, FlowTaskVo::getId) .selectAs("b", FlowHisTask::getUpdateTime, FlowTaskVo::getUpdateTime) @@ -47,32 +53,21 @@ public interface FlwUserMapper extends BaseMapperPlus { .leftJoin(FlowDefinition.class, "d", FlowDefinition::getId, FlowInstance::getDefinitionId) .leftJoin(FlowInstanceBizExt.class, "biz", FlowInstanceBizExt::getInstanceId, FlowInstance::getId) .eq("a", FlowUser::getType, "4") - .eq("a", FlowUser::getDelFlag, "0") - .eq("b", FlowHisTask::getDelFlag, "0") - .eq("d", FlowDefinition::getDelFlag, "0") - .like(hasText(bo.getNodeName()), "b", FlowHisTask::getNodeName, bo.getNodeName()) - .like(hasText(bo.getFlowName()), "d", FlowDefinition::getFlowName, bo.getFlowName()) - .like(hasText(bo.getFlowCode()), "d", FlowDefinition::getFlowCode, bo.getFlowCode()) - .like(hasText(bo.getFlowStatus()), "c", FlowInstance::getFlowStatus, bo.getFlowStatus()) - .in(hasItems(bo.getCreateByIds()), "c", FlowInstance::getCreateBy, bo.getCreateByIds()) - .in(hasItems(categoryIds), "d", FlowDefinition::getCategory, categoryIds) - .between(hasBetween(bo), "a", FlowUser::getCreateTime, bo.getParams().get("beginTime"), bo.getParams().get("endTime")) + .eq("a", FlowUser::getDelFlag, NOT_DELETED) + .eq("b", FlowHisTask::getDelFlag, NOT_DELETED) + .eq("d", FlowDefinition::getDelFlag, NOT_DELETED) + .like(StringUtils.isNotBlank(bo.getNodeName()), "b", FlowHisTask::getNodeName, bo.getNodeName()) + .like(StringUtils.isNotBlank(bo.getFlowName()), "d", FlowDefinition::getFlowName, bo.getFlowName()) + .like(StringUtils.isNotBlank(bo.getFlowCode()), "d", FlowDefinition::getFlowCode, bo.getFlowCode()) + .like(StringUtils.isNotBlank(bo.getFlowStatus()), "c", FlowInstance::getFlowStatus, bo.getFlowStatus()) + .in(CollUtil.isNotEmpty(bo.getCreateByIds()), "c", FlowInstance::getCreateBy, bo.getCreateByIds()) + .in(CollUtil.isNotEmpty(categoryIds), "d", FlowDefinition::getCategory, categoryIds) + .between(params.get("beginTime") != null && params.get("endTime") != null, + "a", FlowUser::getCreateTime, params.get("beginTime"), params.get("endTime")) .eq(StringUtils.isNotBlank(userId), "a", FlowUser::getProcessedBy, userId) .orderByDesc("a", FlowUser::getCreateTime) .orderByDesc("b", FlowHisTask::getUpdateTime); return wrapper.page(page, FlowTaskVo.class); } - default boolean hasText(String value) { - return StringUtils.isNotBlank(value); - } - - default boolean hasItems(List values) { - return values != null && !values.isEmpty(); - } - - default boolean hasBetween(FlowTaskBo bo) { - return bo != null && bo.getParams() != null && bo.getParams().get("beginTime") != null && bo.getParams().get("endTime") != null; - } - }