feat: 实现完整的 RBAC 权限管理系统与基础设施增强

在初始认证基础上,新增完整的 RBAC 权限模型(角色、权限、菜单三级管理),
  集成审计日志、接口限流、登录失败锁定、Refresh Token 机制、Redis 分布式缓存与锁、
  RocketMQ 消息队列,并引入 Flyway 数据库版本管理,同时补充项目文档与使用示例
This commit is contained in:
2026-04-10 10:58:22 +08:00
parent 3a9bf61839
commit 40c85c3c1f
97 changed files with 13434 additions and 351 deletions

View File

@@ -0,0 +1,343 @@
package com.aisi.template.service.impl;
import com.aisi.template.domain.dto.RoleDto;
import com.aisi.template.domain.dto.RoleQueryDto;
import com.aisi.template.domain.entity.SysPermission;
import com.aisi.template.domain.entity.SysRole;
import com.aisi.template.domain.dto.PageResult;
import com.aisi.template.domain.vo.PermissionVo;
import com.aisi.template.domain.vo.RoleVo;
import com.aisi.template.exception.BusinessException;
import com.aisi.template.repository.SysPermissionRepository;
import com.aisi.template.repository.SysRoleRepository;
import com.aisi.template.service.SysRoleService;
import jakarta.persistence.criteria.Predicate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 系统角色服务实现类
* 提供角色的 CRUD 操作、权限分配、查询等功能
*
* 主要功能:
* 1. 角色基本操作:创建、更新、删除、查询
* 2. 权限管理:为角色分配权限、获取角色权限
* 3. 分页查询:支持条件查询和分页
*
* @author Claude
* @since 2024-04-09
*/
@Service
@RequiredArgsConstructor
public class SysRoleServiceImpl implements SysRoleService {
/**
* 角色数据访问接口
* 用于角色的数据库操作
*/
private final SysRoleRepository roleRepository;
/**
* 权限数据访问接口
* 用于权限的数据库操作
*/
private final SysPermissionRepository permissionRepository;
/**
* 创建角色
* 步骤:
* 1. 校验角色编码是否已存在
* 2. 构建角色实体对象
* 3. 保存到数据库
*
* @param roleDto 角色数据传输对象
* @return 创建的角色视图对象
* @throws BusinessException 当角色编码已存在时抛出异常
*/
@Override
@Transactional(rollbackFor = Exception.class)
public RoleVo create(RoleDto roleDto) {
// 1. 检查角色编码是否已存在
roleRepository.findByRoleCode(roleDto.getRoleCode()).ifPresent(role -> {
throw new BusinessException("角色编码已存在: " + role.getRoleCode());
});
// 2. 构建角色实体
SysRole role = new SysRole();
role.setRoleCode(roleDto.getRoleCode());
role.setRoleName(roleDto.getRoleName());
role.setDescription(roleDto.getDescription());
role.setSortOrder(roleDto.getSortOrder());
role.setStatus(roleDto.getStatus());
// 3. 保存角色到数据库
SysRole savedRole = roleRepository.save(role);
return convertToVo(savedRole);
}
/**
* 更新角色
* 步骤:
* 1. 检查角色是否存在
* 2. 如果修改了角色编码,检查新编码是否已被使用
* 3. 更新角色信息
*
* @param id 角色ID
* @param roleDto 角色数据传输对象
* @return 更新后的角色视图对象
* @throws BusinessException 当角色不存在或编码冲突时抛出异常
*/
@Override
@Transactional(rollbackFor = Exception.class)
public RoleVo update(Long id, RoleDto roleDto) {
// 1. 查询角色是否存在
SysRole role = roleRepository.findById(id)
.orElseThrow(() -> new BusinessException("角色不存在: " + id));
// 2. 如果修改了角色编码,检查新编码是否已被使用
if (!role.getRoleCode().equals(roleDto.getRoleCode())) {
roleRepository.findByRoleCode(roleDto.getRoleCode()).ifPresent(r -> {
throw new BusinessException("角色编码已存在: " + r.getRoleCode());
});
}
// 3. 更新角色信息
role.setRoleCode(roleDto.getRoleCode());
role.setRoleName(roleDto.getRoleName());
role.setDescription(roleDto.getDescription());
role.setSortOrder(roleDto.getSortOrder());
role.setStatus(roleDto.getStatus());
// 4. 保存更新
SysRole savedRole = roleRepository.save(role);
return convertToVo(savedRole);
}
/**
* 删除角色
* 步骤:
* 1. 检查角色是否存在
* 2. 删除角色(关联的权限关系会自动级联删除)
*
* 注意:如果角色已分配给用户,需要先解除关联
*
* @param id 角色ID
* @throws BusinessException 当角色不存在时抛出异常
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
// 1. 检查角色是否存在
if (!roleRepository.existsById(id)) {
throw new BusinessException("角色不存在: " + id);
}
// 2. 删除角色
roleRepository.deleteById(id);
}
/**
* 根据ID获取角色
* 步骤:
* 1. 查询角色基本信息
* 2. 使用 JOIN FETCH 一次性加载关联的权限(避免 N+1 问题)
* 3. 转换为视图对象返回
*
* @param id 角色ID
* @return 角色视图对象
* @throws BusinessException 当角色不存在时抛出异常
*/
@Override
public RoleVo getById(Long id) {
// 1. 查询角色(使用 JOIN FETCH 避免懒加载问题)
SysRole role = roleRepository.findByIdWithPermissions(id)
.orElseThrow(() -> new BusinessException("角色不存在: " + id));
// 2. 转换为视图对象
return convertToVo(role);
}
/**
* 获取所有角色
* 步骤:
* 1. 查询所有角色
* 2. 使用 JOIN FETCH 预加载权限
* 3. 转换为视图对象列表
*
* @return 角色视图对象列表
*/
@Override
public List<RoleVo> getAllRoles() {
// 1. 查询所有角色(使用 JOIN FETCH 避免懒加载问题)
List<SysRole> roles = roleRepository.findAllWithPermissions();
// 2. 转换为视图对象列表
return roles.stream()
.map(this::convertToVo)
.collect(Collectors.toList());
}
/**
* 分页查询角色
* 步骤:
* 1. 构建动态查询条件(支持角色编码、名称、状态)
* 2. 执行分页查询
* 3. 转换结果为视图对象
*
* @param queryDto 查询条件对象
* @param page 页码(从 0 开始)
* @param size 每页大小
* @return 分页结果
*/
@Override
public PageResult<RoleVo> queryRoles(RoleQueryDto queryDto, int page, int size) {
// 1. 构建动态查询条件
Specification<SysRole> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 1.1 角色编码模糊查询
if (queryDto.getRoleCode() != null && !queryDto.getRoleCode().isEmpty()) {
predicates.add(cb.like(root.get("roleCode"), "%" + queryDto.getRoleCode() + "%"));
}
// 1.2 角色名称模糊查询
if (queryDto.getRoleName() != null && !queryDto.getRoleName().isEmpty()) {
predicates.add(cb.like(root.get("roleName"), "%" + queryDto.getRoleName() + "%"));
}
// 1.3 状态精确查询
if (queryDto.getStatus() != null) {
predicates.add(cb.equal(root.get("status"), queryDto.getStatus()));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
// 2. 执行分页查询
Page<SysRole> rolePage = roleRepository.findAll(spec, PageRequest.of(page, size));
// 3. 转换为分页结果
return PageResult.of(
rolePage.getContent().stream().map(this::convertToVo).collect(Collectors.toList()),
rolePage.getTotalElements(),
rolePage.getNumber(),
rolePage.getSize()
);
}
/**
* 为角色分配权限
* 步骤:
* 1. 查询角色是否存在
* 2. 根据权限ID列表查询所有权限
* 3. 清空角色原有的权限
* 4. 添加新的权限
* 5. 保存更新
*
* @param roleId 角色ID
* @param permissionIds 权限ID列表
* @throws BusinessException 当角色或权限不存在时抛出异常
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void assignPermissions(Long roleId, List<Long> permissionIds) {
// 1. 检查角色是否存在
SysRole role = roleRepository.findById(roleId)
.orElseThrow(() -> new BusinessException("角色不存在: " + roleId));
// 2. 查询所有权限(如果权限不存在会抛出异常)
Set<SysPermission> permissions = permissionIds.stream()
.map(permissionId -> permissionRepository.findById(permissionId)
.orElseThrow(() -> new BusinessException("权限不存在: " + permissionId)))
.collect(Collectors.toSet());
// 3. 清空原有权限并添加新权限
role.getPermissions().clear();
role.getPermissions().addAll(permissions);
// 4. 保存更新
roleRepository.save(role);
}
/**
* 获取角色的权限ID列表
* 步骤:
* 1. 查询角色(使用 JOIN FETCH 加载权限)
* 2. 提取所有权限的ID
*
* @param roleId 角色ID
* @return 权限ID列表
* @throws BusinessException 当角色不存在时抛出异常
*/
@Override
public List<Long> getRolePermissionIds(Long roleId) {
// 1. 查询角色及权限
SysRole role = roleRepository.findByIdWithPermissions(roleId)
.orElseThrow(() -> new BusinessException("角色不存在: " + roleId));
// 2. 提取权限ID列表
return role.getPermissions().stream()
.map(SysPermission::getId)
.collect(Collectors.toList());
}
/**
* 将角色实体转换为视图对象
* 步骤:
* 1. 复制基本信息
* 2. 转换权限列表
*
* @param role 角色实体
* @return 角色视图对象
*/
private RoleVo convertToVo(SysRole role) {
RoleVo vo = new RoleVo();
vo.setId(role.getId());
vo.setRoleCode(role.getRoleCode());
vo.setRoleName(role.getRoleName());
vo.setDescription(role.getDescription());
vo.setSortOrder(role.getSortOrder());
vo.setStatus(role.getStatus());
vo.setCreatedAt(role.getCreatedAt());
vo.setUpdatedAt(role.getUpdatedAt());
// 1. 如果有权限,转换为视图对象
if (role.getPermissions() != null && !role.getPermissions().isEmpty()) {
Set<PermissionVo> permissionVos = role.getPermissions().stream()
.map(this::convertToPermissionVo)
.collect(Collectors.toSet());
vo.setPermissions(permissionVos);
}
return vo;
}
/**
* 将权限实体转换为视图对象
*
* @param permission 权限实体
* @return 权限视图对象
*/
private PermissionVo convertToPermissionVo(SysPermission permission) {
PermissionVo vo = new PermissionVo();
vo.setId(permission.getId());
vo.setPermissionCode(permission.getPermissionCode());
vo.setPermissionName(permission.getPermissionName());
vo.setResource(permission.getResource());
vo.setAction(permission.getAction());
vo.setDescription(permission.getDescription());
vo.setStatus(permission.getStatus());
vo.setCreatedAt(permission.getCreatedAt());
vo.setUpdatedAt(permission.getUpdatedAt());
return vo;
}
}