总体架构
一句话
把 RuoYi-Vue-Plus 从「单体系统」改造成「平台 + 应用」:平台提供多租户底座, 业务能力做成可以后装、不重启、从市场拉下来直接跑的应用。
为什么需要改造:起点约束
改造前的结构是:所有模块最终打成 ruoyi-admin 一个 jar。这是起点,也是最大的约束。
而前端的页面是构建期用 glob 静态打进来的:
ts
import.meta.glob('./../../views/**/*.vue')后端 sys_menu.component 只能指向构建时已存在的 .vue 文件。 所以在原有架构下,第三方页面在物理上无法热加载 —— 必须先改造前端运行时, 让菜单能指向一份"运行期才知道在哪"的独立 ESM。
这条约束决定了整个改造的分界线:
只要还是"一套系统服务一个客户",单体结构没问题; 一旦要后装应用或开放第三方,前端运行时改造(远程应用)是绕不过去的前置。
分层
┌──────────────────────────────────────────────────────────────┐
│ 平台(宿主) │
│ │
│ 用户 / 角色 / 菜单 / 租户 / 部门 / 岗位 / 字典 / 参数 │ ← 底座
│ 文件存储(OSS) / 短信 / 邮件 / 任务调度 / 工作流 / 代码生成 │
│ │
│ ┌────────────────── 应用市场(ruoyi-app-market) ──────────┐ │
│ │ 目录(catalog) │ 安装编排 │ 插件运行时 │ │
│ │ 上架端(console) │ 任务中心 │ 远程应用运行时 │ │
│ │ 包安全(签名) │ 菜单同步 │ 版本迁移 │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
▲ 依赖契约(编译期) │ 反查实现(运行期)
│ ▼
┌──────────────────────────────────────────────────────────────┐
│ 应用(插件) │
│ │
│ 契约 ruoyi-<name>-api 实现 plugins/ruoyi-<name> │
│ ─ 实体 / BO / VO ─ controller │
│ ─ Mapper 接口 + XML ─ service.impl │
│ ─ Service 接口 ─ listener │
│ ─ 常量 / 事件 / SPI 实现 │
│ │
│ 清单 app.json + 前端远程 ESM + migrations/*.sql │
└──────────────────────────────────────────────────────────────┘平台负责什么 / 应用负责什么
| 平台(宿主) | 应用(插件) | |
|---|---|---|
| 用户与权限 | 用户、角色、部门、岗位、菜单、租户 | 自己的权限标识(写进清单的 menus[].perms) |
| 菜单 | 幂等 upsert、归属保护、按角色授权 | 只声明菜单树 |
| 数据表 | 平台表(sys_* / app_* / gen_* …),并保护它们不被误删 | 自己的业务表,通过 migrations/ 声明 |
| 加载与隔离 | 类加载器、子 Spring 上下文、路由注册、拦截器链 | 只写正常的 Spring 代码 |
| 生命周期 | 部署 / 安装 / 升级 / 卸载 / 停用启用的编排与任务记录 | 无感知 |
| 前后端运行时 | import map 垫片、运行时注入、视图键解析 | 产出符合契约的独立 ESM |
| 包安全 | 产物白名单 + 签名校验 | 发布时签名 |
| 前端页面 | 一层壳 + 布局 + 工作台 | 自己的视图 |
契约与实现分离
这是整套结构里最需要记住的工程约定。
| 放什么 | 放哪 | 为什么 |
|---|---|---|
| 实体 / BO / VO / 枚举 / 事件 | ruoyi-modules/ruoyi-<name>-api | 宿主消费方要在编译期看到这些类型 |
| Mapper 接口与 XML | 同上 | 宿主注册 Mapper bean;XML 必须让宿主的类加载器扫到 |
| Service 接口 | 同上 | 消费方要注入或代理它 |
| 平台 SPI 的实现 | 同上 | 消费方可能是另一个应用 —— 插件只能看见父上下文(宿主),看不见兄弟插件 |
| controller / service.impl / listener | plugins/ruoyi-<name> | 独立构建成 jar,装载进插件运行时 |
最隐蔽的坑:Mapper XML 放错位置
宿主的 mapperLocations: classpath*:mapper/**/*Mapper.xml 是用宿主类加载器扫描的。 XML 若留在插件 jar 里,在「每插件独立类加载器」模式下宿主根本扫不到 → 所有 SQL 静默失效,而且报错信息完全指不到这里。
两个仓库
| 仓库 | 内容 | 作用 |
|---|---|---|
RuoYi-Vue-Plus | 后端(Java / Spring Boot)+ 插件工程 + SQL 脚本 | 平台与后端应用 |
plus-ui | 前端(Vue 3 / Vite)+ 远程应用工程 | 前端壳与远程应用 |
两个仓库没有共同 git 根,各自提交、各自推送。改一个功能经常要两边都提交。
应用市场模块的包结构
ruoyi-modules/ruoyi-app-market 是整套机制的核心,按职责分包:
| 包 | 职责 |
|---|---|
catalog/ | 目录访问的唯一入口:IAppCatalogService + Local / Remote 两种实现 + 服务间接口 |
edition/ | 版本形态开关(official / client)、按路径前缀的形态闸门、启动期自洽校验 |
plugin/ | 插件类加载器、子上下文、独立 HandlerMapping、运行时管理器、事件中继 |
security/ | 包摘要规范化、产物白名单 + 签名校验、离线签名工具、签名就绪度自检 |
service/ | 安装编排、菜单计划与同步、菜单归属、迁移执行、落位、卸载清理、清单校验 |
spi/ | 安装器 SPI(module / plugin / deploy)与生命周期监听、扩展点 |
state/ | 应用生命周期状态机 |
support/ | 任务进度落库、卡死任务清扫 |
controller/ | 市场浏览端、上架端、版本端、安装包端、安装端、运行时答复、工作台 |