RuoYi-Vue-Plus/RuoYi-Vue-Plus项目分析报告.md

840 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RuoYi-Vue-Plus项目分析报告
## 1. 项目概述
RuoYi-Vue-Plus是在RuoYi-Vue基础上进行重构升级的开源项目它专为分布式集群与多租户场景设计采用插件化和扩展包的结构形式极大提高了系统的解耦程度和扩展性。项目使用Spring Boot 3.4、JDK 17/21基于Vue3+TS+ElementPlus重写前端并使用Undertow替代Tomcat作为Web容器。
### 1.1 项目特点
- 采用插件化+扩展包形式,结构解耦,易于扩展
- 严格遵守Alibaba规范代码格式统一
- 支持多种数据库MySQL、Oracle、PostgreSQL、SQLServer等
- 支持多租户架构
- 前端使用Vue3+TS+ElementPlus
- 底层基于Redisson的分布式能力分布式锁、限流等
- 使用Sa-Token+JWT实现权限认证
- 使用Mybatis-Plus作为ORM框架
- 内置丰富的功能模块和实用工具
## 2. 项目架构
### 2.1 整体架构
RuoYi-Vue-Plus采用前后端分离的架构模式
- 后端Spring Boot 3.4 + MyBatis-Plus + Sa-Token
- 前端Vue3 + TypeScript + ElementPlus
项目部署架构图:
![Plus部署架构图](https://foruda.gitee.com/images/1678981882624240692/ae2a3f3e_1766278.png)
### 2.2 目录结构
项目主要分为以下几个部分:
#### 2.2.1 后端结构
```
ruoyi-vue-plus
├── ruoyi-admin -- 启动模块,项目入口
├── ruoyi-common -- 通用模块,各种功能组件
│ ├── ruoyi-common-bom -- 依赖版本管理
│ ├── ruoyi-common-core -- 核心功能
│ ├── ruoyi-common-doc -- 接口文档
│ ├── ruoyi-common-encrypt -- 数据加解密
│ ├── ruoyi-common-excel -- Excel处理
│ ├── ruoyi-common-idempotent -- 幂等处理
│ ├── ruoyi-common-job -- 定时任务
│ ├── ruoyi-common-json -- JSON处理
│ ├── ruoyi-common-log -- 日志处理
│ ├── ruoyi-common-mail -- 邮件处理
│ ├── ruoyi-common-mybatis -- ORM配置
│ ├── ruoyi-common-oss -- 对象存储
│ ├── ruoyi-common-ratelimiter -- 限流处理
│ ├── ruoyi-common-redis -- Redis配置
│ ├── ruoyi-common-satoken -- 认证鉴权
│ ├── ruoyi-common-security -- 安全配置
│ ├── ruoyi-common-sensitive -- 数据脱敏
│ ├── ruoyi-common-sms -- 短信服务
│ ├── ruoyi-common-social -- 社交登录
│ ├── ruoyi-common-sse -- SSE推送
│ ├── ruoyi-common-tenant -- 多租户
│ ├── ruoyi-common-translation -- 数据翻译
│ ├── ruoyi-common-web -- Web功能
│ └── ruoyi-common-websocket -- WebSocket
├── ruoyi-extend -- 扩展模块
│ ├── ruoyi-monitor-admin -- 监控管理
│ └── ruoyi-snailjob-server -- 任务调度
├── ruoyi-modules -- 业务模块
│ ├── ruoyi-demo -- 示例模块
│ ├── ruoyi-generator -- 代码生成
│ ├── ruoyi-job -- 定时任务
│ ├── ruoyi-system -- 系统管理
│ └── ruoyi-workflow -- 工作流
└── ruoyi-plus-soybean -- 前端项目
```
#### 2.2.2 前端结构
```
ruoyi-plus-soybean
├── docs -- 文档
├── packages -- 功能包
├── public -- 静态资源
├── src -- 源代码目录
│ ├── assets -- 静态资源
│ ├── components -- 组件
│ ├── constants -- 常量
│ ├── enum -- 枚举
│ ├── hooks -- 钩子函数
│ ├── layouts -- 布局
│ ├── locales -- 国际化
│ ├── plugins -- 插件
│ ├── router -- 路由
│ ├── service -- 服务调用
│ ├── store -- 状态管理
│ ├── styles -- 样式
│ ├── theme -- 主题
│ ├── typings -- 类型定义
│ ├── utils -- 工具类
│ ├── views -- 视图
│ ├── App.vue -- 主组件
│ └── main.ts -- 入口文件
```
## 3. 功能模块
### 3.1 核心功能
#### 基础功能
- **多租户管理**:支持租户套餐、过期时间、用户数量等管理
- **用户权限**:用户、角色、部门、菜单权限管理
- **系统监控**:在线用户、操作日志、登录日志
- **系统管理**:参数设置、字典管理、附件管理
- **系统工具**:代码生成、表单设计、接口文档
#### 扩展功能
- **工作流**:支持复杂审批流程
- **在线构建器**:拖拽式表单生成
- **定时任务**:任务调度管理
- **系统接口**API文档自动生成
- **服务监控**:监控系统资源和性能
- **缓存监控**Redis监控
### 3.2 技术特性
- **数据权限**基于Mybatis-Plus插件的无感式数据权限过滤
- **数据脱敏**:支持注解+Jackson方式的数据脱敏
- **数据加解密**:支持数据库字段级加解密
- **接口加密**动态AES+RSA加密请求体
- **数据翻译**:注解+序列化期间自动翻译
- **多数据源**:支持动态数据源配置和切换
- **分布式锁**基于Redisson的分布式锁
- **分布式任务调度**基于SnailJob的分布式任务调度
- **文件存储**支持MinIO和S3协议的对象存储
## 4. 代码规范
RuoYi-Vue-Plus项目严格遵循以下规范
### 4.1 项目规范
- 严格遵守Alibaba编码规范
- 使用统一的代码格式化配置
- 采用插件化结构,功能模块独立封装
- 按职责分离不同类型的代码(控制器、服务、实体等)
### 4.2 编码规范
#### 后端规范
1. **命名规范**
- 类名大驼峰命名UserController
- 方法名/变量名小驼峰命名getUserInfo
- 常量全大写下划线分隔MAX_COUNT
2. **包结构**
- controller控制器
- service服务层
- mapper数据访问层
- domain实体类entity、vo、bo、dto等
- util工具类
3. **注释要求**
- 类注释:说明类的用途
- 方法注释:说明方法功能、参数和返回值
- 关键代码注释:解释复杂逻辑
#### 前端规范
1. **命名规范**
- 组件名大驼峰命名UserForm
- 文件名kebab-caseuser-form.vue
- 变量/方法小驼峰命名getUserInfo
2. **目录结构**
- components组件
- views页面
- api接口定义
- utils工具函数
- store状态管理
3. **编码风格**
- 使用TypeScript进行类型检查
- 组件使用组合式API (Composition API)
- 样式使用SCSS并遵循BEM规范
### 4.3 常见代码示例
#### 4.3.1 控制器规范示例
```java
/**
* 用户信息控制器
*
* @author ruoyi-vue-plus
*/
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController {
@Autowired
private ISysUserService userService;
/**
* 获取用户列表
*
* @param user 查询参数
* @return 用户列表
*/
@SaCheckPermission("system:user:list")
@GetMapping("/list")
public TableDataInfo<SysUserVo> list(SysUserBo user) {
startPage();
List<SysUserVo> list = userService.selectUserList(user);
return getDataTable(list);
}
/**
* 新增用户
*
* @param user 用户信息
* @return 结果
*/
@SaCheckPermission("system:user:add")
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@PostMapping
public R<Void> add(@Validated @RequestBody SysUserBo user) {
return toAjax(userService.insertUser(user));
}
}
```
#### 4.3.2 服务接口和实现规范示例
```java
/**
* 用户服务接口
*
* @author ruoyi-vue-plus
*/
public interface ISysUserService {
/**
* 查询用户列表
*
* @param user 查询参数
* @return 用户列表
*/
List<SysUserVo> selectUserList(SysUserBo user);
/**
* 新增用户
*
* @param user 用户信息
* @return 结果
*/
int insertUser(SysUserBo user);
}
/**
* 用户服务接口实现
*
* @author ruoyi-vue-plus
*/
@Service
public class SysUserServiceImpl implements ISysUserService {
@Autowired
private SysUserMapper userMapper;
/**
* 查询用户列表
*
* @param user 查询参数
* @return 用户列表
*/
@Override
public List<SysUserVo> selectUserList(SysUserBo user) {
return userMapper.selectUserList(user);
}
/**
* 新增用户
*
* @param user 用户信息
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertUser(SysUserBo user) {
// 业务逻辑实现
return userMapper.insert(user.toEntity());
}
}
```
#### 4.3.3 实体类规范示例
```java
/**
* 用户实体类
*
* @author ruoyi-vue-plus
*/
@Data
@TableName("sys_user")
@KeySequence("sys_user_seq")
public class SysUser extends TenantEntity {
/**
* 用户ID
*/
@TableId(value = "user_id", type = IdType.ASSIGN_ID)
private Long userId;
/**
* 用户账号
*/
private String userName;
/**
* 用户昵称
*/
private String nickName;
/**
* 用户类型sys_user系统用户
*/
private String userType;
/**
* 手机号码
*/
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phonenumber;
/**
* 帐号状态0正常 1停用
*/
private String status;
}
```
#### 4.3.4 前端页面示例 (Vue3 + TS)
```typescript
<template>
<div class="app-container">
<!-- 搜索表单 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true">
<el-form-item label="用户名称" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮区域 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
v-hasPermi="['system:user:add']"
@click="handleAdd"
>新增</el-button>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="userList">
<el-table-column label="用户编号" prop="userId" />
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" prop="phonenumber" :show-overflow-tooltip="true" />
<el-table-column label="状态" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
type="text"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:user:edit']"
>修改</el-button>
<el-button
type="text"
@click="handleDelete(scope.row)"
v-hasPermi="['system:user:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { listUser, delUser } from '@/api/system/user';
// 定义数据
const loading = ref(false);
const total = ref(0);
const userList = ref([]);
const queryFormRef = ref<any>(null);
// 查询参数
const queryParams = ref({
pageNum: 1,
pageSize: 10,
userName: undefined,
phonenumber: undefined,
});
/** 查询用户列表 */
function getList() {
loading.value = true;
listUser(queryParams.value).then(response => {
userList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
queryFormRef.value.resetFields();
handleQuery();
}
/** 新增按钮操作 */
function handleAdd() {
// 实现新增逻辑
}
/** 修改按钮操作 */
function handleUpdate(row) {
// 实现修改逻辑
}
/** 删除按钮操作 */
function handleDelete(row) {
// 实现删除逻辑
}
onMounted(() => {
getList();
});
</script>
```
## 5. 二次开发指南
### 5.1 开发环境准备
1. **基础环境**
- JDK 17/21
- Maven 3.8+
- Node.js 16+
- MySQL 8.0+/Oracle/PostgreSQL/SQLServer
- Redis 5.0+
2. **IDE推荐**
- IntelliJ IDEA后端
- VSCode前端
### 5.2 开发规范与原则
1. **遵循现有架构**
- 保持与现有代码风格一致
- 保持模块的独立性和可插拔性
- 不随意修改核心模块代码
2. **扩展而非修改**
- 通过扩展现有组件实现功能
- 避免直接修改框架核心代码
3. **注重代码质量**
- 编写单元测试
- 遵循代码规范
- 注释完善
### 5.3 增加PMS模块实施步骤
以下是添加一个产品管理系统(PMS)模块的具体步骤:
#### 5.3.1 后端开发
1. **创建模块结构**
```
ruoyi-modules
└── ruoyi-pms -- PMS模块
├── src/main/java/org/dromara/pms
│ ├── controller -- 控制器
│ ├── domain -- 实体类
│ │ ├── bo -- 业务对象
│ │ ├── entity -- 数据库实体
│ │ └── vo -- 视图对象
│ ├── mapper -- MyBatis接口
│ └── service -- 服务实现
└── src/main/resources
├── mapper -- MyBatis XML
└── i18n -- 国际化资源
```
2. **配置模块POM**
- 创建pom.xml添加必要依赖
- 在父模块中添加新模块引用
3. **创建数据库表**
- 设计表结构
- 编写SQL脚本
4. **开发核心功能**
- 使用代码生成器生成基础CRUD代码
- 扩展实现具体业务逻辑
- 添加权限控制
- 实现多租户和数据权限
- 添加接口文档注释
#### 5.3.2 前端开发
1. **创建模块目录**
```
ruoyi-plus-soybean/src/views/pms
├── product -- 产品管理
├── category -- 分类管理
└── inventory -- 库存管理
```
2. **API接口定义**
```
ruoyi-plus-soybean/src/service/api/pms
├── product.ts -- 产品API
├── category.ts -- 分类API
└── inventory.ts -- 库存API
```
3. **配置路由**
- 在router/routes目录下创建pms.ts
- 在router/index.ts中导入并注册路由
4. **开发页面组件**
- 列表页面
- 表单页面
- 详情页面
5. **添加权限控制**
- 配置菜单与按钮权限
- 实现页面级与按钮级权限控制
#### 5.3.3 集成与测试
1. **数据库脚本集成**
- 将建表SQL添加到初始化脚本中
2. **菜单配置**
- 通过系统管理-菜单管理添加PMS模块菜单
- 配置菜单权限
3. **角色授权**
- 为相关角色分配PMS模块权限
4. **单元测试**
- 编写API测试
- 编写服务层测试
5. **集成测试**
- 测试完整业务流程
- 测试与其他模块的交互
### 5.4 使用框架核心功能的最佳实践
#### 5.4.1 使用多租户
1. **实体类继承TenantEntity**
```java
@Data
@TableName("pms_product")
public class PmsProduct extends TenantEntity {
// 实体字段定义
}
```
2. **多租户过滤配置**
```java
@Configuration
public class TenantConfig {
/**
* 配置需要进行多租户过滤的表
*/
@Bean
public TenantLineHandler tenantLineHandler() {
return new PlusTenantLineHandler() {
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 配置不需要过滤的表
return ArrayUtil.contains(IGNORE_TENANT_TABLES, tableName);
}
};
}
}
```
#### 5.4.2 使用数据权限
1. **在Mapper接口上使用注解**
```java
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
public interface SysUserMapper extends BaseMapperPlus<SysUserMapper, SysUser, SysUserVo> {
// 方法定义
}
```
2. **在查询方法上使用注解**
```java
@Override
@DataScope(userAlias = "u", deptAlias = "d")
public List<SysUserVo> selectUserList(SysUserBo user) {
return baseMapper.selectUserList(user);
}
```
#### 5.4.3 使用数据脱敏
```java
public class UserVo {
// 手机号码脱敏
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phonenumber;
// 邮箱脱敏
@Sensitive(strategy = SensitiveStrategy.EMAIL)
private String email;
// 身份证脱敏
@Sensitive(strategy = SensitiveStrategy.ID_CARD)
private String idCard;
}
```
#### 5.4.4 使用Excel导入导出
```java
@Data
@ExcelIgnoreUnannotated
public class UserImportVo {
@ExcelProperty(value = "用户编号")
private Long userId;
@ExcelProperty(value = "用户名称")
@ExcelRequired
private String userName;
@ExcelProperty(value = "用户昵称")
@ExcelRequired
private String nickName;
@ExcelProperty(value = "手机号码")
@ExcelRequired
private String phonenumber;
@ExcelProperty(value = "邮箱")
private String email;
}
@RestController
public class UserController {
/**
* 导出用户
*/
@SaCheckPermission("system:user:export")
@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, SysUserBo user) {
List<SysUserVo> list = userService.selectUserList(user);
ExcelUtil.exportExcel(list, "用户数据", SysUserVo.class, response);
}
/**
* 导入用户
*/
@SaCheckPermission("system:user:import")
@Log(title = "用户管理", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception {
ExcelResult<UserImportVo> result = ExcelUtil.importExcel(file, UserImportVo.class);
userService.importUser(result.getList(), updateSupport);
return R.ok();
}
}
```
#### 5.4.5 使用接口幂等性控制
```java
@RestController
@RequestMapping("/system/user")
public class SysUserController {
/**
* 新增用户
*/
@SaCheckPermission("system:user:add")
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@RepeatSubmit(interval = 5000) // 5秒内不允许重复提交
@PostMapping
public R<Void> add(@Validated @RequestBody SysUserBo user) {
return toAjax(userService.insertUser(user));
}
}
```
#### 5.4.6 使用Redis缓存
```java
@Service
public class SysConfigServiceImpl implements ISysConfigService {
/**
* 查询参数配置信息
*
* @param configId 参数配置ID
* @return 参数配置信息
*/
@Override
@Cacheable(cacheNames = CacheNames.SYS_CONFIG, key = "#configId")
public SysConfig selectConfigById(Long configId) {
return baseMapper.selectById(configId);
}
/**
* 新增参数配置
*
* @param config 参数配置信息
* @return 结果
*/
@Override
@CachePut(cacheNames = CacheNames.SYS_CONFIG, key = "#config.configId")
public int insertConfig(SysConfig config) {
return baseMapper.insert(config);
}
/**
* 删除参数配置
*
* @param configId 参数ID
* @return 结果
*/
@Override
@CacheEvict(cacheNames = CacheNames.SYS_CONFIG, key = "#configId")
public int deleteConfigById(Long configId) {
return baseMapper.deleteById(configId);
}
}
```
## 6. 总结
RuoYi-Vue-Plus是一个功能完善、架构清晰的企业级应用开发框架其插件化设计和模块化结构使其非常适合二次开发。在进行二次开发时应当遵循项目的架构设计和编码规范通过扩展而非修改的方式实现业务需求确保系统的可维护性和可扩展性。
通过合理利用项目提供的代码生成、多租户、权限控制等特性,可以大幅提高开发效率,专注于业务逻辑的实现,而非底层架构的搭建。
## 7. 常见问题与解决方案
### 7.1 多租户问题
**问题**:如何对特定表或操作排除多租户过滤?
**解决方案**
- 全局排除在TenantConfig中的ignoreTable方法中添加表名
- 局部排除:使用@TenantIgnore注解标注在方法或类上
```java
@TenantIgnore
public List<SysTenant> selectTenantList(SysTenant tenant) {
return baseMapper.selectList(buildQueryWrapper(tenant));
}
```
### 7.2 权限问题
**问题**:如何实现细粒度的数据权限控制?
**解决方案**
- 使用@DataScope注解并配置用户、部门别名
- 在Mapper层使用@DataPermission定义权限字段
- 在XML中引用权限过滤片段
### 7.3 分布式事务问题
**问题**:如何处理跨服务的分布式事务?
**解决方案**
- 对于强一致性需求使用Seata进行分布式事务管理
- 对于最终一致性需求,考虑使用本地消息表+定时任务或消息队列实现
- 避免长事务,将业务拆分成多个小事务
### 7.4 性能优化问题
**问题**:系统运行缓慢,如何优化?
**解决方案**
- 使用Redis缓存热点数据减少数据库访问
- 优化SQL查询避免全表扫描
- 使用分页查询代替全量查询
- 合理使用索引
- 利用多级缓存(本地缓存+Redis缓存
- 考虑使用读写分离或分库分表