refactor: 会话失效、数据权限和实时推送重构
This commit is contained in:
@@ -120,4 +120,14 @@ public class RoleController {
|
||||
roleService.assignMenusToRole(roleId, menuIds);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取角色的部门ID集合(自定义数据权限)")
|
||||
@GetMapping("/{roleId}/dept-ids")
|
||||
@PreAuthorize("@ss.hasPerm('sys:role:update')")
|
||||
public Result<List<Long>> getRoleDeptIds(
|
||||
@Parameter(description = "角色ID") @PathVariable Long roleId
|
||||
) {
|
||||
List<Long> deptIds = roleService.getRoleDeptIds(roleId);
|
||||
return Result.success(deptIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.youlai.boot.system.handler;
|
||||
|
||||
|
||||
import com.youlai.boot.system.service.UserOnlineService;
|
||||
import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher;
|
||||
import com.youlai.boot.platform.websocket.topic.WebSocketTopics;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 在线用户定时任务
|
||||
*
|
||||
* @since 2024/10/7
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class OnlineUserJobHandler {
|
||||
|
||||
private final UserOnlineService userOnlineService;
|
||||
private final WebSocketPublisher webSocketPublisher;
|
||||
|
||||
// 每3分钟统计一次在线用户数,减少服务器压力
|
||||
@Scheduled(cron = "0 */3 * * * ?")
|
||||
public void execute() {
|
||||
log.info("定时任务:统计在线用户数");
|
||||
// 推送在线用户数量到新主题
|
||||
int count = userOnlineService.getOnlineUserCount();
|
||||
webSocketPublisher.publish(WebSocketTopics.TOPIC_ONLINE_COUNT, count);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.youlai.boot.system.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.system.model.entity.RoleDept;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色部门关联持久层
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleDeptMapper extends BaseMapper<RoleDept> {
|
||||
|
||||
/**
|
||||
* 根据角色ID获取部门ID列表
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 部门ID列表
|
||||
*/
|
||||
List<Long> getDeptIdsByRoleId(@Param("roleId") Long roleId);
|
||||
|
||||
/**
|
||||
* 根据角色编码集合获取所有部门ID列表(用于自定义数据权限)
|
||||
*
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 部门ID列表
|
||||
*/
|
||||
List<Long> getDeptIdsByRoleCodes(@Param("roleCodes") List<String> roleCodes);
|
||||
|
||||
}
|
||||
@@ -3,7 +3,10 @@ package com.youlai.boot.system.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.system.model.entity.Role;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -22,4 +25,15 @@ public interface RoleMapper extends BaseMapper<Role> {
|
||||
* @return {@link Integer} – 数据权限范围
|
||||
*/
|
||||
Integer getMaximumDataScope(Set<String> roles);
|
||||
|
||||
/**
|
||||
* 获取角色的数据权限信息列表
|
||||
* <p>
|
||||
* 返回角色编码和数据权限范围的映射列表
|
||||
*
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 角色数据权限信息列表 [{code: 'ADMIN', data_scope: 1}, ...]
|
||||
*/
|
||||
List<Map<String, Object>> getRoleDataScopeList(@Param("roleCodes") Set<String> roleCodes);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.youlai.boot.system.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 角色部门关联实体
|
||||
* <p>
|
||||
* 用于存储角色自定义数据权限时,可访问的部门ID列表
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@TableName("sys_role_dept")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RoleDept {
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import lombok.Data;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.hibernate.validator.constraints.Range;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "角色表单对象")
|
||||
@Data
|
||||
public class RoleForm {
|
||||
@@ -29,7 +31,10 @@ public class RoleForm {
|
||||
@Range(max = 1, min = 0, message = "角色状态不正确")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description="数据权限")
|
||||
@Schema(description="数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据 5-自定义部门数据)")
|
||||
private Integer dataScope;
|
||||
|
||||
@Schema(description="自定义数据权限部门ID列表(当dataScope=5时有效)")
|
||||
private List<Long> deptIds;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.youlai.boot.system.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.system.model.entity.RoleDept;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色部门关联服务接口
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public interface RoleDeptService extends IService<RoleDept> {
|
||||
|
||||
/**
|
||||
* 根据角色ID获取部门ID列表
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 部门ID列表
|
||||
*/
|
||||
List<Long> getDeptIdsByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 根据角色编码集合获取所有部门ID列表(用于自定义数据权限)
|
||||
*
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 部门ID列表
|
||||
*/
|
||||
List<Long> getDeptIdsByRoleCodes(List<String> roleCodes);
|
||||
|
||||
/**
|
||||
* 保存角色部门关联
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param deptIds 部门ID列表
|
||||
*/
|
||||
void saveRoleDepts(Long roleId, List<Long> deptIds);
|
||||
|
||||
/**
|
||||
* 删除角色部门关联
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
void deleteByRoleId(Long roleId);
|
||||
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import java.util.Set;
|
||||
/**
|
||||
* 角色菜单业务接口
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray.Hao
|
||||
* @since 2.5.0
|
||||
*/
|
||||
public interface RoleMenuService extends IService<RoleMenu> {
|
||||
@@ -45,10 +45,12 @@ public interface RoleMenuService extends IService<RoleMenu> {
|
||||
void refreshRolePermsCache(String oldRoleCode, String newRoleCode);
|
||||
|
||||
/**
|
||||
* 获取角色权限集合
|
||||
* 获取角色权限集合(带缓存)
|
||||
* <p>
|
||||
* 采用 Read-Through 缓存策略,缓存未命中时自动回源数据库
|
||||
*
|
||||
* @param roles 角色编码集合
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 权限集合
|
||||
*/
|
||||
Set<String> getRolePermsByRoleCodes(Set<String> roles);
|
||||
Set<String> getRolePermsByRoleCodes(Set<String> roleCodes);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.youlai.boot.system.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.security.model.RoleDataScope;
|
||||
import com.youlai.boot.system.model.entity.Role;
|
||||
import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.system.model.form.RoleForm;
|
||||
@@ -91,5 +92,22 @@ public interface RoleService extends IService<Role> {
|
||||
*/
|
||||
Integer getMaximumDataScope(Set<String> roles);
|
||||
|
||||
/**
|
||||
* 获取角色的部门ID列表(自定义数据权限)
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 部门ID列表
|
||||
*/
|
||||
List<Long> getRoleDeptIds(Long roleId);
|
||||
|
||||
/**
|
||||
* 获取用户所有角色的数据权限列表
|
||||
* <p>
|
||||
* 用于实现多角色数据权限合并(并集策略)
|
||||
*
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 角色数据权限列表
|
||||
*/
|
||||
List<RoleDataScope> getRoleDataScopes(Set<String> roleCodes);
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.List;
|
||||
/**
|
||||
* 用户公告状态服务类
|
||||
*
|
||||
* @author youlaitech
|
||||
* @author Theo
|
||||
* @since 2024-08-28 16:56
|
||||
*/
|
||||
public interface UserNoticeService extends IService<UserNotice> {
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package com.youlai.boot.system.service;
|
||||
|
||||
import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher;
|
||||
import com.youlai.boot.platform.websocket.topic.WebSocketTopics;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户在线状态服务
|
||||
* 负责维护用户的在线状态和相关统计
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserOnlineService {
|
||||
|
||||
// 在线用户映射表,key为用户名,value为用户在线信息
|
||||
private final Map<String, UserOnlineInfo> onlineUsers = new ConcurrentHashMap<>();
|
||||
|
||||
private final WebSocketPublisher webSocketPublisher;
|
||||
|
||||
public UserOnlineService(WebSocketPublisher webSocketPublisher) {
|
||||
this.webSocketPublisher = webSocketPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户上线
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param sessionId WebSocket会话ID(可选)
|
||||
*/
|
||||
public void userConnected(String username, String sessionId) {
|
||||
// 生成会话ID(如果未提供)
|
||||
String actualSessionId = sessionId != null ? sessionId : "session-" + System.nanoTime();
|
||||
UserOnlineInfo info = new UserOnlineInfo(username, actualSessionId, System.currentTimeMillis());
|
||||
onlineUsers.put(username, info);
|
||||
log.info("用户[{}]上线,当前在线用户数:{}", username, onlineUsers.size());
|
||||
|
||||
// 通知在线用户状态变更
|
||||
notifyOnlineUsersChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户下线
|
||||
*
|
||||
* @param username 用户名
|
||||
*/
|
||||
public void userDisconnected(String username) {
|
||||
onlineUsers.remove(username);
|
||||
log.info("用户[{}]下线,当前在线用户数:{}", username, onlineUsers.size());
|
||||
|
||||
// 通知在线用户状态变更
|
||||
notifyOnlineUsersChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户列表
|
||||
*
|
||||
* @return 在线用户名列表
|
||||
*/
|
||||
public List<UserOnlineDTO> getOnlineUsers() {
|
||||
return onlineUsers.values().stream()
|
||||
.map(info -> new UserOnlineDTO(info.getUsername(), info.getLoginTime()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户数量
|
||||
*
|
||||
* @return 在线用户数
|
||||
*/
|
||||
public int getOnlineUserCount() {
|
||||
return onlineUsers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否在线
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 是否在线
|
||||
*/
|
||||
public boolean isUserOnline(String username) {
|
||||
return onlineUsers.containsKey(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有客户端在线用户变更
|
||||
*/
|
||||
private void notifyOnlineUsersChange() {
|
||||
// 发送简化版数据(仅数量)
|
||||
sendOnlineUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送在线用户数量(简化版,不包含用户详情)
|
||||
*/
|
||||
private void sendOnlineUserCount() {
|
||||
try {
|
||||
// 直接发送数量,更轻量
|
||||
int count = onlineUsers.size();
|
||||
webSocketPublisher.publish(WebSocketTopics.TOPIC_ONLINE_COUNT, count);
|
||||
log.debug("已发送在线用户数量: {}", count);
|
||||
} catch (Exception e) {
|
||||
log.error("发送在线用户数量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户在线信息
|
||||
*/
|
||||
@Data
|
||||
private static class UserOnlineInfo {
|
||||
private final String username;
|
||||
private final String sessionId;
|
||||
private final long loginTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户在线Dto(用于返回给前端)
|
||||
*/
|
||||
@Data
|
||||
public static class UserOnlineDTO {
|
||||
private final String username;
|
||||
private final long loginTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线用户变更事件
|
||||
*/
|
||||
@Data
|
||||
private static class OnlineUsersChangeEvent {
|
||||
private String type;
|
||||
private int count;
|
||||
private List<UserOnlineDTO> users;
|
||||
private long timestamp;
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,19 @@ import com.youlai.boot.system.model.entity.UserRole;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户角色业务接口
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public interface UserRoleService extends IService<UserRole> {
|
||||
|
||||
/**
|
||||
* 保存用户角色
|
||||
*
|
||||
* @param userId
|
||||
* @param roleIds
|
||||
* @param userId 用户ID
|
||||
* @param roleIds 角色ID列表
|
||||
* @return
|
||||
*/
|
||||
void saveUserRoles(Long userId, List<Long> roleIds);
|
||||
|
||||
@@ -25,7 +25,7 @@ public interface UserService extends IService<User> {
|
||||
/**
|
||||
* 用户分页列表
|
||||
*
|
||||
* @return {@link IPage<UserPageVo>} 用户分页列表
|
||||
* @return {@link IPage<UserPageVO>} 用户分页列表
|
||||
*/
|
||||
IPage<UserPageVO> getUserPage(UserQuery queryParams);
|
||||
|
||||
@@ -82,7 +82,7 @@ public interface UserService extends IService<User> {
|
||||
* 获取导出用户列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return {@link List<UserExportDto>} 导出用户列表
|
||||
* @return {@link List<UserExportDTO>} 导出用户列表
|
||||
*/
|
||||
List<UserExportDTO> listExportUsers(UserQuery queryParams);
|
||||
|
||||
@@ -90,14 +90,14 @@ public interface UserService extends IService<User> {
|
||||
/**
|
||||
* 获取登录用户信息
|
||||
*
|
||||
* @return {@link CurrentUserDto} 登录用户信息
|
||||
* @return {@link CurrentUserDTO} 登录用户信息
|
||||
*/
|
||||
CurrentUserDTO getCurrentUserInfo();
|
||||
|
||||
/**
|
||||
* 获取个人中心用户信息
|
||||
*
|
||||
* @return {@link UserProfileVo} 个人中心用户信息
|
||||
* @return {@link UserProfileVO} 个人中心用户信息
|
||||
*/
|
||||
UserProfileVO getUserProfile(Long userId);
|
||||
|
||||
|
||||
@@ -25,10 +25,8 @@ 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 com.youlai.boot.platform.websocket.publisher.WebSocketPublisher;
|
||||
import com.youlai.boot.platform.websocket.topic.WebSocketTopics;
|
||||
import com.youlai.boot.platform.websocket.service.WebSocketService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -53,14 +51,13 @@ public class NoticeServiceImpl extends ServiceImpl<NoticeMapper, Notice> impleme
|
||||
private final NoticeConverter noticeConverter;
|
||||
private final UserNoticeService userNoticeService;
|
||||
private final UserService userService;
|
||||
private final WebSocketPublisher webSocketPublisher;
|
||||
private final UserOnlineService userOnlineService;
|
||||
private final WebSocketService webSocketService;
|
||||
|
||||
/**
|
||||
* 获取通知公告分页列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return {@link IPage< NoticePageVo >} 通知公告分页列表
|
||||
* @return {@link IPage< NoticePageVO >} 通知公告分页列表
|
||||
*/
|
||||
@Override
|
||||
public IPage<NoticePageVO> getNoticePage(NoticeQuery queryParams) {
|
||||
@@ -214,9 +211,10 @@ public class NoticeServiceImpl extends ServiceImpl<NoticeMapper, Notice> impleme
|
||||
|
||||
Set<String> receivers = targetUserList.stream().map(User::getUsername).collect(Collectors.toSet());
|
||||
|
||||
Set<String> allOnlineUsers = userOnlineService.getOnlineUsers().stream()
|
||||
.map(UserOnlineService.UserOnlineDTO::getUsername)
|
||||
.collect(Collectors.toSet());
|
||||
// 获取在线用户名集合
|
||||
Set<String> allOnlineUsers = webSocketService.getOnlineUsers().stream()
|
||||
.map(dto -> dto.getUsername())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 找出在线用户的通知接收者
|
||||
Set<String> onlineReceivers = new HashSet<>(CollectionUtil.intersection(receivers, allOnlineUsers));
|
||||
@@ -227,7 +225,8 @@ public class NoticeServiceImpl extends ServiceImpl<NoticeMapper, Notice> impleme
|
||||
noticeDto.setType(notice.getType());
|
||||
noticeDto.setPublishTime(notice.getPublishTime());
|
||||
|
||||
onlineReceivers.forEach(receiver -> webSocketPublisher.publishToUser(receiver, WebSocketTopics.USER_QUEUE_MESSAGE, noticeDto));
|
||||
// 向在线接收者推送通知
|
||||
onlineReceivers.forEach(receiver -> webSocketService.sendNotification(receiver, noticeDto));
|
||||
}
|
||||
return publishResult;
|
||||
}
|
||||
@@ -268,7 +267,7 @@ public class NoticeServiceImpl extends ServiceImpl<NoticeMapper, Notice> impleme
|
||||
/**
|
||||
*
|
||||
* @param id 通知公告ID
|
||||
* @return NoticeDetailVo 通知公告详情
|
||||
* @return NoticeDetailVO 通知公告详情
|
||||
*/
|
||||
@Override
|
||||
public NoticeDetailVO getNoticeDetail(Long id) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.youlai.boot.system.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.system.mapper.RoleDeptMapper;
|
||||
import com.youlai.boot.system.model.entity.RoleDept;
|
||||
import com.youlai.boot.system.service.RoleDeptService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色部门关联服务实现
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RoleDeptServiceImpl extends ServiceImpl<RoleDeptMapper, RoleDept> implements RoleDeptService {
|
||||
|
||||
@Override
|
||||
public List<Long> getDeptIdsByRoleId(Long roleId) {
|
||||
if (roleId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return this.baseMapper.getDeptIdsByRoleId(roleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getDeptIdsByRoleCodes(List<String> roleCodes) {
|
||||
if (CollectionUtil.isEmpty(roleCodes)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return this.baseMapper.getDeptIdsByRoleCodes(roleCodes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveRoleDepts(Long roleId, List<Long> deptIds) {
|
||||
if (roleId == null || CollectionUtil.isEmpty(deptIds)) {
|
||||
return;
|
||||
}
|
||||
// 先删除原有关联
|
||||
this.remove(new LambdaQueryWrapper<RoleDept>().eq(RoleDept::getRoleId, roleId));
|
||||
// 批量插入新关联
|
||||
List<RoleDept> roleDepts = deptIds.stream()
|
||||
.map(deptId -> new RoleDept(roleId, deptId))
|
||||
.toList();
|
||||
this.saveBatch(roleDepts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByRoleId(Long roleId) {
|
||||
if (roleId == null) {
|
||||
return;
|
||||
}
|
||||
this.remove(new LambdaQueryWrapper<RoleDept>().eq(RoleDept::getRoleId, roleId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,14 +7,13 @@ import com.youlai.boot.system.mapper.RoleMenuMapper;
|
||||
import com.youlai.boot.system.model.bo.RolePermsBO;
|
||||
import com.youlai.boot.system.model.entity.RoleMenu;
|
||||
import com.youlai.boot.system.service.RoleMenuService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 角色菜单服务实现类
|
||||
@@ -29,44 +28,17 @@ public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> i
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 启动时初始化权限缓存
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initRolePermsCache() {
|
||||
log.info("开始初始化权限缓存...");
|
||||
|
||||
List<RolePermsBO> allRolePermsList = this.baseMapper.getRolePermsList(null);
|
||||
|
||||
if (CollectionUtil.isEmpty(allRolePermsList)) {
|
||||
log.warn("权限数据为空,跳过缓存初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有数据统一缓存
|
||||
String cacheKey = RedisConstants.System.ROLE_PERMS;
|
||||
allRolePermsList.forEach(rolePerms -> {
|
||||
String roleCode = rolePerms.getRoleCode();
|
||||
Set<String> perms = rolePerms.getPerms();
|
||||
|
||||
if (CollectionUtil.isNotEmpty(perms)) {
|
||||
redisTemplate.opsForHash().put(cacheKey, roleCode, perms);
|
||||
}
|
||||
});
|
||||
log.info("权限缓存初始化完成,共{}条数据", allRolePermsList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新权限缓存
|
||||
*/
|
||||
@Override
|
||||
public void refreshRolePermsCache() {
|
||||
String cacheKey = RedisConstants.System.ROLE_PERMS;
|
||||
|
||||
|
||||
// 清理权限缓存
|
||||
redisTemplate.delete(cacheKey);
|
||||
|
||||
// 重新加载权限
|
||||
|
||||
// 预热权限缓存,避免后续请求触发频繁回源
|
||||
List<RolePermsBO> list = this.baseMapper.getRolePermsList(null);
|
||||
if (CollectionUtil.isNotEmpty(list)) {
|
||||
list.forEach(item -> {
|
||||
@@ -77,7 +49,7 @@ public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> i
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
log.info("权限缓存刷新完成");
|
||||
}
|
||||
|
||||
@@ -87,11 +59,11 @@ public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> i
|
||||
@Override
|
||||
public void refreshRolePermsCache(String roleCode) {
|
||||
String cacheKey = RedisConstants.System.ROLE_PERMS;
|
||||
|
||||
|
||||
// 清理指定角色缓存
|
||||
redisTemplate.opsForHash().delete(cacheKey, roleCode);
|
||||
|
||||
// 重新加载指定角色权限
|
||||
|
||||
// 回源 DB 并更新缓存
|
||||
List<RolePermsBO> list = this.baseMapper.getRolePermsList(roleCode);
|
||||
if (CollectionUtil.isNotEmpty(list)) {
|
||||
RolePermsBO rolePerms = list.get(0);
|
||||
@@ -102,7 +74,7 @@ public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
log.info("角色[{}]权限缓存刷新完成", roleCode);
|
||||
}
|
||||
|
||||
@@ -112,11 +84,12 @@ public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> i
|
||||
@Override
|
||||
public void refreshRolePermsCache(String oldRoleCode, String newRoleCode) {
|
||||
String cacheKey = RedisConstants.System.ROLE_PERMS;
|
||||
|
||||
|
||||
// 清理旧角色权限缓存
|
||||
redisTemplate.opsForHash().delete(cacheKey, oldRoleCode);
|
||||
|
||||
// 添加新角色权限缓存
|
||||
redisTemplate.opsForHash().delete(cacheKey, newRoleCode);
|
||||
|
||||
// 回源 DB 并更新新角色编码缓存
|
||||
List<RolePermsBO> list = this.baseMapper.getRolePermsList(newRoleCode);
|
||||
if (CollectionUtil.isNotEmpty(list)) {
|
||||
RolePermsBO rolePerms = list.get(0);
|
||||
@@ -127,20 +100,72 @@ public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("角色编码变更: {} -> {},权限缓存已更新", oldRoleCode, newRoleCode);
|
||||
|
||||
log.info("角色编码变更: {} -> {},相关权限缓存刷新完成", oldRoleCode, newRoleCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色权限集合
|
||||
* 获取角色权限集合(带缓存)
|
||||
* <p>
|
||||
* 采用 Read-Through 缓存策略:
|
||||
* <ol>
|
||||
* <li>优先从 Redis Hash 缓存读取</li>
|
||||
* <li>缓存未命中时回源 DB 并写入缓存</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param roles 角色编码集合
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 权限集合
|
||||
*/
|
||||
@Override
|
||||
public Set<String> getRolePermsByRoleCodes(Set<String> roles) {
|
||||
// 直接查询数据库(保持原有逻辑)
|
||||
return this.baseMapper.listRolePerms(roles);
|
||||
public Set<String> getRolePermsByRoleCodes(Set<String> roleCodes) {
|
||||
if (CollectionUtil.isEmpty(roleCodes)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
String cacheKey = RedisConstants.System.ROLE_PERMS;
|
||||
Set<String> perms = new HashSet<>();
|
||||
List<String> roleCodeList = new ArrayList<>(roleCodes);
|
||||
|
||||
// 1. 尝试从缓存批量获取
|
||||
List<Object> cachedPermsList = redisTemplate.opsForHash().multiGet(cacheKey, new ArrayList<>(roleCodeList));
|
||||
|
||||
List<String> missingRoles = new ArrayList<>();
|
||||
for (int i = 0; i < roleCodeList.size(); i++) {
|
||||
Object cachedPerms = cachedPermsList.get(i);
|
||||
String roleCode = roleCodeList.get(i);
|
||||
|
||||
if (cachedPerms == null) {
|
||||
// 缓存未命中,记录需要回源的角色
|
||||
missingRoles.add(roleCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Redis JSON 序列化后,Set 会以 Collection 形式反序列化
|
||||
if (cachedPerms instanceof Collection<?> collection) {
|
||||
collection.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Object::toString)
|
||||
.forEach(perms::add);
|
||||
} else {
|
||||
// 兼容单个权限字符串的极端情况
|
||||
perms.add(cachedPerms.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 回源 DB 并同步到缓存
|
||||
if (!missingRoles.isEmpty()) {
|
||||
for (String roleCode : missingRoles) {
|
||||
Set<String> dbPerms = this.baseMapper.listRolePerms(Collections.singleton(roleCode));
|
||||
if (dbPerms == null) {
|
||||
dbPerms = Collections.emptySet();
|
||||
}
|
||||
// 写入缓存(空集也写入,防止缓存穿透)
|
||||
redisTemplate.opsForHash().put(cacheKey, roleCode, dbPerms);
|
||||
perms.addAll(dbPerms);
|
||||
}
|
||||
}
|
||||
|
||||
return perms;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
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.common.enums.DataScopeEnum;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.system.converter.RoleConverter;
|
||||
import com.youlai.boot.system.mapper.RoleMapper;
|
||||
@@ -18,6 +19,7 @@ import com.youlai.boot.system.model.vo.RolePageVO;
|
||||
import com.youlai.boot.common.constant.SystemConstants;
|
||||
import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.service.RoleDeptService;
|
||||
import com.youlai.boot.system.service.RoleMenuService;
|
||||
import com.youlai.boot.system.service.RoleService;
|
||||
import com.youlai.boot.system.service.UserRoleService;
|
||||
@@ -41,6 +43,7 @@ import java.util.Set;
|
||||
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
|
||||
|
||||
private final RoleMenuService roleMenuService;
|
||||
private final RoleDeptService roleDeptService;
|
||||
private final UserRoleService userRoleService;
|
||||
private final RoleConverter roleConverter;
|
||||
|
||||
@@ -48,7 +51,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
* 角色分页列表
|
||||
*
|
||||
* @param queryParams 角色查询参数
|
||||
* @return {@link Page< RolePageVo >} – 角色分页列表
|
||||
* @return {@link Page< RolePageVO >} – 角色分页列表
|
||||
*/
|
||||
@Override
|
||||
public Page<RolePageVO> getRolePage(RoleQuery queryParams) {
|
||||
@@ -99,6 +102,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
* @return {@link Boolean}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean saveRole(RoleForm roleForm) {
|
||||
|
||||
Long roleId = roleForm.getId();
|
||||
@@ -123,6 +127,16 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
|
||||
boolean result = this.saveOrUpdate(role);
|
||||
if (result) {
|
||||
// 保存自定义数据权限部门
|
||||
Long savedRoleId = role.getId();
|
||||
if (DataScopeEnum.CUSTOM.getValue().equals(roleForm.getDataScope())) {
|
||||
// 自定义数据权限时,保存角色部门关联
|
||||
roleDeptService.saveRoleDepts(savedRoleId, roleForm.getDeptIds());
|
||||
} else {
|
||||
// 非自定义数据权限时,删除原有部门关联
|
||||
roleDeptService.deleteByRoleId(savedRoleId);
|
||||
}
|
||||
|
||||
// 判断角色编码或状态是否修改,修改了则刷新权限缓存
|
||||
if (oldRole != null
|
||||
&& (
|
||||
@@ -144,7 +158,13 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
@Override
|
||||
public RoleForm getRoleForm(Long roleId) {
|
||||
Role entity = this.getById(roleId);
|
||||
return roleConverter.toForm(entity);
|
||||
RoleForm roleForm = roleConverter.toForm(entity);
|
||||
// 如果是自定义数据权限,查询关联的部门ID列表
|
||||
if (roleForm != null && DataScopeEnum.CUSTOM.getValue().equals(roleForm.getDataScope())) {
|
||||
List<Long> deptIds = roleDeptService.getDeptIdsByRoleId(roleId);
|
||||
roleForm.setDeptIds(deptIds);
|
||||
}
|
||||
return roleForm;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,4 +274,70 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
return dataScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色的部门ID列表(自定义数据权限)
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 部门ID列表
|
||||
*/
|
||||
@Override
|
||||
public List<Long> getRoleDeptIds(Long roleId) {
|
||||
return roleDeptService.getDeptIdsByRoleId(roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有角色的数据权限列表
|
||||
* <p>
|
||||
* 用于实现多角色数据权限合并(并集策略)
|
||||
*
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 角色数据权限列表
|
||||
*/
|
||||
@Override
|
||||
public List<RoleDataScope> getRoleDataScopes(Set<String> roleCodes) {
|
||||
if (CollectionUtil.isEmpty(roleCodes)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 获取角色的数据权限信息
|
||||
List<Map<String, Object>> roleDataScopeList = this.baseMapper.getRoleDataScopeList(roleCodes);
|
||||
if (CollectionUtil.isEmpty(roleDataScopeList)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 获取所有自定义数据权限的角色编码
|
||||
List<String> customRoleCodes = roleDataScopeList.stream()
|
||||
.filter(map -> DataScopeEnum.CUSTOM.getValue().equals(map.get("data_scope")))
|
||||
.map(map -> (String) map.get("code"))
|
||||
.toList();
|
||||
|
||||
// 批量获取自定义角色的部门ID
|
||||
Map<String, List<Long>> customDeptIdsMap = new java.util.HashMap<>();
|
||||
if (CollectionUtil.isNotEmpty(customRoleCodes)) {
|
||||
// 查询每个角色关联的部门ID
|
||||
for (String roleCode : customRoleCodes) {
|
||||
// 根据角色编码获取角色ID
|
||||
Role role = this.getOne(new LambdaQueryWrapper<Role>()
|
||||
.eq(Role::getCode, roleCode)
|
||||
.select(Role::getId));
|
||||
if (role != null) {
|
||||
List<Long> deptIds = roleDeptService.getDeptIdsByRoleId(role.getId());
|
||||
customDeptIdsMap.put(roleCode, deptIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建角色数据权限列表
|
||||
return roleDataScopeList.stream()
|
||||
.map(map -> {
|
||||
String code = (String) map.get("code");
|
||||
Integer dataScope = (Integer) map.get("data_scope");
|
||||
if (DataScopeEnum.CUSTOM.getValue().equals(dataScope)) {
|
||||
return RoleDataScope.custom(code, customDeptIdsMap.getOrDefault(code, List.of()));
|
||||
}
|
||||
return new RoleDataScope(code, dataScope, null);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.platform.mail.service.MailService;
|
||||
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.platform.sms.service.SmsService;
|
||||
import com.youlai.boot.security.model.RoleDataScope;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.security.service.PermissionService;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.converter.UserConverter;
|
||||
@@ -62,7 +62,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
private final PermissionService permissionService;
|
||||
private final RoleMenuService roleMenuService;
|
||||
|
||||
private final SmsService smsService;
|
||||
|
||||
@@ -81,7 +81,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
* 获取用户分页列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return {@link IPage<UserPageVo>} 用户分页列表
|
||||
* @return {@link IPage<UserPageVO>} 用户分页列表
|
||||
*/
|
||||
@Override
|
||||
public IPage<UserPageVO> getUserPage(UserQuery queryParams) {
|
||||
@@ -215,9 +215,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByUsername(username);
|
||||
if (userAuthInfo != null) {
|
||||
Set<String> roles = userAuthInfo.getRoles();
|
||||
// 获取最大范围的数据权限
|
||||
Integer dataScope = roleService.getMaximumDataScope(roles);
|
||||
userAuthInfo.setDataScope(dataScope);
|
||||
// 获取数据权限列表(用于并集策略)
|
||||
List<RoleDataScope> dataScopes = roleService.getRoleDataScopes(roles);
|
||||
userAuthInfo.setDataScopes(dataScopes);
|
||||
}
|
||||
return userAuthInfo;
|
||||
}
|
||||
@@ -236,9 +236,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByMobile(mobile);
|
||||
if (userAuthInfo != null) {
|
||||
Set<String> roles = userAuthInfo.getRoles();
|
||||
// 获取最大范围的数据权限
|
||||
Integer dataScope = roleService.getMaximumDataScope(roles);
|
||||
userAuthInfo.setDataScope(dataScope);
|
||||
// 获取数据权限列表(用于并集策略)
|
||||
List<RoleDataScope> dataScopes = roleService.getRoleDataScopes(roles);
|
||||
userAuthInfo.setDataScopes(dataScopes);
|
||||
}
|
||||
return userAuthInfo;
|
||||
}
|
||||
@@ -247,7 +247,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
* 获取导出用户列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return {@link List<UserExportDto>} 导出用户列表
|
||||
* @return {@link List<UserExportDTO>} 导出用户列表
|
||||
*/
|
||||
@Override
|
||||
public List<UserExportDTO> listExportUsers(UserQuery queryParams) {
|
||||
@@ -285,7 +285,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
/**
|
||||
* 获取登录用户信息
|
||||
*
|
||||
* @return {@link CurrentUserDto} 用户信息
|
||||
* @return {@link CurrentUserDTO} 用户信息
|
||||
*/
|
||||
@Override
|
||||
public CurrentUserDTO getCurrentUserInfo() {
|
||||
@@ -311,7 +311,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
|
||||
// 用户权限集合
|
||||
if (CollectionUtil.isNotEmpty(roles)) {
|
||||
Set<String> perms = permissionService.getRolePermsFormCache(roles);
|
||||
Set<String> perms = roleMenuService.getRolePermsByRoleCodes(roles);
|
||||
userInfoVo.setPerms(perms);
|
||||
}
|
||||
return userInfoVo;
|
||||
@@ -321,7 +321,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
* 获取个人中心用户信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return {@link UserProfileVo} 个人中心用户信息
|
||||
* @return {@link UserProfileVO} 个人中心用户信息
|
||||
*/
|
||||
@Override
|
||||
public UserProfileVO getUserProfile(Long userId) {
|
||||
|
||||
Reference in New Issue
Block a user