✨ 角色菜单数据权限替代用户级组织/客商权限

将后台数据权限迁移到角色×菜单配置,运行时按 menuCode 解析;组织树取最宽合并;停用用户侧组织与客商授权过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 04:05:01 +08:00
parent 18065d6ae4
commit 8737c4b5f9
34 changed files with 1640 additions and 160 deletions
@@ -0,0 +1,83 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.secure.annotation.IsAdmin;
import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R;
import org.springblade.system.pojo.dto.RoleMenuDataScopeDTO;
import org.springblade.system.pojo.vo.RoleMenuDataScopeTreeVO;
import org.springblade.system.service.IRoleMenuDataScopeService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 角色菜单数据权限配置
*
* @author Chill
*/
@NonDS
@RestController
@AllArgsConstructor
@IsAdmin
@RequestMapping("/role-menu-data-scope")
@Tag(name = "角色菜单数据权限", description = "角色菜单数据权限")
public class RoleMenuDataScopeController extends BladeController {
private final IRoleMenuDataScopeService roleMenuDataScopeService;
/**
* 角色数据权限配置树
*/
@GetMapping("/tree")
@ApiOperationSupport(order = 1)
@Operation(summary = "角色数据权限配置树", description = "传入roleId")
public R<RoleMenuDataScopeTreeVO> tree(@Parameter(description = "角色主键", required = true) @RequestParam Long roleId) {
return R.data(roleMenuDataScopeService.tree(roleId));
}
/**
* 批量保存角色菜单数据权限
*/
@PostMapping("/submit")
@ApiOperationSupport(order = 2)
@Operation(summary = "批量保存角色菜单数据权限", description = "传入RoleMenuDataScopeDTO")
public R submit(@Valid @RequestBody RoleMenuDataScopeDTO dto) {
return R.status(roleMenuDataScopeService.submit(dto));
}
}
@@ -0,0 +1,63 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.feign;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.AllArgsConstructor;
import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.pojo.vo.RoleDataScopeVO;
import org.springblade.system.service.IRoleMenuDataScopeService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 角色数据权限 Feign 实现
*
* @author Chill
*/
@NonDS
@Hidden
@RestController
@AllArgsConstructor
public class RoleDataScopeClient implements IRoleDataScopeClient {
private final IRoleMenuDataScopeService roleMenuDataScopeService;
@Override
@GetMapping(RESOLVE_BY_MENU_CODE)
public R<RoleDataScopeVO> resolveByMenuCode(String roleIds, String menuCode, String deptIds) {
return R.data(roleMenuDataScopeService.resolveByMenuCode(Func.toLongList(roleIds), menuCode, Func.toLongList(deptIds)));
}
@Override
@GetMapping(RESOLVE_WIDEST)
public R<RoleDataScopeVO> resolveWidest(String roleIds, String deptIds) {
return R.data(roleMenuDataScopeService.resolveWidest(Func.toLongList(roleIds), Func.toLongList(deptIds)));
}
}
@@ -0,0 +1,37 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.system.pojo.entity.RoleMenuDataScopeDept;
/**
* 角色菜单数据权限自定义组织 Mapper
*
* @author Chill
*/
public interface RoleMenuDataScopeDeptMapper extends BaseMapper<RoleMenuDataScopeDept> {
}
@@ -0,0 +1,37 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.system.pojo.entity.RoleMenuDataScope;
/**
* 角色菜单数据权限 Mapper
*
* @author Chill
*/
public interface RoleMenuDataScopeMapper extends BaseMapper<RoleMenuDataScope> {
}
@@ -0,0 +1,78 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.service;
import org.springblade.core.mp.base.BaseService;
import org.springblade.system.pojo.dto.RoleMenuDataScopeDTO;
import org.springblade.system.pojo.entity.RoleMenuDataScope;
import org.springblade.system.pojo.vo.RoleDataScopeVO;
import org.springblade.system.pojo.vo.RoleMenuDataScopeTreeVO;
import java.util.List;
/**
* 角色菜单数据权限服务
*
* @author Chill
*/
public interface IRoleMenuDataScopeService extends BaseService<RoleMenuDataScope> {
/**
* 查询角色数据权限配置树
*
* @param roleId 角色主键
* @return 菜单树与已配置权限
*/
RoleMenuDataScopeTreeVO tree(Long roleId);
/**
* 批量保存角色菜单数据权限
*
* @param dto 保存参数
* @return 是否成功
*/
boolean submit(RoleMenuDataScopeDTO dto);
/**
* 按菜单编码合并解析角色数据权限
*
* @param roleIds 角色ID集合
* @param menuCode 菜单编码
* @param userDeptIds 用户所属组织
* @return 解析结果
*/
RoleDataScopeVO resolveByMenuCode(List<Long> roleIds, String menuCode, List<Long> userDeptIds);
/**
* 合并用户全部角色菜单配置为最宽数据权限
*
* @param roleIds 角色ID集合
* @param userDeptIds 用户所属组织
* @return 解析结果
*/
RoleDataScopeVO resolveWidest(List<Long> roleIds, List<Long> userDeptIds);
}
@@ -46,15 +46,15 @@ import org.springblade.core.tool.node.ForestNodeMerger;
import org.springblade.core.tool.utils.*;
import org.springblade.system.cache.SysCache;
import org.springblade.system.mapper.DeptMapper;
import org.springblade.system.mapper.UserDataScopeMapper;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.entity.UserDataScope;
import org.springblade.system.pojo.enums.DeptCategory;
import org.springblade.system.pojo.vo.DeptVO;
import org.springblade.system.pojo.vo.RoleDataScopeVO;
import org.springblade.system.pojo.vo.UserVO;
import org.springblade.system.props.IamSyncProperties;
import org.springblade.system.service.IDeptService;
import org.springblade.system.service.IRoleMenuDataScopeService;
import org.springblade.system.service.IUserService;
import org.springblade.system.wrapper.DeptWrapper;
import org.springblade.system.wrapper.UserWrapper;
@@ -94,19 +94,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
private static final String IAM_SYNC_TENANT_ID = "000000";
private static final Long IAM_SYNC_PARENT_ID = 1123598813738675201L;
private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
/** 数据权限范围:仅所属组织 */
private static final int DATA_SCOPE_OWN = 1;
/** 数据权限范围:全部组织 */
private static final int DATA_SCOPE_ALL = 2;
/** 数据权限范围:自定义 */
private static final int DATA_SCOPE_CUSTOM = 3;
/** 数据层级范围:包含下级 */
private static final int DATA_LEVEL_INCLUDE_CHILDREN = 1;
/** 数据层级范围:仅本级 */
private static final int DATA_LEVEL_CURRENT_ONLY = 2;
private final IUserService userService;
private final UserDataScopeMapper userDataScopeMapper;
private final IRoleMenuDataScopeService roleMenuDataScopeService;
private final IamSyncProperties iamSyncProperties;
private final ObjectMapper objectMapper;
@@ -237,7 +227,8 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
/**
* 解析当前登录用户的组织树数据权限。
* <p>
* 超管或「全部组织」返回 allDept=true;其余按所属组织/自定义组织,并结合「包含下级/仅本级」展开。
* 超管或角色菜单数据权限最宽合并为「全部组织」时不过滤;
* 其余按角色配置的组织并集过滤。仅「本人创建」时按用户所属组织及下级展示组织树。
*
* @return 过滤上下文;无法识别用户时按空权限处理
*/
@@ -246,55 +237,39 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return DeptDataScopeFilter.all();
}
Long userId = AuthUtil.getUserId();
if (Func.isEmpty(userId) || userId <= 0) {
if (Func.isEmpty(userId) || userId <= 0 || AuthUtil.getUser() == null) {
return DeptDataScopeFilter.empty();
}
List<Long> roleIds = Func.toLongList(AuthUtil.getUser().getRoleId());
List<Long> userDeptIds = Func.toLongList(AuthUtil.getDeptId());
User user = userService.getById(userId);
if (Func.isEmpty(user)) {
return DeptDataScopeFilter.empty();
if (Func.isNotEmpty(user) && Func.isNotEmpty(user.getDeptId())) {
userDeptIds = Func.toLongList(user.getDeptId());
}
Integer dataScopeRange = user.getDataScopeRange() == null ? DATA_SCOPE_OWN : user.getDataScopeRange();
Integer dataLevelRange = user.getDataLevelRange() == null ? DATA_LEVEL_INCLUDE_CHILDREN : user.getDataLevelRange();
if (Objects.equals(dataScopeRange, DATA_SCOPE_ALL)) {
RoleDataScopeVO scope = roleMenuDataScopeService.resolveWidest(roleIds, userDeptIds);
if (scope == null || scope.isAllDept()) {
return DeptDataScopeFilter.all();
}
List<Long> baseDeptIds;
if (Objects.equals(dataScopeRange, DATA_SCOPE_CUSTOM)) {
baseDeptIds = userDataScopeMapper.selectList(Wrappers.<UserDataScope>lambdaQuery()
.eq(UserDataScope::getUserId, userId))
.stream()
.map(UserDataScope::getDeptId)
.filter(Objects::nonNull)
.distinct()
.toList();
} else {
// 仅所属组织:优先用户档案中的所属组织,其次当前会话组织。
baseDeptIds = Func.toLongList(user.getDeptId());
if (Func.isEmpty(baseDeptIds)) {
baseDeptIds = Func.toLongList(AuthUtil.getDeptId());
}
}
if (Func.isEmpty(baseDeptIds)) {
return DeptDataScopeFilter.empty();
}
boolean includeChildren = !Objects.equals(dataLevelRange, DATA_LEVEL_CURRENT_ONLY);
LinkedHashSet<Long> allowedDeptIds = new LinkedHashSet<>();
for (Long deptId : baseDeptIds) {
if (Func.isEmpty(deptId) || deptId <= 0) {
continue;
}
if (includeChildren) {
if (scope.isOwnCreate()) {
for (Long deptId : userDeptIds) {
if (Func.isEmpty(deptId) || deptId <= 0) {
continue;
}
List<Long> childIds = SysCache.getDeptChildIds(deptId);
if (Func.isNotEmpty(childIds)) {
allowedDeptIds.addAll(childIds);
} else {
allowedDeptIds.add(deptId);
}
} else {
allowedDeptIds.add(deptId);
}
} else if (Func.isNotEmpty(scope.getDeptIds())) {
allowedDeptIds.addAll(scope.getDeptIds());
}
return new DeptDataScopeFilter(false, includeChildren, allowedDeptIds);
if (Func.isEmpty(allowedDeptIds)) {
return DeptDataScopeFilter.empty();
}
return new DeptDataScopeFilter(false, true, allowedDeptIds);
}
/**
@@ -0,0 +1,288 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.node.ForestNodeMerger;
import org.springblade.core.tool.node.TreeNode;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.mapper.MenuMapper;
import org.springblade.system.mapper.RoleMenuDataScopeDeptMapper;
import org.springblade.system.mapper.RoleMenuDataScopeMapper;
import org.springblade.system.pojo.dto.RoleMenuDataScopeDTO;
import org.springblade.system.pojo.entity.Menu;
import org.springblade.system.pojo.entity.Role;
import org.springblade.system.pojo.entity.RoleMenuDataScope;
import org.springblade.system.pojo.entity.RoleMenuDataScopeDept;
import org.springblade.system.pojo.enums.RoleDataScopeType;
import org.springblade.system.pojo.vo.RoleDataScopeVO;
import org.springblade.system.pojo.vo.RoleMenuDataScopeTreeVO;
import org.springblade.system.pojo.vo.RoleMenuDataScopeVO;
import org.springblade.system.service.IRoleMenuDataScopeService;
import org.springblade.system.service.IRoleService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
/**
* 角色菜单数据权限服务实现
*
* @author Chill
*/
@Service
@RequiredArgsConstructor
public class RoleMenuDataScopeServiceImpl extends BaseServiceImpl<RoleMenuDataScopeMapper, RoleMenuDataScope>
implements IRoleMenuDataScopeService {
private final RoleMenuDataScopeDeptMapper roleMenuDataScopeDeptMapper;
private final MenuMapper menuMapper;
private final IRoleService roleService;
@Override
public RoleMenuDataScopeTreeVO tree(Long roleId) {
if (Func.isEmpty(roleId) || roleId <= 0) {
throw new ServiceException("角色ID不能为空");
}
Role role = roleService.getById(roleId);
if (Func.isEmpty(role)) {
throw new ServiceException("角色不存在");
}
RoleMenuDataScopeTreeVO treeVO = new RoleMenuDataScopeTreeVO();
treeVO.setRoleName(role.getRoleName());
List<TreeNode> menuNodes = menuMapper.grantTree();
treeVO.setMenuTree(ForestNodeMerger.merge(menuNodes));
List<RoleMenuDataScope> scopeList = list(Wrappers.<RoleMenuDataScope>lambdaQuery()
.eq(RoleMenuDataScope::getRoleId, roleId)
.eq(RoleMenuDataScope::getIsDeleted, 0));
if (Func.isEmpty(scopeList)) {
treeVO.setScopes(Collections.emptyList());
return treeVO;
}
List<Long> scopeIds = scopeList.stream().map(RoleMenuDataScope::getId).toList();
Map<Long, List<Long>> deptMap = roleMenuDataScopeDeptMapper.selectList(Wrappers.<RoleMenuDataScopeDept>lambdaQuery()
.in(RoleMenuDataScopeDept::getScopeId, scopeIds))
.stream()
.collect(Collectors.groupingBy(RoleMenuDataScopeDept::getScopeId,
Collectors.mapping(RoleMenuDataScopeDept::getDeptId, Collectors.toList())));
List<Long> menuIds = scopeList.stream().map(RoleMenuDataScope::getMenuId).distinct().toList();
Map<Long, String> menuCodeMap = menuMapper.selectBatchIds(menuIds).stream()
.collect(Collectors.toMap(Menu::getId, Menu::getCode, (left, right) -> left));
List<RoleMenuDataScopeVO> scopeVOList = new ArrayList<>();
for (RoleMenuDataScope scope : scopeList) {
RoleMenuDataScopeVO scopeVO = new RoleMenuDataScopeVO();
scopeVO.setMenuId(scope.getMenuId());
scopeVO.setMenuCode(menuCodeMap.get(scope.getMenuId()));
scopeVO.setScopeType(scope.getScopeType() == null ? RoleDataScopeType.DEFAULT_CODE : scope.getScopeType());
scopeVO.setScopeTypeName(RoleDataScopeType.nameOf(scopeVO.getScopeType()));
scopeVO.setDeptIds(deptMap.getOrDefault(scope.getId(), Collections.emptyList()));
scopeVOList.add(scopeVO);
}
treeVO.setScopes(scopeVOList);
return treeVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submit(RoleMenuDataScopeDTO dto) {
if (dto == null || Func.isEmpty(dto.getRoleId()) || dto.getRoleId() <= 0) {
throw new ServiceException("角色ID不能为空");
}
if (Func.isEmpty(dto.getMenuIds())) {
throw new ServiceException("请选择要设置的菜单");
}
RoleDataScopeType scopeType = RoleDataScopeType.of(dto.getScopeType());
if (scopeType == RoleDataScopeType.CUSTOM && Func.isEmpty(dto.getDeptIds())) {
throw new ServiceException("自定义组织时请勾选组织");
}
Role role = roleService.getById(dto.getRoleId());
if (Func.isEmpty(role)) {
throw new ServiceException("角色不存在");
}
String tenantId = Func.toStrWithEmpty(role.getTenantId(), AuthUtil.getTenantId());
List<Long> menuIds = dto.getMenuIds().stream().filter(Objects::nonNull).distinct().toList();
List<Long> deptIds = scopeType == RoleDataScopeType.CUSTOM
? dto.getDeptIds().stream().filter(Objects::nonNull).distinct().toList()
: Collections.emptyList();
for (Long menuId : menuIds) {
RoleMenuDataScope exists = getOne(Wrappers.<RoleMenuDataScope>lambdaQuery()
.eq(RoleMenuDataScope::getRoleId, dto.getRoleId())
.eq(RoleMenuDataScope::getMenuId, menuId)
.eq(RoleMenuDataScope::getIsDeleted, 0)
.last("limit 1"));
RoleMenuDataScope scope;
if (Func.isEmpty(exists)) {
scope = new RoleMenuDataScope();
scope.setTenantId(tenantId);
scope.setRoleId(dto.getRoleId());
scope.setMenuId(menuId);
scope.setScopeType(scopeType.getCode());
save(scope);
} else {
scope = exists;
scope.setScopeType(scopeType.getCode());
updateById(scope);
roleMenuDataScopeDeptMapper.delete(Wrappers.<RoleMenuDataScopeDept>lambdaQuery()
.eq(RoleMenuDataScopeDept::getScopeId, scope.getId()));
}
if (scopeType == RoleDataScopeType.CUSTOM) {
for (Long deptId : deptIds) {
RoleMenuDataScopeDept scopeDept = new RoleMenuDataScopeDept();
scopeDept.setScopeId(scope.getId());
scopeDept.setDeptId(deptId);
roleMenuDataScopeDeptMapper.insert(scopeDept);
}
}
}
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
return true;
}
@Override
public RoleDataScopeVO resolveByMenuCode(List<Long> roleIds, String menuCode, List<Long> userDeptIds) {
if (Func.isEmpty(roleIds) || Func.isBlank(menuCode)) {
return RoleDataScopeVO.all();
}
Menu menu = menuMapper.selectOne(Wrappers.<Menu>lambdaQuery()
.eq(Menu::getCode, menuCode)
.eq(Menu::getIsDeleted, 0)
.last("limit 1"));
if (Func.isEmpty(menu)) {
return RoleDataScopeVO.all();
}
List<RoleMenuDataScope> scopeList = list(Wrappers.<RoleMenuDataScope>lambdaQuery()
.in(RoleMenuDataScope::getRoleId, roleIds)
.eq(RoleMenuDataScope::getMenuId, menu.getId())
.eq(RoleMenuDataScope::getIsDeleted, 0));
return mergeScopes(scopeList, userDeptIds);
}
@Override
public RoleDataScopeVO resolveWidest(List<Long> roleIds, List<Long> userDeptIds) {
if (Func.isEmpty(roleIds)) {
return RoleDataScopeVO.all();
}
List<RoleMenuDataScope> scopeList = list(Wrappers.<RoleMenuDataScope>lambdaQuery()
.in(RoleMenuDataScope::getRoleId, roleIds)
.eq(RoleMenuDataScope::getIsDeleted, 0));
return mergeScopes(scopeList, userDeptIds);
}
private RoleDataScopeVO mergeScopes(List<RoleMenuDataScope> scopeList, List<Long> userDeptIds) {
if (Func.isEmpty(scopeList)) {
return RoleDataScopeVO.all();
}
boolean hasAll = false;
boolean hasDeptScope = false;
boolean hasOwn = false;
LinkedHashSet<Long> allowedDeptIds = new LinkedHashSet<>();
Map<Long, List<Long>> customDeptMap = loadCustomDeptMap(scopeList);
for (RoleMenuDataScope scope : scopeList) {
RoleDataScopeType type = RoleDataScopeType.of(scope.getScopeType());
switch (type) {
case ALL -> hasAll = true;
case DEPT_AND_CHILD -> {
hasDeptScope = true;
allowedDeptIds.addAll(expandDeptIds(userDeptIds, true));
}
case CUSTOM -> {
hasDeptScope = true;
allowedDeptIds.addAll(customDeptMap.getOrDefault(scope.getId(), Collections.emptyList()));
}
case OWN_CREATE -> hasOwn = true;
default -> {
}
}
}
if (hasAll) {
return RoleDataScopeVO.all();
}
if (hasDeptScope) {
return RoleDataScopeVO.ofDepts(RoleDataScopeType.CUSTOM.getCode(), new ArrayList<>(allowedDeptIds));
}
if (hasOwn) {
return RoleDataScopeVO.ownCreate();
}
return RoleDataScopeVO.all();
}
private Map<Long, List<Long>> loadCustomDeptMap(List<RoleMenuDataScope> scopeList) {
List<Long> customScopeIds = scopeList.stream()
.filter(scope -> RoleDataScopeType.CUSTOM.matches(scope.getScopeType()))
.map(RoleMenuDataScope::getId)
.toList();
if (Func.isEmpty(customScopeIds)) {
return Collections.emptyMap();
}
return roleMenuDataScopeDeptMapper.selectList(Wrappers.<RoleMenuDataScopeDept>lambdaQuery()
.in(RoleMenuDataScopeDept::getScopeId, customScopeIds))
.stream()
.collect(Collectors.groupingBy(RoleMenuDataScopeDept::getScopeId,
Collectors.mapping(RoleMenuDataScopeDept::getDeptId, Collectors.toList())));
}
private List<Long> expandDeptIds(List<Long> baseDeptIds, boolean includeChildren) {
if (Func.isEmpty(baseDeptIds)) {
return Collections.emptyList();
}
LinkedHashSet<Long> result = new LinkedHashSet<>();
for (Long deptId : baseDeptIds) {
if (Func.isEmpty(deptId) || deptId <= 0) {
continue;
}
if (includeChildren) {
List<Long> childIds = SysCache.getDeptChildIds(deptId);
if (Func.isNotEmpty(childIds)) {
result.addAll(childIds);
} else {
result.add(deptId);
}
} else {
result.add(deptId);
}
}
return new ArrayList<>(result);
}
}
@@ -138,7 +138,12 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IR
public boolean grant(@NotEmpty List<Long> roleIds, List<Long> menuIds, List<Long> dataScopeIds, List<Long> apiScopeIds) {
// 租户守卫:批量校验目标角色全部归属当前会话租户
TenantGuard.verifyBatch(this, roleIds, ROLE);
return grantRoleMenu(roleIds, menuIds) && grantDataScope(roleIds, dataScopeIds) && grantApiScope(roleIds, apiScopeIds);
// dataScopeIds 为 null 时跳过原生数据权限更新(角色菜单数据权限已独立配置,避免误清)
boolean result = grantRoleMenu(roleIds, menuIds);
if (dataScopeIds != null) {
result = result && grantDataScope(roleIds, dataScopeIds);
}
return result && grantApiScope(roleIds, apiScopeIds);
}
private boolean grantRoleMenu(List<Long> roleIds, List<Long> menuIds) {
@@ -1116,10 +1116,19 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
}
private void applyUserDefaults(User user) {
if (user.getPersonCategory() == null) user.setPersonCategory(1);
if (user.getDataScopeRange() == null) user.setDataScopeRange(1);
if (user.getDataLevelRange() == null) user.setDataLevelRange(1);
if (user.getIncludeNewCustomer() == null) user.setIncludeNewCustomer(0);
if (user.getPersonCategory() == null) {
user.setPersonCategory(1);
}
// 组织/客商数据权限已迁移至角色菜单配置,用户侧字段仅保留兼容默认值,不再参与过滤。
if (user.getDataScopeRange() == null) {
user.setDataScopeRange(2);
}
if (user.getDataLevelRange() == null) {
user.setDataLevelRange(1);
}
if (user.getIncludeNewCustomer() == null) {
user.setIncludeNewCustomer(0);
}
}
/**
@@ -1133,33 +1142,16 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
}
private boolean submitUserScopes(User user) {
userDataScopeMapper.delete(Wrappers.<UserDataScope>lambdaQuery().eq(UserDataScope::getUserId, user.getId()));
userCustomerScopeMapper.delete(Wrappers.<UserCustomerScope>lambdaQuery().eq(UserCustomerScope::getUserId, user.getId()));
if (Objects.equals(user.getDataScopeRange(), 3) && user.getDataScopeDeptIds() != null) {
user.getDataScopeDeptIds().forEach(deptId -> {
UserDataScope scope = new UserDataScope();
scope.setUserId(user.getId());
scope.setDeptId(deptId);
userDataScopeMapper.insert(scope);
});
}
if (user.getCustomerIds() != null) {
user.getCustomerIds().forEach(customerId -> {
UserCustomerScope scope = new UserCustomerScope();
scope.setUserId(user.getId());
scope.setCustomerId(customerId);
userCustomerScopeMapper.insert(scope);
});
}
// 用户级组织/客商数据权限已停用,改由角色菜单数据权限配置。
return true;
}
private User fillUserScopes(User user) {
if (user == null || user.getId() == null) return user;
user.setDataScopeDeptIds(userDataScopeMapper.selectList(Wrappers.<UserDataScope>lambdaQuery()
.eq(UserDataScope::getUserId, user.getId())).stream().map(UserDataScope::getDeptId).toList());
user.setCustomerIds(userCustomerScopeMapper.selectList(Wrappers.<UserCustomerScope>lambdaQuery()
.eq(UserCustomerScope::getUserId, user.getId())).stream().map(UserCustomerScope::getCustomerId).toList());
if (user == null || user.getId() == null) {
return user;
}
user.setDataScopeDeptIds(Collections.emptyList());
user.setCustomerIds(Collections.emptyList());
return user;
}