4 Commits

Author SHA1 Message Date
RuoYi
28be96930c README.md 2026-01-28 21:21:10 +08:00
RuoYi
6c6a2c0623 update README.md 2026-01-28 21:16:49 +08:00
RuoYi
7d821ed043 优化代码 2026-01-28 21:16:05 +08:00
RuoYi
5d54693b27 添加菜单路由地址和名称的校验规则 2026-01-09 11:19:02 +08:00
24 changed files with 145 additions and 55 deletions

View File

@@ -17,8 +17,8 @@
* 后端采用Spring Boot、Spring Cloud & Alibaba。
* 注册中心、配置中心选型Nacos权限认证使用Redis。
* 流量控制框架选型Sentinel分布式事务选型Seata。
* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev)版本[RuoYi-Cloud-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Vue3)保持同步更新。
* 如需不分离应用,请移步 [RuoYi](https://gitee.com/y_project/RuoYi),如需分离应用,请移步 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)
* 提供了技术栈Vue3 Element Plus Vite的 [RuoYi-Cloud-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Vue3)版本以及技术栈TypeScript[RuoYi-Cloud-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Vue3/tree/typescript)版本,两者保持同步更新。
* 提供了适配 Spring Boot 3 的版本分支 [RuoYi-Cloud (springboot3)](https://gitee.com/y_project/RuoYi-Cloud/tree/springboot3),与 Oracle数据库版本 [RuoYi-Cloud-Oracle](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Oracle),均保持同步更新。
* 阿里云优惠券:[点我进入](http://aly.ruoyi.vip),腾讯云优惠券:[点我进入](http://txy.ruoyi.vip)  
## 系统模块
@@ -74,6 +74,21 @@ com.ruoyi
16. 在线构建器拖动表单元素生成相应的HTML代码。
17. 连接池监视监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈。
# 版本对比
RuoYi-Cloud 前端项目的三个主要演进版本,方便你直观对比其技术栈差异(并行开发维护)。
| 项目名称 | **RuoYi-Cloud** | **RuoYi-Cloud-Vue3** | **RuoYi-Cloud-Vue3-TypeScript** |
| :--- | :--- | :--- | :--- |
| **前端框架** | Vue 2 | Vue 3 | Vue 3 |
| **脚本语言** | JavaScript | JavaScript | TypeScript |
| **构建工具** | Vue CLI | Vite | Vite |
| **UI 组件库** | Element UI | Element Plus | Element Plus |
| **状态管理** | Vuex | Pinia | Pinia |
| **路由管理** | Vue Router 3 | Vue Router 4 | Vue Router 4 |
| **核心特点** | 1. 技术栈经典稳定<br>2. 社区资料丰富<br>3. 当前维护重心已转移 | 1. 现代前端技术栈<br>2. 开发体验与性能更优<br>3. 官方主推的活跃版本 | 1. 类型加持,减少沟通成本<br>2. 开发时有提示,效率更高<br>3. 多人协作企业级开发项目 |
| **仓库地址** | [RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud) | [RuoYi-Cloud-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Vue3) | [RuoYi-Cloud-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Vue3/tree/typescript) |
## 在线体验
- admin/admin123

View File

@@ -97,6 +97,10 @@ public class SysMenuController extends BaseController
{
return error("新增菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
}
else if (!menuService.checkRouteConfigUnique(menu))
{
return error("新增菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
}
menu.setCreateBy(SecurityUtils.getUsername());
return toAjax(menuService.insertMenu(menu));
}
@@ -121,6 +125,10 @@ public class SysMenuController extends BaseController
{
return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
}
else if (!menuService.checkRouteConfigUnique(menu))
{
return error("修改菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
}
menu.setUpdateBy(SecurityUtils.getUsername());
return toAjax(menuService.updateMenu(menu));
}

View File

@@ -122,4 +122,13 @@ public interface SysMenuMapper
* @return 结果
*/
public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId);
/**
* 根据路由路径或名称查询菜单信息(用于唯一性校验)
*
* @param path 路由地址
* @param routeName 路由名称
* @return 匹配的菜单列表
*/
public List<SysMenu> selectMenusByPathOrRouteName(@Param("path") String path, @Param("routeName") String routeName);
}

View File

@@ -141,4 +141,12 @@ public interface ISysMenuService
* @return 结果
*/
public boolean checkMenuNameUnique(SysMenu menu);
/**
* 校验路由组合是否唯一
*
* @param menu 菜单信息
* @return 结果
*/
public boolean checkRouteConfigUnique(SysMenu menu);
}

View File

@@ -8,6 +8,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.constant.Constants;
@@ -32,8 +34,12 @@ import com.ruoyi.system.service.ISysMenuService;
@Service
public class SysMenuServiceImpl implements ISysMenuService
{
private static final Logger log = LoggerFactory.getLogger(SysMenuServiceImpl.class);
public static final String PREMISSION_STRING = "perms[\"{0}\"]";
public static final Long MENU_ROOT_ID = 0L;
@Autowired
private SysMenuMapper menuMapper;
@@ -138,7 +144,7 @@ public class SysMenuServiceImpl implements ISysMenuService
{
menus = menuMapper.selectMenuTreeByUserId(userId);
}
return getChildPerms(menus, 0);
return getChildPerms(menus, MENU_ROOT_ID);
}
/**
@@ -193,7 +199,7 @@ public class SysMenuServiceImpl implements ISysMenuService
childrenList.add(children);
router.setChildren(childrenList);
}
else if (menu.getParentId().intValue() == 0 && isInnerLink(menu))
else if (menu.getParentId().intValue() == MENU_ROOT_ID && isInnerLink(menu))
{
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
router.setPath("/");
@@ -345,6 +351,47 @@ public class SysMenuServiceImpl implements ISysMenuService
return UserConstants.UNIQUE;
}
/**
* 校验路由名称是否唯一
*
* @param menu 菜单信息
* @return 结果
*/
@Override
public boolean checkRouteConfigUnique(SysMenu menu)
{
Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId();
Long parentId = menu.getParentId();
String path = menu.getPath();
String routeName = StringUtils.isEmpty(menu.getRouteName()) ? path : menu.getRouteName();
List<SysMenu> sysMenuList = menuMapper.selectMenusByPathOrRouteName(path, routeName);
for (SysMenu sysMenu : sysMenuList)
{
if (sysMenu.getMenuId().longValue() != menuId.longValue())
{
Long dbParentId = sysMenu.getParentId();
String dbPath = sysMenu.getPath();
String dbRouteName = StringUtils.isEmpty(sysMenu.getRouteName()) ? dbPath : sysMenu.getRouteName();
if (StringUtils.equalsAnyIgnoreCase(path, dbPath) && parentId.longValue() == dbParentId.longValue())
{
log.warn("[同级路由冲突] 同级下已存在相同路由路径 '{}',冲突菜单:{}", dbPath, sysMenu.getMenuName());
return UserConstants.NOT_UNIQUE;
}
else if (StringUtils.equalsAnyIgnoreCase(path, dbPath) && parentId.longValue() == MENU_ROOT_ID)
{
log.warn("[根目录路由冲突] 根目录下路由 '{}' 必须唯一,已被菜单 '{}' 占用", path, sysMenu.getMenuName());
return UserConstants.NOT_UNIQUE;
}
else if (StringUtils.equalsAnyIgnoreCase(routeName, dbRouteName))
{
log.warn("[路由名称冲突] 路由名称 '{}' 需全局唯一,已被菜单 '{}' 使用", routeName, sysMenu.getMenuName());
return UserConstants.NOT_UNIQUE;
}
}
}
return UserConstants.UNIQUE;
}
/**
* 获取路由名称
*
@@ -384,12 +431,12 @@ public class SysMenuServiceImpl implements ISysMenuService
{
String routerPath = menu.getPath();
// 内链打开外网方式
if (menu.getParentId().intValue() != 0 && isInnerLink(menu))
if (menu.getParentId().intValue() != MENU_ROOT_ID && isInnerLink(menu))
{
routerPath = innerLinkReplaceEach(routerPath);
}
// 非外链并且是一级目录(类型为目录)
if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
if (MENU_ROOT_ID == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
&& UserConstants.NO_FRAME.equals(menu.getIsFrame()))
{
routerPath = "/" + menu.getPath();
@@ -415,7 +462,7 @@ public class SysMenuServiceImpl implements ISysMenuService
{
component = menu.getComponent();
}
else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu))
else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != MENU_ROOT_ID && isInnerLink(menu))
{
component = UserConstants.INNER_LINK;
}
@@ -434,10 +481,21 @@ public class SysMenuServiceImpl implements ISysMenuService
*/
public boolean isMenuFrame(SysMenu menu)
{
return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType())
return menu.getParentId().intValue() == MENU_ROOT_ID && UserConstants.TYPE_MENU.equals(menu.getMenuType())
&& menu.getIsFrame().equals(UserConstants.NO_FRAME);
}
/**
* 是否为parent_view组件
*
* @param menu 菜单信息
* @return 结果
*/
public boolean isParentView(SysMenu menu)
{
return menu.getParentId().intValue() != MENU_ROOT_ID && UserConstants.TYPE_DIR.equals(menu.getMenuType());
}
/**
* 是否为内链组件
*
@@ -449,17 +507,6 @@ public class SysMenuServiceImpl implements ISysMenuService
return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath());
}
/**
* 是否为parent_view组件
*
* @param menu 菜单信息
* @return 结果
*/
public boolean isParentView(SysMenu menu)
{
return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType());
}
/**
* 根据父节点的ID获取所有子节点
*
@@ -467,7 +514,7 @@ public class SysMenuServiceImpl implements ISysMenuService
* @param parentId 传入的父节点ID
* @return String
*/
public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId)
public List<SysMenu> getChildPerms(List<SysMenu> list, long parentId)
{
List<SysMenu> returnList = new ArrayList<SysMenu>();
for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();)

