Wujies 无界平台
平台功能(自研)应用市场

应用目录

应用目录(应用 / 版本 / 分类)只存在于本部署自己的库里,安装侧统一经 IAppCatalogService 读取。

应用目录是「这家部署能装哪些应用」的数据:应用、版本、分类、依赖与版本菜单。 它和安装实例一起放在同一个库里,没有第二份。

这一页讲三件事:目录数据与安装数据怎么分家、安装侧为什么只能经 IAppCatalogService 读、 路由清单怎么随目录分发。多套部署怎么各自维护目录见 /deploy/multi-deployment。

一、三类数据先分家

判据是表上有没有 tenant_id,落在两处:DDL 在 wujies-platform/script/sql/app_market_schema.sql,全局表白名单在 wujies-platform/wujies-admin/src/main/resources/application.yml 的 security.tenant.excludes。

类别表位置依据
目录app_info、app_version、app_category、app_dependency、app_plan、app_version_menu、app_version_route本部署的库表上没有 tenant_id,已列入 security.tenant.excludes
安装实例app_install、app_install_task本部署的库表上有 tenant_id,不在 excludes 里,由多租户插件隔离
迁移账本app_schema_history本部署的库同上;唯一键是 (app_code, script),见 /advanced/lifecycle

目录表不带租户列,是「目录对所有租户可见」的原因,不是「目录能被另一台部署读取」的前提 —— 每套部署各有一份目录,互不共享。表间关系见 /design/data-model。

app_version_menu 也在全局表里

它是「版本 → 菜单」的映射,属于目录的一部分(部署内共享)。租户维度的过滤不靠多租户插件, 而是 SQL 自己带条件:AppVersionMenuMapper.selectInvisibleMenuIds(tenantId),消费方是 AppMenuFilterImpl。菜单归属本身的规则见 /advanced/menu-ownership。

二、唯一入口:IAppCatalogService

安装侧的代码只允许通过它读目录,不要直接注入 AppInfoMapper / AppVersionMapper。 接口是「按业务语义取用」而不是「把 Mapper 包一层」,这样目录读取的边界只有一处, 排查「市场页看到的东西和安装用的不一样」时不必翻遍 Mapper。

契约共 10 个方法(wujies-platform/wujies-modules/wujies-app-market/src/main/java/cn/wujies/appmarket/catalog/IAppCatalogService.java), 每个都有真实消费方:

方法语义消费方(都在 appmarket/ 下)
requireOnShelf(appCode)取已上架应用,未上架即抛异常service/impl/AppInstallServiceImpl(安装入口)
requireApp(appId)按 ID 取应用AppInstallServiceImpl、service/AppPackageDeployService、controller/AppRuntimeController
appsByIds(appIds)批量取应用(列表展示补齐,缺的不返回)AppInstallServiceImpl
requireVersion(versionId)取指定版本AppPackageDeployService、spi/impl/PluginAppInstaller、controller/AppInstallController、controller/AppRuntimeController、AppInstallServiceImpl
requireLatestVersion(appId)取该应用最新已发布版本AppInstallServiceImpl
countOnShelf()已上架数量(为 0 时菜单过滤可短路)service/impl/AppMenuFilterImpl.enabled()
searchOnShelf(keyword, categoryId, audience, limit)市场列表(按可见范围筛选)controller/AppMarketController.list
findApp(appCode)按编码取,不校验上架状态,找不到返回 nullAppMarketController.detail、service/AppRouteHintResolver
releasedVersions(appId)已发布版本列表(草稿/弃用不暴露)AppMarketController.versions、service/AppPurgeService
listRoutePatterns()全部应用的插件路由清单(按应用编码分组)service/CatalogRouteIndexSyncService

AppPackageDeployService.deploy(...) 走的是同一条路:安装的「下载并落位」先经目录接口取版本, 不自己查表。

「不许直接读目录表」只针对安装侧

判断标准是一句话:这段代码属于「装应用」吗?

  • 是(安装 / 升级 / 市场页 / 卸载 / 运行时答复)→ 必须走目录接口;
  • 不是(上架端与控制台就是目录的所有者)→ 直接用 Mapper 是正常的。实际这么做的有 controller/AppConsoleController、controller/AppVersionController、 controller/AppPackageController、security/SignatureReadinessService(签名就绪度要按状态查全部版本, 目录接口没有也不需要这种查询)。
读代码时会撞见两处残留,别照着抄

AppInstallServiceImpl 里仍声明着 appInfoMapper / appVersionMapper 两个字段,但已经没有任何调用 (目录读取全部走 appCatalogService,见该类 629 行附近的注释);AppPackageDeployService 顶部也留着 这两个 Mapper 的 import。它们不影响行为,只是改造期的残留。

三、唯一实现:LocalAppCatalogService

目录就在本库,所以只有一个实现:catalog/LocalAppCatalogService,直连 AppInfoMapper / AppVersionMapper / AppVersionRouteMapper。

没有 mode 开关,也没有远程实现 —— 目录不再有「本机 / 远端」两种来源。 曾经为远程读取做的 HTTP 超时、共享令牌、错误信封判断等参数与代码已全部移除。

为什么不加缓存

目录读取的频率是「安装 / 升级 / 逛市场时几次」,不是热路径;而目录的写入(上架、发版) 与读取在同一个库里,写完立刻可见。加缓存只能靠 TTL 猜,反而会让「刚上架却装到旧版本」变得不可解释。 宁可每次直查:慢一点,但结果可解释。

四、路由清单随目录分发

本地墓碑表 app_route_index 是加载时写的:一台从来没装过某个应用的部署,连墓碑都没有, 打它的接口只能回普通 404,而用户期望的是 4610「应用不可用」。

解决办法是把路由清单放进目录:app_version_route(目录侧)记录「哪个应用的哪个包里有这些路由模式」。 链路是:

  1. 写:应用包上传(POST /app/package/parse)时扫出清单落库 —— 优先读包内 route-index.json, 存量包没有它就从包内 jar 的注解扫(不加载插件的类)。
  2. 读:安装侧经 IAppCatalogService.listRoutePatterns() 取到。
  3. 同步:service/CatalogRouteIndexSyncService 把它灌进本部署的 app_route_index(只增不删)—— 启动预热一次,运行时由 /app/runtime/enabled-apps 顺手触发,按 10 分钟节流。
  4. 兜底:404 路径只读「内存墓碑 ∪ 本地表快照」,不查目录。

app_route_index 是墓碑表,不是目录的镜像:判据是「这台部署曾经有过哪些路由」, 所以同步只增不删。墓碑与 4610 的完整规则见 /advanced/plugin-runtime。

五、怎么核对

# 1) 市场列表:应返回本机目录里的已上架应用
curl.exe -H "Authorization: Bearer <token>" "http://localhost:8080/app/market/list"

# 2) 目录侧路由清单是否有数据(按你的库名替换)
docker exec <mysql容器> bash -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD -N -B -e \
  "select count(*) from `<库名>`.app_version_route"'

# 3) 本地墓碑是否已同步
docker exec <mysql容器> bash -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD -N -B -e \
  "select count(*) from `<库名>`.app_route_index"'

市场页是空的先查目录里有没有已发布版本(上架端 → 版本 → 发布),而不是查网络。

相关阅读

目录