☰
若依微服务多租户新模块创建指南:从modules结构到租户隔离
2026/10/5 7:11:29 网站建设 项目流程

1. 开始前必须理解的工程结构与租户模型

1.1 modules目录里到底放了什么

用若依做过二开的人大概都有印象:单体版什么都在一个工程里,前后端分离版是ruoyi-admin、ruoyi-system、ruoyi-framework这几个模块;到了 Cloud 版,代码被拆成了ruoyi-gateway、ruoyi-auth、ruoyi-modules、ruoyi-visual等几大块。我们要聊的就是ruoyi-modules,它是微服务架构下的“业务模块集中营”,里面默认躺着ruoyi-system(系统管理)、ruoyi-job(定时任务)、ruoyi-file(文件服务)、有的版本还有ruoyi-gen(代码生成)和ruoyi-demo(示例)。

每个业务模块的结构高度一致,一般是api子工程加业务工程两层。api子工程专门放对外暴露的 Feign 接口、DTO 和常量,业务工程里才是真正的 Controller、Service、Mapper。这样做的好处是:别的模块要调你的服务,只需要依赖ruoyi-xxx-api,不用把你的 Mapper、Service 全拖过去,微服务之间的调用链干净得多。

所以“在 modules 中创建子模块”这件事,本质上是两件事:第一,在 Maven 层面追加一个业务工程并注册到父级;第二,让这个业务工程具备若依微服务模块该有的“基础设施”,包括 Nacos 注册、配置中心拉取、多租户拦截、统一鉴权、网关路由。这两件事没做齐,模块就跑不起来。

1.2 多租户在若依里是怎么实现的

多租户听起来很高大上,落到若依这套代码里,核心机制其实不复杂:每个需要租户隔离的业务表都带一个tenant_id字段,MyBatis 的拦截器会在执行 SQL 的时候自动追加WHERE tenant_id = ?,把当前登录用户所属的租户 id 拼进查询条件里。这个“当前租户 id”从哪来?前端登录后会把租户信息存起来,后续请求带过来,网关和认证中心解析后塞进上下文,业务模块再从中取。

偷懒点说,你在若依多租户版里建表时只要注意三件事:加tenant_id字段、建好租户字段的索引、实体上继承或标注租户逻辑。代码层面的隔离,MyBatis 拦截器基本替你干完了。这也是为什么一模一样的一套代码,一个租户登录进去只能看到自己的数据——不是靠业务代码里写死WHERE,而是框架层在做统一过滤。

不过拦截器并不是对所有表都生效。系统级别的表,比如sys_menu、sys_config、sys_dict_type这些,不能做租户隔离,否则每个租户登录后连菜单都加载不出来。所以若依提供了忽略表配置,在配置文件里通过tenant.ignore-tables列出来,拦截器碰到这些表就直接跳过。你新建模块时,如果业务表不需要租户隔离,也把它加到这个忽略清单里。

1.3 为什么新业务必须放进 modules,而不是在 ruoyi-system 里硬塞

我见过不少人图省事,把业务代码直接往ruoyi-system里塞,controller 和用户的 controller 放同一个包,觉得少建一个模块少很多麻烦。短期看是省事了,长期看全是坑。

第一个坑是职责混乱。ruoyi-system管的是用户、角色、菜单、字典这类基础数据,属于框架级能力。你往里面塞“订单管理”“设备台账”这种业务功能,系统模块会越滚越大,每次升级若依版本时冲突一大堆,别人接手也看不懂哪些是框架的、哪些是你们自己加的。

第二个坑是发布粒度。微服务最大的价值之一就是独立部署、独立扩容、故障隔离。如果所有业务都塞进 system 服务,哪怕你只是改了个订单查询,也得把整个 system 服务重新发一遍,出问题影响面也会扩大。拆成独立子模块后,新模块的发布、回滚、限流都能单独控制。

第三个坑是团队协作。多人开发时,大家往同一个模块提交代码,合并冲突会非常频繁。而独立模块就是天然的代码边界,每个人负责自己的模块,互不干扰。所以只要你的团队规模超过两三个人,或者业务线比较独立,我都建议规规矩矩在modules下新建子模块,而不是“先塞进去再说”。

2. 新建子模块前要定好的三件事

2.1 命名:包名、服务名、路由前缀怎么统一

