diff --git a/klp-admin/src/main/java/com/klp/web/controller/system/SysMenuController.java b/klp-admin/src/main/java/com/klp/web/controller/system/SysMenuController.java index 10a2dd9a2..7f3cffe50 100644 --- a/klp-admin/src/main/java/com/klp/web/controller/system/SysMenuController.java +++ b/klp-admin/src/main/java/com/klp/web/controller/system/SysMenuController.java @@ -84,6 +84,8 @@ public class SysMenuController extends BaseController { public R add(@Validated @RequestBody SysMenu menu) { if (!menuService.checkMenuNameUnique(menu)) { return R.fail("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } else if (StringUtils.isNotBlank(menu.getMenuKey()) && !menuService.checkMenuKeyUnique(menu)) { + return R.fail("新增菜单'" + menu.getMenuName() + "'失败,菜单唯一标识'" + menu.getMenuKey() + "'已存在"); } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) { return R.fail("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); } @@ -99,6 +101,8 @@ public class SysMenuController extends BaseController { public R edit(@Validated @RequestBody SysMenu menu) { if (!menuService.checkMenuNameUnique(menu)) { return R.fail("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } else if (StringUtils.isNotBlank(menu.getMenuKey()) && !menuService.checkMenuKeyUnique(menu)) { + return R.fail("修改菜单'" + menu.getMenuName() + "'失败,菜单唯一标识'" + menu.getMenuKey() + "'已存在"); } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) { return R.fail("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); } else if (menu.getMenuId().equals(menu.getParentId())) { @@ -124,4 +128,29 @@ public class SysMenuController extends BaseController { } return toAjax(menuService.deleteMenuById(menuId)); } + + /** + * 根据 menuKey 获取菜单信息(供流程图等外部固定引用跳转) + * + * @param menuKey 菜单唯一标识 + */ + @GetMapping("/getByKey") + public R getByKey(@RequestParam String menuKey) { + SysMenu menu = menuService.selectMenuByMenuKey(menuKey); + if (menu == null) { + return R.fail("菜单唯一标识 '" + menuKey + "' 不存在"); + } + return R.ok(menu); + } + + /** + * 为现有菜单自动生成 menu_key(仅处理 menu_key 为空且类型为 C/M 的菜单) + */ + @SaCheckPermission("system:menu:edit") + @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PostMapping("/generateMenuKeys") + public R generateMenuKeys() { + int count = menuService.generateMenuKeys(); + return R.ok("成功为 " + count + " 个菜单生成 menu_key"); + } } diff --git a/klp-common/src/main/java/com/klp/common/core/domain/entity/SysMenu.java b/klp-common/src/main/java/com/klp/common/core/domain/entity/SysMenu.java index c214dc446..aa5ab2cae 100644 --- a/klp-common/src/main/java/com/klp/common/core/domain/entity/SysMenu.java +++ b/klp-common/src/main/java/com/klp/common/core/domain/entity/SysMenu.java @@ -35,6 +35,12 @@ public class SysMenu extends TreeEntity { @Size(min = 0, max = 50, message = "菜单名称长度不能超过{max}个字符") private String menuName; + /** + * 菜单唯一标识(外部固定引用,不受目录变动影响) + */ + @Size(min = 0, max = 64, message = "菜单唯一标识长度不能超过{max}个字符") + private String menuKey; + /** * 显示顺序 */ diff --git a/klp-system/src/main/java/com/klp/system/mapper/SysMenuMapper.java b/klp-system/src/main/java/com/klp/system/mapper/SysMenuMapper.java index 6eb020063..3359a1922 100644 --- a/klp-system/src/main/java/com/klp/system/mapper/SysMenuMapper.java +++ b/klp-system/src/main/java/com/klp/system/mapper/SysMenuMapper.java @@ -79,4 +79,16 @@ public interface SysMenuMapper extends BaseMapperPlus selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + /** + * 根据 menuKey 查询菜单 + * + * @param menuKey 菜单唯一标识 + * @return 菜单信息 + */ + default SysMenu selectMenuByMenuKey(String menuKey) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .eq(SysMenu::getMenuKey, menuKey); + return this.selectOne(lqw); + } + } diff --git a/klp-system/src/main/java/com/klp/system/service/ISysMenuService.java b/klp-system/src/main/java/com/klp/system/service/ISysMenuService.java index 73dbc49d3..4744a786f 100644 --- a/klp-system/src/main/java/com/klp/system/service/ISysMenuService.java +++ b/klp-system/src/main/java/com/klp/system/service/ISysMenuService.java @@ -134,4 +134,27 @@ public interface ISysMenuService { * @return 结果 */ boolean checkMenuNameUnique(SysMenu menu); + + /** + * 校验 menu_key 是否唯一 + * + * @param menu 菜单信息 + * @return 结果 true=唯一 false=不唯一 + */ + boolean checkMenuKeyUnique(SysMenu menu); + + /** + * 根据 menuKey 查询菜单 + * + * @param menuKey 菜单唯一标识 + * @return 菜单信息 + */ + SysMenu selectMenuByMenuKey(String menuKey); + + /** + * 为现有菜单自动生成 menu_key(仅处理 menu_key 为空且 menu_type 为 C/M 的记录) + * + * @return 更新条数 + */ + int generateMenuKeys(); } diff --git a/klp-system/src/main/java/com/klp/system/service/impl/SysMenuServiceImpl.java b/klp-system/src/main/java/com/klp/system/service/impl/SysMenuServiceImpl.java index acc1e8480..62b09f9fc 100644 --- a/klp-system/src/main/java/com/klp/system/service/impl/SysMenuServiceImpl.java +++ b/klp-system/src/main/java/com/klp/system/service/impl/SysMenuServiceImpl.java @@ -303,6 +303,92 @@ public class SysMenuServiceImpl implements ISysMenuService { return !exist; } + /** + * 校验 menu_key 是否唯一 + */ + @Override + public boolean checkMenuKeyUnique(SysMenu menu) { + if (StringUtils.isBlank(menu.getMenuKey())) { + return true; + } + boolean exist = baseMapper.exists(new LambdaQueryWrapper() + .eq(SysMenu::getMenuKey, menu.getMenuKey()) + .ne(ObjectUtil.isNotNull(menu.getMenuId()), SysMenu::getMenuId, menu.getMenuId())); + return !exist; + } + + /** + * 根据 menuKey 查询菜单 + */ + @Override + public SysMenu selectMenuByMenuKey(String menuKey) { + return baseMapper.selectMenuByMenuKey(menuKey); + } + + /** + * 为现有菜单自动生成 menu_key + * 规则: + * C(菜单):去掉 path 的首尾 /,剩余 / 替换为 _,如 /mes/qc/tensile → mes_qc_tensile + * M(目录):同上,若 path 为空则用 menu_name 转拼音首字母 + * F(按钮):不处理 + */ + @Override + public int generateMenuKeys() { + List allMenus = baseMapper.selectList(new LambdaQueryWrapper() + .isNull(SysMenu::getMenuKey) + .in(SysMenu::getMenuType, UserConstants.TYPE_DIR, UserConstants.TYPE_MENU) + .eq(SysMenu::getStatus, UserConstants.MENU_NORMAL)); + + int count = 0; + for (SysMenu menu : allMenus) { + String key = buildMenuKey(menu); + if (StringUtils.isBlank(key)) { + continue; + } + // 确保唯一:如果已有同 key 的菜单,追加数字后缀 + String uniqueKey = key; + int suffix = 1; + while (baseMapper.exists(new LambdaQueryWrapper() + .eq(SysMenu::getMenuKey, uniqueKey) + .ne(SysMenu::getMenuId, menu.getMenuId()))) { + uniqueKey = key + "_" + suffix; + suffix++; + } + menu.setMenuKey(uniqueKey); + baseMapper.updateById(menu); + count++; + } + return count; + } + + /** + * 根据菜单信息构建 menu_key + */ + private String buildMenuKey(SysMenu menu) { + // 优先使用 path + if (StringUtils.isNotBlank(menu.getPath())) { + String path = menu.getPath().trim(); + // 去掉首尾 / + if (path.startsWith("/")) { + path = path.substring(1); + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + // 剩余 / 替换为 _,非字母数字替换为 _ + return path.replaceAll("[^a-zA-Z0-9_\\u4e00-\\u9fa5]", "_") + .replaceAll("_+", "_") + .replaceAll("(^_|_$)", ""); + } + // path 为空时,使用 menuName 作为 key + if (StringUtils.isNotBlank(menu.getMenuName())) { + return menu.getMenuName().trim().replaceAll("[^a-zA-Z0-9_\\u4e00-\\u9fa5]", "_") + .replaceAll("_+", "_") + .replaceAll("(^_|_$)", ""); + } + return null; + } + /** * 获取路由名称 * diff --git a/klp-system/src/main/resources/mapper/system/SysMenuMapper.xml b/klp-system/src/main/resources/mapper/system/SysMenuMapper.xml index 9bebaee34..e9b49f6cd 100644 --- a/klp-system/src/main/resources/mapper/system/SysMenuMapper.xml +++ b/klp-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -7,6 +7,7 @@ + @@ -29,7 +30,7 @@