主题
插件开发
这一页回答一个问题:在 Wujies 无界平台上加一个业务应用,我该动哪里、顺序是什么。
三个前提要先说清(它们是这一章所有页面的共同背景):
- 平台极小,应用外置:新增业务模块 不改平台一行代码。这是"契约随插件走"改造的全部意义。
- 一个应用就是一个自包含工程:实体 / BO / VO / Mapper 接口 与 Mapper XML / Service 接口与实现 / controller / open / listener / task /
app.json/migrations/全在它自己那个目录里。 平台里wujies-modules/*-api的数量是 0,平台对cn.wujies.<app>.*的 Java 引用也是 0。 - 开发者面对两套工程:后端插件(Maven,在插件仓库)+ 前端远程应用(pnpm workspace,同在插件仓库)。
不要按"契约放 *-api、实现放 plugins/*"来写
那是 2026-09-30 之前的旧架构,已被整个推翻。连带作废的还有两条: 「Mapper XML 必须放 *-api」和「靠宿主 mapperLocations 把插件里的 XML 扫进来」。
现状是:XML 随插件 jar 走,由 PluginMapperRegistrar 在插件加载时动态注册进宿主 SqlSessionFactory。动态注册刻意不用 classpath*:mapper/**/*Mapper.xml —— 那个写法是拿宿主类加载器扫整个 classpath,会把宿主的映射文件再捞一遍, 撞 Result maps collection already contains value,宿主直接起不来。
权威说法见必须遵守的不变量。
四个仓库/目录:谁是谁
| 名字 | 是什么 | 别混淆 |
|---|---|---|
wujies-platform/ | 后端平台(Java / Spring Boot):wujies-admin 启动模块、wujies-common-*、wujies-modules/* | 这里没有 *-api 契约模块 |
wujies/wujies-plugins/ | 插件仓库:每个应用一个 wujies-<app>/(后端插件工程)+ frontend/<code>-app/(前端工程)+ package.ps1 + sandbox/ | 打包器与插件本体都在这里,平台仓库里没有 plugins/ |
plus-ui/ | 宿主前端(Vue 3 / Vite) | 它的 package.json 里 name 写的是 wujies-admin —— 名字会骗人,认目录 |
website/ | 本站(VitePress 文档站) | — |
⚠️ wujies-admin 这个名字在三处含义不同,读任何文档时按上下文判断:
| 写法 | 实际指 | 判据 |
|---|---|---|
wujies-platform/wujies-admin/ | 后端启动模块 | 有 pom.xml / src/main,产物 wujies-admin.jar |
工作区顶层的 wujies-admin/ | 前端产物的落位目录 | 里面只有 app-market-dist/,没有 package.json |
plus-ui/ | 宿主前端仓库 | 有 vite.config.ts / src/,但 package.json 的 name 是 wujies-admin |
一个应用长什么样
后端插件工程(以范例应用 hello 为准,wujies/wujies-plugins/wujies-hello/):
wujies-hello/
├── app.json 应用清单:code / version / assets / menus ← 唯一权威描述
├── pom.xml 后端工程(parent = wujies-plugins-parent)
├── migrations/ 建表 / 种子数据 / 字典 / 站点配置(随包走)
│ ├── 01-create-hello-tables.sql
│ ├── 02-create-hello-seed-data.sql
│ └── 03-create-hello-website-config.sql
└── src/main/java/cn/wujies/hello/
├── domain/ 实体(也在插件里)
├── mapper/ Mapper 接口(自定义 SQL 的 XML 放 src/main/resources 下)
├── service/ + impl/ Service 接口与实现
├── controller/ 管理端接口
├── open/ 对外开放接口(`/open/<app>/**`)
└── task/ 定时任务(`@Scheduled` / SnailJob `@JobExecutor`)前端远程应用(wujies/wujies-plugins/frontend/hello-app/):
frontend/hello-app/
├── package.json name = @wujies-plugins/hello-app,scripts.build = vite build
├── vite.config.ts 唯一的工程配置:defineRemoteApp({ appCode, root, ... })
└── src/
├── index.ts 入口契约:默认导出 install(runtime),返回 { manifest, views }
├── runtime.ts 宿主运行时持有者
└── views/ 页面(DemoHome / DemoPluginDev / DemoAbout)命名约定(四处必须一致):目录名、vite.config.ts 的 appCode、后端 app.json 的 code、 菜单里 component 的 remote:<code>/<viewKey> 前缀,都是同一个 <code>。
范例应用 hello:五种写法都有真实源码
要照着做时,优先打开这些文件,而不是照着文档猜:
| 我想知道 | 打开这个文件 | 验证端点 |
|---|---|---|
| 插件怎么调宿主的系统服务 | cn.wujies.hello.controller.HelloDemoHostController(ConfigService / OssService / LoginHelper) | GET /hello/demo/host/whoami |
| 插件怎么调另一个插件 | cn.wujies.hello.controller.HelloDemoCrossPluginController(注入兄弟 Mapper vs 走 SDK 定位器) | GET /hello/demo/cross/service?userId=1 |
| 怎么从插件开放接口给别人 | cn.wujies.hello.open.HelloDemoOpenController | GET /open/hello/greet |
| 定时任务的两种写法 | cn.wujies.hello.task.HelloDemoScheduledTask / HelloDemoSnailJobTask | 服务端日志 / SnailJob 控制台 |
| 清单怎么写 | wujies-hello/app.json | 菜单能否点开 |
| 表结构与种子数据怎么随包走 | wujies-hello/migrations/01..03-*.sql | GET /hello/item/list |
前端页面在菜单**「Hello 示例 → 插件开发范例」**下,对应 frontend/hello-app/src/views/DemoPluginDev.vue。 更细的两栏对照见跟着 hello 读源码。
从零做一个应用:五步
- 复制 hello:
wujies-hello→wujies-<code>。 - 改四处:
pom.xml的artifactId、app.json的code/name/version/menus、 Java 包名cn.wujies.hello→cn.wujies.<code>、迁移脚本里的表名前缀。 - 挂进 reactor:插件仓库根
pom.xml的<modules>加一行。 ⚠️ 漏了这步的表现是"改了代码没生效",很难第一时间想到。 - 写迁移脚本:
migrations/01-create-<code>-tables.sql建表,02-*种子数据, 需要字典 / 站点配置再各加一条 —— 见数据库迁移与种子数据与站点配置。 - 建前端工程:复制
frontend/hello-app→frontend/<code>-app,改package.json的name与vite.config.ts的appCode,页面键与app.json里component的视图键对齐。
构建、打包、验证
powershell
# 1) 平台构件装进本地仓库(插件编译期要用;平台升级后要重跑)
# 在 wujies-platform/ 下:
mvn -B -pl wujies-admin -am -DskipTests install
# 2) 只构建一个插件
# 在插件仓库根 wujies/wujies-plugins/ 下:
mvn -B -pl wujies-<code> package -DskipTests
# 3) 打成可上架的应用包(脚本会自己构建前端 + 后端 → 组装 → 签名 → 出 zip)
cd <插件仓库根>
.\package.ps1 -Plugin wujies-<code> -SigningKey keys\private.pem
# 4) 上架:控制台「应用上架 → 版本管理 → 安装包上传」→ 发布 → 上架 → 客户端部署打包器参数(-FrontendProject / -MigrationPath / -SkipFrontend / -NoSign 等)见 打包应用;签名与密钥纪律见密钥与签名。
验证的顺序(能一步定位"装载问题"还是"数据问题"):
| 打什么 | 验的是什么 | 期望 |
|---|---|---|
GET /hello/greet?name=无界 | 路由挂上了、依赖注入通了(不碰库) | code=200 |
GET /hello/item/list | 建表脚本执行了、Mapper 装配了、SQL 真打到库上 | code=200,2 条种子数据 |
GET /open/hello/greet(不带 token) | /open 开放链路与 @SaIgnore | 200 |
GET /hello/item/list(不带 token) | 插件路由也过鉴权(不能裸奔) | 401 |
不改平台、不装市场也能验:插件仓库自带沙箱 sandbox/(H2 内存库,不连 MySQL / Redis / RabbitMQ), 用法见本地联调。
五个必须记住的约定
- 契约随插件走:实体 / BO / VO / Mapper 接口与 XML / Service 接口全在插件模块自己的
src/下; 平台里没有*-api契约模块,也不要试图新建一个。插件之间要对方契约时, 依赖兄弟插件构件(例如wujies-member-plugin,scope为provided)。 - 平台依赖一律
provided:Spring / MyBatis /wujies-common-*/wujies-common-plugin-sdk/ 兄弟插件构件都如此。打进插件就会出现"同一个接口两个 Class 对象",注入与类型判断全部失效。 插件仓库父 pom 的dependencyManagement已经统一设好,不要在子模块里改它。 - 菜单只能在
app.json的menus里声明,不要手写INSERT INTO sys_menu。type: "F"的按钮也必须写,否则全新部署装完没有按钮,"彻底删除"时也删不掉它们。 详见菜单与权限。 - 表结构与种子数据随包走:脚本放
migrations/,文件名排序即执行顺序, 必须幂等,已执行过的脚本内容不能再改(改了会被摘要校验拒绝执行,要变更就新增脚本)。 这些脚本同时是"彻底删除时 DROP 哪些表"的唯一依据。 - 宿主看不到插件:Spring 父子关系是单向的 —— 插件能注入宿主,宿主不能注入插件。 宿主侧要用插件服务,只能走 SPI 或服务定位器代理;绝不要在宿主上下文里注册与插件实现 同类型的 bean(哪怕只是个转发代理),那会让插件的控制器启动即报
expected single matching bean but found 2。
这一章的其余页面
| 我想… | 看这里 |
|---|---|
| 扫一遍不能违反的规矩 | 必须遵守的不变量 |
搞清 app.json 每个字段 | 应用清单 app.json |
| 写后端工程 | 后端工程 |
| 写前端远程应用 | 前端远程应用 |
| 声明菜单与权限 | 菜单与权限 |
| 加表 / 改表 | 数据库迁移 |
| 让应用装完就有字典和站点配置 | 种子数据与站点配置 |
| 本地跑起来调试 | 本地联调 |
| 照着真实源码写 | 跟着 hello 读源码 |
| 把宿主里已有的业务模块抽成应用 | 从宿主模块抽成应用(SOP) |
| 改平台代码(贡献者) | 开发指南 |
| 打包与上架 | 打包应用 / 上架与发布 |
| 排查装不上 / 起不来 | 故障排查 |
相关阅读
- 必须遵守的不变量 —— 改代码前扫一遍
- 跟着 hello 读源码 —— 五种写法的真实文件索引
- 应用清单 app.json
- 插件运行时 —— 类加载、路由、Mapper 注册的实现
- 应用市场模块 —— 安装 / 升级 / 卸载的生命周期