在部署里使用应用
这一页面向在部署里操作应用的人(租户管理员 / 运维):从哪个页面装、每个动作后端真实做了什么、菜单为什么有时候不立刻出现、卡住时去哪看。
它不解释"为什么这么设计"。机制与取舍见 应用生命周期、插件运行时 和 菜单归属与保护。
下面的操作都在宿主前端里做,需要对应权限:浏览市场 app:market:list,安装/升级/启停/卸载 app:install:edit(或 app:install:add),看任务 app:install:list。接口清单见 HTTP 接口索引。
操作入口在哪
| 想做的事 | 页面 | 路由 | 前端源码 |
|---|---|---|---|
| 逛市场、安装、升级 | 应用市场 | /appmarket/market | plus-ui/src/views/appstore/market/index.vue |
| 看已装应用的版本与状态、启停、卸载 | 我的应用 → 已安装应用 | /myapps/installed | plus-ui/src/views/appstore/installed/index.vue |
| 看进度、步骤、失败原因 | 我的应用 → 应用任务 | /myapps/task | plus-ui/src/views/appstore/tasks/ |
| 从工作台进应用 | 工作台「我的应用」卡片 | /index | plus-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。但任务记录在动作执行之前就已落库,日志一定在。所以失败弹窗会直接把你送去「应用任务」。
菜单为什么没有立刻出现
这是安装/升级/卸载之后最常见的疑问,涉及三样不同的东西。
后端两份数据:
| 数据 | 变化时机 |
|---|---|
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 | 工作台自愈:发现已装应用在旧树里找不到入口,先重建再判定 |
还是没出现时按顺序试:
- 手动刷新页面(等价于重新走一遍登录后的菜单生成);
- 去「应用任务」确认那次动作真的成功了 —— 失败时前端不会重建菜单;
- 菜单行本身漂移了(挂到了已删除的菜单 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)。
| 保留数据(默认) | 彻底删除 | |
|---|---|---|
| 本租户菜单授权 | 回收 | 回收 |
| 后端插件 | unload | unload |
菜单行(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 | 物理删除 |
| 防手滑 | 确认即可 | 要把应用名完整敲一遍才亮确认按钮 |
彻底删除时有两道闸
- 别人还在用就不动全局的东西。 菜单行、数据表、本机产物在一个部署内是所有租户共享的。只要还有别的租户装着同一个应用(
app_install里状态非uninstalled的其它记录),就跳过菜单 / 表 / 迁移记录 / 产物 / 字典与站点配置的删除,只在任务日志里说清楚。判据会排除自己那条记录。 - 平台表前缀受保护。 只删迁移脚本里声明过的表,且表名命中
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 映射冲突)去服务端日志,见 故障排查 |
需要自己确认的两点
这两处在写这一页时没有实际运行验证,只核对了源码,遇到与现象不符时以运行日志为准:
- "不重启就能生效"取决于插件运行时开关。
app-market.plugin-runtime.enabled的默认值在源码里有多个不一致的写法,而且这个键必须用 JVM 参数设置(启动加载器在 Spring 之前执行,读不到 yml)。启动日志里[plugin-loader]那行会说明当前走的是哪条路。详见 配置项索引 与 配置项。 PluginAppInstaller的类注释说"升级后需要重启 JVM",与同一文件里的实现不一致 —— 实现会把磁盘上已就位的 jar 直接load进运行时,并不要求重启。该注释属于未同步的历史描述。判断依据仍以第 1 点的开关与启动日志为准。