建模块之前,先把名字定下来,免得后面改来改去。我的习惯是业务用英文名作为模块名,例如设备管理就是ruoyi-device,包名com.ruoyi.device,Nacos 服务名ruoyi-device,网关路由前缀/device/**。四个地方的命名保持同一个词根,调试时不会被绕晕。

这里有个容易踩的细节:若依的网关路由配置里,predicates 路径最好和服务名弱相关,但不要完全无关。比如你服务名是ruoyi-device,网关路由却写成/equipment/**,前端联调时你就得一直惦记这个映射关系,时间长了必然有人记错。

另外多模块场景下,模块名的ruoyi-前缀建议保留。这是若依的约定,Nacos 服务列表里能一眼识别出哪些是框架服务、哪些是业务服务。网关配置、权限配置、日志系统里也都默认按这个前缀做了不少约定,你换个前缀就得手动处理一堆隐藏逻辑。

2.2 模块拆不拆 api:什么时候需要 ruoyi-xxx-api

新建模块时,第一步就得决定:只建一个业务工程,还是业务工程之外再建一个ruoyi-xxx-api。判断标准很简单:有没有别的模块需要调用你提供的接口。

最典型的场景是工作流模块要调“用户模块”查审批人信息,或者“订单模块”完成后需要通知“库存模块”扣减库存,这些都属于跨模块调用。既然要跨模块,被调方就需要提供一份“给外部看的接口契约”,也就是 Feign 接口。这份契约放在api子工程里,调用方依赖它,被调方实现它,两边只通过接口通信,不直接依赖对方的数据库和内部服务。

如果你确定这个模块做出来就是独立运行,短期内没有其他模块会调它,那可以先不建 api 子工程,只建业务工程。但以我自己的经验,凡是正经做企业级项目的模块,最后几乎都会遇到跨模块调用需求。与其后面再拆分重构,不如第一次就按“api + 业务”的双工程结构建好,api 里先放一个空接口占位也没关系,后面补实现成本低很多。

2.3 表结构与租户字段设计

建表是用代码生成器直接从数据库生成代码,还是在工程里手写建表 SQL?我的建议是:先用 SQL 把表设计好,再用若依的代码生成工具生成代码。核心原因在于,代码生成器读的是表结构,你表设计得越规范,生成的代码质量越高。

多租户表的字段有几个约定俗成的标配:主键id、租户字段tenant_id、审计字段create_by、create_time、update_by、update_time、逻辑删除标志del_flag。只要你的表带上了这些字段,生成的实体、Mapper、Service 基本都能自动沿用若依的 BaseEntity 和 BaseMapper 体系。

还有个小细节值得注意:tenant_id字段的类型,若依常见版本里是bigint,也有版本用varchar存字符串。不管哪种,新建表时一定要和你当前使用的若依版本保持一致,否则拦截器拼接 SQL 时类型对不上,轻则查不到数据,重则直接报错。我的建议是看一眼你们当前项目里sys_user表的租户字段类型,然后照抄。

3. 手把手:从空目录到可调用的完整步骤

3.1 在父工程中注册 module 并配置依赖

假设我们要新建一个“示例教学”模块,模块名定为ruoyi-demo。先在ruoyi-modules/pom.xml里把新模块注册进去:

<modules> <module>ruoyi-system</module> <module>ruoyi-demo</module> </modules>

同时确认父工程pom.xml的<dependencyManagement>里是不是已经引入了新模块的依赖坐标。如果没有,先加进去,比如:

<dependency> <groupId>com.ruoyi</groupId> <artifactId>ruoyi-demo</artifactId> <version>${ruoyi.version}</version> </dependency>

这里要注意版本号统一用项目里已有的${ruoyi.version},不要自己手写一个版本号,否则依赖版本冲突排查起来很头疼。

然后在ruoyi-modules目录下新建两个 Maven 工程:ruoyi-demo-api和ruoyi-demo。如果你的场景暂时不需要对外提供接口,可以只建ruoyi-demo一个工程。API 子工程的依赖很简单,一般只依赖ruoyi-common-core;业务工程的依赖则典型一套:ruoyi-common-security、ruoyi-common-mybatis、ruoyi-common-redis、ruoyi-common-log、ruoyi-demo-api。照抄ruoyi-system的 pom 做删减,是最不容易错的方式。

3.2 编写启动类与 bootstrap 配置

业务工程的入口类长这样:

package com.ruoyi.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication @MapperScan("com.ruoyi.demo.mapper") public class RuoYiDemoApplication { public static void main(String[] args) { SpringApplication.run(RuoYiDemoApplication.class, args); } }

@EnableDiscoveryClient是必须的,没有它服务不会注册进 Nacos。@MapperScan则要保证包路径写对,很多新模块启动后报“找不到 mapper”,十有八九是这里扫描路径配错了,或者 Mapper 接口所在的包不在扫描范围里。

接下来是配置文件,这里有个若依 Cloud 版最容易搞混的地方:本地的application.yml基本都是裸配置,真正的数据源、Redis、Nacos 地址、租户开关、日志级别这些全放在 Nacos 配置中心的“服务名.yaml”文件里,本地的bootstrap.yml只负责告诉应用“你去哪个 Nacos 拉配置”。所以新建模块后,你必须登录 Nacos 控制台,在配置管理里手动新增一个ruoyi-demo.yaml配置,把以下内容填进去:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ry-cloud?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root mybatis-plus: mapper-locations: classpath*:mapper/**/*Mapper.xml type-aliases-package: com.ruoyi.demo.domain tenant: enable: true ignore-tables: - sys_user - sys_menu - sys_config - sys_dict_type

