Wujies 无界平台
平台功能(自研)参考

HTTP 接口索引

本页收录「应用市场」模块对外暴露的全部 HTTP 端点,按用途分组。

来源是源码本身,不是约定:

  • wujies-modules/wujies-app-market/.../controller/ 下的 9 个 *Controller;
  • 目录服务接口 catalog/AppCatalogApiController(注意它在 catalog/ 包,不在 controller/ 包);
  • 路径与请求头常量 catalog/CatalogApiContract;
  • 业务码契约 wujies-common-core 的 plugin/AppRouteHintProvider。

通用约定

项说明
响应体统一为 {code, msg, data},code=200 表示成功。例外:/app/catalog/** 直接返回业务对象,无信封(见下文)
登录除 /app/catalog/**(在 security.excludes 里)外,所有端点都在登录拦截范围内,未带会话返回 code=401
权限由方法上的 @SaCheckPermission("...") 声明;下表「权限」列写的就是注解里的字符串
租户安装实例类接口一律作用于当前登录者的租户;只有超级管理员能通过 tenantId 参数指定别的租户
版本形态app-market.edition=client(用户版)时,上架端前缀一律被 MarketEditionGuardFilter 拦成 403

分组速览

分组路径前缀端点数
市场浏览(安装侧)/app/market3
应用上架(上架端)/app/console7
应用分类/app/category5
版本与安装包/app/version、/app/package9
安装、升级与卸载(安装侧)/app/install10
运行时答复(宿主前端)/app/runtime、/app/edition3
工作台/app/workbench1
目录服务(服务间)/app/catalog9
菜单归属/app/console1
合计48

市场浏览(安装侧)

AppMarketController,前缀 /app/market。只读接口:逛市场与看详情,安装动作在 /app/install。

数据不读本地目录表,而是走 IAppCatalogService——远程目录模式下本地没有目录数据,不这么做就会出现「能装但逛不了市场」。

方法路径权限说明
GET/app/market/listapp:market:list已上架应用列表;可选 keyword(匹配名称或编码)、categoryId;单次上限 200 条
GET/app/market/{appCode}app:market:list应用详情;不校验上架状态(已下架应用的详情页仍要能打开)
GET/app/market/{appCode}/versionsapp:market:list该应用的已发布版本列表(新版本在前),字段经 AppVersionPublicVo 脱敏(无签名、MD5/SHA256、清单原文、制品直链)

可见范围由登录身份解析,不由前端传:超级管理员不限;C 端用户(APP_USER)对应 member;其余 B 端用户对应 tenant。

与上架端的版本列表区别

GET /app/version/list 挂的是 app:console:list,并直接返回 AppVersion 实体(含签名、校验和、清单原文、包直链)。租户侧只能用本节的 /app/market/{appCode}/versions。

应用上架(上架端)

AppConsoleController,前缀 /app/console。面向平台运营,维护应用目录 app_info。

方法路径权限说明
GET/app/console/listapp:console:list应用列表(含草稿与已下架);可选 appName、appCode(模糊)、status
GET/app/console/signature-readinessapp:console:list签名就绪度自检:能否把 security.mode 切到 enforce;会真实下载已发布包(上限 100),属按需调用的诊断接口
GET/app/console/{appId}app:console:list应用详情
POST/app/consoleapp:console:add新增应用(默认落草稿 status=0;installMode 默认 optional、audience 默认 tenant)
PUT/app/consoleapp:console:edit修改应用;latestVersion 不接受前端传值
POST/app/console/{appId}/statusapp:console:edit上架 / 下架 / 退回草稿(status = 1 / 2 / 0)
DELETE/app/console/{appId}app:console:remove删除应用;系统应用与已上架应用一律拒绝,默认只有 demo-app 在白名单里

两处硬约束:

  • 上架前必须至少有一个已发布版本(latest_version 非空),否则报「应用尚未发布任何版本,无法上架」;
  • appId/status 变更会整片失效菜单缓存,因为「已上架数量」决定菜单过滤是否启用。

GET /app/console/owned-menus 也在这个前缀下,但用途是菜单归属,列在「菜单归属」一节。

应用分类

AppCategoryController,前缀 /app/category。分类是全局表(在 tenant.excludes 中)。

方法路径权限说明
GET/app/category/listapp:category:list分类列表(平铺返回,前端用 handleTree 组树);可选 categoryName、status
GET/app/category/{categoryId}app:category:list分类详情
POST/app/categoryapp:category:add新增分类;同层名称唯一
PUT/app/categoryapp:category:edit修改分类;父级不能是自己
DELETE/app/category/{categoryId}app:category:remove删除分类(逻辑删除);存在子分类时拒绝

读接口不是上架端专属:市场页要用 GET /app/category/list 做筛选,所以用户版(edition=client)只拦这里的两条写方法。

版本与安装包

版本的维护与发布在 AppVersionController(前缀 /app/version),安装包解析上传在 AppPackageController(前缀 /app/package)。

方法路径权限说明
GET/app/version/listapp:console:list某应用的全部版本;必填 appId;返回裸实体(含签名与校验和)
GET/app/version/{versionId}app:console:list版本详情
POST/app/versionapp:console:add新增版本(草稿 status=0);保存时即校验 manifestJson,并以清单回填 engineRange / version
PUT/app/versionapp:console:edit修改版本;已发布版本的清单不允许改(要改请发新版本)
POST/app/version/{versionId}/publishapp:console:edit发布版本,并把它回写为该应用的 latest_version
POST/app/version/{versionId}/deprecateapp:console:edit弃用版本;若弃用的是最新版,latest_version 回退到仍已发布的最新版
DELETE/app/version/{versionId}app:console:remove删除版本;仍有租户在用则拒绝
POST/app/version/{versionId}/deployapp:console:edit部署该版本:解包并把前后端产物释放到宿主运行目录;不经迁移、不跑菜单同步
POST/app/package/parseapp:console:add上传 zip 应用包(multipart/form-data):解出根目录清单 → 校验 → 落对象存储 → 回填地址与 MD5/SHA256;可选 appId 用于校验包内 code 一致
上传只登记,部署才落位

/app/package/parse 不做任何产物释放;/app/version/{versionId}/deploy 才落位。安装与升级走的是服务内部的 deploy(versionId, false)(只落位,加载推迟到迁移之后),与这里的按钮行为不同。

安装、升级与卸载(安装侧)

AppInstallController,前缀 /app/install。安装 / 启用 / 停用 / 卸载都返回任务 ID,进度与日志经任务中心查询。

方法路径权限说明
POST/app/installapp:install:add安装应用;请求体 AppInstallBo(appCode,超管可带 tenantId);返回任务 ID
GET/app/install/mineapp:install:list本租户已安装应用;带 statusLabel 与 upgradable 标记
POST/app/install/{installId}/upgradeapp:install:edit升级;可选 versionId,不传则升到最新已发布版本
POST/app/install/{installId}/enableapp:install:edit启用(重新加载插件)
POST/app/install/{installId}/disableapp:install:edit停用(unload;保留数据与授权)
POST/app/install/{installId}/uninstallapp:install:edit卸载;purge 默认 false(保留数据),purge=true 彻底删除且不可恢复
POST/app/install/{installId}/sync-menusapp:install:edit重新同步该安装实例的菜单(幂等);用于修复 app_version_menu 漂移
GET/app/install/task/{taskId}app:install:list单个任务详情(含进度、步骤与日志正文);taskId 只匹配数字
GET/app/install/task/listapp:install:list任务中心分页列表;不含日志正文,只给状态 / 进度 / 当前步骤 / 耗时
DELETE/app/install/task/{taskId}app:install:edit删除一条任务记录
// POST /app/install 请求体(AppInstallBo)
{ "appCode": "gallery", "tenantId": null }

purge=true 只是本租户的意愿,不是执行的保证:只要还有别的租户装着同一应用,删菜单 / 删数据表 / 删迁移记录 / 删本机产物都会被跳过,原因写在任务日志里——那些东西是整个部署共享的。

运行时答复(宿主前端)

AppRuntimeController(前缀 /app/runtime)与 AppEditionController(前缀 /app/edition)。这三个端点没有权限注解,原因见下一节。

方法路径权限说明
GET/app/runtime/remote-apps已登录即可(无权限注解)本租户已启用应用的前端 ESM 入口:{appCode, version, entry},entry 形如 /remote-apps/exam/index.js?v=1.0.1;纯后端应用不出现
GET/app/runtime/enabled-apps已登录即可(无权限注解)本租户已启用应用的编码列表(如 ["member","mall"]);与前端产物无关,查询失败返回空数组
GET/app/edition已登录即可(无权限注解)版本形态自述:{edition, consoleVisible};edition 取 official / client

/app/runtime/remote-apps 的两个细节值得记住:入口带 ?v=<版本号>(不带会被浏览器旧缓存命中,表现为「升级成功但页面还是老样子」);单条坏数据只记 WARN 跳过,不会让整份清单为空。

工作台

AppWorkbenchController,前缀 /app/workbench。

方法路径权限说明
GET/app/workbench/apps已登录即可(@SaCheckLogin)工作台应用卡片:system 组(系统应用,按可见范围)+ installed 组(平台内置可选应用 + 本租户已启用应用);含 homePath 与 upgradable

入口路径 homePath 是三级兜底的结果:当前版本菜单映射 → 跨版本按 appId 找根 → app_info.home_path;三级都取不到时应用卡片会被前端过滤掉。

目录服务(服务间)

AppCatalogApiController,前缀 /app/catalog。调用方是另一个部署的 RemoteAppCatalogService(app-market.catalog.mode=remote 时装配),不是浏览器。

方法路径权限说明
GET/app/catalog/app-by-code共享令牌按 code 取已上架应用;不存在即报错
GET/app/catalog/app共享令牌按 id 取应用(不校验上架状态)
GET/app/catalog/apps共享令牌批量取应用;ids=1,2,3;缺失的不返回
GET/app/catalog/version共享令牌按 id 取指定版本
GET/app/catalog/latest-version共享令牌按 appId 取最新已发布版本
GET/app/catalog/count-on-shelf共享令牌已上架应用数量
GET/app/catalog/market-search共享令牌市场列表;keyword、categoryId、audience(不传表示不限制)、limit(默认 200)
GET/app/catalog/find-app共享令牌按 code 取应用(不校验上架状态);不存在返回 null
GET/app/catalog/released-versions共享令牌某应用已发布版本列表;appId

事实清单:

  • 请求头名是 X-Catalog-Token,常量在 CatalogApiContract.TOKEN_HEADER,两端共用(路径也共用,避免改一处忘一处)。
  • 服务端令牌来自 app-market.catalog.token。留空表示不校验调用方(启动时打一条 WARN);非空时比对,不区分「没带」与「带错」,只回 ServiceException("目录接口令牌校验失败")。
  • 因为调用方没有本机登录会话,/app/catalog/** 写在 wujies-admin 的 security.excludes 里。
  • 它读的是平台级目录数据(app_info / app_version / app_category,均已在 tenant.excludes 中),不含任何租户的安装实例;安装实例(app_install / app_install_task / app_schema_history)始终留在各自部署本地。
  • 不套 R 包装:调用方是自家 Java 客户端,出错时照样由 GlobalExceptionHandler 吐 {code,msg},客户端取 msg 原样抛出,以保证远程模式与本地模式的报错文案一致。
不要把它暴露在公网

令牌留空即等于不校验。内网自用可以,公网部署必须配一个足够长的共享令牌,或用网关限制来源。

配置见 /reference/config(app-market.catalog.mode / base-url / token)。

菜单归属

应用装的菜单是 sys_menu 里的普通行,菜单管理页需要知道「哪些菜单是应用装的」。权威记录是 app_version_menu,反查链为:app_install → app_info → app_version → app_version_menu。

方法路径权限说明
GET/app/console/owned-menussystem:menu:list受应用托管的菜单清单:{menuId, appCode, appName},按 menuId 升序

两点说明:

  • 权限用菜单管理的读权限而不是应用市场权限——这份数据是菜单管理页的附属信息,能看菜单列表的人就该能拿到;
  • 改 path / component / perms / menuType / parentId 与删除这类菜单,由 SysMenuServiceImpl 在服务端拦截(MenuOwnershipProvider,缺席即「没有受保护菜单」)。这一段没有对应的 HTTP 端点,走的是平台既有的菜单管理接口。

不加权限注解的端点

有三个端点刻意不挂 @SaCheckPermission:

端点谁在用为什么不加
GET /app/runtime/remote-apps宿主前端 plugins/appmarket/bootstrap.ts看得到应用菜单的用户不一定有市场权限(菜单按角色授权),而这里返回的信息与他自己菜单里的等价;否则又会回到「能看到菜单却打不开页面」
GET /app/runtime/enabled-apps宿主前端 plugins/appmarket/appAvailability.ts同思路,且不要求有前端产物:宿主页面要在发请求前判断「这个应用现在能不能用」
GET /app/edition前端 plugins/appmarket/edition.ts前端有两条静态路由(/appconsole/app/create、/appconsole/app/edit/:appId)不是菜单给的,手打 URL 就能进;形态必须能问服务端,而任何登录用户都可能需要这个答案。它也不泄露业务数据

三者仍在登录拦截范围内,因此实际要求是「已登录即可」。

GET /app/workbench/apps 走的是同一个判断:它有 @SaCheckLogin 而没有功能权限注解——B 端与 C 端用户都要看工作台,而数据本身已按租户与可见范围收敛。

# 形态自述:任何登录用户的 token 都可以调
curl.exe -H "Authorization: Bearer <token>" http://localhost:8080/app/edition
用户版下的 403 有例外

app-market.edition=client 时,/app/console、/app/version、/app/package 全部方法,以及 /app/category 的写方法,都会被过滤器拦成 403(响应体与 GlobalExceptionHandler 同结构,code=403)。唯一例外是 GET /app/console/owned-menus——用户版也有菜单管理页;signature-readiness 不在此列(它读本地目录,用户版下没有意义)。

业务码

响应体统一 {code, msg, data};code 沿用平台约定(200 成功、401 未授权、403 拒绝、404、500 系统异常、601 警告),前端常量在 plus-ui/src/enums/RespEnum.ts。

应用市场只新增了一个专用码:4610。

4610:应用不可用

含义:你请求的这条路径曾经属于某个插件应用,而那个应用现在没在跑。HTTP 语义仍是「资源不存在」(走的就是同一个 404 兜底),但响应体的 code 是 4610,msg 是应用市场给出的具体措辞。

常量定义在 wujies-common-core 的 AppRouteHintProvider.CODE_APP_UNAVAILABLE,实现是 app-market 的 AppRouteHintResolver。

触发条件(三件同时成立才给提示):

#条件说明
①PluginHandlerMapping.appCodeFor(path) 命中路由墓碑插件路由是运行期注册的,注册时记墓碑、卸载时不清,按 Ant 语义反查({id} → *)
②PluginRuntimeManager.isLoaded(appCode) 为 false应用确实没在运行
③查本租户安装记录,组装措辞安装记录决定说哪一句

措辞(msg)按安装状态分四种:

安装状态msg
无记录(未安装 / 已卸载)应用「X」未安装或已卸载,请在「我的应用」中安装后再试
disabled应用「X」已停用,请在「我的应用」中启用后再试
enabled 但没加载应用「X」已启用,但后端产物未加载,请联系管理员重新部署
failed应用「X」当前状态异常,请在「我的应用」中查看任务日志或重新安装
其它状态应用「X」当前不可用(<状态标签>),请稍后重试或重新安装

未登录时(例如 token 过期后打的请求)以及远程目录模式下查不到安装记录时,走「未安装或已卸载」这句兜底。

前端怎么区分「应用被卸载」和「真 404」:

不要匹配中文文案,比较响应体的 code:

响应体 code前端行为
4610按 msg 弹一次警告(一个页面可能并发打十几个同应用的接口,同一句 msg 只提示一次),并 reject
404走通用错误提示(notifyError)——这条路径真的不存在

plus-ui/src/utils/request.ts 里的判定就是 code === HttpStatus.APP_UNAVAILABLE。

两个刻意的边界
  • 应用正在跑却 404:AppRouteHintResolver 返回空,保持 code=404「请求地址不存在」。否则应用自己的路径 bug 会被「应用不可用」盖掉。
  • 墓碑是内存态:只能认出「本次运行期间被卸载 / 停用」的路径。重启之后应用已卸载、页面也不该存在,通常够用;要连彻底删除很久之后的请求也认账,需要落一张路由索引表。

治本的配套是 GET /app/runtime/enabled-apps:前端在调某个应用的接口之前先问一次「它现在可用吗」,把 4610 从事后补救变成事前避免。

相关阅读

目录