Ruoyi-Vue-Plus多租户实战:从零构建企业级后台管理系统

张开发
2026/4/15 0:36:27 15 分钟阅读

分享文章

Ruoyi-Vue-Plus多租户实战:从零构建企业级后台管理系统
1. 项目缘起为什么选择Ruoyi-Vue-Plus去年我们团队接了一个新项目要为一家做在线教育SaaS的公司搭建后台管理系统。客户的核心诉求很明确他们希望一套系统能同时服务几十家甚至上百家不同的培训机构每家机构的数据必须完全隔离互不可见但功能模块和界面又要保持一致方便他们统一升级和维护。说白了就是要一个多租户的SaaS平台。技术选型会上我们对比了几个主流方案。自己从零搭建一套多租户架构光是设计数据隔离方案、权限体系、租户上下文传递这些底层逻辑至少就得投入两三个月还不算后续的维护成本。用现成的开源框架呢要么功能太简单要么文档不全社区也不活跃踩了坑都没地方问。直到我们发现了Ruoyi-Vue-Plus。说实话若依Ruoyi系列在国内Java开发者圈子里名气不小但之前的版本在多租户支持上总觉得差那么点意思。而这个Plus版本可以说是专门为“企业级”和“多租户”这两个关键词做了深度强化。它内置了一套非常成熟的多租户解决方案从数据库层面的数据隔离到应用层的租户上下文管理再到前端路由的权限过滤都给你安排得明明白白。官方文档里明确写着支持“共享数据库、共享数据表、隔离数据行”的模式这正是我们SaaS场景最需要的。更吸引我们的是它的架构清晰度。后端模块划分得像教科书一样标准admin管全局登录鉴权common放通用工具modules里按业务功能划分比如系统管理、AI模块等等。这种结构对我们这种需要快速迭代、不断新增租户专属功能的项目来说维护起来特别顺手。前端用的Vue 3和Element Plus也是当前的主流技术栈团队上手几乎没有学习成本。最终我们拍板决定就用它了。接下来的内容我就结合这个真实项目带你一步步走通用Ruoyi-Vue-Plus构建多租户系统的全过程分享我们踩过的坑和填坑的经验。2. 从零开始环境搭建与项目初始化工欲善其事必先利其器。第一步就是把项目跑起来别看是基础步骤这里面的小细节决定了你后续开发的顺畅度。2.1 拉取代码与基础环境准备首先你得把代码弄到本地。Ruoyi-Vue-Plus的代码托管在Gitee上直接用Git克隆下来就行。我建议你直接克隆官方仓库别去下那些不知道改了多少手的“优化版”后期升级和排查问题会非常头疼。git clone https://gitee.com/dromara/RuoYi-Vue-Plus.git项目拉下来后先别急着启动。打开后端工程找到ruoyi-admin模块下的src/main/resources目录这里有几个关键的配置文件。application.yml是总入口它会根据你的激活环境比如dev开发环境去加载对应的配置。通常我们开发时修改application-dev.yml就够了。这里有个新手必踩的坑数据库和Redis的配置。框架默认的配置是连本地的MySQL和Redis。你必须确保本地已经安装并启动了这两个服务。MySQL版本建议5.7或8.0Redis版本别太低。配置文件中关于数据库连接的部分长这样spring: datasource: dynamic: primary: master datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ry-cloud?useUnicodetruecharacterEncodingutf8zeroDateTimeBehaviorconvertToNulluseSSLtrueserverTimezoneGMT%2B8 username: root password: 123456你得把url、username、password改成你自己数据库的信息。数据库名ry-cloud可以保留启动时框架会自动执行初始化SQL脚本来建表。Redis的配置也在同一个文件里如果Redis有密码记得填上。2.2 前端环境配置与启动前端项目在ruoyi-ui目录下。现在前端生态日新月异Ruoyi-Vue-Plus选择了pnpm作为包管理工具这比传统的npm更快、更省磁盘空间。如果你没安装过pnpm需要先全局安装一下npm install -g pnpm然后进入前端目录安装依赖。这个过程可能会因为网络问题慢一些可以配置淘宝镜像源来加速。cd ruoyi-ui pnpm install依赖安装成功后就可以启动前端开发服务器了pnpm dev如果一切顺利命令行会输出本地访问地址通常是http://localhost:80。浏览器打开这个地址你应该能看到若依的登录界面。这时候后端服务也需要启动。在IDE里找到RuoYiApplication这个主启动类直接运行。看到控制台打印出熟悉的Spring Boot启动图案并且没有报错就说明后端也启动成功了。至此一个最基础的单体架构版Ruoyi-Vue-Plus项目就在你本地跑起来了。默认的账号密码是admin/admin123。登录进去后你可以先熟悉一下系统的各个功能模块和界面风格这对后续开发自己的业务模块很有帮助。3. 核心实战多租户配置与数据隔离项目跑起来只是第一步接下来才是重头戏配置多租户。Ruoyi-Vue-Plus的多租户实现得非常优雅它没有把复杂性暴露给业务开发者而是通过一些配置和约定让租户隔离自动生效。3.1 理解多租户的数据隔离模式多租户的数据隔离一般有三种模式独立数据库每个租户一个数据库。隔离性最好但成本高。共享数据库独立Schema所有租户共享一个数据库实例但各有自己的表空间Schema。折中方案。共享数据库共享数据表所有租户的数据都存在同一套表里通过一个tenant_id字段来区分。成本最低也是Ruoyi-Vue-Plus默认采用的模式。我们项目采用的正是第三种。它的好处是运维简单一套表结构走天下。但这就要求你在每张业务表上都加上tenant_id字段并且在执行任何CRUD操作时都必须自动带上这个租户条件。框架帮我们做好了后者我们只需要做好前者。3.2 数据库表设计与通用字段当你需要新建一张业务表时比如我们要做一个“课程管理”模块表结构除了你自己的业务字段外必须包含一套租户相关的通用字段。框架提供了一套标准的SQL语句模板你每次建表都可以复用CREATE TABLE edu_course ( course_id bigint NOT NULL COMMENT 课程ID, course_name varchar(255) NOT NULL COMMENT 课程名称, -- ... 其他你的业务字段 ... -- 以下是必须添加的通用字段 tenant_id varchar(20) DEFAULT NULL COMMENT 租户ID, create_dept bigint DEFAULT NULL COMMENT 创建部门, create_by bigint DEFAULT NULL COMMENT 创建者, create_time datetime DEFAULT NULL COMMENT 创建时间, update_by bigint DEFAULT NULL COMMENT 更新者, update_time datetime DEFAULT NULL COMMENT 更新时间, del_flag char(1) DEFAULT 0 COMMENT 删除标志0存在 2删除, PRIMARY KEY (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表;为什么这些字段如此重要tenant_id数据隔离的灵魂。框架的MyBatis-Plus插件会自动在查询语句后面加上AND tenant_id ?确保每个租户只能看到自己的数据。create_by,update_by记录操作人用于审计追踪。create_time,update_time记录操作时间。del_flag实现逻辑删除。删除数据时只是标记而不是物理删除方便数据恢复。我强烈建议你把这个建表语句存成代码片段。在IDEA里你可以把它设置为“Live Template”以后输入tenant就能自动补全这段字段能省下大量重复敲代码的时间。3.3 后端配置与租户上下文管理光有数据库字段还不够我们需要告诉框架如何识别当前是哪个租户在操作。这主要通过租户上下文Tenant Context来实现。在Ruoyi-Vue-Plus中用户的租户信息通常在登录后就保存在其登录凭证Token和Session里。当用户发起一个请求时框架的拦截器会从请求头或Token中解析出租户ID并将其设置到当前线程的上下文中。这个上下文工具类就是TenantHelper。在我们实际开发中绝大多数情况下你不需要手动操作TenantHelper框架已经帮你处理好了。但是有一种情况你必须干预当你新启动一个异步线程去执行任务时。因为租户上下文是基于ThreadLocal实现的而ThreadLocal的值在线程间不共享。新线程里获取不到父线程的租户ID。比如你在一个Controller方法里需要调用一个耗时的AI处理任务并把它丢到线程池去执行。在提交任务到线程池之前你必须先“捕获”当前的租户上下文然后在新线程的任务开始处“还原”它。代码大概长这样// 在父线程如Controller方法中 String currentTenantId TenantHelper.getTenantId(); CompletableFuture.runAsync(() - { // 在新线程中第一步就是设置租户上下文 TenantHelper.setTenantId(currentTenantId); // 然后再执行你的业务逻辑比如调用AI服务 aiService.processTask(...); }, executor);这个细节非常关键忘记设置的话新线程里的数据库操作就可能因为找不到租户ID而报错或者更糟查询到其他租户的数据造成严重的数据泄露问题。我们在开发初期就因为这个坑排查了大半天。4. 业务开发实战以AI模块为例理论讲完了我们来看一个具体的业务模块开发例子。假设我们要在系统中集成一个AI对话功能每个租户的管理员都可以和自己的AI助手聊天并且对话历史要按租户隔离。4.1 模块创建与基础编码首先在ruoyi-modules下新建一个模块比如叫ruoyi-ai。模块的pom.xml继承父工程并引入必要的依赖比如Web、MyBatis-Plus以及可能用到的AI SDK如LangChain4j。接着遵循Ruoyi的规范创建controller、service、mapper和entity包。你的实体类AiChatHistory必须包含前面提到的那些通用字段。Service层编写业务逻辑Mapper层继承框架提供的BaseMapperPlus它会自动获得多租户查询的能力。在Controller里你只需要像开发普通CRUD接口一样写就行。框架的全局拦截器会自动在查询条件里注入tenant_id。例如你写一个查询当前用户聊天历史的接口GetMapping(/list) public TableDataInfo list(AiChatHistory history) { startPage(); // 启动分页 ListAiChatHistory list aiChatHistoryService.selectAiChatHistoryList(history); return getDataTable(list); // 返回的数据自动只包含当前租户的 }你完全不用在SQL里写where tenant_id #{tenantId}框架的MyBatis-Plus插件已经默默加上了。这种“无感”的体验才是优秀框架该有的样子。4.2 多线程环境下的用户信息传递AI对话通常涉及流式响应我们可能会用到类似LangChain4j的TokenStream这种工具它会在回调函数如onNext中异步返回数据。这些回调函数往往执行在另一个线程里。这就回到了我们之前提到的问题新线程里如何获取用户信息仅仅设置TenantHelper可能还不够因为业务逻辑里可能还需要当前的登录用户对象LoginUser。Ruoyi-Vue-Plus的用户登录状态是由Sa-Token管理的。你需要把用户信息也传递过去。我们的做法是这样的public void streamChat(String message) { // 1. 在主线程请求线程中获取当前登录用户和租户信息 LoginUser loginUser LoginHelper.getLoginUser(); String tenantId loginUser.getTenantId(); // 2. 调用AI服务传入用户信息和租户信息 aiService.streamResponse(message, loginUser.getUserId(), tenantId, new StreamingResponseCallback() { Override public void onNext(String token) { // 3. 在回调线程中重新设置用户上下文和租户上下文 // 关键步骤模拟用户登录状态设置Token会话 StpUtil.login(loginUser.getUserId()); StpUtil.getTokenSession().set(LoginHelper.LOGIN_USER_KEY, loginUser); // 关键步骤设置租户上下文 TenantHelper.setTenantId(tenantId); // 4. 现在可以安全地执行需要用户和租户信息的业务逻辑了 // 例如将流式返回的token片段存入数据库需要tenant_id和create_by aiChatHistoryService.saveStreamToken(token, loginUser); } Override public void onComplete() { // 同样需要设置上下文然后处理完成逻辑 StpUtil.login(loginUser.getUserId()); TenantHelper.setTenantId(tenantId); // ... 完成后的业务处理 } }); }这段代码是核心中的核心。它确保了即使在异步、多线程的复杂场景下租户隔离和用户权限依然有效。我们第一次实现AI对话时就是因为忘了在onNext回调里设置TenantHelper导致所有用户的聊天记录都混在了一起闹了个大笑话。4.3 Redis缓存与租户键前缀Ruoyi-Vue-Plus对Redis的使用也做了多租户适配。它通过一个RedisKeyPrefixResolverRedis键前缀解析器来实现。简单说就是当你用框架封装的RedisUtils.set(“key”, value)存数据时它会自动在 key 前面拼接上当前租户的ID形成类似tenant:1:user:100这样的最终键名。这带来了一个便利你不需要在业务代码里手动拼接租户ID。但也带来了一个常见的坑当你需要手动清除某个缓存或者在其他地方比如定时任务、消息监听器操作Redis时如果直接使用原始的key是找不到数据的因为实际存储的key有前缀。解决方案就是在任何需要显式操作Redis key的地方都使用TenantHelper.setDynamic(tenantId)来临时指定租户上下文或者直接使用框架提供的、已经处理好前缀的工具方法。例如在定时任务中清理某个租户的缓存Component public class CleanCacheTask { Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void cleanTenantCache() { // 假设这里从数据库获取所有租户列表 ListTenant tenantList tenantService.list(); for (Tenant tenant : tenantList) { // 为每个租户设置上下文 TenantHelper.setDynamic(tenant.getTenantId()); // 现在使用RedisUtils删除的key会自动带上当前租户的前缀 RedisUtils.deleteObject(“cache:some_data”); // 清理完后可以清除动态设置避免影响后续逻辑如果后续逻辑依赖线程上下文 TenantHelper.clearDynamic(); } } }5. 开发提效与避坑指南项目配置和核心功能都搞定后我们来聊聊怎么让开发过程更丝滑以及如何避开那些我们曾经掉进去的“坑”。5.1 热部署与插件推荐Java后端开发最烦人的一点就是改完代码要重启服务尤其是调试前端联调时频繁重启非常耗时。这里我强烈安利两个IDEA插件JRebel和Lombok。JRebel是一款热部署神器。安装配置好后你修改了Java代码只需要按CtrlShiftF9编译修改的类JRebel就能在几秒钟内将更改热更新到正在运行的应用中无需重启整个Spring Boot应用。这对调试Service层、Controller层的逻辑效率提升是巨大的。当然一些结构性修改如增删方法签名、修改配置文件等还是需要重启。Lombok则通过注解帮你自动生成Getter、Setter、构造方法等样板代码。在实体类Entity上用一个Data注解就能省去一大堆get/set方法让代码更简洁。Ruoyi-Vue-Plus项目默认就集成了Lombok记得在IDEA里安装插件并启用注解处理Enable annotation processing否则编译会报错。5.2 前端开发注意事项前端方面Ruoyi-Vue-Plus基于Vue 3和Element Plus技术栈比较新。有几点需要注意API调用前端请求后端接口使用的是封装好的axios实例它已经自动处理了请求头如携带Token、响应拦截和错误处理。你一般不需要自己再写一套。权限控制前端页面的路由和按钮权限是与后端联动的。系统管理里可以配置菜单和权限标识。在前端组件中可以使用v-hasPermi指令来控制一个按钮是否显示。例如el-button v-hasPermi“[system:user:add]”新增/el-button。多租户前端感知对于普通业务页面前端开发者通常无需关心多租户。但如果你开发的是一个系统管理级别的功能比如“租户管理”页面那么你需要调用对应的租户管理接口。这些接口通常不在tenant_id过滤范围内框架有特殊的注解如IgnoreTenant来标记。5.3 常见问题排查查询不到数据或查到所有租户数据首先检查你的业务表是否正确添加了tenant_id字段。其次检查你的Mapper接口是否继承了框架的BaseMapperPlus或正确的父类。最后在调试模式下打开SQL日志看看最终执行的SQL语句中是否包含了tenant_id ?这个条件。新增数据时报错提示租户ID为空检查你的实体类对象在插入前其tenantId属性是否被正确赋值。通常这个值是在Service层通过LoginHelper.getLoginUser().getTenantId()获取并设置的。框架也可能提供了自动填充处理器MetaObjectHandler你需要确认它是否正常工作。异步任务中数据混乱反复检查在异步线程如线程池任务、Async方法、消息队列监听器的开头是否正确设置了TenantHelper.setTenantId(...)和用户登录上下文。这是多租户开发中最容易出错的地方。Redis缓存错乱确认你的Redis操作是否都使用了框架的RedisUtils。如果需要在非请求线程如定时任务中操作缓存务必使用TenantHelper.setDynamic()来指定租户上下文。6. 项目部署与上线考量当功能开发测试完毕准备部署上线时针对多租户SaaS系统还有一些额外的考量点。6.1 数据库规划与优化虽然我们采用共享数据表模式但随着租户数量和业务数据增长单表数据量可能会非常大。你需要提前规划分库分表当单个数据库实例压力过大时可以考虑按租户ID进行分库。Ruoyi-Vue-Plus的动态数据源功能可以支持但配置较为复杂。索引优化务必为tenant_id字段以及tenant_id与其他常用查询条件的组合字段建立索引。例如(tenant_id, create_time)是一个常见的组合索引用于查询某个租户下按时间排序的数据。数据归档制定老旧数据如3年前的日志、操作记录的归档或清理策略避免主表无限膨胀。6.2 配置管理与租户初始化每个租户入驻你的SaaS平台时可能需要一些初始化的数据比如默认的角色、权限、配置参数等。你需要在“租户管理”功能中提供一个“初始化租户”或“创建租户”的流程。这个流程背后可能涉及到在sys_tenant表插入一条新记录。为该租户初始化一套默认的角色和菜单权限通常通过复制一套预设模板实现。初始化一些该租户独有的配置项。这些操作最好在一个数据库事务内完成保证数据一致性。同时要考虑并发创建租户时的性能问题。6.3 监控与运维上线后监控必不可少租户级监控除了系统整体的CPU、内存、数据库连接数监控最好能实现租户级别的资源使用统计如API调用次数、数据存储量。这既能用于计费也能及时发现某个租户的异常行为如遭到攻击导致请求激增。日志隔离确保应用日志能清晰区分不同租户的操作。可以在日志模式中统一加入[tenantId:xxx]这样的标识。Ruoyi-Vue-Plus的日志框架通常已经集成了MDCMapped Diagnostic Context可以很方便地将租户ID放入上下文在日志中自动打印。备份与恢复虽然数据存在一起但备份和恢复策略最好能支持按租户粒度进行。这样当某个租户误删数据时你可以单独恢复他的数据而不影响其他租户。回顾整个从零到一的过程Ruoyi-Vue-Plus框架确实大大加速了我们这类多租户SaaS后台管理系统的开发。它把复杂的数据隔离、权限体系封装起来让我们能更专注于业务逻辑的实现。当然没有完美的框架深入使用后你可能会根据自身业务需求对其进行定制或扩展。但无论如何它提供了一个非常坚实和规范的起点。

更多文章