Wujies 无界平台
基础

在部署里使用应用

本文是 1.x 的归档文档(冻结于 1.x)。该分支只接收勘误修正, 内容基本不再变动;文中的源码路径、配置项与版本号均以 1.x 为准,不建议用于新项目。

新项目请查看 5.x 最新文档。已在 1.x 上运行的项目,本文仍是可靠依据。

这一页面向在部署里操作应用的人(租户管理员 / 运维):从哪个页面装、每个动作后端真实做了什么、菜单为什么有时候不立刻出现、卡住时去哪看。

它不解释"为什么这么设计"。机制与取舍见 应用生命周期、插件运行时 和 菜单归属与保护。

前提

下面的操作都在宿主前端里做,需要对应权限:浏览市场 app:market:list,安装/升级/启停/卸载 app:install:edit(或 app:install:add),看任务 app:install:list。接口清单见 HTTP 接口索引。

操作入口在哪

想做的事页面路由前端源码
逛市场、安装、升级应用市场/appmarket/marketplus-ui/src/views/appstore/market/index.vue
看已装应用的版本与状态、启停、卸载我的应用 → 已安装应用/myapps/installedplus-ui/src/views/appstore/installed/index.vue
看进度、步骤、失败原因我的应用 → 应用任务/myapps/taskplus-ui/src/views/appstore/tasks/
从工作台进应用工作台「我的应用」卡片/indexplus-ui/src/views/workplace/index.vue

两个页面上的动作都会在任务中心留下一条 app_install_task:安装、升级、启用、停用、卸载都是异步的,接口先返回 taskId,过程在后台跑。排查任何生命周期问题,第一站都是应用任务,见 任务中心与日志。

安装

界面上是「应用市场 → 选应用 → 安装 → 确认」,装完会自动启用(前端提示「安装成功,已自动启用」)。

后端按固定顺序做四件事(PluginAppInstaller,源码:wujies-platform/wujies-modules/wujies-app-market/src/main/java/cn/wujies/appmarket/spi/impl/PluginAppInstaller.java):

