feat(system): 实现跨租户克隆授权功能

- 新增RoleMenuMapper和UserRoleMapper跨租户查询方法,支持忽略租户拦截器
- 实现grantCmsPermission方法,支持从参考租户克隆角色、菜单、角色菜单和用户角色到当前租户
- 角色克隆按role_code去重,已存在角色复用,新增角色批量保存
- 菜单克隆按层级复制,避免重复插入,保证父子菜单顺序
- 角色菜单克隆使用映射关系去重,批量保存新增绑定
- 用户角色克隆根据角色代码对齐用户名,避免重复绑定
- 返回克隆后的各资源统计数据,包括新增角色、菜单、角色菜单和用户角色数量
This commit is contained in:
2026-08-06 21:21:10 +08:00
parent 644188288a
commit 2e51710e3a
5 changed files with 182 additions and 63 deletions

View File

@@ -36,4 +36,13 @@ public interface RoleMenuMapper extends BaseMapper<RoleMenu> {
@InterceptorIgnore(tenantLine = "true")
List<Menu> listMenuByRoleIds(@Param("roleIds") List<Integer> roleIds, @Param("menuType") Integer menuType);
/**
* 跨租户查询角色菜单(忽略租户拦截器)
*
* @param tenantId 租户id
* @return List<RoleMenu>
*/
@InterceptorIgnore(tenantLine = "true")
List<RoleMenu> selectListAll(@Param("tenantId") Integer tenantId);
}

View File

@@ -62,4 +62,13 @@ public interface UserRoleMapper extends BaseMapper<UserRole> {
*/
List<UserRole> selectListRel(@Param("param") UserRoleParam param);
/**
* 跨租户查询用户角色(忽略租户拦截器)
*
* @param tenantId 租户id
* @return List<UserRole>
*/
@InterceptorIgnore(tenantLine = "true")
List<UserRole> selectListAll(@Param("tenantId") Integer tenantId);
}

View File

@@ -39,4 +39,9 @@
ORDER BY a.sort_number
</select>
<!-- 跨租户查询角色菜单 -->
<select id="selectListAll" resultType="com.gxwebsoft.common.system.entity.RoleMenu">
SELECT * FROM sys_role_menu WHERE tenant_id = #{tenantId}
</select>
</mapper>

View File

@@ -64,4 +64,9 @@
<include refid="selectSql"></include>
</select>
<!-- 跨租户查询用户角色 -->
<select id="selectListAll" resultType="com.gxwebsoft.common.system.entity.UserRole">
SELECT * FROM sys_user_role WHERE tenant_id = #{tenantId}
</select>
</mapper>

View File

