Browse Source

feat(crm-auth): Ticket 02 — 全量资源树查询 + 新增/编辑节点(含层级约束)

- GET /api/resources 全量扁平数组(含 catalog/menu/button)
- POST /api/resources JSON body 新增/编辑 + 6 种层级约束校验
- 字段必填:menu 缺 route 拒绝、button 缺 perms/denyBehavior/apiUrl/status 拒绝
- 编辑锁 type(不可变)+ 编辑时 route 未传保留原值
- SecurityConfig DEFAULT_IGNORE_URLS 放行 /api/resources/**
- TDD: 27 个单测覆盖正反例,105 tests 全绿
master
luoweijian 1 month ago
parent
commit
b54ff47350
  1. 2
      crm-auth/src/main/java/com/crm/auth/config/SecurityConfig.java
  2. 32
      crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java
  3. 17
      crm-auth/src/main/java/com/crm/auth/service/IResourceService.java
  4. 180
      crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java
  5. 437
      crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java

2
crm-auth/src/main/java/com/crm/auth/config/SecurityConfig.java

@ -29,6 +29,8 @@ public class SecurityConfig {
private static final String[] DEFAULT_IGNORE_URLS = {
// 登录接口
"/api/auth/login/**",
// 资源树管理接口(ADR-0011)
"/api/resources/**",
// 接口文档
"/doc.html",
"/webjars/**",

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

@ -0,0 +1,32 @@
package com.crm.auth.controller;
import com.crm.auth.domain.dto.ResourceNode;
import com.crm.auth.service.IResourceService;
import com.crm.base.domain.result.Result;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 统一权限资源树管理端接口ADR-0011
*/
@RestController
@RequestMapping("/api/resources")
@RequiredArgsConstructor
public class ResourceController {
private final IResourceService resourceService;
/** 全量资源树(扁平数组,前端按 parentId 组树) */
@GetMapping
public Result<List<ResourceNode>> listAll() {
return Result.success(resourceService.listAll());
}
/** 新增或编辑节点(含校验) */
@PostMapping
public Result<ResourceNode> save(@RequestBody ResourceNode node) {
return Result.success(resourceService.save(node));
}
}

17
crm-auth/src/main/java/com/crm/auth/service/IResourceService.java

@ -0,0 +1,17 @@
package com.crm.auth.service;
import com.crm.auth.domain.dto.ResourceNode;
import java.util.List;
/**
* 统一权限资源树服务ADR-0011
*/
public interface IResourceService {
/** 全量资源树扁平数组(所有节点,不按角色过滤) */
List<ResourceNode> listAll();
/** 新增或编辑节点(含层级约束、字段必填、编辑锁 type) */
ResourceNode save(ResourceNode node);
}

180
crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java

