Skip to content

框架设计 ​

这一页回答**"分成几块、谁依赖谁、为什么这么分"**。它不列目录(那是 仓库目录 的事),也不列硬约束(那是 必须遵守的不变量 的事)。

全文的技术断言都给了落点(路径 / 类名 / 配置键 / 建表脚本)。结构变了请重读文件系统再改这一页, 不要凭记忆补。

一句话 ​

把上游 RuoYi-Vue-Plus 的"一套系统服务一个客户"形态改造成 平台 + 应用:平台提供多租户底座 与应用市场机制,业务能力做成可后装、不重启、从市场拉下来直接跑的应用;宿主前端退化成一层壳, 业务页面由应用在运行期交付。

术语:平台 / 宿主 / 宿主前端 / 应用 ​

说法指什么判据
平台(后端)wujies-platform/,Java / Spring Boot + SQL 脚本有 pom.xml;产物 wujies-admin.jar
宿主运行插件的那一个 JVM 进程 —— 就是平台后端插件子上下文的 parent 就是它
宿主前端plus-ui/,Vue 3 / Vite有 vite.config.ts / src/;package.json 的 name 是 wujies-admin
应用wujies/wujies-plugins/wujies-<app>/,一个应用一个目录含 app.json、migrations/、src/main/java/cn/wujies/<app>/

"宿主"与"平台后端"是同一份进程的两个视角:讲类加载器与 Spring 父子上下文时叫宿主, 讲多租户底座与市场机制时叫平台。两者不是两个进程。

上游那套旧前缀不要照写

本项目源自上游 RuoYi-Vue-Plus,但自己的模块名、包名一律是 wujies-* / cn.wujies.*。 源码或旧文档里出现上游的旧前缀与旧包名,那是历史遗留 —— 改的时候换成 wujies-* / cn.wujies.*。

一、三个仓库 ​

仓库内容默认分支
wujies-platform后端(Java / Spring Boot)+ 平台 SQL 脚本feature/app-market
wujies/wujies-plugins插件本体 + 插件前端工程(pnpm workspace)+ 打包器 + docs/master
plus-ui宿主前端(Vue 3 / Vite)feature/app-market

三者没有共同 git 根,各自分支、各自提交、各自推送 —— 加一个应用经常要跨仓库提交两到三次。 目录级细节见 仓库目录。

⚠️ wujies-admin 这一个名字指三处不同地方(历史上已经因此误导过两次排查):

写法实际指判据
wujies-platform/wujies-admin/后端启动模块(Java)有 pom.xml / src/main / Dockerfile;产物 wujies-admin.jar
工作区顶层的 wujies-admin/前端产物落位目录里面只有 app-market-dist/,没有 package.json、没有 src/
plus-ui/宿主前端仓库有 vite.config.ts / src/;package.json 的 name = wujies-admin

