主题
打包与上架
这一章讲发布方要做的事:把插件仓库里的一个 wujies-<code>/ 变成能上传、能签名、能被别人的部署真正跑起来的应用包。
前置阅读:应用清单 说明 app.json 每个字段的含义,数据库迁移 说明 migrations/*.sql 怎么写。包被上传之后平台侧接住了什么,见 应用生命周期。
本章分工
| 页 | 解决什么 |
|---|---|
| 本页 | 整条链路有哪几步、每一步落在哪个端点、哪一步动了磁盘 |
| 打包应用 | package.ps1 完整参数表、产物结构、-Version、纯前端应用 |
| 密钥与签名 | 密钥生成、signature.txt 的算法与覆盖范围、就地重签 |
| 上架与发布 | 上架端端点表、版本状态流转、市场可见范围 |
| 安全校验模式 | app-market.security.mode 三种模式与切 enforce 的前置条件 |
发布时会同时碰到三个仓库,别混(依据见 仓库目录):
| 角色 | 路径 | 与本页相关的部分 |
|---|---|---|
| 后端平台 | wujies-platform/ | 宿主服务、script/sql/ |
| 插件仓库 | wujies/wujies-plugins/ | wujies-<code>/(app.json + 后端 + migrations/)、frontend/<code>-app/(前端工程)、package.ps1、keys/ |
| 宿主前端 | plus-ui/ | 远程产物的提供方;它的 package.json 里 name 是 wujies-admin,check:* 自检脚本在这一侧 |
范例应用是 wujies-hello:一张表 + 四个演示点(调宿主服务 / 跨插件调用 / 开放 /open 接口 / 两种定时任务)。逐文件对照见 跟着 hello 读源码。
一次完整发布链路
powershell
# 下面的命令都在插件仓库根执行:wujies\wujies-plugins\
# ① 生成密钥(只做一次;目录里已有 private.pem 会直接拒绝,不覆盖)
powershell -NoProfile -ExecutionPolicy Bypass -File package.ps1 -Keygen
# ② 打包 + 签名(私钥不存在时自动 keygen 到该目录,再接着签)
powershell -NoProfile -ExecutionPolicy Bypass -File package.ps1 `
-Plugin wujies-hello -SigningKey wujies\wujies-plugins\keys\private.pem② 的产物是 dist-packages\hello-1.0.0.zip 与同名的 .package-info.json(appCode / version / packageSize / packageMd5 / packageSha256 / builtAt)。脚本内部依次做:构建前端远程产物 → 构建后端 jar → 组装包目录 → 签名 → 打 zip 并算校验值。
包打好之后剩下的动作都在服务端执行,每一步都能单独核对:
| 步 | 动作 | 端点 | 权限 | 做了什么 |
|---|---|---|---|---|
| ③ | 上传 | POST /app/package/parse(multipart:file,可选 appId) | app:console:add | 解包读清单 → 清单校验 → 安全检查(白名单 + 验签)→ 落对象存储;返回 packageUrl / packageSize / packageMd5 / packageSha256 / manifestJson / securityNote |
| ④ | 保存版本 | POST /app/version | app:console:add | 新增一行 app_version,状态 0 待发布 |
| ⑤ | 发布 | POST /app/version/{versionId}/publish | app:console:edit | 状态置 1,release_time 置当前,回写 app_info.latest_version |
| ⑥ | 上架 | POST /app/console/{appId}/status?status=1 | app:console:edit | app_info.status 置 1;没有 latest_version 会被拒 |
| ⑦ | 部署 | POST /app/version/{versionId}/deploy | app:console:edit | 下载 → 校验校验和 → 安全检查 → 解包落位 → 热加载后端插件 |
| ⑦ | 安装 | POST /app/install | app:install:add | 与部署同一段落位代码,另外执行迁移脚本并同步菜单 |
端点、权限与其余动作(下架、弃用、卸载、任务查询)的完整表见 上架与发布;这几个动作在平台侧各自做了什么见 应用生命周期。
⑦ 是整条链路里最容易漏掉的一步
上传、保存、发布、上架都只是登记与状态变更,宿主磁盘上什么都没有。少了部署/安装,应用在市场里看得见、状态也正常,但后端 jar 不在 plugin-dir 里、前端产物不在 frontend-dir/<appCode>/ 下 —— 表现是插件接口回一句 应用「xxx」已启用,但后端产物未加载,请联系管理员重新部署(文案出自 AppRouteHintResolver)、菜单点开是「远程视图不可用」。
打包 → 上传 → 发布 → 上架 → 部署(或安装),少一步就不生效。
落位落到哪
AppPackageDeployService.deploy 落三样东西,配置键都在 app-market.deploy.*:
| 产物 | 落点 | 配置键 |
|---|---|---|
| 后端 jar | <plugin-dir>/ 根下 —— loader.path 只扫根、不递归子目录 | app-market.deploy.plugin-dir,注解兜底 plugins-dist |
| 前端产物 | <frontend-dir>/<appCode>/(取包内 frontend/ 整棵树,去掉这个前缀) | app-market.deploy.frontend-dir |
| 迁移脚本 | <plugin-dir>/migrations/<appCode>/,落位前先清空该目录 | 由 plugin-dir 派生 |
后端 jar 的「当前生效版本」记在 <plugin-dir>/active-plugins.txt:运行期的 PluginRuntimeManager 与启动期的 PluginClassPathLoader.listJars 都按这份清单选 jar,所以同一应用名下留着旧版本 jar 不会再被同时挂载(那条「两个版本的 jar 同时挂上 → 重复 mapper XML → 服务起不来」的老坑就是这样收口的)。见 插件运行时。
frontend-dir 的兜底值与 yml 当前值不是同一个目录
AppPackageDeployService 的注解兜底是 ../plus-ui/app-market-dist/remote-apps,而 application.yml 里现在写的是 ../wujies-admin/app-market-dist/remote-apps。这个物理路径必须就是宿主 /remote-apps/** 的位置,否则入口 404。改之前先读 配置项 与 前端部署与 nginx。
上传、发布、上架、部署、安装的区别
| 上传 | 发布 | 上架 | 部署 | 安装 / 升级 | |
|---|---|---|---|---|---|
| 谁做 | 发布方(上架端) | 发布方 | 发布方 | 上架端操作者 | 租户(市场页 / 我的应用) |
| 接口 | POST /app/package/parse | POST /app/version/{id}/publish | POST /app/console/{appId}/status?status=1 | POST /app/version/{id}/deploy | POST /app/install、POST /app/install/{installId}/upgrade |
| 权限 | app:console:add | app:console:edit | app:console:edit | app:console:edit | app:install:add / app:install:edit |
| 动宿主磁盘吗 | 否(只落对象存储) | 否 | 否 | 是 | 是 |
跑 migrations/ 吗 | 否 | 否 | 否 | 否(刻意的) | 是 |
| 热加载后端插件吗 | 否 | 否 | 否 | 是 | 是(排在迁移之后) |
「部署」为什么不跑迁移脚本
落位与加载被刻意拆成两个方法(都在 AppPackageDeployService):
| 方法 | 行为 | 谁调用 |
|---|---|---|
deploy(versionId) | 落位 + 立即热加载 | 控制台「部署」按钮(AppVersionController.deploy) |
deploy(versionId, false) | 只落位 | 安装 / 升级(PluginAppInstaller 传 false) |
安装链路把加载交给排在迁移之后的 assertBackendLoaded,于是三者的顺序被钉死为:落位 → 变更表结构 → 切换代码。顺序反过来的话,插件在启动时机读一个还没建的列就会炸在数据库上。
所以:给已有部署补产物用「部署」;要变更表结构必须走安装/升级。
部署报告的字段可以直接核对:hotLoaded / hotLoadNote / restartRequired / security / staleJars / activeJar / migrationCount / releasedCount。hotLoaded=true 表示热加载成功、不用重启;restartRequired=true 只会出现在「后端产物落位了但没加载起来」的情况(例如插件运行时被 -Dapp-market.plugin-runtime.enabled=false 关掉)。
「部署」不会改安装记录里的版本号
AppPackageDeployService 只写磁盘与运行时,不碰 app_install。所以「部署 1.0.1 → 停用 → 启用」会按安装记录去加载旧版本,而落位新版本时旧 jar 已被清理 —— 报错出自 PluginAppInstaller.assertBackendLoaded。
要让安装记录跟着走就走升级(POST /app/install/{installId}/upgrade,权限 app:install:edit),或者在「我的应用」里升级到新版本。两条升级线(平台 / 应用)的分工见 升级与补丁。
包长什么样
zip 的根目录就是包内容,没有多余的一层 <code>-<version>/:
text
<code>-<version>.zip
├── app.json 清单(唯一事实来源)
├── backend/<artifactId>-<version>.jar 后端产物,文件名 = assets.backend 的最后一段
├── frontend/ 前端产物整棵树(assets.frontend 的前缀部分)
│ ├── index.js
│ └── assets/...
├── migrations/01-create-<code>-tables.sql 有才打(-MigrationPath 可指向别处)
└── signature.txt 分离式签名,覆盖除它以外的全部文件包内允许出现哪些文件由白名单说了算(PackageSecurityInspector,可与源码逐条对照):
| 约束 | 值 |
|---|---|
| 顶层只允许 | app.json、signature.txt 以及 backend/、frontend/、migrations/ 三个目录 |
backend/ 下 | 只允许 .jar,且只能有清单 assets.backend 声明的那一个 |
migrations/ 下 | 只允许 .sql |
frontend/ 下 | 扩展名受允许列表与禁用列表双重约束:.jsp / .php / .war / .jar / .exe / .sh / .properties / .xml 等无论如何都拒绝 |
| 包体积 | 不超过 200MB,文件名必须以 .zip 结尾,清单读取上限 1MB(AppPackageController 里的常量) |
assets.frontend 要写带目录前缀的值
落位时按 assets.frontend 的目录前缀挑选要释放哪些条目:AppPackageDeployService.dirPrefix() 对不含 / 的值返回空串,紧接着的判断是 StringUtils.isNotBlank(frontendPrefix),于是那个分支整段不执行。也就是说清单里写 "index.js" 时,包里的 frontend/** 一条都不会被释放;后端 jar 也在包里时 released 非空,因此不会报错,而 /app/runtime/remote-apps 拼出的入口是 /remote-apps/<code>/index.js,前端只能拿到 404(AppRuntimeController.buildEntry 只取文件名)。
仓库里多数应用(gallery / mall / member / social …)写的是 "frontend/index.js"。⚠️ wujies-hello/app.json 当前写的是 "index.js",与这条约定不符 —— 照抄范例时以 frontend/index.js 为准。
纯前端应用是合法形态:清单里只写 assets.frontend、不写 assets.backend(仓库里的 wujies-finance 就是这种),包里没有 jar,打包时跳过后端构建与 jar 组装(签名仍要 JDK,签名工具是 Java 写的)。详见 打包应用。
打包前自检
| # | 检查 | 依据 / 怎么核对 |
|---|---|---|
| 1 | 清单 code / version / assets 三者自洽 | assets.backend 的文件名必须等于包内 backend/ 下那个 jar;code 要与上架端那条 app_code 一致(上传时传了 appId 就会校验) |
| 2 | 版本号用 x.y.z | 打包脚本的正则比后端宽松:1.0、1.0.0.1、1.0.0+build.5 都能打出包,但会在上传/保存版本时被后端清单校验拒掉 —— 两套正则的对照表见 上架与发布 |
| 3 | 菜单树含 type: "F" 按钮,且每条菜单都有 key | 按钮不进 app_version_menu 的归属映射,漏写有两个都不报错的后果:全新部署装完没有按钮、彻底删除也删不掉它们。见 菜单归属与保护 |
| 4 | 建表脚本在 wujies-<code>/migrations/ 下,且幂等 | 它既是安装时的建表依据,也是「彻底删除」时删表的唯一依据(AppMigrationService.declaredTables 只从 CREATE TABLE 解析表名);已执行过的脚本不要再改,app_schema_history 按 (app_code, script) 比对 checksum |
| 5 | 私钥就绪 | wujies/wujies-plugins/keys/private.pem(脚本会自动发现),或显式传 -SigningKey;本机没有时会自动 keygen 到该目录 |
| 6 | 宿主的安全模式与签名现状匹配 | application.yml 当前是 app-market.security.mode: enforce,未签名的包在上传时就会被拒。切换模式前先跑 GET /app/console/signature-readiness(权限 app:console:list),见 安全校验模式 |
| 7 | 前端自检干净 | 在宿主前端仓库 plus-ui/ 下跑 npm run check:remote(前端工程与已构建产物对账)、npm run check:globals(产物里有没有宿主自动导入宇宙之外的裸标识符)、npm run check:runtime(产物真正 import 的名字与垫片导出对账)。这三个脚本在 plus-ui/package.json 里;插件工程自己的 package.json 只有 build / watch / typecheck / clean |
| 8 | 远程产物目录名等于 code | frontend/<code>-app 的构建输出、-FrontendDist 的默认值、后端落位目录、/app/runtime/remote-apps 拼出的 URL,四处是同一个约定。见 前端远程应用 |
卡住了先看哪里
| 现象 | 先看 |
|---|---|
| 上传被拒 / 部署被拒 | 报错原文(enforce 下会写清是「未签名」还是「签名校验失败」)→ 密钥与签名 |
| 状态正常但功能不可用 | 是不是漏了第 ⑦ 步;再看部署报告的 hotLoaded 与 staleJars |
| 升级后表结构没变 / 升级被拒 | app_schema_history 的 checksum 判定与 migrations/ 的落位目录 → 数据库迁移 |
| 菜单点开是「远程视图不可用」 | frontend-dir 与宿主 /remote-apps/ 是不是同一个物理目录 → 前端部署与 nginx |
| 上面任一步失败 | 任务中心:步骤与日志边跑边落库 → 任务中心与日志 |