接口调整

This commit is contained in:
2026-03-23 11:43:29 +08:00
parent 5a799d6a0d
commit 95e6f1faea
8 changed files with 236 additions and 7 deletions

View File

@@ -27,6 +27,9 @@ public interface ErrorCodeConstants {
ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1_002_001_005, "父菜单的类型必须是目录或者菜单");
ErrorCode MENU_COMPONENT_NAME_DUPLICATE = new ErrorCode(1_002_001_006, "已经存在该组件名的菜单");
ErrorCode MENU_NOT_ENABLE = new ErrorCode(1_002_001_007, "名字为【{}】的菜单已被禁用");
ErrorCode MENU_ROUTE_KIND_INVALID = new ErrorCode(1_002_001_008, "路由类型不合法");
ErrorCode MENU_ROUTE_PROPS_JSON_INVALID = new ErrorCode(1_002_001_009, "路由 props JSON 不合法");
ErrorCode MENU_ROUTE_IFRAME_URL_REQUIRED = new ErrorCode(1_002_001_010, "iframe 路由必须配置 props.url");
// ========== 角色模块 1-002-002-000 ==========
ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在");

View File

@@ -0,0 +1,38 @@
package com.njcn.rdms.module.system.enums.permission;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 菜单路由类型枚举
*/
@Getter
@AllArgsConstructor
public enum MenuRouteKindEnum {
DIR("dir"), // 目录路由
VIEW("view"), // 普通页面
SINGLE("single"), // 顶级单页
IFRAME("iframe"), // iframe 页面
EXTERNAL("external"), // 外链页面
REDIRECT("redirect"); // 重定向路由
/**
* 路由类型值
*/
private final String kind;
public static MenuRouteKindEnum valueOfKind(String kind) {
if (StrUtil.isBlank(kind)) {
return null;
}
for (MenuRouteKindEnum value : values()) {
if (StrUtil.equalsIgnoreCase(value.getKind(), StrUtil.trim(kind))) {
return value;
}
}
return null;
}
}

View File