#任务日志里的步骤做了什么
1「从应用市场拉取安装包」先判断本机有没有该版本产物;缺(或正在换版本)就按目录里的 packageUrl 下载 → 校验 MD5/SHA256 → 产物白名单 + 签名校验 → 解包落位 → 写 active-plugins.txt。这一步不加载代码
2「执行版本迁移脚本」扫 <plugin-dir>/migrations/<appCode>/,按文件名排序逐个执行包内 migrations/*.sql,结果记进 app_schema_history;执行后清字典与参数的 Redis 缓存
3「校验后端产物:<jar>」先问插件运行时,再问 classpath 探针,最后"磁盘上有就装上";三种都拿不到就抛错,安装失败
4「同步菜单与授权」按 app.json 的 menus 幂等 upsert,再把该版本菜单授权给本租户的管理员角色

进度台阶是 10 / 20 / 30 / 40 / 60 / 100,状态迁移为 installing → installed → enabled,最后 AppMenuCache 按租户失效一次。编排与状态机在 wujies-platform/wujies-modules/wujies-app-market/src/main/java/cn/wujies/appmarket/service/impl/AppInstallServiceImpl.java。

迁移在加载之前,顺序是刻意的

真实顺序是 产物落位 → 变更表结构 → 切换代码 → 同步菜单。落位与加载被拆成两步(安装/升级调 AppPackageDeployService.deploy(versionId, false) 只落位),加载交给第 3 步。

反过来(先加载新代码再改表)的话,插件在启动时机读一个还没建的列就会炸。迁移失败会中断,且不会加载新代码 —— 宁可停在旧代码上。

安装失败可能拿不到 taskId

某些失败(例如后端插件未加载)会让接口直接抛异常,前端拿不到 taskId。但任务记录在动作执行之前就已落库,日志一定在。所以失败弹窗会直接把你送去「应用任务」。

菜单为什么没有立刻出现

这是安装/升级/卸载之后最常见的疑问,涉及三样不同的东西。

后端两份数据:

数据变化时机
sys_menu 里的菜单行安装/升级时由清单幂等 upsert(按 menu_key 复用既有 menu_id);卸载(保留数据)时保留,彻底删除时才删
本租户可见哪些菜单停用/卸载后,AppMenuFilterImpl.invisibleMenuIds() 让这些菜单不再出现在 /system/menu/getRouters 的结果里。该结果按租户缓存在 AppMenuCache,安装/升级/启用/停用/卸载的状态落库处会主动失效

前端第三样:菜单树只在登录时生成一次。 所以上述动作改完数据后,同一会话里那份旧树会继续生效,表现是「装完回工作台没有这个应用,强刷才出来」或「升级后页面还在按旧路径解析」。

前端为此提供了重建入口:plus-ui/src/plugins/appmarket/menuRefresh.ts 的 refreshMenus() —— 重新拉菜单并重建路由(幂等)。它失败不抛给调用方,只打 console.warn,顶多这次没刷新到。

动作成功后刷新菜单的地方说明
plus-ui/src/views/appstore/market/index.vue安装成功、市场内一键升级成功后各调一次
plus-ui/src/views/appstore/installed/index.vue升级/启用/停用/卸载走同一个 runAndShowTask,任务成功即调一次
plus-ui/src/views/workplace/index.vue工作台自愈:发现已装应用在旧树里找不到入口,先重建再判定

还是没出现时按顺序试:

  1. 手动刷新页面(等价于重新走一遍登录后的菜单生成);
  2. 去「应用任务」确认那次动作真的成功了 —— 失败时前端不会重建菜单;
  3. 菜单行本身漂移了(挂到了已删除的菜单 ID 上),用 POST /app/install/{installId}/sync-menus(权限 app:install:edit)重新同步,它是幂等的;自愈逻辑丢弃死映射时会在任务日志里 WARN 出被丢弃的 menu_key。

升级

「我的应用」列表里版本旁出现「可升级 x.y.z」时才显示升级按钮;市场卡片上也能一键升级到最新版。

相对安装,升级的行为差异是:

差异说明
强制重新拉包落位只要目标版本与当前版本不同,本机现有产物(按定义是旧版本)一律不复用。「文件存在就跳过」只在同版本重装时成立
菜单按新版本清单幂等 upsert同名 menu_key 复用既有 menu_id,租户已分配的权限不失效
保留原启停状态已停用的应用升级完仍是停用(状态迁移为 upgrading → disabled)

第一次差异是刻意做的:沿用旧产物会同时踩两个静默坑 —— 前端还是旧版、包内 migrations/*.sql 根本不落位。

同版本不能"升级"

后端会直接拒绝:「当前已是该版本,无需升级」。想重装请走重新安装或控制台部署。

回到旧版本

从市场选一个更低的版本安装或升级即可,菜单会按旧版本清单同步回去。

但表结构不会回滚 —— 迁移脚本只做 up。回退前请自己确认旧代码能不能吃新表结构,规则见 数据库迁移。

停用与启用

动作后端真实行为状态可迁移自
停用只做 unload(关子 Spring 上下文与类加载器、注销接口)。不动 app_install.status 之外的任何数据:菜单行、授权都保留enabled
启用先 load(把插件后端装回来)再补授权、放出菜单installed / disabled
停用不是"撤授权"

停用后菜单入口消失,是因为 AppMenuFilter 按安装状态隐藏了它们,而不是授权被回收。所以重新启用能立刻恢复,也不会破坏管理员此前的手工分配。

想彻底让入口和数据都消失,请看下面的卸载。停用只适合"临时收起来"。

按钮的可用性判断在 plus-ui/src/views/appstore/installed/index.vue(canEnable / 停用按钮条件),它刻意与后端的合法状态迁移表对齐:实现是 canEnable = (status) => status === 'installed' || status === 'disabled',只有 installed / disabled 才显示「启用」按钮。

所以 failed / uninstalled 的行不会出现启用按钮。反过来,如果图省事把它写成 status !== 'enabled',那才会给这两个状态也显示按钮,点下去必然报「非法的应用状态迁移」—— 同文件 canEnable 上方的源码注释说的正是这个反例。

启用时加载失败必须让启用失败,不能"看起来启用了":否则就是一个"菜单在、点进去打不开"的坏状态。这也是停用只隐藏入口、不删菜单的原因之一。

卸载:两种模式

前端弹窗只给两个选项,默认落在可恢复的那条路(plus-ui/src/views/appstore/installed/index.vue)。

保留数据(默认)彻底删除
本租户菜单授权回收回收
后端插件unloadunload
菜单行(sys_menu)保留(重装即恢复可见)删除(含后代,如 menu_type='F' 的按钮)
数据表全部保留DROP TABLE,表名取自包内迁移脚本的 CREATE TABLE
迁移记录(app_schema_history)保留删除(所以重装会重新建表)
字典与站点配置保留删除(dict_type LIKE '<app>_%' / config_category / config_key LIKE '<app>.%'),删完失效缓存
本机产物(后端 jar、前端目录、active-plugins.txt 记录)保留删除(Windows 上 jar 被 JVM 占用时会如实报出删不掉)
安装记录(app_install)保留,状态 uninstalled物理删除
防手滑确认即可要把应用名完整敲一遍才亮确认按钮

彻底删除时有两道闸

  1. 别人还在用就不动全局的东西。 菜单行、数据表、本机产物在一个部署内是所有租户共享的。只要还有别的租户装着同一个应用(app_install 里状态非 uninstalled 的其它记录),就跳过菜单 / 表 / 迁移记录 / 产物 / 字典与站点配置的删除,只在任务日志里说清楚。判据会排除自己那条记录。
  2. 平台表前缀受保护。 只删迁移脚本里声明过的表,且表名命中 sys_ / app_ / gen_ / flow_ / act_ / qrtz_ / snailjob_ / undo_log 前缀时一律跳过并告警 —— 防止一个写错的脚本把平台表带下水。

这两道闸都在 AppPurgeService 里,只跳过了哪一步会逐条写进任务日志。

刻意不删的两样

  • 任务记录(app_install_task)保留 —— 它是审计线索。代价是彻底删除后那些历史行显示「(应用已删除)」。
  • 对象存储里的文件不删 —— 平台不知道它们属于谁,也不该替用户删文件。需要时自行清理。

停用或卸载后,它的接口会说什么

插件路由是运行期注册的,应用一停用/卸载就被摘掉,而前端手里的页面可能还在调它的接口。此时宿主不会只回一句"请求地址不存在",而是给出可区分的提示:

情况提示
应用已停用明确说"已停用"
应用未安装或已卸载明确说"未安装或已卸载"
已启用但产物没加载明确说"产物未加载"
应用正在跑却 404保持原文案 —— 那是它自己的路径 bug,不能被"应用不可用"盖掉

HTTP 仍是 404 语义,但 body 用独立的业务码 4610,前端据此区分"应用不可用 / 真 404",而不是去 match 中文。同一应用的消息只提示一次(一个页面可能并发打十几个同应用的接口)。细节与事前规避手段(GET /app/runtime/enabled-apps)见 HTTP 接口索引。

排查速查表

症状先看哪里
安装/升级失败,原因不明「应用任务」的 current_step + error_msg + 日志尾部,见 任务中心与日志
失败在「校验后端产物」日志会区分"jar 不存在,还没部署"与"jar 存在但不在运行时 classpath 上",后者通常是启动方式不对
失败在「执行版本迁移脚本」日志里最后一个没有"已执行"记录的脚本就是罪魁;app_schema_history 会留 success='0' 一行
提示包未签名 / 安全检查未通过app-market.security.mode 与公钥配置,见 包安全与签名
装完菜单或工作台卡片要强刷才出来上文「菜单为什么没有立刻出现」
任务一直显示"执行中"进程中途被杀,等超时清扫按 update_time 收尾(默认 10 分钟)
界面报错但日志里没线索框架级原因(类加载、Spring 上下文、MyBatis 映射冲突)去服务端日志,见 故障排查

需要自己确认的两点

这两处在写这一页时没有实际运行验证,只核对了源码,遇到与现象不符时以运行日志为准:

  1. "不重启就能生效"取决于插件运行时开关。 app-market.plugin-runtime.enabled 的默认值在源码里有多个不一致的写法,而且这个键必须用 JVM 参数设置(启动加载器在 Spring 之前执行,读不到 yml)。启动日志里 [plugin-loader] 那行会说明当前走的是哪条路。详见 配置项索引 与 配置项。
  2. PluginAppInstaller 的类注释说"升级后需要重启 JVM",与同一文件里的实现不一致 —— 实现会把磁盘上已就位的 jar 直接 load 进运行时,并不要求重启。该注释属于未同步的历史描述。判断依据仍以第 1 点的开关与启动日志为准。

相关阅读

目录