首页
/ yudaocode/ruoyi-vue-pro:租户菜单权限配置

yudaocode/ruoyi-vue-pro:租户菜单权限配置

2026-02-04 04:09:53作者:凤尚柏Louis

概述

在现代SaaS(Software as a Service)多租户系统中,租户菜单权限管理是核心功能之一。yudaocode/ruoyi-vue-pro 基于 Spring Boot + MyBatis Plus + Vue 实现了完善的租户菜单权限配置体系,支持动态权限分配、套餐化管理等高级特性。

本文将深入解析 ruoyi-vue-pro 的租户菜单权限配置机制,涵盖核心概念、配置流程、代码实现和最佳实践。

核心概念解析

多租户架构

flowchart TD
    A[系统租户] --> B[拥有全部菜单权限]
    C[普通租户] --> D[租户套餐]
    D --> E[菜单权限集合]
    F[租户管理员角色] --> G[继承套餐权限]
    H[自定义角色] --> I[权限交集控制]

关键数据模型

实体 描述 核心字段
TenantDO 租户实体 id, name, packageId, contactUserId
TenantPackageDO 租户套餐 id, name, status, menuIds
MenuDO 菜单实体 id, name, permission, type
RoleDO 角色实体 id, name, code, tenantId

配置流程详解

1. 创建租户套餐

租户套餐是菜单权限的容器,每个套餐包含一组特定的菜单权限:

// 创建租户套餐示例
TenantPackageDO package = TenantPackageDO.builder()
    .name("企业版套餐")
    .status(CommonStatusEnum.ENABLE.getStatus())
    .menuIds(Set.of(1001L, 1002L, 1003L, 2001L)) // 菜单ID集合
    .build();

2. 创建租户并关联套餐

创建租户时自动关联套餐,并初始化管理员权限:

@Override
@DSTransactional
@DataPermission(enable = false)
public Long createTenant(TenantSaveReqVO createReqVO) {
    // 校验租户名称和域名
    validTenantNameDuplicate(createReqVO.getName(), null);
    validTenantWebsiteDuplicate(createReqVO.getWebsites(), null);
    
    // 校验套餐有效性
    TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId());

    // 创建租户
    TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class);
    tenantMapper.insert(tenant);
    
    // 在租户上下文中初始化权限
    TenantUtils.execute(tenant.getId(), () -> {
        // 创建角色并分配套餐权限
        Long roleId = createRole(tenantPackage);
        // 创建用户并关联角色
        Long userId = createUser(roleId, createReqVO);
        tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId));
    });
    return tenant.getId();
}

3. 权限分配机制

租户管理员角色权限分配

private Long createRole(TenantPackageDO tenantPackage) {
    // 创建租户管理员角色
    RoleSaveReqVO reqVO = new RoleSaveReqVO();
    reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName())
        .setCode(RoleCodeEnum.TENANT_ADMIN.getCode())
        .setSort(0).setRemark("系统自动生成");
    
    Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType());
    
    // 分配套餐菜单权限
    permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds());
    return roleId;
}

自定义角色权限控制

@Override
@DSTransactional
public void updateTenantRoleMenu(Long tenantId, Set<Long> menuIds) {
    TenantUtils.execute(tenantId, () -> {
        List<RoleDO> roles = roleService.getRoleList();
        
        roles.forEach(role -> {
            if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) {
                // 租户管理员重新分配全套权限
                permissionService.assignRoleMenu(role.getId(), menuIds);
            } else {
                // 自定义角色进行权限交集控制
                Set<Long> roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId());
                roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds);
                permissionService.assignRoleMenu(role.getId(), roleMenuIds);
            }
        });
    });
}

权限校验与过滤

菜单权限过滤

在分配权限时,系统会自动过滤掉租户未开通的菜单:

@PostMapping("/assign-role-menu")
@Operation(summary = "赋予角色菜单")
public CommonResult<Boolean> assignRoleMenu(@Validated @RequestBody PermissionAssignRoleMenuReqVO reqVO) {
    // 多租户环境下过滤未开通菜单
    tenantService.handleTenantMenu(menuIds -> 
        reqVO.getMenuIds().removeIf(menuId -> !CollUtil.contains(menuIds, menuId)));

    // 执行菜单分配
    permissionService.assignRoleMenu(reqVO.getRoleId(), reqVO.getMenuIds());
    return success(true);
}

租户菜单处理器

@Override
public void handleTenantMenu(TenantMenuHandler handler) {
    if (isTenantDisable()) return;
    
    TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId());
    Set<Long> menuIds;
    
    if (isSystemTenant(tenant)) {
        // 系统租户拥有全部菜单
        menuIds = CollectionUtils.convertSet(menuService.getMenuList(), MenuDO::getId);
    } else {
        // 普通租户使用套餐权限
        menuIds = tenantPackageService.getTenantPackage(tenant.getPackageId()).getMenuIds();
    }
    
    handler.handle(menuIds);
}

最佳实践

1. 套餐设计策略

套餐类型 适用场景 菜单配置建议
基础版 小微企业 核心功能菜单,限制高级功能
标准版 中型企业 增加报表、审批等进阶功能
企业版 大型企业 全功能开放,包含定制化菜单

2. 权限变更管理

当租户套餐变更时,系统会自动调整所有角色的权限:

// 更新租户时检查套餐变更
if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) {
    updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds());
}

3. 系统租户特殊处理

系统租户(内置租户)拥有所有菜单权限,不受套餐限制:

private static boolean isSystemTenant(TenantDO tenant) {
    return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM);
}

常见问题解决方案

问题1:菜单权限不生效

排查步骤:

  1. 检查租户套餐状态是否为启用
  2. 验证套餐中的菜单ID是否存在
  3. 确认角色是否正确关联了租户

问题2:权限变更后用户需要重新登录

原因: 权限变更后,用户的登录态中缓存的权限信息需要更新

解决方案: 引导用户重新登录或实现权限动态刷新机制

问题3:自定义角色权限被意外裁剪

预防措施: 在更新套餐时,系统会自动对自定义角色权限进行交集处理,确保不超出套餐范围

性能优化建议

  1. 缓存策略: 对租户套餐菜单列表进行缓存,减少数据库查询
  2. 批量操作: 使用批量接口进行权限分配,减少网络开销
  3. 异步处理: 对耗时的权限同步操作采用异步方式执行

总结

yudaocode/ruoyi-vue-pro 的租户菜单权限配置系统提供了完整的多租户权限管理解决方案,具有以下特点:

  • 灵活的套餐管理: 支持按需配置不同等级的菜单权限套餐
  • 精细的权限控制: 支持租户管理员和自定义角色的差异化权限管理
  • 自动化的权限同步: 套餐变更时自动同步所有相关角色的权限
  • 安全的权限校验: 严格防止越权访问,确保数据隔离

通过合理的套餐设计和权限配置,可以满足从中小企业到大型企业的各种业务场景需求,为SaaS系统的多租户架构提供坚实的权限基础。

提示:在实际使用中,建议根据业务需求合理设计套餐内容,并建立完善的权限变更审核流程,确保系统的安全性和稳定性。

登录后查看全文
热门项目推荐
相关项目推荐