From 3f05f773515b95df6ea5212276b2c115a6fd3a0c Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Fri, 12 Dec 2025 22:26:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9D=83=E9=99=90=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/tenant_add.sql | 7 + sql/mysql/tenant_remove.sql | 4 + .../boot/auth/controller/AuthController.java | 5 +- .../config/property/TenantProperties.java | 13 -- .../security/service/PermissionService.java | 26 ++- .../system/controller/TenantController.java | 17 +- .../boot/system/model/bo/RolePermsBO.java | 5 + .../youlai/boot/system/model/entity/User.java | 2 + .../boot/system/service/TenantService.java | 18 +- .../boot/system/service/UserService.java | 9 +- .../service/impl/RoleMenuServiceImpl.java | 167 +++++++++++++----- .../service/impl/TenantServiceImpl.java | 4 +- .../system/service/impl/UserServiceImpl.java | 6 +- src/main/resources/application-dev.yml | 12 +- .../mapper/system/RoleMenuMapper.xml | 6 +- 15 files changed, 210 insertions(+), 91 deletions(-) diff --git a/sql/mysql/tenant_add.sql b/sql/mysql/tenant_add.sql index b1b4b0ee..350bb7f1 100644 --- a/sql/mysql/tenant_add.sql +++ b/sql/mysql/tenant_add.sql @@ -67,6 +67,13 @@ ADD INDEX `idx_tenant_id` (`tenant_id`); UPDATE `sys_role` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; +-- 角色菜单关联表 +ALTER TABLE `sys_role_menu` +ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `role_id`, +ADD INDEX `idx_role_menu_tenant_id` (`tenant_id`); + +UPDATE `sys_role_menu` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; + -- 部门表 ALTER TABLE `sys_dept` ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, diff --git a/sql/mysql/tenant_remove.sql b/sql/mysql/tenant_remove.sql index c179895b..1088c46d 100644 --- a/sql/mysql/tenant_remove.sql +++ b/sql/mysql/tenant_remove.sql @@ -36,6 +36,10 @@ ALTER TABLE `sys_user` ADD UNIQUE KEY `login_name` (`username`); ALTER TABLE `sys_role` DROP INDEX `idx_tenant_id`; ALTER TABLE `sys_role` DROP COLUMN `tenant_id`; +-- 角色菜单关联表 +ALTER TABLE `sys_role_menu` DROP INDEX `idx_role_menu_tenant_id`; +ALTER TABLE `sys_role_menu` DROP COLUMN `tenant_id`; + -- 部门表 ALTER TABLE `sys_dept` DROP INDEX `idx_tenant_id`; ALTER TABLE `sys_dept` DROP COLUMN `tenant_id`; diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index ddfe2773..e94ecc31 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -46,7 +46,6 @@ public class AuthController { private final UserService userService; private final TenantService tenantService; private final TenantProperties tenantProperties; - private final PasswordEncoder passwordEncoder; @Operation(summary = "获取验证码") @GetMapping("/captcha") @@ -75,8 +74,8 @@ public class AuthController { return Result.success(authenticationToken); } - // 多租户模式:未指定租户ID,查询该用户名在所有租户下的记录 - List users = userService.listUsersByUsername(username); + // 多租户模式:未指定租户ID,查询该用户名在所有租户下的账户 + List users = userService.findUserAcrossAllTenants(username); if (users.isEmpty()) { return Result.failed("用户不存在"); diff --git a/src/main/java/com/youlai/boot/config/property/TenantProperties.java b/src/main/java/com/youlai/boot/config/property/TenantProperties.java index 66c9c04e..a28a94fc 100644 --- a/src/main/java/com/youlai/boot/config/property/TenantProperties.java +++ b/src/main/java/com/youlai/boot/config/property/TenantProperties.java @@ -48,18 +48,5 @@ public class TenantProperties { */ private String headerName = "tenant-id"; - /** - * 初始化默认忽略的表 - */ - public TenantProperties() { - // 系统表默认忽略多租户 - ignoreTables.add("sys_tenant"); - ignoreTables.add("sys_dict"); - ignoreTables.add("sys_dict_item"); - ignoreTables.add("sys_config"); - // 代码生成表(平台共用,不做租户隔离) - ignoreTables.add("gen_table"); - ignoreTables.add("gen_table_column"); - } } diff --git a/src/main/java/com/youlai/boot/security/service/PermissionService.java b/src/main/java/com/youlai/boot/security/service/PermissionService.java index 0ebd87e3..1fc3dab1 100644 --- a/src/main/java/com/youlai/boot/security/service/PermissionService.java +++ b/src/main/java/com/youlai/boot/security/service/PermissionService.java @@ -3,6 +3,8 @@ package com.youlai.boot.security.service; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; +import com.youlai.boot.common.tenant.TenantContextHolder; +import com.youlai.boot.config.property.TenantProperties; import com.youlai.boot.security.util.SecurityUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,6 +26,7 @@ import java.util.*; public class PermissionService { private final RedisTemplate redisTemplate; + private final TenantProperties tenantProperties; /** * 判断当前登录用户是否拥有操作权限 @@ -67,21 +70,36 @@ public class PermissionService { /** - * 从缓存中获取角色权限列表 + * 构建租户权限缓存key + * + * @param tenantId 租户ID + * @return 缓存key + */ + private String buildRolePermsCacheKey(Long tenantId) { + if (!tenantProperties.getEnabled() || tenantId == null) { + return RedisConstants.System.ROLE_PERMS; + } + return RedisConstants.System.ROLE_PERMS + ":" + tenantId; + } + + /** + * 从缓存中获取角色权限列表(兼容单租户和多租户) * * @param roleCodes 角色编码集合 * @return 角色权限列表 */ public Set getRolePermsFormCache(Set roleCodes) { - // 检查输入是否为空 if (CollectionUtil.isEmpty(roleCodes)) { return Collections.emptySet(); } + // 获取当前租户ID并构建缓存Key + Long tenantId = TenantContextHolder.getTenantId(); + String cacheKey = buildRolePermsCacheKey(tenantId); + Set perms = new HashSet<>(); - // 从缓存中一次性获取所有角色的权限 Collection roleCodesAsObjects = new ArrayList<>(roleCodes); - List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + List rolePermsList = redisTemplate.opsForHash().multiGet(cacheKey, roleCodesAsObjects); for (Object rolePermsObj : rolePermsList) { if (rolePermsObj instanceof Set) { diff --git a/src/main/java/com/youlai/boot/system/controller/TenantController.java b/src/main/java/com/youlai/boot/system/controller/TenantController.java index f7cdf190..c899661f 100644 --- a/src/main/java/com/youlai/boot/system/controller/TenantController.java +++ b/src/main/java/com/youlai/boot/system/controller/TenantController.java @@ -43,12 +43,12 @@ public class TenantController { * * @return 租户列表 */ - @Operation(summary = "获取当前用户的租户列表") + @Operation(summary = "获取当前用户可访问的租户列表") @GetMapping - public Result> getTenantList() { + public Result> getAccessibleTenants() { Long userId = SecurityUtils.getUserId(); - List tenantList = tenantService.getTenantListByUserId(userId); - log.info("获取用户 {} 的租户列表,共 {} 个租户", userId, tenantList.size()); + List tenantList = tenantService.getAccessibleTenants(userId); + log.debug("用户 {} 可访问 {} 个租户", userId, tenantList.size()); return Result.success(tenantList); } @@ -88,11 +88,10 @@ public class TenantController { log.info("用户 {} 请求切换租户:{} -> {}", userId, fromTenantId, tenantId); - // 验证用户是否有权限访问该租户 - boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId); - if (!hasPermission) { - log.warn("用户 {} 无权限访问租户 {}", userId, tenantId); - return Result.failed("无权限访问该租户"); + // 验证用户是否可以访问该租户 + if (!tenantService.canAccessTenant(userId, tenantId)) { + log.warn("用户 {} 无权访问租户 {}", userId, tenantId); + return Result.failed("无权访问该租户"); } // 验证租户是否存在且正常 diff --git a/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java b/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java index 198e8180..6deca02f 100644 --- a/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java +++ b/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java @@ -13,6 +13,11 @@ import java.util.Set; @Data public class RolePermsBO { + /** + * 租户ID + */ + private Long tenantId; + /** * 角色编码 */ diff --git a/src/main/java/com/youlai/boot/system/model/entity/User.java b/src/main/java/com/youlai/boot/system/model/entity/User.java index 3bea52be..b49e4a05 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/User.java +++ b/src/main/java/com/youlai/boot/system/model/entity/User.java @@ -18,11 +18,13 @@ public class User extends BaseEntity { */ private String username; + /** * 昵称 */ private String nickname; + /** * 性别((1-男 2-女 0-保密) */ diff --git a/src/main/java/com/youlai/boot/system/service/TenantService.java b/src/main/java/com/youlai/boot/system/service/TenantService.java index 2d265cea..c1421fcb 100644 --- a/src/main/java/com/youlai/boot/system/service/TenantService.java +++ b/src/main/java/com/youlai/boot/system/service/TenantService.java @@ -15,12 +15,15 @@ import java.util.List; public interface TenantService extends IService { /** - * 根据用户ID查询用户所属的租户列表 + * 获取用户可访问的租户列表 + *

+ * 通过用户名查询该用户在所有租户下的账户,返回可访问的租户列表 + *

* * @param userId 用户ID - * @return 租户列表 + * @return 可访问的租户列表 */ - List getTenantListByUserId(Long userId); + List getAccessibleTenants(Long userId); /** * 根据租户ID查询租户信息 @@ -39,11 +42,14 @@ public interface TenantService extends IService { Long getTenantIdByDomain(String domain); /** - * 验证用户是否有权限访问指定租户 + * 检查用户是否可以访问指定租户 + *

+ * 验证该用户名在目标租户下是否存在账户 + *

* * @param userId 用户ID * @param tenantId 租户ID - * @return true-有权限,false-无权限 + * @return true-可访问,false-不可访问 */ - boolean hasTenantPermission(Long userId, Long tenantId); + boolean canAccessTenant(Long userId, Long tenantId); } diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index 89cb5bd7..2e3528e9 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -83,12 +83,15 @@ public interface UserService extends IService { UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId); /** - * 根据用户名查询该用户在所有租户下的记录(用于多租户登录时判断是否需要选择租户) + * 跨租户查询用户账户列表 + *

+ * 查询该用户名在所有租户下的账户记录,用于多租户登录时判断是否需要选择租户 + *

* * @param username 用户名 - * @return 用户列表(每个租户一条记录) + * @return 用户账户列表(每个租户一条记录) */ - List listUsersByUsername(String username); + List findUserAcrossAllTenants(String username); /** diff --git a/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java index 39465cc1..9c51f9a0 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java @@ -3,10 +3,11 @@ package com.youlai.boot.system.service.impl; import cn.hutool.core.collection.CollectionUtil; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.common.constant.RedisConstants; +import com.youlai.boot.common.tenant.TenantContextHolder; +import com.youlai.boot.config.property.TenantProperties; 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.common.constant.SecurityConstants; import com.youlai.boot.system.service.RoleMenuService; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; @@ -18,7 +19,7 @@ import java.util.List; import java.util.Set; /** - * 角色菜单服务实现类 + * 角色菜单服务实现类(多租户优化版) * * @author Ray.Hao * @since 2.5.0 @@ -29,76 +30,159 @@ import java.util.Set; public class RoleMenuServiceImpl extends ServiceImpl implements RoleMenuService { private final RedisTemplate redisTemplate; + private final TenantProperties tenantProperties; /** - * 初始化权限缓存 + * 构建租户权限缓存key + * + * @param tenantId 租户ID + * @return 缓存key + * - 多租户开启: system:role:perms:{tenantId} + * - 多租户关闭: system:role:perms */ - @PostConstruct - public void initRolePermsCache() { - log.info("初始化权限缓存... "); - refreshRolePermsCache(); + private String buildRolePermsCacheKey(Long tenantId) { + // 判断是否启用多租户 + if (!tenantProperties.getEnabled() || tenantId == null) { + // 单租户模式或多租户未开启:使用原有Key + return RedisConstants.System.ROLE_PERMS; + } + // 多租户模式开启:Key按租户隔离 + return RedisConstants.System.ROLE_PERMS + ":" + tenantId; } /** - * 刷新权限缓存 + * 启动时初始化权限缓存 + */ + @PostConstruct + public void initRolePermsCache() { + log.info("开始初始化权限缓存..."); + + List allRolePermsList = this.baseMapper.getRolePermsList(null); + + if (CollectionUtil.isEmpty(allRolePermsList)) { + log.warn("权限数据为空,跳过缓存初始化"); + return; + } + + if (tenantProperties.getEnabled()) { + // 多租户模式:按租户分组缓存 + allRolePermsList.forEach(rolePerms -> { + Long tenantId = rolePerms.getTenantId(); + if (tenantId == null) { + log.warn("多租户模式下,角色[{}]缺少tenantId,跳过", rolePerms.getRoleCode()); + return; + } + String cacheKey = RedisConstants.System.ROLE_PERMS + ":" + tenantId; + String roleCode = rolePerms.getRoleCode(); + Set perms = rolePerms.getPerms(); + + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(cacheKey, roleCode, perms); + } + }); + log.info("权限缓存初始化完成(多租户模式),共{}条数据", allRolePermsList.size()); + } else { + // 单租户模式:所有数据统一缓存 + String cacheKey = RedisConstants.System.ROLE_PERMS; + allRolePermsList.forEach(rolePerms -> { + String roleCode = rolePerms.getRoleCode(); + Set perms = rolePerms.getPerms(); + + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(cacheKey, roleCode, perms); + } + }); + log.info("权限缓存初始化完成(单租户模式),共{}条数据", allRolePermsList.size()); + } + } + + /** + * 刷新当前租户权限缓存 */ @Override public void refreshRolePermsCache() { - // 清理权限缓存 - redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, "*"); - + Long tenantId = TenantContextHolder.getTenantId(); + String cacheKey = buildRolePermsCacheKey(tenantId); + + // 清理当前租户权限缓存 + redisTemplate.delete(cacheKey); + + // 重新加载当前租户权限 List list = this.baseMapper.getRolePermsList(null); if (CollectionUtil.isNotEmpty(list)) { list.forEach(item -> { String roleCode = item.getRoleCode(); Set perms = item.getPerms(); if (CollectionUtil.isNotEmpty(perms)) { - redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, roleCode, perms); + redisTemplate.opsForHash().put(cacheKey, roleCode, perms); } }); } - } - - /** - * 刷新权限缓存 - */ - @Override - public void refreshRolePermsCache(String roleCode) { - // 清理权限缓存 - redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, roleCode); - - List list = this.baseMapper.getRolePermsList(roleCode); - if (CollectionUtil.isNotEmpty(list)) { - RolePermsBO rolePerms = list.get(0); - if (rolePerms == null) { - return; - } - - Set perms = rolePerms.getPerms(); - if (CollectionUtil.isNotEmpty(perms)) { - redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, roleCode, perms); - } + + if (tenantId == null) { + log.info("权限缓存刷新完成(单租户模式)"); + } else { + log.info("租户[{}]权限缓存刷新完成", tenantId); } } /** - * 刷新权限缓存 (角色编码变更时调用) + * 刷新单个角色权限缓存 + */ + @Override + public void refreshRolePermsCache(String roleCode) { + Long tenantId = TenantContextHolder.getTenantId(); + String cacheKey = buildRolePermsCacheKey(tenantId); + + // 清理指定角色缓存 + redisTemplate.opsForHash().delete(cacheKey, roleCode); + + // 重新加载指定角色权限 + List list = this.baseMapper.getRolePermsList(roleCode); + if (CollectionUtil.isNotEmpty(list)) { + RolePermsBO rolePerms = list.get(0); + if (rolePerms != null) { + Set perms = rolePerms.getPerms(); + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(cacheKey, roleCode, perms); + } + } + } + + if (tenantId == null) { + log.info("角色[{}]权限缓存刷新完成(单租户模式)", roleCode); + } else { + log.info("租户[{}]角色[{}]权限缓存刷新完成", tenantId, roleCode); + } + } + + /** + * 刷新权限缓存(角色编码变更时调用) */ @Override public void refreshRolePermsCache(String oldRoleCode, String newRoleCode) { + Long tenantId = TenantContextHolder.getTenantId(); + String cacheKey = buildRolePermsCacheKey(tenantId); + // 清理旧角色权限缓存 - redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, oldRoleCode); - + redisTemplate.opsForHash().delete(cacheKey, oldRoleCode); + // 添加新角色权限缓存 List list = this.baseMapper.getRolePermsList(newRoleCode); if (CollectionUtil.isNotEmpty(list)) { RolePermsBO rolePerms = list.get(0); - if (rolePerms == null) { - return; + if (rolePerms != null) { + Set perms = rolePerms.getPerms(); + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(cacheKey, newRoleCode, perms); + } } - - Set perms = rolePerms.getPerms(); - redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, newRoleCode, perms); + } + + if (tenantId == null) { + log.info("角色编码变更: {} -> {},权限缓存已更新(单租户模式)", oldRoleCode, newRoleCode); + } else { + log.info("租户[{}]角色编码变更: {} -> {},权限缓存已更新", tenantId, oldRoleCode, newRoleCode); } } @@ -110,6 +194,7 @@ public class RoleMenuServiceImpl extends ServiceImpl i */ @Override public Set getRolePermsByRoleCodes(Set roles) { + // 直接查询数据库(保持原有逻辑) return this.baseMapper.listRolePerms(roles); } diff --git a/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java index b4dfed9c..072e2b0c 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java @@ -32,7 +32,7 @@ public class TenantServiceImpl extends ServiceImpl impleme private final UserMapper userMapper; @Override - public List getTenantListByUserId(Long userId) { + public List getAccessibleTenants(Long userId) { // 临时忽略租户过滤,查询所有租户 TenantContextHolder.setIgnoreTenant(true); try { @@ -123,7 +123,7 @@ public class TenantServiceImpl extends ServiceImpl impleme } @Override - public boolean hasTenantPermission(Long userId, Long tenantId) { + public boolean canAccessTenant(Long userId, Long tenantId) { TenantContextHolder.setIgnoreTenant(true); try { // 先根据用户ID查询用户信息(获取 username) diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index b81bba24..78626fb1 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -271,15 +271,15 @@ public class UserServiceImpl extends ServiceImpl implements Us } @Override - public List listUsersByUsername(String username) { - // 临时忽略租户过滤,查询该用户名在所有租户下的记录 + public List findUserAcrossAllTenants(String username) { + // 临时忽略租户过滤,查询该用户名在所有租户下的账户记录 TenantContextHolder.setIgnoreTenant(true); try { return this.list( new LambdaQueryWrapper() .eq(User::getUsername, username) .eq(User::getIsDeleted, 0) - .orderByAsc(User::getTenantId) // 按租户ID排序,优先返回较小的租户ID + .orderByAsc(User::getTenantId) ); } finally { TenantContextHolder.setIgnoreTenant(false); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index cee52f9e..c93e4fa0 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -288,9 +288,11 @@ youlai: # 忽略多租户过滤的表名列表(系统表、租户表等不需要租户隔离的表) ignore-tables: - - sys_tenant # 租户表本身 - - sys_menu # 菜单表(功能入口定义,所有租户共享) - - sys_dict # 字典表(通常共享) - - sys_dict_item # 字典项表(通常共享) - - sys_config # 系统配置表(通常共享) + - sys_tenant # 租户表本身 + - sys_menu # 菜单表(功能入口定义,所有租户共享) + - sys_dict # 字典表(通常共享) + - sys_dict_item # 字典项表(通常共享) + - sys_config # 系统配置表(通常共享) + - gen_table # 代码生成表(平台共用) + - gen_table_column # 代码生成字段表(平台共用) # ============================================ \ No newline at end of file diff --git a/src/main/resources/mapper/system/RoleMenuMapper.xml b/src/main/resources/mapper/system/RoleMenuMapper.xml index 11fc527d..6fc89ea0 100644 --- a/src/main/resources/mapper/system/RoleMenuMapper.xml +++ b/src/main/resources/mapper/system/RoleMenuMapper.xml @@ -17,6 +17,7 @@ + @@ -26,8 +27,9 @@