diff --git a/rdms-gateway/src/main/java/com/njcn/rdms/gateway/handler/GlobalExceptionHandler.java b/rdms-gateway/src/main/java/com/njcn/rdms/gateway/handler/GlobalExceptionHandler.java index 803a635..4d3053e 100644 --- a/rdms-gateway/src/main/java/com/njcn/rdms/gateway/handler/GlobalExceptionHandler.java +++ b/rdms-gateway/src/main/java/com/njcn/rdms/gateway/handler/GlobalExceptionHandler.java @@ -8,12 +8,14 @@ import org.springframework.core.annotation.Order; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; +import org.springframework.web.reactive.resource.NoResourceFoundException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import static com.njcn.rdms.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static com.njcn.rdms.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_FOUND; /** * Gateway 的全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 @@ -27,6 +29,8 @@ import static com.njcn.rdms.framework.common.exception.enums.GlobalErrorCodeCons @Slf4j public class GlobalExceptionHandler implements ErrorWebExceptionHandler { + private static final String CHROME_DEVTOOLS_RESOURCE_PATH = "/.well-known/appspecific/com.chrome.devtools.json"; + @Override public Mono handle(ServerWebExchange exchange, Throwable ex) { // 已经 commit,则直接返回异常 @@ -37,7 +41,9 @@ public class GlobalExceptionHandler implements ErrorWebExceptionHandler { // 转换成 CommonResult CommonResult result; - if (ex instanceof ResponseStatusException) { + if (ex instanceof NoResourceFoundException) { + result = noResourceFoundExceptionHandler(exchange, (NoResourceFoundException) ex); + } else if (ex instanceof ResponseStatusException) { result = responseStatusExceptionHandler(exchange, (ResponseStatusException) ex); } else { result = defaultExceptionHandler(exchange, ex); @@ -58,6 +64,24 @@ public class GlobalExceptionHandler implements ErrorWebExceptionHandler { return CommonResult.error(ex.getStatusCode().value(), ex.getReason()); } + /** + * 处理 WebFlux 静态资源不存在异常 + */ + private CommonResult noResourceFoundExceptionHandler(ServerWebExchange exchange, + NoResourceFoundException ex) { + ServerHttpRequest request = exchange.getRequest(); + String path = request.getPath().value(); + log.debug("[noResourceFoundExceptionHandler][uri({}/{}) 请求地址不存在]", request.getURI(), request.getMethod()); + return CommonResult.error(NOT_FOUND.getCode(), buildNoResourceMessage(path)); + } + + private String buildNoResourceMessage(String path) { + if (CHROME_DEVTOOLS_RESOURCE_PATH.equals(path)) { + return "当前服务未提供浏览器调试探测资源"; + } + return String.format("请求地址不存在:%s", path); + } + /** * 处理系统异常,兜底处理所有的一切 */ diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/AuthController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/AuthController.java index b807708..29d4ad6 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/AuthController.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/AuthController.java @@ -6,7 +6,12 @@ import com.njcn.rdms.framework.common.enums.CommonStatusEnum; import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.security.config.SecurityProperties; import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; -import com.njcn.rdms.module.system.controller.admin.auth.vo.*; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthLoginReqVO; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthLoginRespVO; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthRegisterReqVO; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthUserRouteRespVO; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthUserInfoRespVO; import com.njcn.rdms.module.system.convert.auth.AuthConvert; import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO; import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO; @@ -26,7 +31,12 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.util.Collections; import java.util.List; @@ -83,29 +93,48 @@ public class AuthController { return success(authService.refreshToken(refreshToken)); } - @GetMapping("/get-permission-info") - @Operation(summary = "获取登录用户的权限信息") - public CommonResult getPermissionInfo() { + @GetMapping("/get-user-info") + @Operation(summary = "获取登录用户信息") + public CommonResult getUserInfo() { // 1.1 获得用户信息 AdminUserDO user = userService.getUser(getLoginUserId()); if (user == null) { return success(null); } - // 1.2 获得角色列表 - Set roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId()); - if (CollUtil.isEmpty(roleIds)) { + // 1.2 获得角色和按钮权限 + List roles = getCurrentUserRoles(); + List menuList = getCurrentUserMenus(roles); + return success(AuthConvert.INSTANCE.convertUserInfo(user, roles, menuList)); + } + + @GetMapping("/get-user-routes") + @Operation(summary = "获取登录用户路由信息") + public CommonResult getUserRoutes() { + AdminUserDO user = userService.getUser(getLoginUserId()); + if (user == null) { + return success(null); + } + + List roles = getCurrentUserRoles(); + List menuList = getCurrentUserMenus(roles); + return success(AuthConvert.INSTANCE.convertUserRoutes(menuList)); + } + + @GetMapping("/get-permission-info") + @Operation(summary = "获取登录用户的权限信息") + public CommonResult getPermissionInfo() { + AdminUserDO user = userService.getUser(getLoginUserId()); + if (user == null) { + return success(null); + } + + List roles = getCurrentUserRoles(); + if (CollUtil.isEmpty(roles)) { return success(AuthConvert.INSTANCE.convert(user, Collections.emptyList(), Collections.emptyList())); } - List roles = roleService.getRoleList(roleIds); - roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色 - // 1.3 获得菜单列表 - Set menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId)); - List menuList = menuService.getMenuList(menuIds); - menuList = menuService.filterDisableMenus(menuList); - - // 2. 拼接结果返回 + List menuList = getCurrentUserMenus(roles); return success(AuthConvert.INSTANCE.convert(user, roles, menuList)); } @@ -116,4 +145,25 @@ public class AuthController { return success(authService.register(registerReqVO)); } + private List getCurrentUserRoles() { + Set roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId()); + if (CollUtil.isEmpty(roleIds)) { + return Collections.emptyList(); + } + + List roles = roleService.getRoleList(roleIds); + roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); + return roles; + } + + private List getCurrentUserMenus(List roles) { + if (CollUtil.isEmpty(roles)) { + return Collections.emptyList(); + } + + Set menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId)); + List menuList = menuService.getMenuList(menuIds); + return menuService.filterDisableMenus(menuList); + } + } diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthRouteMetaRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthRouteMetaRespVO.java new file mode 100644 index 0000000..6c07f30 --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthRouteMetaRespVO.java @@ -0,0 +1,46 @@ +package com.njcn.rdms.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 用户路由 Meta Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthRouteMetaRespVO { + + @Schema(description = "菜单或页面标题", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; + + @Schema(description = "国际化 key") + private String i18nKey; + + @Schema(description = "图标名") + private String icon; + + @Schema(description = "本地图标名") + private String localIcon; + + @Schema(description = "排序值") + private Integer order; + + @Schema(description = "是否缓存") + private Boolean keepAlive; + + @Schema(description = "是否在菜单中隐藏") + private Boolean hideInMenu; + + @Schema(description = "当前页面高亮的菜单路由名") + private String activeMenu; + + @Schema(description = "是否支持多标签页") + private Boolean multiTab; + + @Schema(description = "标签页固定位置") + private Integer fixedIndexInTab; + +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthRouteNodeRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthRouteNodeRespVO.java new file mode 100644 index 0000000..a16229b --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthRouteNodeRespVO.java @@ -0,0 +1,42 @@ +package com.njcn.rdms.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 用户路由节点 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthRouteNodeRespVO { + + @Schema(description = "路由节点 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private String id; + + @Schema(description = "路由名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_user") + private String name; + + @Schema(description = "完整路由路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "/system/user") + private String path; + + @Schema(description = "前端组件白名单 key", example = "view.system_user") + private String component; + + @Schema(description = "重定向路径") + private String redirect; + + @Schema(description = "路由 props") + private Object props; + + @Schema(description = "路由 meta", requiredMode = Schema.RequiredMode.REQUIRED) + private AuthRouteMetaRespVO meta; + + @Schema(description = "子路由列表") + private List children; + +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthUserInfoRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthUserInfoRespVO.java new file mode 100644 index 0000000..3d5d784 --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthUserInfoRespVO.java @@ -0,0 +1,31 @@ +package com.njcn.rdms.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 登录用户信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthUserInfoRespVO { + + @Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String userId; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin") + private String userName; + + @Schema(description = "角色编码列表", example = "[\"SUPER_ADMIN\"]") + private List roles; + + @Schema(description = "按钮权限码列表", requiredMode = Schema.RequiredMode.REQUIRED, + example = "[\"system:user:add\", \"system:user:update\"]") + private List buttons; + +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthUserRouteRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthUserRouteRespVO.java new file mode 100644 index 0000000..6bcc4bc --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/vo/AuthUserRouteRespVO.java @@ -0,0 +1,24 @@ +package com.njcn.rdms.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 用户路由 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthUserRouteRespVO { + + @Schema(description = "用户可访问路由树", requiredMode = Schema.RequiredMode.REQUIRED) + private List routes; + + @Schema(description = "默认首页路由名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_user") + private String home; + +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/convert/auth/AuthConvert.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/convert/auth/AuthConvert.java index 72c2cc9..c10cad7 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/convert/auth/AuthConvert.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/convert/auth/AuthConvert.java @@ -4,7 +4,11 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthRouteMetaRespVO; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthRouteNodeRespVO; import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthUserInfoRespVO; +import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthUserRouteRespVO; import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO; import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO; import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO; @@ -16,10 +20,15 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertList; import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertSet; import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.filterList; import static com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; @@ -39,6 +48,86 @@ public interface AuthConvert { .build(); } + default AuthUserInfoRespVO convertUserInfo(AdminUserDO user, List roleList, List menuList) { + return AuthUserInfoRespVO.builder() + .userId(String.valueOf(user.getId())) + .userName(user.getUsername()) + .roles(sortDistinctStrings(convertList(roleList, RoleDO::getCode))) + .buttons(sortDistinctStrings(convertList(menuList, MenuDO::getPermission, + menu -> StrUtil.isNotBlank(menu.getPermission())))) + .build(); + } + + default AuthUserRouteRespVO convertUserRoutes(List menuList) { + if (CollUtil.isEmpty(menuList)) { + return AuthUserRouteRespVO.builder() + .routes(Collections.emptyList()) + .home("") + .build(); + } + + List routeMenus = filterList(menuList, this::canExposeAsRoute); + if (CollUtil.isEmpty(routeMenus)) { + return AuthUserRouteRespVO.builder() + .routes(Collections.emptyList()) + .home("") + .build(); + } + + Map menuMap = new LinkedHashMap<>(); + routeMenus.forEach(menu -> menuMap.put(menu.getId(), menu)); + + Map fullPathCache = new HashMap<>(); + List validMenus = filterList(routeMenus, menu -> { + String fullPath = resolveFullPath(menu, menuMap, fullPathCache); + return StrUtil.isNotBlank(fullPath) && !isExternalPath(fullPath); + }); + if (CollUtil.isEmpty(validMenus)) { + return AuthUserRouteRespVO.builder() + .routes(Collections.emptyList()) + .home("") + .build(); + } + + Set validMenuIds = convertSet(validMenus, MenuDO::getId); + Set parentIds = new HashSet<>(); + validMenus.forEach(menu -> { + if (validMenuIds.contains(menu.getParentId())) { + parentIds.add(menu.getParentId()); + } + }); + + Map routeNameMap = buildRouteNameMap(validMenus, fullPathCache); + Map nodeMap = new LinkedHashMap<>(); + validMenus.forEach(menu -> nodeMap.put(menu.getId(), buildRouteNode(menu, + fullPathCache.get(menu.getId()), routeNameMap.get(menu.getId()), parentIds.contains(menu.getId())))); + + List roots = new ArrayList<>(); + validMenus.forEach(menu -> { + AuthRouteNodeRespVO node = nodeMap.get(menu.getId()); + if (!validMenuIds.contains(menu.getParentId()) || ID_ROOT.equals(menu.getParentId())) { + roots.add(node); + return; + } + + AuthRouteNodeRespVO parentNode = nodeMap.get(menu.getParentId()); + if (parentNode == null) { + roots.add(node); + return; + } + if (parentNode.getChildren() == null) { + parentNode.setChildren(new ArrayList<>()); + } + parentNode.getChildren().add(node); + }); + + List routes = sortRouteNodes(roots); + return AuthUserRouteRespVO.builder() + .routes(routes) + .home(StrUtil.blankToDefault(resolveHome(routes), "")) + .build(); + } + /** * 将菜单列表,构建成菜单树。 */ @@ -69,4 +158,176 @@ public interface AuthConvert { return filterList(treeNodeMap.values(), node -> ID_ROOT.equals(node.getParentId())); } + default List sortDistinctStrings(List values) { + if (CollUtil.isEmpty(values)) { + return Collections.emptyList(); + } + return values.stream() + .filter(StrUtil::isNotBlank) + .distinct() + .sorted() + .collect(Collectors.toList()); + } + + default boolean canExposeAsRoute(MenuDO menu) { + return menu != null + && !MenuTypeEnum.BUTTON.getType().equals(menu.getType()) + && (MenuTypeEnum.DIR.getType().equals(menu.getType()) || !Boolean.FALSE.equals(menu.getVisible())); + } + + default String resolveFullPath(MenuDO menu, Map menuMap, Map cache) { + String cachedPath = cache.get(menu.getId()); + if (cachedPath != null) { + return cachedPath; + } + + String rawPath = StrUtil.trimToEmpty(menu.getPath()); + String fullPath; + if (StrUtil.isBlank(rawPath)) { + fullPath = ""; + } else if (isExternalPath(rawPath)) { + fullPath = rawPath; + } else if (StrUtil.startWith(rawPath, "/")) { + fullPath = normalizePath(rawPath); + } else { + MenuDO parent = menuMap.get(menu.getParentId()); + if (parent == null || ID_ROOT.equals(menu.getParentId())) { + fullPath = normalizePath("/" + rawPath); + } else { + String parentPath = resolveFullPath(parent, menuMap, cache); + fullPath = normalizePath(parentPath + "/" + rawPath); + } + } + + cache.put(menu.getId(), fullPath); + return fullPath; + } + + default Map buildRouteNameMap(List menus, Map fullPathCache) { + Map routeNameMap = new LinkedHashMap<>(); + Set usedNames = new HashSet<>(); + menus.forEach(menu -> { + String fullPath = fullPathCache.get(menu.getId()); + String baseName = normalizeRouteName(fullPath); + if (StrUtil.isBlank(baseName)) { + baseName = "route_" + menu.getId(); + } + String routeName = baseName; + if (usedNames.contains(routeName)) { + routeName = baseName + "_" + menu.getId(); + } + usedNames.add(routeName); + routeNameMap.put(menu.getId(), routeName); + }); + return routeNameMap; + } + + default AuthRouteNodeRespVO buildRouteNode(MenuDO menu, String fullPath, String routeName, boolean hasChildren) { + return AuthRouteNodeRespVO.builder() + .id(String.valueOf(menu.getId())) + .name(routeName) + .path(fullPath) + .component(resolveComponentKey(menu, routeName, hasChildren)) + .meta(buildRouteMeta(menu)) + .build(); + } + + default AuthRouteMetaRespVO buildRouteMeta(MenuDO menu) { + return AuthRouteMetaRespVO.builder() + .title(menu.getName()) + .icon(resolveIcon(menu.getIcon())) + .order(menu.getSort()) + .keepAlive(menu.getKeepAlive()) + .build(); + } + + default String resolveComponentKey(MenuDO menu, String routeName, boolean hasChildren) { + if (hasChildren) { + return "layout.base"; + } + if (ID_ROOT.equals(menu.getParentId())) { + return "layout.base$view." + routeName; + } + return "view." + routeName; + } + + default String resolveIcon(String icon) { + if (StrUtil.isBlank(icon) || "#".equals(icon)) { + return null; + } + return icon; + } + + default String normalizePath(String path) { + String normalized = path.replace('\\', '/'); + normalized = normalized.replaceAll("/{2,}", "/"); + if (!normalized.startsWith("/")) { + normalized = "/" + normalized; + } + if (normalized.length() > 1 && normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized; + } + + default boolean isExternalPath(String path) { + return StrUtil.startWithAnyIgnoreCase(path, "http://", "https://"); + } + + default String normalizeRouteName(String fullPath) { + if (StrUtil.isBlank(fullPath)) { + return ""; + } + return fullPath.replaceAll("^/+", "") + .replaceAll("/+$", "") + .replaceAll("[/\\-.]+", "_") + .replaceAll("_+", "_") + .toLowerCase(); + } + + default List sortRouteNodes(List routes) { + if (CollUtil.isEmpty(routes)) { + return Collections.emptyList(); + } + + List sortedRoutes = new ArrayList<>(routes); + sortedRoutes.sort(Comparator + .comparing((AuthRouteNodeRespVO route) -> route.getMeta() != null && route.getMeta().getOrder() != null + ? route.getMeta().getOrder() : Integer.MAX_VALUE) + .thenComparing(AuthRouteNodeRespVO::getPath, Comparator.nullsLast(String::compareTo)) + .thenComparing(AuthRouteNodeRespVO::getId, Comparator.nullsLast(String::compareTo))); + + sortedRoutes.forEach(route -> { + if (CollUtil.isEmpty(route.getChildren())) { + route.setChildren(null); + return; + } + + List children = sortRouteNodes(route.getChildren()); + route.setChildren(children); + route.setRedirect(children.get(0).getPath()); + }); + return sortedRoutes; + } + + default String resolveHome(List routes) { + if (CollUtil.isEmpty(routes)) { + return null; + } + + for (AuthRouteNodeRespVO route : routes) { + if (CollUtil.isNotEmpty(route.getChildren())) { + String childHome = resolveHome(route.getChildren()); + if (StrUtil.isNotBlank(childHome)) { + return childHome; + } + } + + if (CollUtil.isEmpty(route.getChildren())) { + return route.getName(); + } + } + return null; + } + }