@@ -10,8 +10,12 @@ import com.gxwebsoft.common.core.utils.DomainUtil;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.system.entity.*;
import com.gxwebsoft.common.system.mapper.MenuMapper;
import com.gxwebsoft.common.system.mapper.RoleMapper;
import com.gxwebsoft.common.system.mapper.RoleMenuMapper;
import com.gxwebsoft.common.system.mapper.UserRoleMapper;
import com.gxwebsoft.common.system.mapper.TenantMapper;
import com.gxwebsoft.common.system.param.MenuParam;
import com.gxwebsoft.common.system.param.RoleParam;
import com.gxwebsoft.common.system.service.*;
import com.gxwebsoft.common.system.param.TenantParam;
import com.gxwebsoft.common.core.web.ApiResult;
@@ -26,6 +30,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -37,10 +42,8 @@ import java.util.stream.Collectors;
* @author 科技小王子
* @since 2023-07-17 17:49:53
*/
@Service
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
// cms建站权限标识前缀
private static final String CMS_AUTHORITY_PREFIX = "cms:";
@Service
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
@Resource
private CompanyService companyService;
@@ -49,6 +52,12 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
@Resource
private MenuMapper menuMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private RoleMenuMapper roleMenuMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Resource
private RoleMenuService roleMenuService;
@Resource
private RoleService roleService;
@@ -654,21 +663,70 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
@Override
@Transactional(rollbackFor = {Exception.class})
public ApiResult<?> grantCmsPermission(Integer refTenant, Integer curTenant) {
// 1.当前租户的超级管理员角色, 必须带租户条件
final Role superAdmin = roleService.getOne(new LambdaQueryWrapper<Role>()
.eq(Role::getRoleCode, "superAdmin")
.eq(Role::getTenantId, curTenant)
.last("limit 1"));
if (superAdmin == null) {
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "当前租户没有superAdmin角色, 无法授权");
if (refTenant == null || curTenant == null) {
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户或当前租户为空");
}
final Integer roleId = superAdmin.getRoleId();
if (refTenant.equals(curTenant)) {
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户不能与当前租户相同");
}
final Map<String, Object> data = new HashMap<>();
data.put("refTenantId", refTenant);
data.put("tenantId", curTenant);
// 2.跨租户读取参考租户的全部未删除菜单
final MenuParam refParam = new MenuParam();
refParam.setTenantId(refTenant);
refParam.setDeleted(0);
final List<Menu> refMenus = menuMapper.getMenuByClone(refParam);
// ===== 1. 克隆角色 sys_role按 role_code 去重superAdmin/admin/user 已存在则复用)=====
final RoleParam refRoleParam = new RoleParam();
refRoleParam.setTenantId(refTenant);
final List<Role> refRoles = roleMapper.selectListAll(refRoleParam);
if (CollectionUtils.isEmpty(refRoles)) {
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的角色");
}
final RoleParam curRoleParam = new RoleParam();
curRoleParam.setTenantId(curTenant);
// role_code -> Role当前租户已存在的角色
final Map<String, Role> curRoleByCode = roleMapper.selectListAll(curRoleParam).stream()
.collect(Collectors.toMap(Role::getRoleCode, r -> r, (a, b) -> a));
// oldRoleId -> newRoleId 映射
final Map<Integer, Integer> roleIdMapping = new HashMap<>();
// 待新增角色(按 role_code 去重)
final Map<String, Role> toCreateByCode = new LinkedHashMap<>();
for (Role r : refRoles) {
final Role exist = curRoleByCode.get(r.getRoleCode());
if (exist != null) {
roleIdMapping.put(r.getRoleId(), exist.getRoleId());
} else if (!toCreateByCode.containsKey(r.getRoleCode())) {
final Role copy = new Role();
copy.setRoleCode(r.getRoleCode());
copy.setRoleName(r.getRoleName());
copy.setComments(r.getComments());
copy.setSortNumber(r.getSortNumber());
copy.setTenantId(curTenant);
toCreateByCode.put(r.getRoleCode(), copy);
}
}
int roleCreated = 0;
if (!toCreateByCode.isEmpty()) {
roleService.saveBatch(new ArrayList<>(toCreateByCode.values()));
// 重新读取当前租户角色,回填 oldRoleId -> newRoleId 映射
final Map<String, Role> refreshed = roleMapper.selectListAll(curRoleParam).stream()
.collect(Collectors.toMap(Role::getRoleCode, r -> r, (a, b) -> a));
for (Role r : refRoles) {
final Role nr = refreshed.get(r.getRoleCode());
if (nr != null) {
roleIdMapping.put(r.getRoleId(), nr.getRoleId());
curRoleByCode.putIfAbsent(r.getRoleCode(), nr);
}
}
roleCreated = toCreateByCode.size();
}
// 当前租户全部角色roleId -> Role含新克隆的供后续 role_code 对齐使用
final Map<Integer, Role> newRoleById = new HashMap<>();
curRoleByCode.values().forEach(r -> newRoleById.put(r.getRoleId(), r));
// ===== 2. 克隆菜单 sys_menu按层级复制父先于子标题/权限去重)=====
final MenuParam refMenuParam = new MenuParam();
refMenuParam.setTenantId(refTenant);
refMenuParam.setDeleted(0);
final List<Menu> refMenus = menuMapper.getMenuByClone(refMenuParam);
if (CollectionUtils.isEmpty(refMenus)) {
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的菜单");
}
@@ -676,42 +734,19 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
for (Menu menu : refMenus) {
refMenuMap.put(menu.getMenuId(), menu);
}
// 3.筛选cms:*子树, 权限标识以cms:开头的节点 + 其全部祖先, 保证父级完整
final Set<Integer> cmsMenuIds = new HashSet<>();
for (Menu menu : refMenus) {
if (menu.getAuthority() == null || !menu.getAuthority().startsWith(CMS_AUTHORITY_PREFIX)) {
continue;
}
Menu current = menu;
// add返回false说明该节点及其祖先已收录, 无需继续向上追溯
while (current != null && cmsMenuIds.add(current.getMenuId())) {
final Integer parentId = current.getParentId();
current = (parentId == null || parentId == 0) ? null : refMenuMap.get(parentId);
}
}
if (cmsMenuIds.isEmpty()) {
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有cms权限菜单");
}
// 4.当前租户已有菜单, 用于去重, 保证接口幂等
final MenuParam curParam = new MenuParam();
curParam.setTenantId(curTenant);
curParam.setDeleted(0);
final MenuParam curMenuParam = new MenuParam();
curMenuParam.setTenantId(curTenant);
curMenuParam.setDeleted(0);
final Map<String, Integer> curMenuKeys = new HashMap<>();
for (Menu menu : menuMapper.getMenuByClone(curParam)) {
for (Menu menu : menuMapper.getMenuByClone(curMenuParam)) {
curMenuKeys.putIfAbsent(getMenuKey(menu), menu.getMenuId());
}
// 5.按层级复制菜单, 父菜单先于子菜单插入
final List<Menu> sources = refMenus.stream()
.filter(d -> cmsMenuIds.contains(d.getMenuId()))
.sorted(Comparator.comparingInt((Menu d) -> getMenuDepth(d, refMenuMap)))
.collect(Collectors.toList());
final Map<Integer, Integer> menuIdMapping = new HashMap<>();
int menuCount = 0;
for (Menu source : sources) {
// 已存在的菜单不重复插入, 直接复用其菜单id作为子菜单的父级
final Integer existsMenuId = curMenuKeys.get(getMenuKey(source));
if (existsMenuId != null) {
menuIdMapping.put(source.getMenuId(), existsMenuId);
@@ -737,32 +772,88 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
menuCount++;
}
// 6.绑定cms菜单到超级管理员角色, 已绑定的跳过
final Set<Integer> boundMenuIds = roleMenuService.list(new LambdaQueryWrapper<RoleMenu>()
.eq(RoleMenu::getRoleId, roleId))
.stream().map(RoleMenu::getMenuId).collect(Collectors.toSet());
final List<RoleMenu> roleMenus = new ArrayList<>();
for (Integer menuId : menuIdMapping.values()) {
if (menuId == null || boundMenuIds.contains(menuId)) {
// ===== 3. 克隆角色菜单 sys_role_menuroleId/menuId 用映射转换,去重)=====
final Set<String> curRmKeys = roleMenuMapper.selectListAll(curTenant).stream()
.map(rm -> rm.getRoleId() + "_" + rm.getMenuId())
.collect(Collectors.toSet());
final List<RoleMenu> rmsToSave = new ArrayList<>();
for (RoleMenu rm : roleMenuMapper.selectListAll(refTenant)) {
final Integer newRoleId = roleIdMapping.get(rm.getRoleId());
final Integer newMenuId = menuIdMapping.get(rm.getMenuId());
if (newRoleId == null || newMenuId == null) {
continue;
}
final RoleMenu roleMenu = new RoleMenu();
roleMenu.setRoleId(roleId);
roleMenu.setMenuId(menuId);
roleMenu.setTenantId(curTenant);
roleMenus.add(roleMenu);
final String key = newRoleId + "_" + newMenuId;
if (curRmKeys.contains(key)) {
continue;
}
final RoleMenu copy = new RoleMenu();
copy.setRoleId(newRoleId);
copy.setMenuId(newMenuId);
copy.setTenantId(curTenant);
rmsToSave.add(copy);
curRmKeys.add(key);
}
if (!roleMenus.isEmpty()) {
roleMenuService.saveBatch(roleMenus);
int roleMenuCreated = 0;
if (!rmsToSave.isEmpty()) {
roleMenuService.saveBatch(rmsToSave);
roleMenuCreated = rmsToSave.size();
}
final Map<String, Object> data = new HashMap<>();
data.put("refTenantId", refTenant);
data.put("tenantId", curTenant);
data.put("roleId", roleId);
// ===== 4. 克隆用户角色 sys_user_role按 role_code 对齐当前租户用户,去重)=====
final List<UserRole> curUserRoles = userRoleMapper.selectListAll(curTenant);
final Set<String> curUrKeys = curUserRoles.stream()
.map(ur -> ur.getUserId() + "_" + ur.getRoleId())
.collect(Collectors.toSet());
// 当前租户 userId -> 其 role_code 集合
final Map<Integer, Set<String>> userRoleCodes = new HashMap<>();
for (UserRole ur : curUserRoles) {
final Role role = newRoleById.get(ur.getRoleId());
if (role == null) {
continue;
}
userRoleCodes.computeIfAbsent(ur.getUserId(), k -> new HashSet<>()).add(role.getRoleCode());
}
final List<UserRole> ursToSave = new ArrayList<>();
for (UserRole ur : userRoleMapper.selectListAll(refTenant)) {
final Integer newRoleId = roleIdMapping.get(ur.getRoleId());
if (newRoleId == null) {
continue;
}
final Role newRole = newRoleById.get(newRoleId);
if (newRole == null) {
continue;
}
final String roleCode = newRole.getRoleCode();
// 找当前租户中 role_code 相同的用户,建立绑定
for (Map.Entry<Integer, Set<String>> e : userRoleCodes.entrySet()) {
if (!e.getValue().contains(roleCode)) {
continue;
}
final String key = e.getKey() + "_" + newRoleId;
if (curUrKeys.contains(key)) {
continue;
}
final UserRole copy = new UserRole();
copy.setUserId(e.getKey());
copy.setRoleId(newRoleId);
copy.setTenantId(curTenant);
ursToSave.add(copy);
curUrKeys.add(key);
}
}
int userRoleCreated = 0;
if (!ursToSave.isEmpty()) {
userRoleService.saveBatch(ursToSave);
userRoleCreated = ursToSave.size();
}
data.put("roleTotal", refRoles.size());
data.put("roleCreated", roleCreated);
data.put("menuTotal", menuIdMapping.size());
data.put("menuCreated", menuCount);
data.put("roleMenuCreated", roleMenus.size());
data.put("roleMenuCreated", roleMenuCreated);
data.put("userRoleCreated", userRoleCreated);
return new ApiResult<>(Constants.RESULT_OK_CODE, "授权成功", data);
}