RuoYi-Vue-Plus 项目详解:架构、原理与设计思想
项目概述
RuoYi-Vue-Plus 是基于 Vue3 和 SpringBoot 的一款现代化后台管理系统,它对原始的 RuoYi-Vue 进行了全面重构,专为分布式集群和多租户场景设计。项目不仅代码和文档开放且免费,还提供了高度的可定制性和扩展性,适用于商业应用。
infoRuoYi-Vue-Plus 是 dromara 组织下的开源项目,完全遵循 MIT 开源协议,可以免费用于个人及商业用途。
项目定位
RuoYi-Vue-Plus 定位为企业级多租户后台管理系统,旨在提供一个开箱即用、高度可扩展的开发平台,使开发者能够快速构建企业级应用,减少重复开发工作,提高开发效率。
主要特点
- 前后端分离:采用 Vue3 + SpringBoot 的前后端分离架构,便于团队协作和独立部署
- 多租户支持:原生支持多租户架构,实现数据隔离和权限控制
- 插件化设计:核心功能模块化,支持插件式扩展,降低系统耦合度
- 权限精细控制:基于 Sa-Token 的权限认证和授权,支持数据权限和菜单权限
- 工作流引擎:集成 Warm-Flow 工作流引擎,支持复杂业务流程设计
- 代码生成:提供强大的代码生成器,一键生成前后端代码
技术架构
前端技术栈
- Vue 3:采用 Vue 3 作为前端框架,利用 Composition API 提高代码复用性和可维护性
- TypeScript:全面使用 TypeScript 增强代码类型安全性和开发体验
- Element Plus:基于 Element Plus 组件库构建 UI 界面,提供丰富的组件和主题定制能力
- Vite:使用 Vite 作为构建工具,提供快速的开发服务器和优化的生产构建
- Pinia:采用 Pinia 作为状态管理库,替代 Vuex,提供更简洁的 API 和更好的 TypeScript 支持
后端技术栈
- Spring Boot:基于 Spring Boot 3.x 框架,简化应用开发和部署
- Spring Security:集成 Spring Security 提供安全认证和授权功能
- MyBatis-Plus:使用 MyBatis-Plus 作为 ORM 框架,简化数据库操作
- Sa-Token:采用 Sa-Token 替代传统 Shiro,提供更轻量、更强大的权限控制
- Hutool:集成 Hutool 工具库,提供丰富的 Java 工具方法
- Warm-Flow:集成 Warm-Flow 工作流引擎,支持复杂业务流程设计
系统架构设计
系统采用分层架构设计,清晰划分各层职责,实现高内聚低耦合。前端层负责用户界面展示和交互,后端层负责业务逻辑处理和数据访问,数据层负责数据存储和缓存。
模块划分
项目采用模块化设计,主要包含以下核心模块:
- ruoyi-admin:系统启动模块,包含系统配置和启动类
- ruoyi-common:公共模块,包含工具类、通用注解和异常处理等
- ruoyi-framework:框架核心模块,包含安全、权限、缓存等核心功能
- ruoyi-system:系统管理模块,包含用户、角色、菜单等系统管理功能
- ruoyi-job:定时任务模块,提供定时任务调度功能
- ruoyi-generator:代码生成模块,提供代码生成功能
核心特性
apartment多租户支持
原生支持多租户架构,通过租户 ID 实现数据隔离,每个租户拥有独立的数据空间,确保数据安全。支持动态数据源切换,可配置不同租户使用不同数据库。
security权限管理
基于 Sa-Token 的权限认证和授权,支持 JWT 认证。提供细粒度的权限控制,包括菜单权限、按钮权限和数据权限,满足企业级应用的复杂权限需求。
account_tree工作流引擎
集成 Warm-Flow 工作流引擎,支持可视化流程设计、动态流程调整和复杂分支逻辑。与系统权限体系无缝集成,实现精细化的流程权限控制。
code代码生成
提供强大的代码生成器,支持一键生成前后端代码。可根据数据库表结构生成实体类、Mapper、Service、Controller 以及前端页面,大幅提高开发效率。
数据权限实现
RuoYi-Vue-Plus 5.3.0 版本对数据权限系统进行了彻底重构,采用基于 Mybatis-Plus 插件的数据权限解决方案,实现了权限控制与业务逻辑的解耦。
/** * 数据权限注解 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataScope { /** * 表别名 */ String tableAlias() default ""; /** * 部门ID字段名 */ String deptId() default ""; /** * 用户ID字段名 */ String userId() default ""; }
通过 @DataScope 注解,可以在 Mapper 接口上声明数据权限规则,Mybatis-Plus 插件会自动根据当前用户的角色数据权限,动态生成对应的 WHERE 条件,实现数据权限的无感过滤。
多数据源支持
系统支持多数据源配置,可以同时连接多种类型的数据库,实现读写分离和分库分表。通过动态数据源路由,可以根据业务需求自动切换数据源。
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://localhost:3306/ry-vue
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3307/ry-vue
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
设计思想
架构思想
RuoYi-Vue-Plus 遵循以下架构思想:
- 分层架构:采用经典的分层架构,清晰划分各层职责,实现高内聚低耦合
- 模块化设计:将系统功能划分为多个独立模块,每个模块负责特定功能,便于维护和扩展
- 插件化扩展:核心功能采用插件化设计,支持动态加载和卸载,提高系统灵活性
- 前后端分离:采用前后端分离架构,前端负责界面展示和用户交互,后端负责业务逻辑和数据处理
开发模式
项目采用以下开发模式:
- 约定优于配置:遵循约定优于配置的原则,减少配置工作,提高开发效率
- 面向接口编程:采用面向接口编程的方式,定义清晰的接口规范,降低模块间耦合
- 领域驱动设计:在核心业务模块采用领域驱动设计思想,以业务领域为中心组织代码
- 测试驱动开发:鼓励编写单元测试和集成测试,确保代码质量和系统稳定性
设计原则
项目遵循以下设计原则:
- 单一职责原则:每个类和模块只负责一项功能,保持职责单一
- 开闭原则:对扩展开放,对修改关闭,通过扩展而非修改来增加新功能
- 里氏替换原则:子类必须能够替换其基类,保持系统的稳定性
- 接口隔离原则:使用多个专门的接口,而不是使用单一的总接口
- 依赖倒置原则:依赖于抽象而非具体实现,降低模块间耦合
原理分析
权限控制原理
RuoYi-Vue-Plus 采用 Sa-Token 作为权限控制框架,其核心原理如下:
/**
* 权限服务接口
*/
public interface PermissionService {
/**
* 获取角色数据权限
*
* @param roleId 角色ID
* @return 数据权限范围
*/
Set<Long> getRoleDataScope(Long roleId);
/**
* 获取菜单数据权限
*
* @param userId 用户ID
* @return 菜单权限标识集合
*/
Set<String> getMenuPermission(Long userId);
}
权限控制的核心是通过 PermissionService 接口实现的,该接口定义了获取角色数据权限和菜单权限的方法。系统在用户登录时,会加载用户的权限信息并缓存,后续请求通过拦截器进行权限校验。
多租户实现原理
多租户是 RuoYi-Vue-Plus 的核心特性之一,其实现原理如下:
/**
* 多租户拦截器
*/
public class TenantInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 获取租户ID
Long tenantId = TenantHelper.getTenantId();
if (tenantId == null) {
return;
}
// 修改SQL,添加租户条件
String originalSql = boundSql.getSql();
String newSql = originalSql + " AND tenant_id = " + tenantId;
ReflectUtil.setFieldValue(boundSql, "sql", newSql);
}
}
多租户实现的核心是通过 Mybatis-Plus 的拦截器机制,在 SQL 执行前动态添加租户条件,实现数据隔离。系统通过 TenantHelper 工具类获取当前租户 ID,并在 SQL 中自动添加租户条件,确保每个租户只能访问自己的数据。
工作流引擎原理
RuoYi-Vue-Plus 集成了 Warm-Flow 工作流引擎,其核心原理如下:
/**
* 工作流服务接口
*/
public interface IFlwCommonService {
/**
* 构建工作流用户
*
* @param userId 用户ID
* @return 工作流用户对象
*/
User buildWorkFlowUser(Long userId);
/**
* 启动流程
*
* @param processKey 流程定义键
* @param businessKey 业务键
* @param variables 变量
* @return 流程实例ID
*/
String startProcess(String processKey, String businessKey, Map<String, Object> variables);
}
工作流引擎的核心是通过流程定义、流程实例和任务三个核心概念实现的。流程定义定义了业务流程的结构和规则,流程实例是流程定义的一次执行,任务是流程实例中的具体工作项。系统通过工作流服务接口提供流程启动、任务处理、流程查询等功能。
插件化设计原理
RuoYi-Vue-Plus 采用插件化设计,其核心原理如下:
/**
* 插件接口
*/
public interface Plugin {
/**
* 插件名称
*/
String getName();
/**
* 插件版本
*/
String getVersion();
/**
* 插件描述
*/
String getDescription();
/**
* 初始化插件
*/
void initialize();
/**
* 启动插件
*/
void start();
/**
* 停止插件
*/
void stop();
}
插件化设计的核心是通过定义统一的插件接口,实现功能的动态加载和卸载。系统通过插件管理器负责插件的加载、初始化、启动和停止,实现功能的动态扩展。插件可以独立开发和部署,不影响系统的稳定性。
代码示例
配置示例
# 项目配置
ruoyi:
# 名称
name: RuoYi-Vue-Plus
# 版本
version: 5.3.0
# 版权年份
copyrightYear: 2023
# 实例演示开关
demoEnabled: true
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
profile: D:/ruoyi/uploadPath
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数组计算 char 字符验证
captchaType: math
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# tomcat最大线程数,默认为200
max-threads: 800
# Tomcat启动初始化的线程数,默认值25
min-spare-threads: 30
核心类示例
/** * 登录服务实现 */ @Service public class SysLoginService { @Autowired private SysUserMapper userMapper; @Autowired private SysPermissionService permissionService; /** * 登录验证 * * @param username 用户名 * @param password 密码 * @param code 验证码 * @param uuid 唯一标识 * @return 结果 */ public String login(String username, String password, String code, String uuid) { // 验证码校验 validateCaptcha(username, code, uuid); // 登录前置校验 loginPreCheck(username, password); // 用户验证 SysUser user = loadUserByUsername(username); if (!SecurityUtils.matchesPassword(password, user.getPassword())) { throw new UserPasswordNotMatchException(); } if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { throw new UserBlockedException(); } // 生成token return createToken(user); } /** * 创建令牌 */ public String createToken(SysUser user) { // 生成token String token = IdUtil.fastSimpleUUID(); // 缓存用户信息 LoginUser loginUser = new LoginUser(); loginUser.setUserId(user.getUserId()); loginUser.setUsername(user.getUserName()); loginUser.setPermission(permissionService.getMenuPermission(user.getUserId())); // 缓存登录用户到Redis RedisUtils.setCacheObject(CacheConstants.LOGIN_TOKEN_KEY + token, loginUser, CacheConstants.LOGIN_TOKEN_TTL, TimeUnit.MINUTES); return token; } }
接口设计示例
/** * 用户管理接口 */ @RestController @RequestMapping("/system/user") public class SysUserController extends BaseController { @Autowired private ISysUserService userService; /** * 获取用户列表 */ @SaCheckPermission("system:user:list") @GetMapping("/list") public TableDataInfo<SysUserVo> list(SysUserBo user, PageQuery pageQuery) { return userService.selectPageUserList(user, pageQuery); } /** * 根据用户编号获取详细信息 */ @SaCheckPermission("system:user:query") @GetMapping("/{userId}") public R<SysUserVo> getInfo(@PathVariable Long userId) { userService.checkUserDataScope(userId); return R.ok(userService.selectUserById(userId)); } /** * 新增用户 */ @SaCheckPermission("system:user:add") @Log(title = "用户管理", businessType = BusinessType.INSERT) @PostMapping public R<Void> add(@Validated @RequestBody SysUserBo user) { if (!userService.checkUserNameUnique(user.getUserName())) { return R.fail("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); } return toAjax(userService.insertUser(user)); } /** * 修改用户 */ @SaCheckPermission("system:user:edit") @Log(title = "用户管理", businessType = BusinessType.UPDATE) @PutMapping public R<Void> edit(@Validated @RequestBody SysUserBo user) { userService.checkUserAllowed(user); userService.checkUserDataScope(user.getUserId()); if (!userService.checkUserNameUnique(user.getUserName())) { return R.fail("修改用户'" + user.getUserName() + "'失败,登录账号已存在"); } return toAjax(userService.updateUser(user)); } }
讨论回复
0 条回复还没有人回复,快来发表你的看法吧!