From a0e30d5834f725a7fb4546ea3c3aa5415ff87dc2 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 4 Nov 2026 03:14:44 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E7=94=A8=E6=88=B7=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E6=94=AF=E6=8C=81=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=A4=9A=E9=80=89=E7=BB=84=E7=BB=87=E5=B9=B6?= =?UTF-8?q?=E4=B8=8E=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90=E5=B9=B6=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../springblade/system/cache/UserCache.java | 32 ++++++++ .../springblade/system/feign/IUserClient.java | 12 +++ .../springblade/system/feign/UserClient.java | 9 +++ .../system/service/IUserService.java | 8 ++ .../system/service/impl/UserServiceImpl.java | 73 ++++++++++++++++++- .../support/TransportBusinessSupport.java | 68 +++++++++++++++-- 6 files changed, 193 insertions(+), 9 deletions(-) diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/UserCache.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/UserCache.java index b181e1f5..aeb72901 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/UserCache.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/cache/UserCache.java @@ -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 getManageDeptIds(Long userId) { + if (userId == null || userId <= 0) { + return Collections.emptyList(); + } + try { + List 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 loadManageDeptIds(Long userId) { + R> 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); diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java index 50be69e0..534d7437 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java @@ -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 userInfoById(@RequestParam("userId") Long userId); + /** + * 解析用户「数据管理权限」组织(含下级) + * + * @param userId 用户主键 + * @return 可管理组织 ID 集合 + */ + @GetMapping(MANAGE_DEPT_IDS) + R> manageDeptIds(@RequestParam("userId") Long userId); + /** * 根据账号获取用户信息 diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java index f25da255..1a0cfc9b 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java @@ -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> manageDeptIds(@RequestParam("userId") Long userId) { + return R.data(service.resolveManageDeptIds(userId)); + } + @Override @GetMapping(USER_BY_ACCOUNT) public R userByAccount(String tenantId, String account) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java index fedecc77..e4160480 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java @@ -257,6 +257,14 @@ public interface IUserService extends BaseService { */ boolean removeUser(String userIds); + /** + * 解析用户「数据管理权限」组织(含下级展开) + * + * @param userId 用户主键 + * @return 可管理组织 ID 集合;无配置时返回空列表 + */ + List resolveManageDeptIds(Long userId); + /** * 导入用户数据(仅超管 + Excel 显式指定时允许跨租户,否则归属当前会话租户) * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index 67ef69fb..67122919 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -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 implement boolean tempUser = this.deleteLogic(Func.toLongList(userIds)); boolean tempUserDept = userDeptService.remove(Wrappers.lambdaQuery().in(UserDept::getUserId, Func.toLongList(userIds))); if (tempUser && tempUserDept) { + userDataScopeMapper.delete(Wrappers.lambdaQuery().in(UserDataScope::getUserId, idList)); UserOauth userOauth = new UserOauth(); userOauth.delete(Wrappers.lambdaQuery().in(UserOauth::getUserId, Func.toLongList(userIds))); UserWeb userWeb = new UserWeb(); @@ -672,6 +674,7 @@ public class UserServiceImpl extends BaseServiceImpl implement UserOther userOther = new UserOther(); userOther.delete(Wrappers.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 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 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.lambdaQuery().eq(UserDataScope::getUserId, user.getId())); + List 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.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 implement if (user == null || user.getId() == null) { return user; } - user.setDataScopeDeptIds(Collections.emptyList()); + List deptIdList = userDataScopeMapper.selectList(Wrappers.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 resolveManageDeptIds(Long userId) { + if (Func.isEmpty(userId) || userId <= 0) { + return Collections.emptyList(); + } + List baseDeptIds = userDataScopeMapper.selectList(Wrappers.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 allowedDeptIds = new LinkedHashSet<>(); + for (Long deptId : baseDeptIds) { + List childIds = SysCache.getDeptChildIds(deptId); + if (Func.isNotEmpty(childIds)) { + allowedDeptIds.addAll(childIds); + } else { + allowedDeptIds.add(deptId); + } + } + return List.copyOf(allowedDeptIds); + } + /** * 统一的租户范围过滤:超管放行,其他用户强制限定为当前会话租户。 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java index 93f9619d..842f6ec0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java @@ -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); + } + + /** + * 角色组织权限与用户数据管理组织取并集。 + *

+ * 全部组织 / 本人创建 不因用户管理组织改变;其余类型将用户管理组织(含下级)并入 deptIds。 + */ + private static RoleDataScopeVO mergeUserManageDepts(RoleDataScopeVO scope) { + if (scope == null || scope.isAllDept() || scope.isOwnCreate()) { + return scope; + } + Long userId = AuthUtil.getUserId(); + List manageDeptIds = UserCache.getManageDeptIds(userId); + if (Func.isEmpty(manageDeptIds)) { + return scope; + } + LinkedHashSet 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)} 判定保持一致。 + *

+ * 超管 / 全部组织 / 本人创建数据权限下可编辑;指定组织权限下,仅所属组织在授权范围内时可编辑; + * 否则仅当前登录用户所属组织的数据可编辑。 + * + * @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 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);