diff --git a/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java b/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java index 9194984..7a7e000 100644 --- a/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java +++ b/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java @@ -29,4 +29,11 @@ public class ResourceController { public Result save(@RequestBody ResourceNode node) { return Result.success(resourceService.save(node)); } + + /** 删除节点(含子节点检查 + 级联清理) */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + resourceService.delete(id); + return Result.success(); + } } diff --git a/crm-auth/src/main/java/com/crm/auth/service/IResourceService.java b/crm-auth/src/main/java/com/crm/auth/service/IResourceService.java index 8b92e3a..432d80d 100644 --- a/crm-auth/src/main/java/com/crm/auth/service/IResourceService.java +++ b/crm-auth/src/main/java/com/crm/auth/service/IResourceService.java @@ -14,4 +14,7 @@ public interface IResourceService { /** 新增或编辑节点(含层级约束、字段必填、编辑锁 type) */ ResourceNode save(ResourceNode node); + + /** 删除节点(含子节点检查 + 级联清理 sys_role_menu 授权引用) */ + void delete(Long id); } diff --git a/crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java b/crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java index 5eb1fc2..8a21ec8 100644 --- a/crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java +++ b/crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java @@ -1,10 +1,13 @@ package com.crm.auth.service.impl; import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.crm.auth.constant.AuthConstants; import com.crm.auth.domain.dto.ResourceNode; import com.crm.auth.domain.entity.SysMenu; +import com.crm.auth.domain.entity.SysRoleMenu; import com.crm.auth.domain.enums.MenuType; +import com.crm.auth.mapper.SysRoleMenuMapper; import com.crm.auth.service.IResourceService; import com.crm.auth.service.ISysMenuService; import com.crm.base.domain.exception.BusinessErrorException; @@ -27,6 +30,7 @@ public class ResourceServiceImpl implements IResourceService { private static final int CODE_RESOURCE_INVALID = 61009; private final ISysMenuService sysMenuService; + private final SysRoleMenuMapper sysRoleMenuMapper; @Override public List listAll() { @@ -79,6 +83,32 @@ public class ResourceServiceImpl implements IResourceService { return ResourceNode.fromEntity(entity); } + @Override + public void delete(Long id) { + SysMenu node = sysMenuService.getById(id); + if (node == null) { + throw new BusinessErrorException(CODE_RESOURCE_INVALID, "资源节点不存在:id=" + id); + } + + // 子节点检查 + long childCount = sysMenuService.count( + new LambdaQueryWrapper().eq(SysMenu::getParentId, id)); + if (childCount > 0) { + throw new BusinessErrorException(CODE_RESOURCE_INVALID, + "节点下有 " + childCount + " 个子节点,请先删除子节点"); + } + + // 级联清理角色授权引用 + int deletedRefs = sysRoleMenuMapper.delete( + new LambdaQueryWrapper().eq(SysRoleMenu::getMenuId, id)); + if (deletedRefs > 0) { + log.info("删除资源节点时级联清理角色授权引用:menuId={}, 清理了 {} 条记录", id, deletedRefs); + } + + sysMenuService.removeById(id); + log.info("删除资源节点:id={}, name={}", id, node.getMenuName()); + } + // ==================== 校验逻辑 ==================== /** 层级约束:6 种非法父子组合 */ diff --git a/crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java b/crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java index ad34bc2..c082693 100644 --- a/crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java +++ b/crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java @@ -1,8 +1,11 @@ package com.crm.auth.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.crm.auth.domain.dto.ResourceNode; import com.crm.auth.domain.entity.SysMenu; +import com.crm.auth.domain.entity.SysRoleMenu; import com.crm.auth.domain.enums.MenuType; +import com.crm.auth.mapper.SysRoleMenuMapper; import com.crm.auth.service.ISysMenuService; import com.crm.base.domain.exception.BusinessErrorException; import org.junit.jupiter.api.BeforeEach; @@ -29,6 +32,8 @@ class ResourceServiceImplTest { @Mock private ISysMenuService sysMenuService; + @Mock + private SysRoleMenuMapper sysRoleMenuMapper; @InjectMocks private ResourceServiceImpl resourceService; @@ -398,6 +403,64 @@ class ResourceServiceImplTest { assertThat(captor.getValue().getPath()).isEqualTo("/new/path"); } + // ==================== 删除节点 ==================== + + @Test + @DisplayName("删除不存在的节点 -> 抛异常") + void delete_nodeNotFound_throwsException() { + when(sysMenuService.getById(999L)).thenReturn(null); + + assertThatThrownBy(() -> resourceService.delete(999L)) + .isInstanceOf(BusinessErrorException.class) + .hasMessageContaining("不存在"); + + verify(sysMenuService, never()).removeById(any()); + } + + @Test + @DisplayName("有子节点 -> 拒绝删除") + void delete_hasChildren_rejected() { + when(sysMenuService.getById(1L)).thenReturn(catalog); + // stub count: parentId=1 有子节点 + when(sysMenuService.count(any(LambdaQueryWrapper.class))).thenReturn(3L); + + assertThatThrownBy(() -> resourceService.delete(1L)) + .isInstanceOf(BusinessErrorException.class) + .hasMessageContaining("子节点"); + + verify(sysMenuService, never()).removeById(any()); + verify(sysRoleMenuMapper, never()).delete(any(LambdaQueryWrapper.class)); + } + + @Test + @DisplayName("叶子节点无授权引用 -> 删除成功") + void delete_leafNoAuth_success() { + when(sysMenuService.getById(100L)).thenReturn(button); + when(sysMenuService.count(any(LambdaQueryWrapper.class))).thenReturn(0L); + when(sysMenuService.removeById(100L)).thenReturn(true); + + resourceService.delete(100L); + + verify(sysMenuService).removeById(100L); + } + + @Test + @DisplayName("叶子节点有授权引用 -> 删除节点 + 级联清理 sys_role_menu") + void delete_leafWithAuth_cascadeCleanup() { + when(sysMenuService.getById(100L)).thenReturn(button); + when(sysMenuService.count(any(LambdaQueryWrapper.class))).thenReturn(0L); + when(sysMenuService.removeById(100L)).thenReturn(true); + when(sysRoleMenuMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(2); + + resourceService.delete(100L); + + verify(sysMenuService).removeById(100L); + // 有引用,级联清理 + ArgumentCaptor> captor = + ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(sysRoleMenuMapper).delete(captor.capture()); + } + // ==================== 辅助方法 ==================== private SysMenu buildMenu(Long id, Long parentId, String name, int menuType) {