View File

@@ -130,7 +130,12 @@
<select id="checkMenuNameUnique" parameterType="SysMenu" resultMap="SysMenuResult">
<include refid="selectMenuVo"/>
where menu_name=#{menuName} and parent_id = #{parentId} limit 1
where menu_name= #{menuName} and parent_id = #{parentId} limit 1
</select>
<select id="selectMenusByPathOrRouteName" parameterType="SysMenu" resultMap="SysMenuResult">
<include refid="selectMenuVo"/>
where menu_type in ('M', 'C') and (path = #{path} or path = #{routeName} or route_name = #{path} or route_name = #{routeName})
</select>
<update id="updateMenu" parameterType="SysMenu">

View File

@@ -1,5 +1,5 @@
import request from '@/utils/request'
import { parseStrEmpty } from "@/utils/ruoyi";
import { parseStrEmpty } from "@/utils/ruoyi"
// 查询用户列表
export function listUser(query) {

View File

@@ -118,8 +118,6 @@ export default {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
minMatchCharLength: 1,
keys: [{
name: 'title',

View File

@@ -477,13 +477,13 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.jobId != undefined) {
updateJob(this.form).then(response => {
updateJob(this.form).then(() => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addJob(this.form).then(response => {
addJob(this.form).then(() => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();

View File

@@ -128,7 +128,7 @@ export default {
this.$refs.registerForm.validate(valid => {
if (valid) {
this.loading = true
register(this.registerForm).then(res => {
register(this.registerForm).then(() => {
const username = this.registerForm.username
this.$alert("<font color='red'>恭喜你,您的账号 " + username + " 注册成功!</font>", '系统提示', {
dangerouslyUseHTMLString: true,

View File

@@ -301,13 +301,13 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.configId != undefined) {
updateConfig(this.form).then(response => {
updateConfig(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addConfig(this.form).then(response => {
addConfig(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()

View File

@@ -311,13 +311,13 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.deptId != undefined) {
updateDept(this.form).then(response => {
updateDept(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addDept(this.form).then(response => {
addDept(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()

View File

@@ -363,14 +363,14 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.dictCode != undefined) {
updateData(this.form).then(response => {
updateData(this.form).then(() => {
this.$store.dispatch('dict/removeDict', this.queryParams.dictType)
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addData(this.form).then(response => {
addData(this.form).then(() => {
this.$store.dispatch('dict/removeDict', this.queryParams.dictType)
this.$modal.msgSuccess("新增成功")
this.open = false

View File

@@ -304,13 +304,13 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.dictId != undefined) {
updateType(this.form).then(response => {
updateType(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addType(this.form).then(response => {
addType(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()

View File

@@ -448,13 +448,13 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.menuId != undefined) {
updateMenu(this.form).then(response => {
updateMenu(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addMenu(this.form).then(response => {
addMenu(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()

View File

@@ -282,13 +282,13 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.noticeId != undefined) {
updateNotice(this.form).then(response => {
updateNotice(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addNotice(this.form).then(response => {
addNotice(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()

View File

@@ -273,13 +273,13 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.postId != undefined) {
updatePost(this.form).then(response => {
updatePost(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addPost(this.form).then(response => {
addPost(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()

View File

@@ -184,7 +184,7 @@ export default {
}).catch(() => {})
},
/** 批量取消授权按钮操作 */
cancelAuthUserAll(row) {
cancelAuthUserAll() {
const roleId = this.queryParams.roleId
const userIds = this.userIds.join(",")
this.$modal.confirm('是否取消选中用户授权数据项?').then(function() {

View File

@@ -557,14 +557,14 @@ export default {
if (valid) {
if (this.form.roleId != undefined) {
this.form.menuIds = this.getMenuAllCheckedKeys()
updateRole(this.form).then(response => {
updateRole(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
this.form.menuIds = this.getMenuAllCheckedKeys()
addRole(this.form).then(response => {
addRole(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
@@ -577,7 +577,7 @@ export default {
submitDataScope: function() {
if (this.form.roleId != undefined) {
this.form.deptIds = this.getDeptAllCheckedKeys()
dataScope(this.form).then(response => {
dataScope(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.openDataScope = false
this.getList()

View File

@@ -108,7 +108,7 @@ export default {
submitForm() {
const userId = this.form.userId
const roleIds = this.roleIds.join(",")
updateAuthRole({ userId: userId, roleIds: roleIds }).then((response) => {
updateAuthRole({ userId: userId, roleIds: roleIds }).then(() => {
this.$modal.msgSuccess("授权成功")
this.close()
})

View File

@@ -476,7 +476,7 @@ export default {
}
},
}).then(({ value }) => {
resetUserPwd(row.userId, value).then(response => {
resetUserPwd(row.userId, value).then(() => {
this.$modal.msgSuccess("修改成功,新密码是:" + value)
})
}).catch(() => {})
@@ -491,13 +491,13 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.userId != undefined) {
updateUser(this.form).then(response => {
updateUser(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addUser(this.form).then(response => {
addUser(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()

View File

@@ -55,7 +55,7 @@ export default {
submit() {
this.$refs["form"].validate(valid => {
if (valid) {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(response => {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(() => {
this.$modal.msgSuccess("修改成功")
})
}

View File

@@ -72,7 +72,7 @@ export default {
submit() {
this.$refs["form"].validate(valid => {
if (valid) {
updateUserProfile(this.form).then(response => {
updateUserProfile(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.user.phonenumber = this.form.phonenumber
this.user.email = this.form.email

View File

@@ -87,9 +87,9 @@
<span>{{(queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1}}</span>
</template>
</el-table-column>
<el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" width="120" />
<el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" width="120" />
<el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" width="120" />
<el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" width="140" />
<el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" width="140" />
<el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" width="140" />
<el-table-column label="创建时间" align="center" prop="createTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="160" />
<el-table-column label="更新时间" align="center" prop="updateTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="160" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
@@ -249,7 +249,7 @@ export default {
return
}
if(row.genType === "1") {
genCode(row.tableName).then(response => {
genCode(row.tableName).then(() => {
this.$modal.msgSuccess("成功生成到自定义路径:" + row.genPath)
})
} else {