接口调整

This commit is contained in:
2026-03-22 19:11:28 +08:00
parent 569fa57838
commit 5a799d6a0d
7 changed files with 495 additions and 17 deletions

View File

@@ -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<Void> 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);
}
/**
* 处理系统异常,兜底处理所有的一切
*/

View File

@@ -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<AuthPermissionInfoRespVO> getPermissionInfo() {
@GetMapping("/get-user-info")
@Operation(summary = "获取登录用户信息")
public CommonResult<AuthUserInfoRespVO> getUserInfo() {
// 1.1 获得用户信息
AdminUserDO user = userService.getUser(getLoginUserId());
if (user == null) {
return success(null);
}
// 1.2 获得角色列表
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
if (CollUtil.isEmpty(roleIds)) {
// 1.2 获得角色和按钮权限
List<RoleDO> roles = getCurrentUserRoles();
List<MenuDO> menuList = getCurrentUserMenus(roles);
return success(AuthConvert.INSTANCE.convertUserInfo(user, roles, menuList));
}
@GetMapping("/get-user-routes")
@Operation(summary = "获取登录用户路由信息")
public CommonResult<AuthUserRouteRespVO> getUserRoutes() {
AdminUserDO user = userService.getUser(getLoginUserId());
if (user == null) {
return success(null);
}
List<RoleDO> roles = getCurrentUserRoles();
List<MenuDO> menuList = getCurrentUserMenus(roles);
return success(AuthConvert.INSTANCE.convertUserRoutes(menuList));
}
@GetMapping("/get-permission-info")
@Operation(summary = "获取登录用户的权限信息")
public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo() {
AdminUserDO user = userService.getUser(getLoginUserId());
if (user == null) {
return success(null);
}
List<RoleDO> roles = getCurrentUserRoles();
if (CollUtil.isEmpty(roles)) {
return success(AuthConvert.INSTANCE.convert(user, Collections.emptyList(), Collections.emptyList()));
}
List<RoleDO> roles = roleService.getRoleList(roleIds);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
// 1.3 获得菜单列表
Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
List<MenuDO> menuList = menuService.getMenuList(menuIds);
menuList = menuService.filterDisableMenus(menuList);
// 2. 拼接结果返回
List<MenuDO> 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<RoleDO> getCurrentUserRoles() {
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
if (CollUtil.isEmpty(roleIds)) {
return Collections.emptyList();
}
List<RoleDO> roles = roleService.getRoleList(roleIds);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus()));
return roles;
}
private List<MenuDO> getCurrentUserMenus(List<RoleDO> roles) {
if (CollUtil.isEmpty(roles)) {
return Collections.emptyList();
}
Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
List<MenuDO> menuList = menuService.getMenuList(menuIds);
return menuService.filterDisableMenus(menuList);
}
}

View File

@@ -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;
}

View File

@@ -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<AuthRouteNodeRespVO> children;
}

View File

@@ -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<String> roles;
@Schema(description = "按钮权限码列表", requiredMode = Schema.RequiredMode.REQUIRED,
example = "[\"system:user:add\", \"system:user:update\"]")
private List<String> buttons;
}

View File

@@ -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<AuthRouteNodeRespVO> routes;
@Schema(description = "默认首页路由名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_user")
private String home;
}

View File

@@ -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<RoleDO> roleList, List<MenuDO> 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<MenuDO> menuList) {
if (CollUtil.isEmpty(menuList)) {
return AuthUserRouteRespVO.builder()
.routes(Collections.emptyList())
.home("")
.build();
}
List<MenuDO> routeMenus = filterList(menuList, this::canExposeAsRoute);
if (CollUtil.isEmpty(routeMenus)) {
return AuthUserRouteRespVO.builder()
.routes(Collections.emptyList())
.home("")
.build();
}
Map<Long, MenuDO> menuMap = new LinkedHashMap<>();
routeMenus.forEach(menu -> menuMap.put(menu.getId(), menu));
Map<Long, String> fullPathCache = new HashMap<>();
List<MenuDO> 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<Long> validMenuIds = convertSet(validMenus, MenuDO::getId);
Set<Long> parentIds = new HashSet<>();
validMenus.forEach(menu -> {
if (validMenuIds.contains(menu.getParentId())) {
parentIds.add(menu.getParentId());
}
});
Map<Long, String> routeNameMap = buildRouteNameMap(validMenus, fullPathCache);
Map<Long, AuthRouteNodeRespVO> 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<AuthRouteNodeRespVO> 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<AuthRouteNodeRespVO> 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<String> sortDistinctStrings(List<String> 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<Long, MenuDO> menuMap, Map<Long, String> 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<Long, String> buildRouteNameMap(List<MenuDO> menus, Map<Long, String> fullPathCache) {
Map<Long, String> routeNameMap = new LinkedHashMap<>();
Set<String> 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<AuthRouteNodeRespVO> sortRouteNodes(List<AuthRouteNodeRespVO> routes) {
if (CollUtil.isEmpty(routes)) {
return Collections.emptyList();
}
List<AuthRouteNodeRespVO> 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<AuthRouteNodeRespVO> children = sortRouteNodes(route.getChildren());
route.setChildren(children);
route.setRedirect(children.get(0).getPath());
});
return sortedRoutes;
}
default String resolveHome(List<AuthRouteNodeRespVO> 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;
}
}