记法:路径里出现 src/plugins/**、src/views/**、app-market-dist/** → 说的是 plus-ui/; 出现 src/main/resources/**、wujies-admin.jar、mvn -pl wujies-admin → 说的是 wujies-platform/wujies-admin/。

二、三者各负责什么 ​

平台(宿主)应用(插件)宿主前端(plus-ui/)
用户与权限用户 / 角色 / 部门 / 岗位 / 租户 / 菜单的授权只声明自己的权限标识(app.json 的 menus[].perms)按登录返回的菜单树渲染侧边栏
数据平台表(sys_* / app_* …),并保护它们不被误删自己的业务表,建表语句放 migrations/—
加载与隔离类加载器、子 Spring 上下文、独立的 PluginHandlerMapping、宿主拦截器链只写普通 Spring 代码—
生命周期部署 / 安装 / 升级 / 卸载 / 停用的编排与任务记录(app_install_task)无感装完 / 升级 / 停用后调 refreshMenus() 刷新菜单树
前端页面一层壳:布局、登录、工作台自己的视图,产出独立 ESM远程应用装载器 + 静态路由兜底
包安全产物白名单 + 签名校验(app-market.security.mode,当前 enforce)发布时签名—

关键类是单向的,别记反:

  • 插件能注入宿主,宿主不能注入插件(插件子上下文以宿主为 parent)。 宿主侧要拿插件服务只有两条路:PluginServiceProvider.getPluginBean(appCode, type) (wujies-common-core 的 cn.wujies.common.core.plugin.PluginServiceProvider) 或应用自己的 XxxServiceLocator.proxy(...)(每次调用都反查)。 细节见 定位器 与 必须遵守的不变量。
  • 宿主前端在构建期不可能知道租户装了哪些应用。菜单里的 component = remote:<appCode>/<视图键> 只是视图键,入口 JS 由运行期问后端得到: AppRuntimeController 的 GET /app/runtime/remote-apps。见 前端远程应用。

三、为什么前端必须先改造 ​

后端把业务做成插件还不够 —— 起点约束在前端。改造前,页面是构建期用 glob 静态打进来的 (今天仍是这条,见 plus-ui/src/store/modules/permission.ts 第 17 行):

ts
const modules = import.meta.glob('./../../views/**/*.vue');

于是 sys_menu.component 只能指向构建时已存在的 .vue 文件,第三方页面在物理上无法热加载。 所以"应用外置"必须先把前端运行时改出来:让菜单能指向一份运行期才知道在哪的独立 ESM。

宿主前端为此做了两件事:

  1. plus-ui/src/plugins/appmarket/ 里的装载器(loader.ts / bootstrap.ts / runtime.ts / menuRefresh.ts / edition.ts / appAvailability.ts)在运行期问后端要入口并执行;
  2. 不靠菜单驱动的页面(详情页、编辑页这类静态路由)也收藏到同一条路: plus-ui/src/router/index.ts 里大量使用 createRemoteViewComponent('<视图键>', '<appCode>'), 否则菜单能点开、手打 URL 却打不开。

四、契约随插件走:平台里没有 *-api 模块 ​

这是决定整个仓库结构的一条约定,也是最容易照着旧文档写错的一条。

现状:平台里 wujies-modules/*-api 数量为 0;平台对 cn.wujies.<app>.* 的 Java 引用为 0 (只允许出现在注释里)。每个应用自包含:

内容位置
实体 / BO / VO / 枚举wujies/wujies-plugins/wujies-<app>/src/main/java/cn/wujies/<app>/domain/**
Mapper 接口 与 Mapper XML同上的 mapper/ 包,与 src/main/resources/mapper/<app>/*.xml
Service 接口与实现同上的 service/、service/impl/
controller / open / listener / task同上的 controller/、open/、listener/、task/
清单与建表脚本插件根目录的 app.json、migrations/*.sql

怎么自己核验(本工作区实测) ​

powershell
# ① 平台里有没有 *-api 契约模块?(期望:没有任何输出)
Get-ChildItem .\wujies-platform -Recurse -Directory -Filter '*-api' |
    Where-Object { $_.FullName -notmatch '\\target\\' }

# ② 平台源码有没有真的 import 应用包?(期望:只命中 docs\removed\ 下的存档)
Get-ChildItem .\wujies-platform -Recurse -Include *.java |
    Where-Object { $_.FullName -notmatch '\\target\\' } |
    Select-String -Pattern '^\s*import\s+cn\.wujies\.(gallery|exam|im|pay|quote|social|mall|member|enterprise|hello|finance)\.'

实测(2026-10 工作区):① 无输出;② 命中全部落在 wujies-platform/docs/removed/ (已移出反应堆的 wujies-portal / wujies-openapi 存档源码),活代码里一条都没有。

由它派生的四条硬事实 ​

事实说明
新增业务模块不用动平台一行只写插件。这是这条改造的全部意义
宿主拿插件服务靠反查不依赖插件构件,走 PluginServiceProvider 或 SDK 里的 MemberServiceLocator / PayServiceLocator / ImServiceLocator(在 wujies-common-plugin-sdk)
跨插件调用靠"依赖兄弟插件构件"买到的是类型可见,不是 bean 可见:兄弟的 Mapper 接口能直接注入(注册在宿主 bean factory),兄弟的 Service bean 不能 —— 必须走定位器。范例见 跟着 hello 读源码
平台依赖一律 provided打进插件就会"同一个接口两个 Class 对象",注入与类型判断全部失效

Mapper XML 随插件 jar 走,由代码注册 —— 别指望宿主的扫描配置

宿主 application.yml 里那行 classpath*:mapper/**/*Mapper.xml 是拿宿主类加载器扫 classpath: 插件 jar 里的 XML 它扫不到,把宿主自己的映射文件再注册一遍又会撞 Result Maps collection already contains value,宿主直接起不来。

