mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-08 06:05:33 +08:00
feat(iam): 菜单子页面分组与类型不可变校验
支持 subpage_group 父子约束与统一处理;更新菜单时禁止修改 menuType,并补充错误与枚举 i18n。
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
Target Server Version : 160014 (160014)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 11/07/2026 10:27:03
|
||||
Date: 11/07/2026 11:04:59
|
||||
*/
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ COMMENT ON COLUMN "public"."iam_perm_menu"."creator" IS '创建者ID';
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."last_modifier" IS '最后修改ID';
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."version" IS '版本号';
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."deleted" IS '删除标志';
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."menu_type" IS '菜单类型: catalog-目录, menu-菜单, embedded-内嵌, link-外链';
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."menu_type" IS '菜单类型: catalog-目录, menu-菜单, subpage-子页面, subpage_group-子页面分组, embedded-内嵌, link-外链';
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."active_icon" IS '激活状态图标';
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."badge" IS '徽章显示文本';
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."badge_type" IS '徽章类型: dot-圆点, normal-文本';
|
||||
@@ -160,6 +160,7 @@ INSERT INTO "public"."iam_perm_menu" VALUES (803, 8, 'develop:auth', 'admin', 'C
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (310, 308, 'system:notify:wechat-config', 'admin', 'WechatNotify', '微信消息通知', 'Wechat Message Notification', 'menu.system.notify.wechatConfig', 'lucide:message-circle', 'f', 'f', '/system/notify/wechat/index', '/system/notify/wechat', NULL, 30, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-09 00:00:00+00', '2026-07-09 00:00:00+00');
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (40504, 304, 'system:config:mobile_app', 'admin', 'MobileAppConfig', '移动端应用管理', 'Mobile App Management', 'menu.system.config.mobileApp', 'lucide:smartphone', 'f', 'f', '/system/config/mobileApp/MobileAppConfig', '/system/config/mobile-app', NULL, 6, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 00:00:00+00', '2026-07-10 08:54:32.213917+00');
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (40505, 40504, 'system:config:mobile_app_detail', 'admin', 'MobileAppDetail', '移动端应用详情', 'Mobile App Detail', 'menu.system.config.mobileAppDetail', NULL, 't', 'f', '/system/config/mobileApp/detail/MobileAppDetail', '/system/config/mobile-app/detail/:appType', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 00:00:00+00', '2026-07-10 08:54:32.222476+00');
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (4040121, 40401, 'merchant:gateway-aggregate', 'admin', 'AggregateScanConfig', '聚合扫码', 'Aggregate QR Pay', 'menu.payment.merchant.aggregateScan', NULL, 't', 'f', '/payment/merchant/aggregate/AggregateScanConfig', '/payment/merchant/aggregate', NULL, 3, 'f', 't', 'f', 1, 1, 1, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-11 03:00:13.374496+00', '2026-07-11 03:00:13.374496+00');
|
||||
|
||||
-- ----------------------------
|
||||
-- Primary Key structure for table iam_perm_menu
|
||||
|
||||
@@ -57,13 +57,82 @@ WHERE "id" = 9 AND "menu_code" = 'device';
|
||||
-- ----------------------------
|
||||
-- 聚合扫码配置菜单(应用级子页面)
|
||||
-- 对接后端 GatewayAggregateConfigAdminController, 三级配置深度(AUTO/METHOD/DIRECT)
|
||||
-- 注意: id 不能用 4040120, 该 ID 已被"微信域名验证"(merchant:wx_verify)占用
|
||||
-- ----------------------------
|
||||
INSERT INTO "public"."iam_perm_menu"
|
||||
SELECT 4040120, 40401, 'merchant:gateway-aggregate', 'admin', 'AggregateScanConfig', '聚合扫码', 'Aggregate QR Pay',
|
||||
'menu.payment.merchant.aggregateScan', NULL, 't', 'f',
|
||||
'/payment/merchant/aggregate/AggregateScanConfig', '/payment/merchant/aggregate', NULL, 3, 'f', 't', 'f', 1, 1, 1, 'f', 'subpage',
|
||||
NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM "public"."iam_perm_menu" WHERE "id" = 4040120 OR "menu_code" = 'merchant:gateway-aggregate'
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (
|
||||
4040121, 40401, 'merchant:gateway-aggregate', 'admin', 'AggregateScanConfig',
|
||||
'聚合扫码', 'Aggregate QR Pay', 'menu.payment.merchant.aggregateScan',
|
||||
NULL, 't', 'f',
|
||||
'/payment/merchant/aggregate/AggregateScanConfig', '/payment/merchant/aggregate',
|
||||
NULL, 3, 'f', 't', 'f', 1, 1, 1, 'f', 'subpage',
|
||||
NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ----------------------------
|
||||
-- 2026-07-11 菜单子页面分组(subpage_group)升级
|
||||
-- 新增 subpage_group 类型用于组织 menu 下大量平铺的 subpage, 不在侧边栏显示
|
||||
-- 详见方案: catalog > menu > subpage_group(隐藏) > subpage, 面包屑显示完整四级
|
||||
-- ----------------------------
|
||||
|
||||
-- 更新 menu_type 字段注释
|
||||
COMMENT ON COLUMN "public"."iam_perm_menu"."menu_type" IS '菜单类型: catalog-目录, menu-菜单, subpage-子页面, subpage_group-子页面分组, embedded-内嵌, link-外链';
|
||||
|
||||
-- 新建 4 个子页面分组节点(纯容器: 无 path/component/menu_code, 强制隐藏)
|
||||
-- 40401 商户列表下 3 个分组
|
||||
INSERT INTO "public"."iam_perm_menu"
|
||||
(id, pid, menu_code, client_code, name, title_cn, title_en, i18n_key, icon,
|
||||
hidden, hide_children_menu, component, path, redirect, sort_no, root,
|
||||
keep_alive, affix_tab, creator, last_modifier, version, deleted, menu_type,
|
||||
active_icon, badge, badge_type, badge_variants, iframe_src, link,
|
||||
create_time, last_modified_time)
|
||||
VALUES
|
||||
(4040130, 40401, NULL, 'admin', 'MchManageGroup', '商户管理', 'Merchant Management',
|
||||
'menu.payment.merchant.group.manage', 'lucide:settings-2',
|
||||
true, false, NULL, NULL, NULL, 1, false,
|
||||
false, false, 1, 1, 0, false, 'subpage_group',
|
||||
NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(4040131, 40401, NULL, 'admin', 'ChannelMerchantGroup', '通道商户', 'Channel Merchant',
|
||||
'menu.payment.merchant.group.channelMerchant', 'lucide:repeat',
|
||||
true, false, NULL, NULL, NULL, 2, false,
|
||||
false, false, 1, 1, 0, false, 'subpage_group',
|
||||
NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(4040132, 40401, NULL, 'admin', 'ChannelAppGroup', '渠道应用', 'Channel App',
|
||||
'menu.payment.merchant.group.channelApp', 'lucide:layout-grid',
|
||||
true, false, NULL, NULL, NULL, 3, false,
|
||||
false, false, 1, 1, 0, false, 'subpage_group',
|
||||
NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
-- 40105 支付产品管理下 1 个分组
|
||||
(40508, 40105, NULL, 'admin', 'ChannelIsvConfigGroup', '渠道服务商配置', 'Channel ISV Config',
|
||||
'menu.payment.config.group.channelIsv', 'lucide:server',
|
||||
true, false, NULL, NULL, NULL, 1, false,
|
||||
false, false, 1, 1, 0, false, 'subpage_group',
|
||||
NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 将子页面 pid 迁移到对应分组
|
||||
-- 4040130 商户管理组(9个)
|
||||
UPDATE "public"."iam_perm_menu"
|
||||
SET "pid" = 4040130, "last_modified_time" = CURRENT_TIMESTAMP
|
||||
WHERE "id" IN (4040101, 4040103, 4040102, 4040110, 4040111, 4040108, 4040117, 4040121, 4040120);
|
||||
|
||||
-- 4040131 通道商户组(3个)
|
||||
UPDATE "public"."iam_perm_menu"
|
||||
SET "pid" = 4040131, "last_modified_time" = CURRENT_TIMESTAMP
|
||||
WHERE "id" IN (4040106, 4040109, 4040112);
|
||||
|
||||
-- 4040132 渠道应用组(4个)
|
||||
UPDATE "public"."iam_perm_menu"
|
||||
SET "pid" = 4040132, "last_modified_time" = CURRENT_TIMESTAMP
|
||||
WHERE "id" IN (4040113, 4040114, 4040115, 4040119);
|
||||
|
||||
-- 40508 渠道服务商配置组(3个)
|
||||
UPDATE "public"."iam_perm_menu"
|
||||
SET "pid" = 40508, "last_modified_time" = CURRENT_TIMESTAMP
|
||||
WHERE "id" IN (40502, 40503, 40506);
|
||||
|
||||
-- 注: 40501(产品配置详情)、40505(移动端应用详情) 仅1个子页面, 保留直挂 menu 不强制分组
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"catalog": "Catalog",
|
||||
"menu": "Menu",
|
||||
"subpage": "Subpage",
|
||||
"subpage_group": "Subpage Group",
|
||||
"embedded": "Embedded Page",
|
||||
"link": "External Link"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
"subpageNoChildren": "Subpage cannot have child menus",
|
||||
"hasChildrenCannotDelete": "Cannot delete menu with child menus",
|
||||
"invalidType": "Invalid menu type",
|
||||
"typeImmutable": "Menu type cannot be changed after creation",
|
||||
"subpageNeedParent": "Subpage must select a parent menu",
|
||||
"parentNotExist": "Parent menu not found",
|
||||
"subpageParentMustBeMenu": "Subpage parent must be a menu type",
|
||||
"subpageParentInvalid": "Subpage parent must be a menu or subpage group",
|
||||
"subpageGroupNeedParent": "Subpage group must select a parent menu",
|
||||
"subpageGroupParentMustBeMenu": "Subpage group parent must be a menu type",
|
||||
"subpageGroupChildMustBeSubpage": "Subpage group can only contain subpages",
|
||||
"needParentCatalog": "Menu, embedded page, or external link must select a parent directory",
|
||||
"parentMustBeCatalog": "Parent of menu, embedded page, or external link must be a directory type",
|
||||
"notExist": "Menu not found"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"catalog": "目录",
|
||||
"menu": "菜单",
|
||||
"subpage": "子页面",
|
||||
"subpage_group": "子页面分组",
|
||||
"embedded": "内嵌页面",
|
||||
"link": "外链"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
"subpageNoChildren": "子页面不能有下级菜单",
|
||||
"hasChildrenCannotDelete": "有子菜单不可以删除",
|
||||
"invalidType": "无效的菜单类型",
|
||||
"typeImmutable": "菜单类型创建后不可修改",
|
||||
"subpageNeedParent": "子页面必须选择上级菜单",
|
||||
"parentNotExist": "上级菜单不存在",
|
||||
"subpageParentMustBeMenu": "子页面的上级菜单必须是菜单类型",
|
||||
"subpageParentInvalid": "子页面的上级必须是菜单或子页面分组",
|
||||
"subpageGroupNeedParent": "子页面分组必须选择上级菜单",
|
||||
"subpageGroupParentMustBeMenu": "子页面分组的上级必须是菜单类型",
|
||||
"subpageGroupChildMustBeSubpage": "子页面分组下只能包含子页面",
|
||||
"needParentCatalog": "菜单、内嵌页面、外链必须选择上级目录",
|
||||
"parentMustBeCatalog": "菜单、内嵌页面、外链的上级菜单必须是目录类型",
|
||||
"notExist": "菜单不存在"
|
||||
|
||||
@@ -16,6 +16,8 @@ public enum MenuTypeEnum implements I18nSupport {
|
||||
MENU("menu"),
|
||||
/// 子页面
|
||||
SUBPAGE("subpage"),
|
||||
/// 子页面分组
|
||||
SUBPAGE_GROUP("subpage_group"),
|
||||
/// 内嵌页面
|
||||
EMBEDDED("embedded"),
|
||||
/// 外链
|
||||
@@ -46,7 +48,7 @@ public enum MenuTypeEnum implements I18nSupport {
|
||||
}
|
||||
|
||||
public boolean canAddChild() {
|
||||
return this == CATALOG || this == MENU;
|
||||
return this == CATALOG || this == MENU || this == SUBPAGE_GROUP;
|
||||
}
|
||||
|
||||
public boolean needParent() {
|
||||
|
||||
@@ -61,7 +61,7 @@ public class PermMenuResult {
|
||||
@Schema(description = "是否固定标签页")
|
||||
private boolean affixTab;
|
||||
|
||||
@Schema(description = "菜单类型: catalog-目录, menu-菜单, embedded-内嵌, link-外链")
|
||||
@Schema(description = "菜单类型: catalog-目录, menu-菜单, subpage-子页面, subpage_group-子页面分组, embedded-内嵌, link-外链")
|
||||
private String menuType;
|
||||
|
||||
@Schema(description = "徽章显示文本")
|
||||
|
||||
@@ -47,9 +47,8 @@ public class PermMenuService {
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.codeExists");
|
||||
}
|
||||
}
|
||||
if (MenuTypeEnum.SUBPAGE.equalsCode(param.getMenuType())) {
|
||||
param.setHidden(true);
|
||||
}
|
||||
// 子页面类菜单(subpage/subpage_group)统一处理
|
||||
applySubpageLikeRules(param, false);
|
||||
PermMenu permMenu = PermMenu.init(param);
|
||||
permMenuManager.save(permMenu);
|
||||
}
|
||||
@@ -61,6 +60,10 @@ public class PermMenuService {
|
||||
// 权限: 菜单不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.iam.menu.notExist"));
|
||||
|
||||
// 菜单类型创建后不可修改
|
||||
if (!Objects.equals(permMenu.getMenuType(), param.getMenuType())) {
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.typeImmutable");
|
||||
}
|
||||
// 校验父级菜单类型
|
||||
validateParentType(param.getPid(), param.getMenuType());
|
||||
// 检查上级菜单是否出现了循环依赖
|
||||
@@ -79,9 +82,8 @@ public class PermMenuService {
|
||||
// 权限: 子页面不能有下级菜单
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.subpageNoChildren");
|
||||
}
|
||||
if (MenuTypeEnum.SUBPAGE.equalsCode(param.getMenuType())) {
|
||||
param.setHidden(true);
|
||||
}
|
||||
// 子页面类菜单统一处理(含分组下级类型校验)
|
||||
applySubpageLikeRules(param, true);
|
||||
PermMenuConvert.CONVERT.copy(param, permMenu);
|
||||
permMenuManager.updateById(permMenu);
|
||||
}
|
||||
@@ -168,7 +170,8 @@ public class PermMenuService {
|
||||
/// 校验父级菜单类型
|
||||
/// - 目录类型可以选择目录作为父级,也可以不选(作为根目录)
|
||||
/// - 菜单、内嵌页面、外链的上级必须是目录类型
|
||||
/// - 子页面的上级必须是菜单类型
|
||||
/// - 子页面的上级必须是菜单或子页面分组
|
||||
/// - 子页面分组的上级必须是菜单(一级限制,不可嵌套)
|
||||
///
|
||||
/// @param pid 父级菜单ID
|
||||
/// @param menuType 当前菜单类型
|
||||
@@ -182,18 +185,21 @@ public class PermMenuService {
|
||||
return;
|
||||
}
|
||||
if (MenuTypeEnum.SUBPAGE.equals(typeEnum)) {
|
||||
if (pid == null) {
|
||||
// 权限: 子页面必须选择上级菜单
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.subpageNeedParent");
|
||||
}
|
||||
PermMenu parent = permMenuManager.findById(pid).orElse(null);
|
||||
if (parent == null) {
|
||||
// 权限: 上级菜单不存在
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.parentNotExist");
|
||||
PermMenu parent = validateAndGetParent(pid, "error.iam.menu.subpageNeedParent");
|
||||
String parentType = parent.getMenuType();
|
||||
// 子页面的上级可以是菜单或子页面分组
|
||||
if (!MenuTypeEnum.MENU.equalsCode(parentType) && !MenuTypeEnum.SUBPAGE_GROUP.equalsCode(parentType)) {
|
||||
// 权限: 子页面的上级必须是菜单或子页面分组
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.subpageParentInvalid");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (MenuTypeEnum.SUBPAGE_GROUP.equals(typeEnum)) {
|
||||
PermMenu parent = validateAndGetParent(pid, "error.iam.menu.subpageGroupNeedParent");
|
||||
// 子页面分组的上级必须是菜单(不可嵌套分组)
|
||||
if (!MenuTypeEnum.MENU.equalsCode(parent.getMenuType())) {
|
||||
// 权限: 子页面的上级菜单必须是菜单类型
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.subpageParentMustBeMenu");
|
||||
// 权限: 子页面分组的上级必须是菜单类型
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.subpageGroupParentMustBeMenu");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -212,6 +218,56 @@ public class PermMenuService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验并获取父级菜单(子页面/子页面分组共用)
|
||||
///
|
||||
/// @param pid 父级菜单ID
|
||||
/// @param needParentKey pid 为空时抛出的错误消息 key
|
||||
/// @return 父级菜单实体
|
||||
private PermMenu validateAndGetParent(Long pid, String needParentKey) {
|
||||
if (pid == null) {
|
||||
throw new BizException(CommonCode.FAIL_CODE, needParentKey);
|
||||
}
|
||||
PermMenu parent = permMenuManager.findById(pid).orElse(null);
|
||||
if (parent == null) {
|
||||
// 权限: 上级菜单不存在
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.parentNotExist");
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// 子页面类菜单(subpage/subpage_group)的统一处理
|
||||
/// - 强制隐藏(不在侧边栏显示)
|
||||
/// - 子页面分组额外校验下级只能是子页面,并清空路由/组件/编码(纯容器)
|
||||
///
|
||||
/// @param param 菜单参数
|
||||
/// @param checkChildren 是否校验下级类型(编辑时为 true)
|
||||
private void applySubpageLikeRules(PermMenuParam param, boolean checkChildren) {
|
||||
String menuType = param.getMenuType();
|
||||
boolean isSubpage = MenuTypeEnum.SUBPAGE.equalsCode(menuType);
|
||||
boolean isGroup = MenuTypeEnum.SUBPAGE_GROUP.equalsCode(menuType);
|
||||
if (!isSubpage && !isGroup) {
|
||||
return;
|
||||
}
|
||||
// 强制隐藏
|
||||
param.setHidden(true);
|
||||
if (isGroup) {
|
||||
// 子页面分组: 编辑时校验下级只能是子页面
|
||||
if (checkChildren && param.getId() != null) {
|
||||
List<PermMenu> children = permMenuManager.findAllByPid(param.getId());
|
||||
for (PermMenu child : children) {
|
||||
if (!MenuTypeEnum.SUBPAGE.equalsCode(child.getMenuType())) {
|
||||
// 权限: 子页面分组下只能包含子页面
|
||||
throw new BizException(CommonCode.FAIL_CODE, "error.iam.menu.subpageGroupChildMustBeSubpage");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 清空路由/组件/编码(纯容器节点)
|
||||
param.setMenuCode(null);
|
||||
param.setComponent(null);
|
||||
param.setPath(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查菜单编码是否存在(同一终端下)
|
||||
/// @param menuCode 菜单编码
|
||||
/// @param clientCode 终端编码
|
||||
|
||||
Reference in New Issue
Block a user