@@ -50,6 +50,12 @@ public class MenuRespVO {
@Schema(description = "组件名", example = "SystemUser")
private String componentName;
@Schema(description = "路由类型,例如 dir/view/single/iframe", example = "view")
private String routeKind;
@Schema(description = "路由 props JSON 字符串", example = "{\"url\":\"https://cn.vuejs.org/\"}")
private String routePropsJson;
@Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
private Integer status;

View File

@@ -48,6 +48,13 @@ public class MenuSaveVO {
@Schema(description = "组件名", example = "SystemUser")
private String componentName;
@Schema(description = "路由类型,例如 dir/view/single/iframe", example = "view")
@Size(max = 32, message = "路由类型长度不能超过32个字符")
private String routeKind;
@Schema(description = "路由 props JSON 字符串", example = "{\"url\":\"https://cn.vuejs.org/\"}")
private String routePropsJson;
@Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
private Integer status;

View File

@@ -3,6 +3,8 @@ package com.njcn.rdms.module.system.convert.auth;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
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;
@@ -12,6 +14,7 @@ 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;
import com.njcn.rdms.module.system.enums.permission.MenuRouteKindEnum;
import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@@ -208,7 +211,7 @@ public interface AuthConvert {
Set<String> usedNames = new HashSet<>();
menus.forEach(menu -> {
String fullPath = fullPathCache.get(menu.getId());
String baseName = normalizeRouteName(fullPath);
String baseName = resolveBaseRouteName(menu, fullPath);
if (StrUtil.isBlank(baseName)) {
baseName = "route_" + menu.getId();
}
@@ -222,12 +225,21 @@ public interface AuthConvert {
return routeNameMap;
}
default String resolveBaseRouteName(MenuDO menu, String fullPath) {
if (StrUtil.isNotBlank(menu.getComponentName())) {
return StrUtil.trim(menu.getComponentName());
}
return normalizeRouteName(fullPath);
}
default AuthRouteNodeRespVO buildRouteNode(MenuDO menu, String fullPath, String routeName, boolean hasChildren) {
MenuRouteKindEnum routeKind = resolveRouteKind(menu, hasChildren);
return AuthRouteNodeRespVO.builder()
.id(String.valueOf(menu.getId()))
.name(routeName)
.path(fullPath)
.component(resolveComponentKey(menu, routeName, hasChildren))
.component(resolveComponentKey(menu, routeKind, routeName))
.props(resolveRouteProps(menu.getRoutePropsJson()))
.meta(buildRouteMeta(menu))
.build();
}
@@ -238,17 +250,60 @@ public interface AuthConvert {
.icon(resolveIcon(menu.getIcon()))
.order(menu.getSort())
.keepAlive(menu.getKeepAlive())
.hideInMenu(Boolean.FALSE.equals(menu.getVisible()))
.build();
}
default String resolveComponentKey(MenuDO menu, String routeName, boolean hasChildren) {
default MenuRouteKindEnum resolveRouteKind(MenuDO menu, boolean hasChildren) {
if (hasChildren) {
return "layout.base";
return MenuRouteKindEnum.DIR;
}
MenuRouteKindEnum routeKind = MenuRouteKindEnum.valueOfKind(menu.getRouteKind());
if (routeKind != null) {
return routeKind;
}
if (MenuTypeEnum.DIR.getType().equals(menu.getType())) {
return MenuRouteKindEnum.DIR;
}
if (isExternalPath(menu.getPath())) {
return MenuRouteKindEnum.EXTERNAL;
}
if (ID_ROOT.equals(menu.getParentId())) {
return MenuRouteKindEnum.SINGLE;
}
return MenuRouteKindEnum.VIEW;
}
default String resolveComponentKey(MenuDO menu, MenuRouteKindEnum routeKind, String routeName) {
if (StrUtil.isNotBlank(menu.getComponent())) {
return StrUtil.trim(menu.getComponent());
}
if (MenuRouteKindEnum.DIR.equals(routeKind)) {
return "layout.base";
}
if (MenuRouteKindEnum.IFRAME.equals(routeKind)) {
return "view.iframe-page";
}
if (MenuRouteKindEnum.SINGLE.equals(routeKind)) {
return "layout.base$view." + routeName;
}
return "view." + routeName;
if (MenuRouteKindEnum.VIEW.equals(routeKind)) {
return "view." + routeName;
}
return null;
}
default Object resolveRouteProps(String routePropsJson) {
if (StrUtil.isBlank(routePropsJson)) {
return null;
}
Object routeProps = JsonUtils.parseObjectQuietly(routePropsJson, new TypeReference<Object>() {
});
if (routeProps != null) {
return routeProps;
}
LoggerFactory.getLogger(getClass()).warn("[resolveRouteProps][routePropsJson({}) 解析失败]", routePropsJson);
return null;
}
default String resolveIcon(String icon) {
@@ -278,10 +333,15 @@ public interface AuthConvert {
if (StrUtil.isBlank(fullPath)) {
return "";
}
return fullPath.replaceAll("^/+", "")
return fullPath
.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2")
.replaceAll("([a-z\\d])([A-Z])", "$1_$2")
.replaceAll("^/+", "")
.replaceAll("/+$", "")
.replaceAll("[/\\-.]+", "_")
.replaceAll("[/\\\\.\\-\\s]+", "_")
.replaceAll("_+", "_")
.replaceAll("^_+", "")
.replaceAll("_+$", "")
.toLowerCase();
}

View File

@@ -77,6 +77,14 @@ public class MenuDO extends BaseDO {
* 组件名
*/
private String componentName;
/**
* 路由类型
*/
private String routeKind;
/**
* 路由 props JSON
*/
private String routePropsJson;
/**
* 状态
*

View File

@@ -3,13 +3,16 @@ package com.njcn.rdms.module.system.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO;
import com.njcn.rdms.module.system.dal.mysql.permission.MenuMapper;
import com.njcn.rdms.module.system.dal.redis.RedisKeyConstants;
import com.njcn.rdms.module.system.enums.permission.MenuRouteKindEnum;
import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum;
import com.google.common.annotations.VisibleForTesting;
@@ -53,6 +56,7 @@ public class MenuServiceImpl implements MenuService {
// 校验菜单(自己)
validateMenuName(createReqVO.getParentId(), createReqVO.getName(), null);
validateMenuComponentName(createReqVO.getComponentName(), null);
validateMenuRoute(createReqVO);
// 插入数据库
MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class);
@@ -75,6 +79,7 @@ public class MenuServiceImpl implements MenuService {
// 校验菜单(自己)
validateMenuName(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId());
validateMenuComponentName(updateReqVO.getComponentName(), updateReqVO.getId());
validateMenuRoute(updateReqVO);
// 更新到数据库
MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class);
@@ -296,6 +301,43 @@ public class MenuServiceImpl implements MenuService {
}
}
@VisibleForTesting
void validateMenuRoute(MenuSaveVO reqVO) {
if (reqVO == null || MenuTypeEnum.BUTTON.getType().equals(reqVO.getType())) {
return;
}
MenuRouteKindEnum routeKind = MenuRouteKindEnum.valueOfKind(reqVO.getRouteKind());
if (StrUtil.isNotBlank(reqVO.getRouteKind()) && routeKind == null) {
throw exception(MENU_ROUTE_KIND_INVALID);
}
String routePropsJson = StrUtil.trim(reqVO.getRoutePropsJson());
if (StrUtil.isBlank(routePropsJson)) {
if (MenuRouteKindEnum.IFRAME.equals(routeKind)) {
throw exception(MENU_ROUTE_IFRAME_URL_REQUIRED);
}
return;
}
Object routeProps = JsonUtils.parseObjectQuietly(routePropsJson, new TypeReference<Object>() {
});
if (routeProps == null) {
throw exception(MENU_ROUTE_PROPS_JSON_INVALID);
}
if (!MenuRouteKindEnum.IFRAME.equals(routeKind)) {
return;
}
if (!(routeProps instanceof Map<?, ?> routePropsMap)) {
throw exception(MENU_ROUTE_PROPS_JSON_INVALID);
}
Object url = routePropsMap.get("url");
if (!(url instanceof String urlString) || StrUtil.isBlank(urlString)) {
throw exception(MENU_ROUTE_IFRAME_URL_REQUIRED);
}
}
/**
* 初始化菜单的通用属性。
* <p>
@@ -304,13 +346,22 @@ public class MenuServiceImpl implements MenuService {
* @param menu 菜单
*/
private void initMenuProperty(MenuDO menu) {
menu.setRouteKind(normalizeRouteKind(menu.getRouteKind()));
menu.setRoutePropsJson(StrUtil.blankToDefault(StrUtil.trim(menu.getRoutePropsJson()), null));
// 菜单为按钮类型时,无需 component、icon、path 属性,进行置空
if (MenuTypeEnum.BUTTON.getType().equals(menu.getType())) {
menu.setComponent("");
menu.setComponentName("");
menu.setIcon("");
menu.setPath("");
menu.setRouteKind(null);
menu.setRoutePropsJson(null);
}
}
private String normalizeRouteKind(String routeKind) {
MenuRouteKindEnum routeKindEnum = MenuRouteKindEnum.valueOfKind(routeKind);
return routeKindEnum != null ? routeKindEnum.getKind() : null;
}
}

View File

@@ -646,4 +646,60 @@ CREATE TABLE `system_users` (
-- ----------------------------
INSERT INTO `system_users` VALUES (1, 'admin', '$2a$04$KljJDa/LK7QfDm0lF5OhuePhlPfjRH3tB2Wu351Uidz.oQGJXevPi', '灿能源码', '管理员', 103, 2, NULL, '[1,2]', '11aoteman@126.com', '18818260272', 2, 'http://test.rdms.iocoder.cn/20250921/avatar_1758423875594.png', 0, '192.168.2.125', '2026-03-18 16:18:02', 'admin', '2021-01-05 17:03:47', 'system', '2026-03-19 13:59:04', b'0');
-- ----------------------------
-- system_menu 路由扩展字段
-- ----------------------------
ALTER TABLE `system_menu`
ADD COLUMN `route_kind` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '路由类型dir/view/single/iframe/external/redirect' AFTER `component_name`,
ADD COLUMN `route_props_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '路由 props JSON' AFTER `route_kind`;
UPDATE `system_menu`
SET `component` = 'view.iframe-page',
`component_name` = 'document_antd',
`route_kind` = 'iframe',
`route_props_json` = '{\"url\":\"https://www.antdv.com/\"}'
WHERE `id` = 900010;
UPDATE `system_menu`
SET `component` = 'view.iframe-page',
`component_name` = 'document_naive',
`route_kind` = 'iframe',
`route_props_json` = '{\"url\":\"https://www.naiveui.com/\"}'
WHERE `id` = 900011;
UPDATE `system_menu`
SET `component` = 'view.iframe-page',
`component_name` = 'document_element_plus',
`route_kind` = 'iframe',
`route_props_json` = '{\"url\":\"https://cn.element-plus.org/\"}'
WHERE `id` = 900012;
UPDATE `system_menu`
SET `component` = 'view.iframe-page',
`component_name` = 'document_alova',
`route_kind` = 'iframe',
`route_props_json` = '{\"url\":\"https://alova.js.org/\"}'
WHERE `id` = 900013;
UPDATE `system_menu`
SET `component` = 'view.iframe-page',
`component_name` = 'document_unocss',
`route_kind` = 'iframe',
`route_props_json` = '{\"url\":\"https://unocss.dev/\"}'
WHERE `id` = 900014;
UPDATE `system_menu`
SET `component` = 'view.iframe-page',
`component_name` = 'document_vite',
`route_kind` = 'iframe',
`route_props_json` = '{\"url\":\"https://vite.dev/\"}'
WHERE `id` = 900015;
UPDATE `system_menu`
SET `component` = 'view.iframe-page',
`component_name` = 'document_vue',
`route_kind` = 'iframe',
`route_props_json` = '{\"url\":\"https://cn.vuejs.org/\"}'
WHERE `id` = 900016;
SET FOREIGN_KEY_CHECKS = 1;