所以插件 XML 由 PluginMapperRegistrar (wujies-platform/wujies-modules/wujies-app-market/.../plugin/PluginMapperRegistrar.java) 在插件加载时只读传入的那个 jar 动态注册进宿主 SqlSessionFactory。

另外:定时任务也随应用走(包落在 cn.wujies.<app>.task),宿主只提供 wujies-common-job; 跨插件的事件类必须放在宿主类加载器可见的位置(cn.wujies.common.core.domain.event), 跟着发布方插件的 jar 走就会"订阅方永远收不到、且不报错"。

五、平台侧的模块地图 ​

wujies-platform/                       38 个 pom.xml(2026-10 实测,不含 target/)
├── pom.xml                            反应堆根:<modules> 只有 4 个聚合模块
├── wujies-admin/                      启动模块 → wujies-admin.jar;PluginClassPathLoader 在这里
├── wujies-common/                     27 个子模块(基础设施)
│   ├── wujies-common-core/            PluginServiceProvider / MenuOwnershipProvider
│   ├── wujies-common-web/             GlobalExceptionHandler(异常 → 前端的唯一出口)
│   ├── wujies-common-plugin-sdk/      插件扩展点契约(Platform*Api / *ServiceLocator)
│   ├── wujies-common-mybatis/         MyBatis-Plus 集成(BaseMapperPlus)
│   ├── wujies-common-job/             定时任务宿主侧支持(任务实现随应用走)
│   └── …(其余 22 个:tenant / satoken / security / redis / oss / mail / sms / rabbitmq / …)
├── wujies-extend/                     旁挂服务,独立进程
│   ├── wujies-monitor-admin/
│   └── wujies-snailjob-server/
├── wujies-modules/                    只剩 4 个
│   ├── wujies-app-market/             ★ 应用市场:目录 + 安装编排 + 插件运行时 + 包安全
│   ├── wujies-system/                 系统管理(用户 / 部门 / 角色 / 菜单 / 字典 / OSS / 消息)
│   ├── wujies-workflow/               工作流
│   └── wujies-plugin-probe/           插件运行时探针
└── plugins-dist/                      插件 jar 落位目录 + active-plugins.txt
要找什么去哪
宿主给插件用的能力契约wujies-common/wujies-common-plugin-sdk/:PlatformUserApi / PlatformDeptApi / PlatformMessageApi / PlatformOssApi / FollowRelationApi,加三个定位器。索引见 平台 SDK 契约
宿主反查插件与菜单归属的 SPIwujies-common-core 的 cn.wujies.common.core.plugin.PluginServiceProvider 与 cn.wujies.common.core.menu.MenuOwnershipProvider
应用市场实现wujies-modules/wujies-app-market/,分包见下一节
插件 jar 落在哪配置键 app-market.deploy.plugin-dir(当前值 plugins-dist),必须与启动参数 -Dloader.path 一致
前端产物落在哪配置键 app-market.deploy.frontend-dir,该物理目录必须就是宿主 /remote-apps/ 的位置
本机启用哪些插件plugins-dist/active-plugins.txt(两个加载时机都按它选版本;读不到则不过滤)

