Skip to content

打包与上架 ​

这一章讲发布方要做的事:把插件仓库里的一个 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/versionapp:console:add新增一行 app_version,状态 0 待发布
⑤发布POST /app/version/{versionId}/publishapp:console:edit状态置 1,release_time 置当前,回写 app_info.latest_version
⑥上架POST /app/console/{appId}/status?status=1app:console:editapp_info.status 置 1;没有 latest_version 会被拒
⑦部署POST /app/version/{versionId}/deployapp:console:edit下载 → 校验校验和 → 安全检查 → 解包落位 → 热加载后端插件
⑦安装POST /app/installapp: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/parsePOST /app/version/{id}/publishPOST /app/console/{appId}/status?status=1POST /app/version/{id}/deployPOST /app/install、POST /app/install/{installId}/upgrade
权限app:console:addapp:console:editapp:console:editapp:console:editapp: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远程产物目录名等于 codefrontend/<code>-app 的构建输出、-FrontendDist 的默认值、后端落位目录、/app/runtime/remote-apps 拼出的 URL,四处是同一个约定。见 前端远程应用

卡住了先看哪里 ​

现象先看
上传被拒 / 部署被拒报错原文(enforce 下会写清是「未签名」还是「签名校验失败」)→ 密钥与签名
状态正常但功能不可用是不是漏了第 ⑦ 步;再看部署报告的 hotLoaded 与 staleJars
升级后表结构没变 / 升级被拒app_schema_history 的 checksum 判定与 migrations/ 的落位目录 → 数据库迁移
菜单点开是「远程视图不可用」frontend-dir 与宿主 /remote-apps/ 是不是同一个物理目录 → 前端部署与 nginx
上面任一步失败任务中心:步骤与日志边跑边落库 → 任务中心与日志

相关阅读 ​

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