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/market | 3 |
| 应用上架(上架端) | /app/console | 7 |
| 应用分类 | /app/category | 5 |
| 版本与安装包 | /app/version、/app/package | 9 |
| 安装、升级与卸载(安装侧) | /app/install | 10 |
| 运行时答复(宿主前端) | /app/runtime、/app/edition | 3 |
| 工作台 | /app/workbench | 1 |
| 目录服务(服务间) | /app/catalog | 9 |
| 菜单归属 | /app/console | 1 |
| 合计 | 48 |
市场浏览(安装侧)
AppMarketController,前缀 /app/market。只读接口:逛市场与看详情,安装动作在 /app/install。
数据不读本地目录表,而是走 IAppCatalogService——远程目录模式下本地没有目录数据,不这么做就会出现「能装但逛不了市场」。
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| GET | /app/market/list | app:market:list | 已上架应用列表;可选 keyword(匹配名称或编码)、categoryId;单次上限 200 条 |
| GET | /app/market/{appCode} | app:market:list | 应用详情;不校验上架状态(已下架应用的详情页仍要能打开) |
| GET | /app/market/{appCode}/versions | app: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/list | app:console:list | 应用列表(含草稿与已下架);可选 appName、appCode(模糊)、status |
| GET | /app/console/signature-readiness | app:console:list | 签名就绪度自检:能否把 security.mode 切到 enforce;会真实下载已发布包(上限 100),属按需调用的诊断接口 |
| GET | /app/console/{appId} | app:console:list | 应用详情 |
| POST | /app/console | app:console:add | 新增应用(默认落草稿 status=0;installMode 默认 optional、audience 默认 tenant) |
| PUT | /app/console | app:console:edit | 修改应用;latestVersion 不接受前端传值 |
| POST | /app/console/{appId}/status | app: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/list | app:category:list | 分类列表(平铺返回,前端用 handleTree 组树);可选 categoryName、status |
| GET | /app/category/{categoryId} | app:category:list | 分类详情 |
| POST | /app/category | app:category:add | 新增分类;同层名称唯一 |
| PUT | /app/category | app:category:edit | 修改分类;父级不能是自己 |
| DELETE | /app/category/{categoryId} | app:category:remove | 删除分类(逻辑删除);存在子分类时拒绝 |
读接口不是上架端专属:市场页要用 GET /app/category/list 做筛选,所以用户版(edition=client)只拦这里的两条写方法。
版本与安装包
版本的维护与发布在 AppVersionController(前缀 /app/version),安装包解析上传在 AppPackageController(前缀 /app/package)。
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| GET | /app/version/list | app:console:list | 某应用的全部版本;必填 appId;返回裸实体(含签名与校验和) |
| GET | /app/version/{versionId} | app:console:list | 版本详情 |
| POST | /app/version | app:console:add | 新增版本(草稿 status=0);保存时即校验 manifestJson,并以清单回填 engineRange / version |
| PUT | /app/version | app:console:edit | 修改版本;已发布版本的清单不允许改(要改请发新版本) |
| POST | /app/version/{versionId}/publish | app:console:edit | 发布版本,并把它回写为该应用的 latest_version |
| POST | /app/version/{versionId}/deprecate | app:console:edit | 弃用版本;若弃用的是最新版,latest_version 回退到仍已发布的最新版 |
| DELETE | /app/version/{versionId} | app:console:remove | 删除版本;仍有租户在用则拒绝 |
| POST | /app/version/{versionId}/deploy | app:console:edit | 部署该版本:解包并把前后端产物释放到宿主运行目录;不经迁移、不跑菜单同步 |
| POST | /app/package/parse | app: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/install | app:install:add | 安装应用;请求体 AppInstallBo(appCode,超管可带 tenantId);返回任务 ID |
| GET | /app/install/mine | app:install:list | 本租户已安装应用;带 statusLabel 与 upgradable 标记 |
| POST | /app/install/{installId}/upgrade | app:install:edit | 升级;可选 versionId,不传则升到最新已发布版本 |
| POST | /app/install/{installId}/enable | app:install:edit | 启用(重新加载插件) |
| POST | /app/install/{installId}/disable | app:install:edit | 停用(unload;保留数据与授权) |
| POST | /app/install/{installId}/uninstall | app:install:edit | 卸载;purge 默认 false(保留数据),purge=true 彻底删除且不可恢复 |
| POST | /app/install/{installId}/sync-menus | app:install:edit | 重新同步该安装实例的菜单(幂等);用于修复 app_version_menu 漂移 |
| GET | /app/install/task/{taskId} | app:install:list | 单个任务详情(含进度、步骤与日志正文);taskId 只匹配数字 |
| GET | /app/install/task/list | app: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-menus | system: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/editionapp-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 从事后补救变成事前避免。