已删 / 已归档的模块不要再当现存列

wujies-job、wujies-demo、wujies-generator 已删除;wujies-portal、wujies-openapi 归档在 wujies-platform/docs/removed/,已不在反应堆里。看到引用它们的旧文档,按归档理解,别去 mvn -pl。

平台内置模块的逐个说明在 平台内置模块;想自己核实模块数量, Get-ChildItem .\wujies-platform -Recurse -Filter pom.xml | Where-Object { $_.FullName -notmatch '\\target\\' } 数一下即可。

六、应用市场在平台里的分包 ​

wujies-modules/wujies-app-market 是整套机制的核心(目录 + 运行时),按职责分包:

包职责代表类
catalog/目录访问的唯一入口;本地 / 远程两种实现IAppCatalogService、LocalAppCatalogService、RemoteAppCatalogService、AppCatalogApiController
edition/版本形态(官方版 / 用户版)与上架端闸门MarketEditionGuardFilter、MarketEditionStartupCheck
plugin/插件类加载器、子上下文、独立 HandlerMapping、运行时管理、Mapper 与事件注册PluginClassLoader、PluginApplicationContext、PluginHandlerMapping、PluginRuntimeManager、PluginMapperRegistrar、PluginEventRelay
security/包摘要规范化、白名单 + 验签、离线签名工具、签名就绪度自检PackageDigest、PackageSecurityInspector、PackageSignerMain、PackageKeys、SignatureReadinessService
service/生命周期编排、落位、迁移、菜单计划与同步、归属、卸载清理、清单校验AppInstallServiceImpl、AppPackageDeployService、AppMigrationService、AppMenuPlanner、AppMenuOwnershipProvider、AppPurgeService、AppManifestValidator
spi/安装器 SPI(module / plugin / deploy)AppInstaller 及其实现
state/应用生命周期状态机—
support/任务进度落库与卡死清扫AppTaskRecorder、AppTaskTimeoutSweeper
controller/市场浏览端、上架端、版本端、安装包端、安装端、运行时答复、工作台AppMarketController、AppConsoleController、AppVersionController、AppPackageController、AppInstallController、AppRuntimeController、AppWorkbenchController
domain/ mapper/ enums/ constant/ util/实体、Mapper、枚举、常量与工具—

分包与关键类的完整说明见 应用市场模块;目录集中化与两个版本形态见 目录中心化与版本形态。

七、一次安装把三者串起来 ​

步谁做什么
1宿主前端用户在 plus-ui 的应用市场页点「安装」
2平台AppInstallServiceImpl 编排:AppPackageDeployService 拉包 → 校验签名与校验和 → 落位(jar 进 plugin-dir 根下,前端产物进 frontend-dir/<appCode>/)
3平台AppMigrationService 按文件名顺序执行该应用的 migrations/*.sql,把账记进 app_schema_history
4平台PluginRuntimeManager 建子上下文加载插件;AppMenuPlanner 按 menu_key 幂等 upsert 菜单
5宿主前端菜单里是 component = remote:<code>/<视图键>,装载器问 GET /app/runtime/remote-apps 拿到入口 JS 并执行
6应用自己的接口:管理端走 @SaCheckPermission、开放端写 /open/<code>/**;路由与鉴权都挂在宿主的拦截器链上(这一步不做,接口就是裸奔的)

顺序上刻意保证 落位 → 变更表结构 → 切换代码:安装与升级走的是 AppPackageDeployService.deploy(versionId, false)(只落位),加载推迟到迁移之后。

全过程的任务、步骤与日志落在 app_install_task,在「我的应用 → 应用任务」里看 —— 排查问题先看那里,比翻服务端日志快。见 任务中心、 应用生命周期。

相关阅读 ​

我们坚信,即使再复杂的技术,也可以用清晰、干练、易懂的文字描述清楚。如果你在阅读时有难以理解的章节,那一定是我们还没有优化好它 —— 欢迎反馈。