端口号建议在现有模块端口号段之外顺延。比如你的ruoyi-system是 9201、ruoyi-file是 9300,那新模块可以分配 9401 这种,在 Nacos 配置文件里用server.port指定。如果和别的服务端口撞了,启动时会报端口占用,排查起来非常明显,所以不用太担心,但提前规划好端口分段会省很多事。

3.3 编写第一个带租户隔离的业务接口

工程骨架有了,接下来写一个最简单的带租户隔离的 CRUD。表结构可以先用现成的业务表做测试,也可以用下面这种简化的学生表:

CREATE TABLE demo_student ( id bigint not null auto_increment comment '主键', tenant_id bigint default null comment '租户ID', student_name varchar(50) not null comment '学生姓名', class_name varchar(50) default null comment '班级', create_by varchar(64) default '' comment '创建者', create_time datetime default null comment '创建时间', update_by varchar(64) default '' comment '更新者', update_time datetime default null comment '更新时间', del_flag char(1) default '0' comment '删除标志', primary key (id) ) engine=innodb comment='示例学生表';

实体类继承若依的BaseEntity,并在租户字段上标注@TableField映射。注意 MyBatis-Plus 的租户拦截器是靠着tenant_id字段名做匹配的,你在实体里不要把这个字段改名成别的,否则拦截器拼 SQL 时会找不到列。代码生成器如果手动生成的话,认准默认的 domain 模板就行。

Controller 部分,参考ruoyi-system里的写法,Controller 继承BaseController,Service 实现类里调用startPage()和getDataTable()这套若依封装好的分页逻辑,再在需要权限的接口上标注@PreAuthorize("@ss.hasPermi('demo:student:list')")。支撑这套注解的鉴权组件在ruoyi-common-security里,这也是刚才强调依赖不能省的原因。

3.4 注册 Nacos 与网关路由

启动新模块后,去 Nacos 的服务列表里应该能看到ruoyi-demo。看不到就先检查是不是@EnableDiscoveryClient没加,或者 Nacos 地址配置错了。服务注册好了,网关转发还没配上,前端请求依然进不来。

在ruoyi-gateway模块的配置文件里增加路由:

spring: cloud: gateway: routes: - id: ruoyi-demo uri: lb://ruoyi-demo predicates: - Path=/demo/** filters: - StripPrefix=1

路由id要全局唯一,uri用lb://前缀表示走 Nacos 负载均衡,核心对应关系是:前端只要发起/demo/xxx的请求,网关就会把它转发给ruoyi-demo服务。StripPrefix=1表示转发时去掉第一级前缀,也就是请求/demo/student/list经过网关后,后端收到的实际路径是/student/list。如果你的 Controller 映射路径本身带了/demo,那这行过滤器就去掉,否则会多剥一层路径导致 404。

网关配置改完需要重启网关服务,不是热加载就能生效的。这一点每次都要跟同事强调,十次有五次排查路由不生效,最后发现是网关没重启。

3.5 前端菜单与按钮权限接入

后端接口通了,还要让前端能进到这个模块的页面。若依前端的菜单是后端返回的,动态渲染,所以要走完一套“菜单初始化”的 SQL。

这里我给个可以直接执行的思路:先查sys_menu表,把自己新模块目录菜单的menu_id记下来,然后插入子菜单。核心字段包括:

  • menu_name:菜单显示名称
  • parent_id:父菜单 ID
  • order_num:排序号
  • path:前端路由路径,例如demo
  • component:前端组件路径,例如demo/student/index
  • menu_type:C代表菜单,F代表按钮
  • perms:权限标识,必须和@PreAuthorize里的字符串一致,例如demo:student:list
  • visible:显示状态,0表示显示
  • status:菜单状态,0表示正常

