diff --git a/crm-auth/src/main/java/com/crm/auth/domain/dto/ResourceNode.java b/crm-auth/src/main/java/com/crm/auth/domain/dto/ResourceNode.java new file mode 100644 index 0000000..d57f847 --- /dev/null +++ b/crm-auth/src/main/java/com/crm/auth/domain/dto/ResourceNode.java @@ -0,0 +1,79 @@ +package com.crm.auth.domain.dto; + +import com.crm.auth.domain.entity.SysMenu; +import com.crm.auth.domain.enums.MenuType; +import com.crm.base.utils.BeanCopyUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 权限资源树节点 DTO(ADR-0011) + *
管理端资源树的输入输出载体;与 SysMenu 通过 BeanCopyUtils 互转
+ */ +@Data +public class ResourceNode implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "节点 ID,新增时不传、编辑时必传") + private Long id; + + @Schema(description = "父节点 ID,根节点传 null/0") + private Long parentId; + + @Schema(description = "节点名称") + private String name; + + @Schema(description = "节点类型", example = "CATALOG") + private MenuType type; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "描述/备注") + private String description; + + @Schema(description = "前端路由路径,menu 必填") + private String route; + + @Schema(description = "权限码(如 crm:user:list),button 必填") + private String perms; + + @Schema(description = "无权限时的前端行为:hide / disable,button 必填") + private String denyBehavior; + + @Schema(description = "接口 URL,button 必填") + private String apiUrl; + + @Schema(description = "权限点状态:enabled 启用 / disabled 停用") + private String status; + + @Schema(description = "图标 URL,catalog/menu 可选") + private String icon; + + /** SysMenu → ResourceNode(id 映射 + menuName→name + path→route) */ + public static ResourceNode fromEntity(SysMenu entity) { + ResourceNode dto = BeanCopyUtils.copy(entity, ResourceNode::new); + if (entity != null) { + dto.setName(entity.getMenuName()); + dto.setType(MenuType.fromCode(entity.getMenuType())); + dto.setRoute(entity.getPath()); + } + return dto; + } + + /** ResourceNode → SysMenu(反向映射 name→menuName + route→path) */ + public SysMenu toEntity() { + SysMenu entity = BeanCopyUtils.copy(this, SysMenu::new); + entity.setMenuName(this.name); + if (this.type != null) { + entity.setMenuType(this.type.getCode()); + } + entity.setPath(this.route); + return entity; + } +} diff --git a/crm-auth/src/main/java/com/crm/auth/domain/entity/SysMenu.java b/crm-auth/src/main/java/com/crm/auth/domain/entity/SysMenu.java index f0eaad6..3d89262 100644 --- a/crm-auth/src/main/java/com/crm/auth/domain/entity/SysMenu.java +++ b/crm-auth/src/main/java/com/crm/auth/domain/entity/SysMenu.java @@ -48,6 +48,28 @@ public class SysMenu extends BaseEntity { @Column(columnDefinition = "tinyint default 1") private Boolean visible; + // ────────────── ADR-0011 按钮/权限点扩展字段 ────────────── + + @Comment("描述/备注,仅 button 类型使用") + @Column(columnDefinition = "varchar(100)") + private String description; + + @Comment("权限码(按钮级权限标识,如 crm:user:list),仅 button 类型使用") + @Column(columnDefinition = "varchar(100)") + private String perms; + + @Comment("无权限时的前端行为:hide 隐藏,disable 禁用") + @Column(columnDefinition = "varchar(16)") + private String denyBehavior; + + @Comment("权限点状态:enabled 启用,disabled 停用(全局断路)") + @Column(columnDefinition = "varchar(16) default 'enabled'") + private String status; + + @Comment("接口 URL(按钮对应的后端 API 路径),仅 button 类型使用") + @Column(columnDefinition = "varchar(200)") + private String apiUrl; + /** 子菜单列表(非数据库字段,仅供 TreeUtils 构建树时填充) */ @TableField(exist = false) private ListDB 存储 tinyint(1/2/3),代码层用枚举;提供 fromCode 工厂方法和 toCode 映射
+ */ +@Getter +@RequiredArgsConstructor +public enum MenuType { + + /** 菜单分组:收纳容器,不承载页面,只能挂 menu 子节点 */ + CATALOG(1, "catalog"), + + /** 菜单页面:可导航的页面,只能挂 button 子节点 */ + MENU(2, "menu"), + + /** 按钮/权限点:叶子节点,不可再挂子节点,承载权限码和接口映射 */ + BUTTON(3, "button"); + + private final int code; + private final String value; + + /** 从 DB tinyint 值反查枚举,找不到返回 null */ + public static MenuType fromCode(Integer code) { + if (code == null) return null; + return Arrays.stream(values()) + .filter(t -> t.code == code) + .findFirst() + .orElse(null); + } + + /** 是否允许作为某父类型的子节点 */ + public static boolean isLegalChild(MenuType parentType, MenuType childType) { + if (childType == null) return false; + switch (parentType) { + case CATALOG: + return childType == MENU; + case MENU: + return childType == BUTTON; + case BUTTON: + return false; // 叶子节点 + default: + return false; + } + } +}