打包与上架
这一章讲发布方要做的事:把应用打成可上传的包、签名、上架,最终让它在某个部署上真正跑起来。
前置阅读:应用清单 说明 app.json 每个字段的含义,生命周期 说明平台侧接住这些包之后做了什么。
一次完整的发布链路
① 生成密钥(只做一次,JDK 是硬前提)
package.ps1 -Keygen
└─ 产出 plugins/keys/private.pem(留发布方)+ public.pem(贴进宿主 yml)
② 打包 + 签名
package.ps1 -Plugin ruoyi-xxx -SigningKey plugins\keys\private.pem
└─ 构建前端 → 构建后端 → 组装包目录 → 签名 → 打 zip 并算校验值
└─ 产出 dist-packages/<code>-<version>.zip + .package-info.json
③ 上传(只登记,不落位)
POST /app/package/parse multipart
└─ 解包读清单 → 清单校验 → 安全检查(白名单 + 验签)→ 落对象存储 → 记录 URL/大小/MD5/SHA256
④ 保存版本
POST /app/version
└─ 登记版本行(含清单 JSON),状态 = 0 待发布
⑤ 发布
POST /app/version/{versionId}/publish
└─ 状态置 1 已发布,并回写 app_info.latest_version
⑥ 上架
POST /app/console/{appId}/status?status=1
└─ 应用状态置 1 已上架,市场里才能看到、才能被安装
⑦ 在部署里点「部署 / 安装」
POST /app/version/{versionId}/deploy ← 上架端「部署」按钮
或 POST /app/install ← 市场页「安装」按钮
└─ 下载 → 校验校验和 → 安全检查 → 解包落位 → (安装路径还要跑迁移)→ 加载插件 → 同步菜单第 ⑦ 步是整条链路上最容易漏的一步
上传、保存、发布、上架都只是登记与状态变更,磁盘上什么都没有。少了「部署」这一步,应用在市场里看得见、装得上(状态正常),但后端 jar 不在宿主 classpath 上、前端产物不在 /remote-apps/ 下 —— 表现是接口返回「应用未安装或未启用」、菜单点开是「远程视图不可用」。
上传完成 ≠ 生效。打包 → 上传 → 发布 → 部署四步一件都不能少。
三个动作的区别
这三个动作经常被混为一谈。它们的执行者、是否跑迁移、是否热加载都不一样:
| 上传 | 发布 | 部署 | |
|---|---|---|---|
| 谁做 | 发布方(上架端) | 发布方(上架端) | 上架端操作者,「部署」按钮 |
| 接口 | POST /app/package/parse | POST /app/version/{id}/publish | POST /app/version/{id}/deploy |
| 权限 | app:console:add | app:console:edit | app:console:edit |
| 干了什么 | 解包读清单 → 校验 → 安全检查 → 落对象存储 → 记 URL/大小/MD5/SHA256 | 版本状态置 1 已发布,回写 app_info.latest_version | 下载包 → 校验校验和 → 安全检查 → 解包落位到宿主磁盘 → 热加载后端插件 |
| 动磁盘吗 | ❌ 只落对象存储 | ❌ 只改状态 | ✅ 落位到 plugin-dir 与 frontend-dir |
| 跑迁移脚本吗 | ❌ | ❌ | ❌(控制台「部署」不跑迁移,见下) |
| 热加载吗 | ❌ | ❌ | ✅ 卸载旧插件 → 用新 jar 建类加载器与子上下文 → 注册接口 |
| 前端用重启吗 | —— | —— | 不用,刷新页面即可(entry 带 ?v=<版本> 破缓存) |
| 后端用重启吗 | —— | —— | 热加载成功就不用(响应 hotLoaded: true / restartRequired: false);热加载失败才要(响应会带 hotLoadNote 说明原因) |
「部署」按钮不跑迁移脚本 —— 这是刻意的
安装/升级走的是 deploy(versionId, false)(只落位),加载交给排在迁移之后的步骤;而控制台「部署」按钮走单参版本(落位即加载),因为它不跑迁移脚本。
所以:给已有部署补产物用「部署」是对的;要变更表结构必须走安装/升级。 顺序保证是 落位 → 变更表结构 → 切换代码 —— 反过来的话,新代码在启动时机读一个还没建的列就会炸。
包内目录结构规范
zip 的根目录就是包内容,没有多余的一层目录:
<code>-<version>.zip
├── app.json 清单(唯一事实来源)
├── backend/
│ └── <artifactId>-<version>.jar 后端产物,文件名必须与 assets.backend 一致
├── frontend/ 前端产物整棵树
│ ├── index.js 远程 ESM 入口(通常 300KB+)
│ └── assets/ruoyi-vue-plus.css 自带样式
├── migrations/ 建表/改表脚本(有才打)
│ ├── 01-create-<app>-tables.sql
│ └── 02-backfill-xxx.sql
└── signature.txt 分离式签名(覆盖除它以外的全部文件)产物校验清单:
| 检查项 | 期望 |
|---|---|
app.json 位置 | zip 根目录,不是 <code>-<version>/app.json |
| 后端 jar 名 | 与 app.json 的 assets.backend 最后一段完全一致 |
| 前端文件 | frontend/index.js + frontend/assets/* |
*.map | 一个都没有(打包脚本自动剔除) |
signature.txt | 在包根(mode=enforce 时必须有) |
清单 code | 与上架端那个应用的 app_code 一致(上传接口会校验) |
清单 version | 与要发布的版本号一致 |
包结构不是随便定的,白名单会拒
包根只允许 app.json、signature.txt 以及 backend/、frontend/、migrations/ 三个目录;backend/ 下只允许 .jar 且只能有清单声明的那一个;migrations/ 下只允许 .sql;frontend/ 下只允许白名单内的静态资源扩展名,且 .jsp / .php / .war / .jar / .exe / .sh / .properties / .xml 等无论如何都拒绝(前端目录就是宿主的静态资源目录)。
前后端必须打成一个包的三个理由
这不是「顺手」的决定,是为了避开三类无法挽回的错误:
- 版本一致性 —— 分开传迟早出现「前端 1.0 + 后端 0.9」的错配,而且运行期极难排查:页面能打开、接口也返回 200,只是行为对不上,因为没有哪一层会报错。
- 一次签名 —— 签名是分离式的
signature.txt,它覆盖包内除自己以外的全部文件。MD5/SHA256 与签名只有覆盖整包才有防篡改意义;分开传就等于有两份可以各自被替换的产物。 - 卸载与回滚能整体回收 —— 「彻底删除」要按
appCode回收后端 jar、前端目录与active-plugins.txt记录;降级回旧版本也要整体换一套。分散成多个包之后,「装的是哪个组合」没有任何地方记录。
顺带一个合法形态要记住:纯前端应用(清单里只写 assets.frontend、不写 assets.backend)是受支持的,包里只有 app.json + frontend/。它适合「只提供聚合视图、读别的应用现成接口」的场景。打包时跳过后端构建(连 JDK / Maven 都不需要),但签名仍要 JDK —— 签名工具是 Java 写的。
打包前检查清单
| # | 检查 | 怎么看 |
|---|---|---|
| 1 | app.json 的 code / version / assets 三者自洽 | assets.backend 的文件名 = <artifactId>-<version>.jar;assets.frontend 指向 frontend/index.js |
| 2 | 菜单树含按钮(type: "F") | 漏写按钮的后果是双向的且都不报错:全新部署装完没有按钮;彻底删除删不掉按钮(留成孤儿) |
| 3 | 建表脚本在 <插件>/migrations/(或 -MigrationPath 指得到) | 它不只是「安装时建表」,还是卸载时删表的唯一依据。包里没有建表脚本 → 那个应用的「彻底删除」一张表都删不掉 |
| 4 | 已有部署上要补按钮时配了 seed SQL | 否则升级会建出一套重复按钮(把新 menu_key 映射到既有按钮 ID) |
| 5 | 前端工程构建通过,且自检干净 | npm run check:remote(模板组件解析 + 白名单名字 + 产物里有没有 process.env 残留);npm run check:globals(产物里有没有「宿主自动导入宇宙」里未绑定的标识符);npm run check:runtime(产物真正 import 的名字与垫片导出对账) |
| 6 | 远程产物目录名等于 code | app-market-poc/<code>-app、app-market-dist/remote-apps/<code>、后端落位目录、宿主拼出的入口 URL 四处是同一个约定 |
| 7 | 版本文案格式能过后端校验 | 后端清单校验要求 x.y.z,比打包脚本严格 —— 见 上架与发布 |
| 8 | 私钥就绪 | plugins/keys/private.pem 存在(脚本会自动发现),或显式传 -SigningKey;本机没有会自动 keygen |
| 9 | 宿主配的公钥与私钥成对 | 打包脚本会顺手比对 application.yml 的 public-key,一致打 ✓,不一致/为空会警告 |
| 10 | 改了表结构就新增脚本,不改老脚本 | 已执行过的脚本内容不能再改(按 (app_code, script) 比对摘要),否则升级会被拒绝 |