This commit is contained in:
jhd
2026-07-01 13:29:38 +08:00
7 changed files with 166 additions and 2 deletions

View File

@@ -84,6 +84,8 @@ public class SysMenuController extends BaseController {
public R<Void> 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<Void> 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<SysMenu> 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<Void> generateMenuKeys() {
int count = menuService.generateMenuKeys();
return R.ok("成功为 " + count + " 个菜单生成 menu_key");
}
}

View File

@@ -35,6 +35,12 @@ public class SysMenu extends TreeEntity<SysMenu> {
@Size(min = 0, max = 50, message = "菜单名称长度不能超过{max}个字符")
private String menuName;
/**
* 菜单唯一标识(外部固定引用,不受目录变动影响)
*/
@Size(min = 0, max = 64, message = "菜单唯一标识长度不能超过{max}个字符")
private String menuKey;
/**
* 显示顺序
*/

View File

@@ -79,4 +79,16 @@ public interface SysMenuMapper extends BaseMapperPlus<SysMenuMapper, SysMenu, Sy
*/
List<Long> selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly);
/**
* 根据 menuKey 查询菜单
*
* @param menuKey 菜单唯一标识
* @return 菜单信息
*/
default SysMenu selectMenuByMenuKey(String menuKey) {
LambdaQueryWrapper<SysMenu> lqw = new LambdaQueryWrapper<SysMenu>()
.eq(SysMenu::getMenuKey, menuKey);
return this.selectOne(lqw);
}
}

View File

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

View File

@@ -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<SysMenu>()
.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<SysMenu> allMenus = baseMapper.selectList(new LambdaQueryWrapper<SysMenu>()
.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<SysMenu>()
.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;
}
/**
* 获取路由名称
*

View File

@@ -7,6 +7,7 @@
<resultMap type="SysMenu" id="SysMenuResult">
<id property="menuId" column="menu_id"/>
<result property="menuName" column="menu_name"/>
<result property="menuKey" column="menu_key"/>
<result property="parentName" column="parent_name"/>
<result property="parentId" column="parent_id"/>
<result property="orderNum" column="order_num"/>
@@ -29,7 +30,7 @@
</resultMap>
<select id="selectMenuListByUserId" parameterType="SysMenu" resultMap="SysMenuResult">
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query_param, m.visible, m.status,
select distinct m.menu_id, m.parent_id, m.menu_name, m.menu_key, m.path, m.component, m.query_param, m.visible, m.status,
m.perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.style, m.order_num, m.create_time
from sys_menu m
left join sys_role_menu rm on m.menu_id = rm.menu_id
@@ -42,6 +43,7 @@
select distinct m.menu_id,
m.parent_id,
m.menu_name,
m.menu_key,
m.path,
m.component,
m.query_param,

View File

@@ -729,7 +729,13 @@ export default {
...this.detailQueryParams,
testMainId: this.currentTestId
}).then(response => {
this.detailList = response.rows
// 归一化单字母前缀字段名Jackson将cContent序列化为ccontent
this.detailList = (response.rows || []).map(row => ({
...row,
cContent: row.ccontent ?? row.cContent,
pContent: row.pcontent ?? row.pContent,
sContent: row.scontent ?? row.sContent
}))
this.detailTotal = response.total
this.detailLoading = false
}).catch(() => {