前端src/views/demo/student/index.vue页面要真实存在,否则菜单点进去白屏。组件路径写错是最常见的前端问题,排查时先看浏览器控制台有没有报“找不到组件”的错。

4. 多租户隔离的验证方法与常见翻车现场

4.1 如何验证租户过滤真的生效了

很多同学建完模块,用超管账号登录,看到数据是正常的,就以为租户隔离没毛病。这个验证方式是有问题的,因为超管账号在某些若依版本里会跳过租户过滤,你根本测不出真实效果。

正确姿势是:建两个租户,分别在两个租户下各建一条数据,然后用一个非超管的租户管理员账号登录,看它是否只能看到自己租户的数据。如果能看到别的租户的数据,说明拦截器没有生效,优先检查表里有没有tenant_id字段、租户开关是不是true、实体上有没有丢租户标注。

再补一刀:打开后端日志,把 MyBatis 打印出来的 SQL 捞出来看,确认是不是自动拼了AND tenant_id = ?。如果 SQL 里没有这个条件,说明拦截器压根没走到,问题在配置;如果条件有了但查出的数据还是不对,那问题就在数据本身,比如某条数据tenant_id存了 0 或者 NULL。

4.2 常见问题速查表

我把这几年在若依多租户模块创建中遇到的高频问题整理了一下,直接给结论:

症状常见原因解决办法
启动报“找不到 Nacos 配置”Nacos 上没创建对应的 yaml 配置登录 Nacos,新增服务名.yaml,并检查 bootstrap.yml 的 namespace/group 是否匹配
服务启动成功但 Nacos 列表里没有启动类缺@EnableDiscoveryClient补上注解并重启
接口报“找不到 Mapper”@MapperScan路径没覆盖到 mapper 包修改扫描路径为com.ruoyi.xxx.mapper,或直接在 Mapper 接口上标注@Mapper
分页不生效或列表不显示Controller 没继承BaseController确认继承关系,并调用startPage()再查询
查询能通但能看到别人租户数据业务表缺tenant_id或拦截器忽略表配置误伤给表补字段,检查 tenant.ignore-tables 是否包含该表
前端菜单能打开但接口 401权限标识不一致核对 sys_menu.perms 与 @PreAuthorize 里的标识
网关请求 404路由没重启或 StripPrefix 层级不对重启网关,检查路由断言与过滤器层级
前端白屏报找不到组件component 路径写错或目录不存在修正 sys_menu.component 字段,确认前端文件存在
跨模块 Feign 调用报无权限或租户丢失缺少租户上下文透传确认 Feign 拦截器已配置,必要时手动传递租户 id

表里列的问题,我基本都踩过一遍。其中最隐蔽的是最后一个:跨模块调用时的租户上下文传递。

4.3 复杂场景:Feign 跨模块调用时的租户传递

假设ruoyi-demo模块需要调用ruoyi-system的接口查用户信息,在请求传递过程中,租户 id 必须跟着请求头传到下游服务,否则下游查询时不知道当前是哪个租户,SQL 里tenant_id参数就是空的,要么查错数据,要么直接被拦截器挡掉。

若依自己的 Feign 拦截器在ruoyi-common-security里,正常配置时它会自动把请求头里的租户信息转发到下游。但如果你用了@Async异步线程、或者手动 new 了一个 Thread 去发起调用,上下文传递就会断掉。遇到这种情况,最稳妥的做法是在调用前手动从上下文中取出tenantId,拼进请求参数或新线程的上下文中。

还有一类情况是跨模块回调场景,比如订单模块处理完成后通知库存模块,库存模块又回调订单模块查状态,这种链路一长,任何一环丢失租户上下文,后面的查询全乱套。我的经验是:在模块入口做一个租户 id 的过滤器,统一从请求头解析并放入当前线程变量,而不是依赖分散在业务代码里的各种赋值。这样至少可以保证“进来的请求都带租户”,出问题最多是起点的源头丢了租户,追溯起来好查很多。

5. 版本差异与扩展建议

5.1 若依 plus、Vue3、TS 版本需要注意什么

