✨ 用户编辑支持数据管理权限多选组织并与角色权限并集

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 03:14:44 +08:00
parent fd3ca7496d
commit a0e30d5834
6 changed files with 193 additions and 9 deletions
@@ -35,6 +35,9 @@ import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.feign.IUserClient;
import java.util.Collections;
import java.util.List;
import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
import static org.springblade.core.launch.constant.FlowConstant.TASK_USR_PREFIX;
@@ -47,6 +50,7 @@ import static org.springblade.core.launch.constant.FlowConstant.TASK_USR_PREFIX;
public class UserCache {
private static final String USER_CACHE_ID = "user:id:";
private static final String USER_CACHE_ACCOUNT = "user:account:";
private static final String USER_CACHE_MANAGE_DEPT = "user:manage-dept:";
private static IUserClient userClient;
@@ -89,6 +93,26 @@ public class UserCache {
}
}
/**
* 获取用户「数据管理权限」组织(含下级展开)
*
* @param userId 用户主键
* @return 可管理组织 ID 集合
*/
@SuppressWarnings("unchecked")
public static List<Long> getManageDeptIds(Long userId) {
if (userId == null || userId <= 0) {
return Collections.emptyList();
}
try {
List<Long> deptIdList = CacheUtil.get(USER_CACHE, USER_CACHE_MANAGE_DEPT, userId, () -> loadManageDeptIds(userId));
return deptIdList == null ? Collections.emptyList() : deptIdList;
} catch (RuntimeException exception) {
log.warn("用户数据管理组织缓存读取失败,改为直连查询, userId={}", userId, exception);
return loadManageDeptIds(userId);
}
}
/**
* 获取用户姓名
*
@@ -153,6 +177,14 @@ public class UserCache {
return result.getData();
}
private static List<Long> loadManageDeptIds(Long userId) {
R<List<Long>> result = getUserClient().manageDeptIds(userId);
if (result == null || !result.isSuccess() || result.getData() == null) {
return Collections.emptyList();
}
return result.getData();
}
private static void clearBrokenUserCache() {
CacheUtil.clear(USER_CACHE);
CacheUtil.clear(USER_CACHE, Boolean.FALSE);
@@ -37,6 +37,8 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
* User Feign接口类
*
@@ -62,6 +64,7 @@ public interface IUserClient {
String REGISTER_USER = API_PREFIX + "/register-user";
String REMOVE_USER = API_PREFIX + "/remove-user";
String BIND_WX_MINI_OPENID = API_PREFIX + "/bind-wx-mini-openid";
String MANAGE_DEPT_IDS = API_PREFIX + "/manage-dept-ids";
/**
* 获取用户信息
@@ -72,6 +75,15 @@ public interface IUserClient {
@GetMapping(USER_BY_ID)
R<User> userInfoById(@RequestParam("userId") Long userId);
/**
* 解析用户「数据管理权限」组织(含下级)
*
* @param userId 用户主键
* @return 可管理组织 ID 集合
*/
@GetMapping(MANAGE_DEPT_IDS)
R<List<Long>> manageDeptIds(@RequestParam("userId") Long userId);
/**
* 根据账号获取用户信息
@@ -38,9 +38,12 @@ import org.springblade.system.service.IUserService;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Hidden;
import java.util.List;
/**
* 用户服务Feign实现类
*
@@ -60,6 +63,12 @@ public class UserClient implements IUserClient {
return R.data(service.getById(userId));
}
@Override
@GetMapping(MANAGE_DEPT_IDS)
public R<List<Long>> manageDeptIds(@RequestParam("userId") Long userId) {
return R.data(service.resolveManageDeptIds(userId));
}
@Override
@GetMapping(USER_BY_ACCOUNT)
public R<User> userByAccount(String tenantId, String account) {
@@ -257,6 +257,14 @@ public interface IUserService extends BaseService<User> {
*/
boolean removeUser(String userIds);
/**
* 解析用户「数据管理权限」组织(含下级展开)
*
* @param userId 用户主键
* @return 可管理组织 ID 集合;无配置时返回空列表
*/
List<Long> resolveManageDeptIds(Long userId);
/**
* 导入用户数据(仅超管 + Excel 显式指定时允许跨租户,否则归属当前会话租户)
*
@@ -86,6 +86,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -663,6 +664,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
boolean tempUser = this.deleteLogic(Func.toLongList(userIds));
boolean tempUserDept = userDeptService.remove(Wrappers.<UserDept>lambdaQuery().in(UserDept::getUserId, Func.toLongList(userIds)));
if (tempUser && tempUserDept) {
userDataScopeMapper.delete(Wrappers.<UserDataScope>lambdaQuery().in(UserDataScope::getUserId, idList));
UserOauth userOauth = new UserOauth();
userOauth.delete(Wrappers.<UserOauth>lambdaQuery().in(UserOauth::getUserId, Func.toLongList(userIds)));
UserWeb userWeb = new UserWeb();
@@ -672,6 +674,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
UserOther userOther = new UserOther();
userOther.delete(Wrappers.<UserOther>lambdaQuery().in(UserOther::getUserId, Func.toLongList(userIds)));
CacheUtil.clear(USER_CACHE);
CacheUtil.clear(USER_CACHE, Boolean.FALSE);
return true;
} else {
throw new ServiceException("删除用户失败!");
@@ -1119,8 +1122,10 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
if (user.getPersonCategory() == null) {
user.setPersonCategory(1);
}
// 组织/客商数据权限已迁移至角色菜单配置,用户侧字段仅保留兼容默认值,不再参与过滤。
if (user.getDataScopeRange() == null) {
// 有「数据管理权限」组织时标记为自定义;否则仅补兼容默认值(业务过滤以角色权限 ∪ 用户管理组织为准)
if (Func.isNotEmpty(user.getDataScopeDeptIds())) {
user.setDataScopeRange(3);
} else if (user.getDataScopeRange() == null) {
user.setDataScopeRange(2);
}
if (user.getDataLevelRange() == null) {
@@ -1141,8 +1146,33 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
return null;
}
/**
* 保存用户「数据管理权限」组织;dataScopeDeptIds 为 null 时保留原配置(兼容 IAM/同步等未传该字段的调用)。
*/
private boolean submitUserScopes(User user) {
// 用户级组织/客商数据权限已停用,改由角色菜单数据权限配置。
if (user == null || user.getId() == null) {
return true;
}
if (user.getDataScopeDeptIds() == null) {
return true;
}
userDataScopeMapper.delete(Wrappers.<UserDataScope>lambdaQuery().eq(UserDataScope::getUserId, user.getId()));
List<Long> deptIdList = user.getDataScopeDeptIds().stream()
.filter(Objects::nonNull)
.filter(deptId -> deptId > 0)
.distinct()
.toList();
for (Long deptId : deptIdList) {
UserDataScope scope = new UserDataScope();
scope.setUserId(user.getId());
scope.setDeptId(deptId);
userDataScopeMapper.insert(scope);
}
this.update(Wrappers.<User>lambdaUpdate()
.set(User::getDataScopeRange, deptIdList.isEmpty() ? 2 : 3)
.eq(User::getId, user.getId()));
CacheUtil.clear(USER_CACHE);
CacheUtil.clear(USER_CACHE, Boolean.FALSE);
return true;
}
@@ -1150,11 +1180,46 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
if (user == null || user.getId() == null) {
return user;
}
user.setDataScopeDeptIds(Collections.emptyList());
List<Long> deptIdList = userDataScopeMapper.selectList(Wrappers.<UserDataScope>lambdaQuery()
.eq(UserDataScope::getUserId, user.getId()))
.stream()
.map(UserDataScope::getDeptId)
.filter(Objects::nonNull)
.distinct()
.toList();
user.setDataScopeDeptIds(deptIdList);
user.setCustomerIds(Collections.emptyList());
return user;
}
@Override
public List<Long> resolveManageDeptIds(Long userId) {
if (Func.isEmpty(userId) || userId <= 0) {
return Collections.emptyList();
}
List<Long> baseDeptIds = userDataScopeMapper.selectList(Wrappers.<UserDataScope>lambdaQuery()
.eq(UserDataScope::getUserId, userId))
.stream()
.map(UserDataScope::getDeptId)
.filter(Objects::nonNull)
.filter(deptId -> deptId > 0)
.distinct()
.toList();
if (Func.isEmpty(baseDeptIds)) {
return Collections.emptyList();
}
LinkedHashSet<Long> allowedDeptIds = new LinkedHashSet<>();
for (Long deptId : baseDeptIds) {
List<Long> childIds = SysCache.getDeptChildIds(deptId);
if (Func.isNotEmpty(childIds)) {
allowedDeptIds.addAll(childIds);
} else {
allowedDeptIds.add(deptId);
}
}
return List.copyOf(allowedDeptIds);
}
/**
* 统一的租户范围过滤:超管放行,其他用户强制限定为当前会话租户。
*/
@@ -32,6 +32,7 @@ import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.system.cache.RoleDataScopeCache;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.vo.RoleDataScopeVO;
import org.springblade.transport.mapper.ProjectApplyMapper;
@@ -41,6 +42,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -132,14 +134,14 @@ public final class TransportBusinessSupport {
}
/**
* 解析指定菜单下的角色数据权限(超管视为全部组织)。
* 解析指定菜单下的角色数据权限(超管视为全部组织),并与用户「数据管理权限」组织取并集。
*/
public static RoleDataScopeVO resolveRoleDataScope(String menuCode) {
if (isAdministrator()) {
return RoleDataScopeVO.all();
}
RoleDataScopeVO scope = RoleDataScopeCache.resolveByMenuCode(menuCode);
return scope == null ? RoleDataScopeVO.all() : scope;
return mergeUserManageDepts(scope == null ? RoleDataScopeVO.all() : scope);
}
/**
@@ -159,10 +161,43 @@ public final class TransportBusinessSupport {
if (isAdministrator()) {
return true;
}
RoleDataScopeVO scope = RoleDataScopeCache.resolveWidest();
RoleDataScopeVO scope = resolveWidestDataScope();
return scope == null || scope.isAllDept();
}
/**
* 解析最宽角色数据权限,并与用户「数据管理权限」组织取并集。
*/
public static RoleDataScopeVO resolveWidestDataScope() {
if (isAdministrator()) {
return RoleDataScopeVO.all();
}
RoleDataScopeVO scope = RoleDataScopeCache.resolveWidest();
return mergeUserManageDepts(scope == null ? RoleDataScopeVO.all() : scope);
}
/**
* 角色组织权限与用户数据管理组织取并集。
* <p>
* 全部组织 / 本人创建 不因用户管理组织改变;其余类型将用户管理组织(含下级)并入 deptIds。
*/
private static RoleDataScopeVO mergeUserManageDepts(RoleDataScopeVO scope) {
if (scope == null || scope.isAllDept() || scope.isOwnCreate()) {
return scope;
}
Long userId = AuthUtil.getUserId();
List<Long> manageDeptIds = UserCache.getManageDeptIds(userId);
if (Func.isEmpty(manageDeptIds)) {
return scope;
}
LinkedHashSet<Long> mergedDeptIds = new LinkedHashSet<>();
if (Func.isNotEmpty(scope.getDeptIds())) {
mergedDeptIds.addAll(scope.getDeptIds());
}
mergedDeptIds.addAll(manageDeptIds);
return RoleDataScopeVO.ofDepts(scope.getScopeType(), List.copyOf(mergedDeptIds));
}
/**
* 是否仅本人创建数据权限
*/
@@ -187,6 +222,29 @@ public final class TransportBusinessSupport {
return dept;
}
/**
* 列表行是否只读:与 {@link #assertCurrentDept(Long, String, String)} 判定保持一致。
* <p>
* 超管 / 全部组织 / 本人创建数据权限下可编辑;指定组织权限下,仅所属组织在授权范围内时可编辑;
* 否则仅当前登录用户所属组织的数据可编辑。
*
* @param deptId 业务数据所属组织
* @param menuCode 菜单编码
* @return true-前端仅展示查看
*/
public static boolean isDeptReadonly(Long deptId, String menuCode) {
RoleDataScopeVO scope = resolveRoleDataScope(menuCode);
if (scope.isAllDept() || scope.isOwnCreate()) {
return false;
}
List<Long> allowedDeptIds = scope.getDeptIds();
if (Func.isNotEmpty(allowedDeptIds)) {
return deptId == null || deptId <= 0 || !allowedDeptIds.contains(deptId);
}
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
return currentDeptId != null && !Objects.equals(deptId, currentDeptId);
}
/**
* 校验目标业务数据属于当前用户可操作的组织。
*/
@@ -214,7 +272,7 @@ public final class TransportBusinessSupport {
if (hasAllDeptScope()) {
return;
}
RoleDataScopeVO scope = RoleDataScopeCache.resolveWidest();
RoleDataScopeVO scope = resolveWidestDataScope();
if (scope != null && scope.isOwnCreate()) {
return;
}
@@ -396,7 +454,7 @@ public final class TransportBusinessSupport {
}
return Collections.emptyList();
}
RoleDataScopeVO scope = RoleDataScopeCache.resolveWidest();
RoleDataScopeVO scope = resolveWidestDataScope();
if (scope != null && scope.isOwnCreate()) {
if (Objects.equals(allDept, 1) && Func.isNotEmpty(deptId) && deptId > 0) {
return Collections.singletonList(deptId);