From 92a7a2c478c644c3cb74a34bfee6cdb91e7b9687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Fri, 4 Sep 2026 00:16:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=83=E9=99=90=E5=9B=9E=E6=94=B6=E7=BA=A7?= =?UTF-8?q?=E8=81=94=E6=B8=85=E7=90=86=EF=BC=9A=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E8=A2=AB=E5=9B=9E=E6=94=B6=E6=9D=83=E9=99=90=E5=90=8E=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=B8=85=E7=90=86=E5=85=B6=E7=9B=B4=E5=BB=BA=E5=91=98?= =?UTF-8?q?=E5=B7=A5=E7=9A=84=E8=B6=8A=E6=9D=83=E6=8E=88=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 保存管理员权限时,若其权限被回收,级联删除其直接创建的员工中超出 该管理员当前有效范围的授权(创建时快照遗留的越权项) - 只清理越权项,保留员工合法持有的其他授权;超级管理员不受此限制 --- .../service/PermissionMenuService.java | 39 +++++++++++++++++++ .../service/PermissionMenuServiceTest.java | 37 ++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java index dd6ecb8a..1a841b7b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java @@ -15,6 +15,7 @@ import com.nanri.aiimage.modules.permission.model.vo.ImageVideoDataPermissionUse import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo; import com.nanri.aiimage.modules.permission.model.vo.UserColumnIdsVo; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -41,6 +42,7 @@ import java.util.stream.Collectors; */ @Service @RequiredArgsConstructor +@Slf4j public class PermissionMenuService { public static final String MENU_TYPE_APP = "app"; @@ -401,6 +403,43 @@ public class PermissionMenuService { entity.setColumnId(columnId); userColumnPermissionMapper.insert(entity); } + + // 目标管理员的权限被回收后,其直接创建的员工若仍持有超出范围的 + // 授权(创建时快照遗留,员工权限独立于管理员后续变更),必须级联 + // 删除,否则回收了管理员的菜单权限,员工菜单权限却还在。 + cascadeCleanupSubordinateOverreach(target, finalGrantIds); + } + + /** + * 删除普通管理员直接创建的员工中,超出该管理员当前有效权限范围的授权。 + * 只清理越权项,保留员工合法持有的其他授权。超级管理员无此限制。 + */ + private void cascadeCleanupSubordinateOverreach(AdminUserEntity target, Set targetDirectIds) { + if (target == null || target.getId() == null || isSuperAdmin(target)) { + return; + } + Set targetEffectiveIds = expandDescendantIds(new LinkedHashSet<>(targetDirectIds), loadMenus(null)); + List subordinates = adminUserMapper.selectList(new LambdaQueryWrapper() + .eq(AdminUserEntity::getCreatedById, target.getId())); + for (AdminUserEntity subordinate : subordinates) { + Long subordinateId = subordinate.getId(); + if (subordinateId == null || subordinateId.equals(target.getId())) { + continue; + } + List subordinateGrantIds = loadDirectColumnIds(subordinateId); + if (subordinateGrantIds.isEmpty()) { + continue; + } + List overreachIds = subordinateGrantIds.stream() + .filter(id -> !targetEffectiveIds.contains(id)) + .toList(); + if (overreachIds.isEmpty()) { + continue; + } + userColumnPermissionMapper.deleteByUserIdAndColumnIds(subordinateId, overreachIds); + log.info("removed overreach permissions target={} subordinate={} droppedColumnIds={}", + target.getId(), subordinateId, overreachIds); + } } private List loadMenus(String menuType) { diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/permission/service/PermissionMenuServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/permission/service/PermissionMenuServiceTest.java index 51368e36..96d40806 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/permission/service/PermissionMenuServiceTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/permission/service/PermissionMenuServiceTest.java @@ -413,6 +413,43 @@ class PermissionMenuServiceTest { .containsExactly(1L); } + @Test + void adminPermissionRevocationCascadesToCreatedSubordinates() { + // 回收管理员的菜单权限后,其直接创建员工超出范围的授权必须级联删除。 + // 生产场景:管理员 1000 的"去重数据汇总"被回收后,员工 1002 仍持有它。 + PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class); + UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class); + AdminUserMapper userMapper = mock(AdminUserMapper.class); + PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper); + + // 超管操作员编辑管理员 10:管理员仅保留 id=1 的授权(回收 2) + AdminUserEntity operator = user(1L, "super_admin", 1); + AdminUserEntity target = user(10L, "admin", 1); + target.setCreatedById(1L); + AdminUserEntity subordinate = user(20L, "normal", 0); + subordinate.setCreatedById(10L); + when(userMapper.selectById(10L)).thenReturn(target); + when(menuMapper.selectCount(any())).thenReturn(1L); + when(menuMapper.selectOne(any())).thenReturn(null); + when(menuMapper.selectList(any())).thenReturn(List.of( + menu(1L, null, "admin", 1), + menu(2L, null, "admin", 2))); + // 管理员当前授权:1(新)+ 2(被回收前存在) + when(permissionMapper.selectByUserId(10L)) + .thenReturn(List.of(grant(10L, 1L), grant(10L, 2L))); + // 员工 20 的授权:1(合法)+ 2(越权,应被级联删除) + when(permissionMapper.selectByUserId(20L)) + .thenReturn(List.of(grant(20L, 1L), grant(20L, 2L))); + when(userMapper.selectList(any())).thenReturn(List.of(subordinate)); + UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest(); + request.setColumnIds(List.of(1L)); + + service.updateUserColumnPermissions(operator, 10L, request); + + // 员工越权授权 2 必须被清除,合法授权 1 保留 + verify(permissionMapper).deleteByUserIdAndColumnIds(20L, List.of(2L)); + } + @Test void replacementStoresOnlyDirectRequestedParentId() { PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);