现在很多人用的是ruoyi-vue-plus或者自己改造过的 Vue3 + TypeScript 版本,这些版本在模块创建上跟经典版有差别,但核心逻辑没有变。Plus 系列把系统管理拆得更细,MyBatis-Plus 的租户插件默认是开启的,也是靠TenantLineInnerInterceptor拼 SQL,只不过配置项名字可能不同,而且部分版本默认不开启租户,需要自己确认一下配置。

Vue3 + TS 版的前端在做菜单路由时,常见一个坑:meta类型推断报 TS 错误,比如meta.title提示可能为 undefined。这是因为动态路由表没有定义好类型接口。解决方式是在src/router相关位置找到路由元信息接口,把title、icon、hidden等字段声明成可选项即可。

MES、ERP 这类企业级项目,需要跟第三方服务集成时,我建议还是保持“若依只做管理和鉴权,重业务逻辑放独立服务”的模式。下面这种架构在项目里已经被验证过很多次:若依负责用户、组织、菜单、权限、审计,独立 Python 或 Go 服务负责图像识别、算法计算这种高频高算力任务,两者通过 HTTP 或消息队列通信,Python 服务从若依的接口获取认证信息,业务数据各自落库,必要时通过定时任务或事件回调同步。这样两边技术栈都能发挥优势,若依的更新升级也不会被业务算法牵连。新模块要接入这套架构,不需要改模块本身,只需要在新增业务模块里多写一个 Feign 接口,暴露给独立服务调用,或者在网关层做路由转发即可。

5.2 多租户的三种隔离级别,为什么默认选了行级隔离

建模块前还有必要想清楚一个问题:你需要的多租户是行级隔离、库级隔离,还是 Schema 级隔离。若依默认的是行级隔离,也就是所有租户共用一张表,靠tenant_id区分数据。这是一个成本和隔离性平衡后的选择,维护简单、升级方便、硬件资源占用小。

但如果你的客户有强合规要求,比如财务数据、医疗数据必须物理隔离,行级隔离就不够用了。这时候需要把动态数据源路由接进来,让每个租户对应独立的数据库,框架根据当前租户 id 路由到不同库。代价是连接管理、建库流程、备份恢复都会复杂不少。我的建议是,除非客户明确要求物理隔离,否则不要一上来就上库级隔离,先用行级隔离把业务跑通,等项目成熟了再演进。多租户模块的代码结构在这三种隔离模式下差别不大,核心都是“根据租户 id 确定数据范围”,只是在实现层面从 MyBatis 拦截器换成了数据源路由。

5.3 配置多模块并行开发的小经验

最后分享一个团队协作层面的经验。如果你所在的团队有多个人同时开发不同模块,建议在 modules 下的每个业务模块里单独维护自己的 mapper XML 目录和 controller 包,不要跨模块共用。很多团队喜欢把公共的查询逻辑抽到一个 common 包,起初没问题,但模块一多,common 包就变成了大杂烩,改一个公共方法可能影响所有模块,发布时又得把全部模块重新走一遍流水线。

更推荐的做法是:每个模块内部建立适合自己业务的分层,跨模块真正通用的内容,通过 api 接口调用,而不是直接把对方的 service 类拿来用。模块间依赖关系越弱,编译和发布效率越高。这也是微服务架构里“模块自治”的朴素实现。

我在实际项目中见过太多因为“图省事”导致的隐性耦合,比如订单模块的 Service 直接注入库存模块的 Mapper,早期单体部署时没什么问题,一拆微服务,库存模块独立扩展了,订单模块引用它的 Mapper 就出问题。坚持模块边界,短期看是多写了一个 Feign 接口,长期看省的是整个团队的调试时间。

写在最后

多租户 modules 子模块的创建,本质上并不难,难点在于理解若依这套代码里微服务、鉴权、多租户三条线是怎么交织起来的。只要把服务注册、配置中心、网关路由、租户字段这四件事理清楚,后续每个新模块都能按同样的套路复制。

我个人建议,第一次建模块时,不要从零手写 pom 和启动类,直接复制一个现有模块,比如ruoyi-demo或ruoyi-file,全局替换包名、模块名、服务名,再删掉用不上的业务代码。这样能保证公共依赖一个不落,配置风格也和项目保持一致。等新模块顺利跑通了,再往里填自己的业务逻辑。

最后再提一个很多人容易忽略的点:新模块建好后,先在测试环境完整走一遍“建租户、建用户、分配菜单、登录、增删改查、切换租户验证隔离”的流程,不要只验接口通没通。多租户系统的核心价值是数据隔离,接口通不等于隔离正确,只有用两个租户的数据实际对比过,才算真正完成了一个子模块的接入。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询