Browse Source

feat(crm-auth): Ticket 03 — 删除节点(含子节点检查 + 级联清理)

- DELETE /api/resources/{id} 删除资源节点
- 有子节点时拒绝并返回子节点数
- 无子节点时删除 + 级联清理 sys_role_menu 授权引用
- TDD: 31 个单测,109 tests 全绿
master
luoweijian 1 month ago
parent
commit
02cc9356c3
  1. 7
      crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java
  2. 3
      crm-auth/src/main/java/com/crm/auth/service/IResourceService.java
  3. 30
      crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java
  4. 63
      crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java

7
crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java

@ -29,4 +29,11 @@ public class ResourceController {
public Result<ResourceNode> save(@RequestBody ResourceNode node) {
return Result.success(resourceService.save(node));
}
/** 删除节点(含子节点检查 + 级联清理) */
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
resourceService.delete(id);
return Result.success();
}
}

3
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);
}

30
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<ResourceNode> 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<SysMenu>().eq(SysMenu::getParentId, id));
if (childCount > 0) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID,
"节点下有 " + childCount + " 个子节点,请先删除子节点");
}
// 级联清理角色授权引用
int deletedRefs = sysRoleMenuMapper.delete(
new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getMenuId, id));
if (deletedRefs > 0) {
log.info("删除资源节点时级联清理角色授权引用:menuId={}, 清理了 {} 条记录", id, deletedRefs);
}
sysMenuService.removeById(id);
log.info("删除资源节点:id={}, name={}", id, node.getMenuName());
}
// ==================== 校验逻辑 ====================
/** 层级约束:6 种非法父子组合 */

63
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<LambdaQueryWrapper<SysRoleMenu>> captor =
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(sysRoleMenuMapper).delete(captor.capture());
}
// ==================== 辅助方法 ====================
private SysMenu buildMenu(Long id, Long parentId, String name, int menuType) {

Loading…
Cancel
Save