@ -0,0 +1,180 @@
package com.crm.auth.service.impl;
import cn.hutool.core.util.StrUtil;
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.enums.MenuType;
import com.crm.auth.service.IResourceService;
import com.crm.auth.service.ISysMenuService;
import com.crm.base.domain.exception.BusinessErrorException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 统一权限资源树服务实现ADR-0011
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ResourceServiceImpl implements IResourceService {
/** 资源节点参数非法 */
private static final int CODE_RESOURCE_INVALID = 61009;
private final ISysMenuService sysMenuService;
@Override
public List<ResourceNode> listAll() {
return sysMenuService.list().stream()
.map(ResourceNode::fromEntity)
.collect(Collectors.toList());
}
@Override
public ResourceNode save(ResourceNode node) {
if (node == null) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "资源节点不能为空");
}
if (StrUtil.isBlank(node.getName())) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "节点名称不能为空");
}
if (node.getType() == null) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "节点类型不能为空");
}
SysMenu entity;
if (node.getId() != null) {
// 编辑模式
entity = sysMenuService.getById(node.getId());
if (entity == null) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "资源节点不存在:id=" + node.getId());
}
// 编辑锁:type 不可变
if (entity.getMenuType() != null && !entity.getMenuType().equals(node.getType().getCode())) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "节点类型不可变更(现有类型="
+ MenuType.fromCode(entity.getMenuType()) + ")");
}
} else {
entity = new SysMenu();
}
// 字段必填(按 type,编辑模式可复用已有值)
validateRequiredFields(node, entity);
// 层级约束
validateHierarchy(node);
// DTO → Entity 映射
applyToEntity(node, entity);
sysMenuService.saveOrUpdate(entity);
log.info("保存资源节点:id={}, name={}, type={}", entity.getId(), entity.getMenuName(),
MenuType.fromCode(entity.getMenuType()));
return ResourceNode.fromEntity(entity);
}
// ==================== 校验逻辑 ====================
/** 层级约束:6 种非法父子组合 */
private void validateHierarchy(ResourceNode node) {
MenuType childType = node.getType();
if (node.getParentId() == null || node.getParentId() == 0) {
// 根节点:只允许 catalog / menu
if (childType == MenuType.BUTTON) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "根节点不允许添加 button 类型");
}
return;
}
SysMenu parent = sysMenuService.getById(node.getParentId());
if (parent == null) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "父节点不存在:parentId=" + node.getParentId());
}
MenuType parentType = MenuType.fromCode(parent.getMenuType());
// 叶子节点
if (parentType == MenuType.BUTTON) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 是叶子节点,不可添加子节点");
}
// catalog 只能挂 menu
if (parentType == MenuType.CATALOG && childType != MenuType.MENU) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "catalog 节点下只能添加 menu 类型");
}
// menu 只能挂 button
if (parentType == MenuType.MENU && childType != MenuType.BUTTON) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "menu 节点下只能添加 button 类型");
}
}
/** 字段必填校验(编辑模式:若 route 未传但 DB 已有 path 则放行) */
private void validateRequiredFields(ResourceNode node, SysMenu existing) {
MenuType type = node.getType();
if (type == MenuType.MENU) {
boolean hasRoute = StrUtil.isNotBlank(node.getRoute());
boolean hasExistingPath = existing != null && StrUtil.isNotBlank(existing.getPath());
if (!hasRoute && !hasExistingPath) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "menu 类型必须提供路由(route)");
}
}
if (type == MenuType.BUTTON) {
if (StrUtil.isBlank(node.getPerms())) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 类型必须提供权限码(perms)");
}
if (StrUtil.isBlank(node.getDenyBehavior())) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 类型必须提供无权限行为(denyBehavior)");
}
if (StrUtil.isBlank(node.getApiUrl())) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 类型必须提供接口 URL(apiUrl)");
}
if (StrUtil.isBlank(node.getStatus())) {
throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 类型必须提供状态(status)");
}
}
}
// ==================== DTO → Entity ====================
/** 将 ResourceNode 中的业务字段写入 SysMenu(映射 name→menuName, route→path, type→menuType) */
private void applyToEntity(ResourceNode node, SysMenu entity) {
entity.setMenuName(node.getName());
entity.setParentId(node.getParentId() != null ? node.getParentId() : 0L);
if (node.getType() != null) {
entity.setMenuType(node.getType().getCode());
}
// route / path:若未传则保留原有值
if (node.getRoute() != null) {
entity.setPath(node.getRoute());
}
entity.setSort(node.getSort() != null ? node.getSort() : 0);
entity.setIcon(node.getIcon());
entity.setVisible(true); // 默认可见
// button 专用字段(仅 button 类型时写入)
if (node.getType() == MenuType.BUTTON) {
entity.setDescription(node.getDescription());
entity.setPerms(node.getPerms());
entity.setDenyBehavior(node.getDenyBehavior());
entity.setStatus(node.getStatus());
entity.setApiUrl(node.getApiUrl());
} else {
// 非 button 节点清掉这些字段
entity.setDescription(null);
entity.setPerms(null);
entity.setDenyBehavior(null);
entity.setStatus(null);
entity.setApiUrl(null);
}
}
}

437
crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java

