应用目录
应用目录(应用 / 版本 / 分类)只存在于本部署自己的库里,安装侧统一经 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。
它是「版本 → 菜单」的映射,属于目录的一部分(部署内共享)。租户维度的过滤不靠多租户插件,
而是 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) | 按编码取,不校验上架状态,找不到返回 null | AppMarketController.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(目录侧)记录「哪个应用的哪个包里有这些路由模式」。
链路是:
- 写:应用包上传(
POST /app/package/parse)时扫出清单落库 —— 优先读包内route-index.json, 存量包没有它就从包内 jar 的注解扫(不加载插件的类)。 - 读:安装侧经
IAppCatalogService.listRoutePatterns()取到。 - 同步:
service/CatalogRouteIndexSyncService把它灌进本部署的app_route_index(只增不删)—— 启动预热一次,运行时由/app/runtime/enabled-apps顺手触发,按 10 分钟节流。 - 兜底: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"'市场页是空的先查目录里有没有已发布版本(上架端 → 版本 → 发布),而不是查网络。