@ -0,0 +1,437 @@
package com.crm.auth.service.impl;
import com.crm.auth.domain.dto.ResourceNode;
import com.crm.auth.domain.entity.SysMenu;
import com.crm.auth.domain.enums.MenuType;
import com.crm.auth.service.ISysMenuService;
import com.crm.base.domain.exception.BusinessErrorException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* {@link ResourceServiceImpl} 单元测试覆盖资源树查询新增/编辑校验层级约束字段必填
*/
@ExtendWith(MockitoExtension.class)
class ResourceServiceImplTest {
@Mock
private ISysMenuService sysMenuService;
@InjectMocks
private ResourceServiceImpl resourceService;
// ========== 常用桩数据 ==========
private SysMenu catalog;
private SysMenu menu;
private SysMenu button;
@BeforeEach
void setUp() {
catalog = buildMenu(1L, 0L, "系统管理", 1);
menu = buildMenu(10L, 1L, "用户管理", 2);
menu.setPath("/system/user");
button = buildMenu(100L, 10L, "查询用户", 3);
button.setPerms("crm:user:list");
button.setDenyBehavior("hide");
button.setApiUrl("/api/system/users/page");
button.setStatus("enabled");
}
// ==================== 查询 ====================
@Test
@DisplayName("全量资源树查询 -> 返回扁平 ResourceNode 数组,含 catalog/menu/button")
void listAll_returnsFlatListWithAllTypes() {
when(sysMenuService.list()).thenReturn(List.of(catalog, menu, button));
List<ResourceNode> result = resourceService.listAll();
assertThat(result).hasSize(3);
assertThat(result.get(0).getType()).isEqualTo(MenuType.CATALOG);
assertThat(result.get(0).getName()).isEqualTo("系统管理");
assertThat(result.get(1).getType()).isEqualTo(MenuType.MENU);
assertThat(result.get(1).getRoute()).isEqualTo("/system/user");
assertThat(result.get(2).getType()).isEqualTo(MenuType.BUTTON);
assertThat(result.get(2).getPerms()).isEqualTo("crm:user:list");
}
@Test
@DisplayName("空库 -> 返回空数组不抛异常")
void listAll_emptyDb_returnsEmptyList() {
when(sysMenuService.list()).thenReturn(List.of());
List<ResourceNode> result = resourceService.listAll();
assertThat(result).isEmpty();
}
// ==================== 新增 ====================
@Test
@DisplayName("新增 catalog 节点 -> 成功 save")
void save_newCatalog_success() {
ResourceNode node = newResourceNode(null, null, "新目录", MenuType.CATALOG);
resourceService.save(node);
ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
verify(sysMenuService).saveOrUpdate(captor.capture());
SysMenu saved = captor.getValue();
assertThat(saved.getId()).isNull();
assertThat(saved.getMenuType()).isEqualTo(1);
assertThat(saved.getMenuName()).isEqualTo("新目录");
}
@Test
@DisplayName("新增 menu 挂在 catalog 下 -> 成功 save")
void save_newMenuUnderCatalog_success() {
when(sysMenuService.getById(1L)).thenReturn(catalog);
ResourceNode node = newResourceNode(null, 1L, "新菜单", MenuType.MENU);
node.setRoute("/new/menu");
resourceService.save(node);
verify(sysMenuService).saveOrUpdate(any(SysMenu.class));
}
@Test
@DisplayName("新增 button 挂在 menu 下 -> 成功 save")
void save_newButtonUnderMenu_success() {
when(sysMenuService.getById(10L)).thenReturn(menu);
ResourceNode node = newButtonNode(null, 10L, "新增按钮", "crm:user:add", "hide", "/api/user/add", "enabled");
resourceService.save(node);
verify(sysMenuService).saveOrUpdate(any(SysMenu.class));
}
// ==================== 层级约束 ====================
@Test
@DisplayName("catalog 下挂 button -> 拒绝(catalog 只能挂 menu)")
void save_buttonUnderCatalog_rejected() {
when(sysMenuService.getById(1L)).thenReturn(catalog);
ResourceNode node = newButtonNode(null, 1L, "非法按钮", "crm:x:y", "hide", "/api/x", "enabled");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("catalog")
.hasMessageContaining("menu");
}
@Test
@DisplayName("menu 下挂 catalog -> 拒绝(menu 只能挂 button)")
void save_catalogUnderMenu_rejected() {
when(sysMenuService.getById(10L)).thenReturn(menu);
ResourceNode node = newResourceNode(null, 10L, "非法目录", MenuType.CATALOG);
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("menu")
.hasMessageContaining("button");
}
@Test
@DisplayName("menu 下挂 menu -> 拒绝(menu 只能挂 button)")
void save_menuUnderMenu_rejected() {
when(sysMenuService.getById(10L)).thenReturn(menu);
ResourceNode node = newResourceNode(null, 10L, "非法子菜单", MenuType.MENU);
node.setRoute("/sub/menu");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class);
}
@Test
@DisplayName("button 下挂子节点 -> 拒绝(button 是叶子)")
void save_childUnderButton_rejected() {
when(sysMenuService.getById(100L)).thenReturn(button);
// 子 button 补全必填字段,让字段校验通过后再触发层级约束
ResourceNode node = newButtonNode(null, 100L, "非法孙子", "crm:x:y", "hide", "/api/x", "enabled");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("叶子");
}
@Test
@DisplayName("根节点挂 button -> 拒绝(只允许 catalog/menu 作为根)")
void save_buttonAsRoot_rejected() {
ResourceNode node = newButtonNode(null, null, "根按钮", "crm:x:y", "hide", "/api/x", "enabled");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("根节点");
}
@Test
@DisplayName("根节点挂 catalog -> 成功")
void save_catalogAsRoot_success() {
ResourceNode node = newResourceNode(null, null, "根目录", MenuType.CATALOG);
resourceService.save(node);
verify(sysMenuService).saveOrUpdate(any(SysMenu.class));
}
@Test
@DisplayName("根节点挂 menu -> 成功")
void save_menuAsRoot_success() {
ResourceNode node = newResourceNode(null, null, "根菜单", MenuType.MENU);
node.setRoute("/root-menu");
resourceService.save(node);
verify(sysMenuService).saveOrUpdate(any(SysMenu.class));
}
// ==================== 字段必填校验 ====================
@Test
@DisplayName("menu 缺 route -> 拒绝")
void save_menuWithoutRoute_rejected() {
ResourceNode node = newResourceNode(null, null, "无路由菜单", MenuType.MENU);
// route 为 null
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("路由");
}
@Test
@DisplayName("menu route 为空字符串 -> 拒绝")
void save_menuWithBlankRoute_rejected() {
ResourceNode node = newResourceNode(null, null, "空路由菜单", MenuType.MENU);
node.setRoute(" ");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("路由");
}
@Test
@DisplayName("button 缺 perms -> 拒绝")
void save_buttonWithoutPerms_rejected() {
ResourceNode node = newButtonNode(null, null, "无perms", null, "hide", "/api/x", "enabled");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("perms");
}
@Test
@DisplayName("button perms 为空字符串 -> 拒绝")
void save_buttonWithBlankPerms_rejected() {
ResourceNode node = newButtonNode(null, null, "空perms", " ", "hide", "/api/x", "enabled");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("perms");
}
@Test
@DisplayName("button 缺 denyBehavior -> 拒绝")
void save_buttonWithoutDenyBehavior_rejected() {
ResourceNode node = newButtonNode(null, null, "无deny", "crm:x:y", null, "/api/x", "enabled");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("denyBehavior");
}
@Test
@DisplayName("button 缺 apiUrl -> 拒绝")
void save_buttonWithoutApiUrl_rejected() {
ResourceNode node = newButtonNode(null, null, "无api", "crm:x:y", "hide", null, "enabled");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("apiUrl");
}
@Test
@DisplayName("button 缺 status -> 拒绝")
void save_buttonWithoutStatus_rejected() {
ResourceNode node = newButtonNode(null, null, "无status", "crm:x:y", "hide", "/api/x", null);
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("status");
}
// ==================== 编辑 type 锁 ====================
@Test
@DisplayName("编辑时尝试改 type(catalog→menu)-> 拒绝")
void save_editChangeType_rejected() {
when(sysMenuService.getById(1L)).thenReturn(catalog); // type=1 (catalog)
ResourceNode node = newResourceNode(1L, null, "改类型", MenuType.MENU);
node.setRoute("/new-route");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("类型");
verify(sysMenuService, never()).saveOrUpdate(any());
}
@Test
@DisplayName("编辑时 type 不变 -> 成功")
void save_editSameType_success() {
when(sysMenuService.getById(1L)).thenReturn(catalog); // type=1(catalog)
ResourceNode node = newResourceNode(1L, null, "改名字", MenuType.CATALOG);
resourceService.save(node);
ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
verify(sysMenuService).saveOrUpdate(captor.capture());
assertThat(captor.getValue().getMenuName()).isEqualTo("改名字");
}
// ==================== 编辑时 type 保留原有节点 field ====================
@Test
@DisplayName("编辑 menu + path/component 未传入 -> 使用原有值")
void save_editMenuPreserveOldPath() {
when(sysMenuService.getById(10L)).thenReturn(menu); // path="/system/user"
ResourceNode node = newResourceNode(10L, null, "改名", MenuType.MENU);
// 不设 route——预期保留原有 path
resourceService.save(node);
ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
verify(sysMenuService).saveOrUpdate(captor.capture());
SysMenu saved = captor.getValue();
assertThat(saved.getMenuName()).isEqualTo("改名");
// route 未传入:保留原有 path
assertThat(saved.getPath()).isEqualTo("/system/user");
}
// ==================== 编辑后 type 保持不变 ====================
@Test
@DisplayName("编辑 catalog -> catalog 的 perms/apiUrl 等无用字段为 null")
void save_editCatalog_noButtonFields() {
when(sysMenuService.getById(1L)).thenReturn(catalog);
ResourceNode node = newResourceNode(1L, null, "改昵称", MenuType.CATALOG);
node.setPerms("unused-perm"); // 前端可能误传,但 catalog 应该忽略
resourceService.save(node);
ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
verify(sysMenuService).saveOrUpdate(captor.capture());
SysMenu saved = captor.getValue();
assertThat(saved.getMenuType()).isEqualTo(1);
assertThat(saved.getPerms()).isNull(); // catalog 不存 perms
assertThat(saved.getApiUrl()).isNull(); // catalog 不存 apiUrl
}
// ==================== 非空校验 ====================
@Test
@DisplayName("ResourceNode 为 null -> 拒绝")
void save_nullNode_rejected() {
assertThatThrownBy(() -> resourceService.save(null))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("不能为空");
}
@Test
@DisplayName("type 为 null -> 拒绝")
void save_nullType_rejected() {
ResourceNode node = new ResourceNode();
node.setName("无类型");
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("类型");
}
@Test
@DisplayName("name 为空 -> 拒绝")
void save_blankName_rejected() {
ResourceNode node = newResourceNode(null, null, " ", MenuType.CATALOG);
assertThatThrownBy(() -> resourceService.save(node))
.isInstanceOf(BusinessErrorException.class)
.hasMessageContaining("名称");
}
// ==================== 编辑时传入 extra field 覆盖 ====================
@Test
@DisplayName("编辑 menu + 正常更新 route -> 覆盖原有 path")
void save_editMenuUpdateRoute() {
when(sysMenuService.getById(10L)).thenReturn(menu); // path="/system/user"
ResourceNode node = newResourceNode(10L, null, "改名", MenuType.MENU);
node.setRoute("/new/path");
resourceService.save(node);
ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
verify(sysMenuService).saveOrUpdate(captor.capture());
assertThat(captor.getValue().getPath()).isEqualTo("/new/path");
}
// ==================== 辅助方法 ====================
private SysMenu buildMenu(Long id, Long parentId, String name, int menuType) {
SysMenu m = new SysMenu();
m.setId(id);
m.setParentId(parentId);
m.setMenuName(name);
m.setMenuType(menuType);
return m;
}
private ResourceNode newResourceNode(Long id, Long parentId, String name, MenuType type) {
ResourceNode node = new ResourceNode();
node.setId(id);
node.setParentId(parentId);
node.setName(name);
node.setType(type);
node.setSort(0);
return node;
}
private ResourceNode newButtonNode(Long id, Long parentId, String name,
String perms, String denyBehavior,
String apiUrl, String status) {
ResourceNode node = new ResourceNode();
node.setId(id);
node.setParentId(parentId);
node.setName(name);
node.setType(MenuType.BUTTON);
node.setSort(0);
node.setPerms(perms);
node.setDenyBehavior(denyBehavior);
node.setApiUrl(apiUrl);
node.setStatus(status);
return node;
}
}
Loading…
Cancel
Save