Wujies 无界平台
部署与运维

配置项索引

本页收录「应用市场」的全部可配置项。每项都标注了它在源码中的位置。

  • yml 显式值:wujies-platform/wujies-admin/src/main/resources/application.yml 里写出来的值。
  • 注解兜底值:@Value("${app-market.xxx:默认值}") 里的默认值。没有在 yml 里出现的键,以注解为准; 两者不一致时必须按 yml 的实际值去理解运行行为,本页在「默认值」列用 yml 值 / 注解兜底 的形式标出。

spring.profiles.active 当前为 prod。仓库里只有 application.yml、application-dev.yml、application-prod.yml 三个 profile 文件(没有 application-docker.yml),其中后面两个都没有任何 app-market.* 配置 —— 所以 Spring profile 层面没有覆盖,应用市场的配置只在 application.yml 一处。

唯一的例外是容器化部署:1Panel 部署包里的 docker-compose.yml 用环境变量覆盖了其中若干项,见 容器化部署的覆盖方式。

读取时机决定改完要不要重启

public-key、edition、catalog.*、security.* 都是 Spring 启动期读入(@Value / @PostConstruct)。 改了这些,必须重建 wujies-admin.jar 并重启才生效 —— application.yml 被打进 jar,只改源码目录里的 yml 而不重新打包,等于没改。

部署路径

配置项类型默认值说明
app-market.deploy.plugin-dirstring(路径)plugins-dist / 注解兜底 plugins-dist(容器化部署被环境变量覆盖为 /app/plugins-dist)后端插件 jar 的落位目录,也是宿主启动参数 -Dloader.path 指向的目录。包内 assets.backend 释放到这里根下。loader.path 不递归子目录,所以迁移脚本落在 migrations/<appCode>/ 子目录里是安全的。消费方:AppPackageDeployService、PluginRuntimeManager、PluginAppInstaller、AppMigrationService、宿主的 PluginClassPathLoader(读系统属性)。
app-market.deploy.frontend-dirstring(路径)yml 显式 ../wujies-admin/app-market-dist/remote-apps / 注解兜底 ../plus-ui/app-market-dist/remote-apps(两者不一致,见下)前端远程应用产物的落位目录。包内 frontend/ 整棵树释放到 <frontend-dir>/<appCode>/(去掉 frontend/ 前缀)。该物理目录必须就是宿主 /remote-apps/<appCode>/ 的位置。消费方:AppPackageDeployService、PluginAppInstaller。
app-market:
  deploy:
    # 后端插件 jar 落位目录,必须与宿主启动参数 -Dloader.path 一致
    # ⚠️ loader.path 只加载该目录**根下**的 jar,不递归子目录
    plugin-dir: plugins-dist
    # 前端远程应用落位目录,必须与宿主 /remote-apps/ 的物理路径一致
    # ⚠️ 前后端分离部署时必须改成共享/分发目录,否则前端服务取不到产物
    frontend-dir: ../wujies-admin/app-market-dist/remote-apps

两个值都是相对路径,按进程工作目录解析(Paths.get(...).toAbsolutePath())。script/prune/start-host.ps1 因此会先 Set-Location 到 wujies-platform 根目录,工作目录错了就会把产物写到别处。

⚠️ frontend-dir 的 yml 显式值与注解兜底值不一致:yml 里是 ../wujies-admin/...(工作区顶层的 前端产物落位目录,基准是工作目录 wujies-platform/),注解兜底是 ../plus-ui/...(plus-ui 自己的 仓库根)。以 yml 为准;而 plus-ui 的 dev server / 构建只认自己仓库根下的 app-market-dist (vite/plugins/appMarketStatic.ts),两者是独立物理目录 —— 详见 仓库与目录结构。

插件运行时

配置项类型默认值说明
app-market.plugin-runtime.enabledbooleanyml 未配置 / 注解兜底 true是否启用「每插件独立上下文」运行时。开启时部署完直接热加载(不用重启);关闭则退回 PluginClassPathLoader + loader.path 的启动期加载,部署只落位、重启才生效。这个键必须用 JVM 参数 -D 设置,不能只写在 yml 里,见 不能只写在 yml 里的配置。
app-market.task.timeout-minuteslong(分钟)yml 未配置 / 注解兜底 10任务超过多久没有任何进度就判定为失败。由 AppTaskTimeoutSweeper 按 update_time 判定(不用 start_time,避免误杀耗时较长但一直在推进的任务)。只在进度卡死时触发,不是任务总时长上限。
app-market.task.sweep-initial-delay-mslong(毫秒)yml 未配置 / 注解兜底 30000卡死任务清扫器的首次延迟(@Scheduled 的 initialDelayString)。
app-market.task.sweep-interval-mslong(毫秒)yml 未配置 / 注解兜底 60000卡死任务清扫器的固定间隔(@Scheduled 的 fixedDelayString)。
@Value("${app-market.deploy.plugin-dir:plugins-dist}")
private String pluginDir;

@Value("${" + ENABLED_PROPERTY + ":true}")   // ENABLED_PROPERTY = "app-market.plugin-runtime.enabled"
private boolean enabled;
plugin-runtime.enabled 的同一属性在源码里有四个不一致的默认值
位置默认值
PluginClassPathLoader.java(宿主,读系统属性)"true" —— 让路
PluginRuntimeManager 的 @Value(实际注入值)true
PluginRuntimeManager.isEnabledByProperty()(读系统属性)"false",且全仓无调用方(死代码)
PluginRuntimeManager 的类注释写成「默认 false(仍走旧路径)」

实际生效的是 true:启动加载器让路,由插件运行时按每插件独立上下文加载。类注释与 isEnabledByProperty() 的 "false" 都是未同步的残留。application.yml 里没有这个键, 全靠注解兜底;script/、Dockerfile、docker-compose 里也都不设置它。

应用目录(catalog)

配置项类型默认值说明
app-market.catalog.modeenum local / remoteyml 显式 local / 注解兜底 local目录访问方式。local 直连本库,remote 走 base-url 指向的目录服务。装配用 @ConditionalOnProperty 二选一:LocalAppCatalogService 匹配 local(matchIfMissing = true),RemoteAppCatalogService 匹配 remote。刻意不用 @ConditionalOnMissingBean(那依赖 bean 定义顺序)。
app-market.catalog.base-urlstring(URL)yml 显式 '' / 注解兜底 ''(空串)目录服务地址。mode=remote 时必填,缺失则 RemoteAppCatalogService 构造时抛 IllegalStateException,启动失败。
app-market.catalog.tokenstringyml 显式 '' / 注解兜底 ''(空串)服务间共享令牌,两端必须配成一致。服务端(AppCatalogApiController)留空表示 /app/catalog/** 不校验调用方,只在启动时打一条 WARN。该接口只暴露平台级目录数据,但不应该暴露在公网。
app-market.catalog.connect-timeout-msint(毫秒)yml 显式 3000 / 注解兜底 3000远程目录服务的连接超时。目录读取不该拖垮安装流程,宁可快速失败。
app-market.catalog.read-timeout-msint(毫秒)yml 显式 15000 / 注解兜底 15000远程目录服务的读取超时。
  catalog:
    # local  直连本库(默认,单机部署用这个)
    # remote 走 base-url 指向的目录服务(多部署集中时用)
    mode: local
    # 目录服务地址;mode=remote 时必填,否则启动即失败
    base-url: ''
    # 服务间共享令牌。服务端(本文件的这个值)与调用方配成一致;
    # 服务端留空表示 /app/catalog/** 不校验调用方 —— 内网自用可以,公网必须配。
    token: ''
    # 远程模式的超时:目录读取不该拖垮安装流程,宁可快速失败
    connect-timeout-ms: 3000
    read-timeout-ms: 15000

目录服务接口 /app/catalog/** 已在 security.excludes 里(服务间调用没有本机会话), 身份由 app-market.catalog.token 兜底。详见 目录与版本形态。

版本形态(edition)

配置项类型默认值说明
app-market.editionenum official / clientyml 显式 official / 注解兜底 official同一套代码扮演两个角色。official 官方版:目录在自己库里 + 含「应用上架」全套功能 + 对外提供 /app/catalog/**。client 用户版:不含上架端,目录从官方 API 读。取值经 StringUtils.trim + equalsIgnoreCase 规范化,大小写不敏感;任何非 client 的值都按 official 处理(不报错)。
  edition: official
用户版必须与目录配置自洽,否则启动直接失败

MarketEditionStartupCheck 在 @PostConstruct 里校验组合,不自洽就抛 IllegalStateException:

  • edition=client 且 catalog.mode != remote → 启动失败(这个组合「能起来但什么都装不了」);
  • edition=client 且 catalog.base-url 为空 → 启动失败。

client 下上架端接口被 MarketEditionGuardFilter 拦截,返回 403 并附明确文案(不是 404、不是静默失效)。 例外只有 /app/console/owned-menus。详见 目录与版本形态。

包安全(security)

配置项类型默认值说明
app-market.security.modeenum off / warn / enforceyml 显式 enforce / 注解兜底 warn(不一致);容器化部署被 APP_MARKET_SECURITY_MODE 覆盖(1Panel 安装表单默认 warn)包安全检查模式。off 完全不检查;warn 检查并记录问题但照常安装/上架;enforce 有问题就拒绝。这是一处 yml 与注解兜底值不一致的配置,见下方说明。消费方:PackageSecurityInspector、SignatureReadinessService。
app-market.security.public-keystring(PEM 或裸 base64)yml 显式已配置一把公钥(BEGIN PUBLIC KEY 块)/ 注解兜底 ''(空串)包签名公钥(X.509 SubjectPublicKeyInfo 的 PEM,或裸 base64)。留空表示尚未配置校验能力 —— 此时 enforce 会拒绝所有包(包括自己签的),报错里会说明原因。启动期读入,改了必须重建宿主 + 重启。
app-market.security.allowed-frontend-extensionsstring(逗号分隔的扩展名列表)yml 显式 .js,.mjs,.cjs,.css,.json,.svg,.png,.jpg,.jpeg,.gif,.webp,.ico,.bmp,.woff,.woff2,.ttf,.otf,.eot,.map,.txt,.html,.wasm / 注解兜底同一份列表(字面量逐字符一致)包内 frontend/ 下允许出现的文件类型。白名单之外一律拒绝;.jsp / .php / .war / .jar 等由代码硬拒,不受此项影响。前端目录就是宿主的静态资源目录,所以这项管的是结构性问题。
  security:
    # off     完全不检查(等价于改造前的行为)
    # warn    检查并记录问题,但照常安装/上架
    # enforce 有问题就拒绝    ← 当前
    mode: enforce
    public-key: |
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAowY2/3CZDZrqwo7jber8
      ...
      -----END PUBLIC KEY-----
    allowed-frontend-extensions: >-
      .js,.mjs,.cjs,.css,.json,.svg,.png,.jpg,.jpeg,.gif,.webp,.ico,.bmp,
      .woff,.woff2,.ttf,.otf,.eot,.map,.txt,.html,.wasm
yml 里是 enforce,注解兜底是 warn

两者不同,且以 yml 为准。application.yml 的注释解释了原因:warn 是给「存量包都还没签名」的过渡期用的, 默认 enforce 会让它们全部装不上;2026-09-13 把所有已发布版本就地重签后(signature-readiness 给出 ready=true、signed=13 / unsigned=0)才正式切到 enforce。

如果这段 yml 被删掉或被 profile 覆盖,运行行为会静默退回 warn —— 未签名包会被照常安装,没有任何报错。 排查「为什么未签名的包也装上了」时,先确认实际的 mode 值。

切 enforce 的前提是 public-key 已配置:没配公钥时 enforce 会拒绝所有包。

容器化部署更容易踩这一条:1Panel 安装表单的 SECURITY_MODE 默认值就是 warn, compose 把它作为 APP_MARKET_SECURITY_MODE 传进来,覆盖掉镜像里 yml 的 enforce。 镜像里的 yml 是 enforce,实际生效的却可能是 warn。

密钥生成与轮换

公钥不是秘密,放在 yml 里没有安全问题 —— 它只回答「这个包是不是这把私钥签的」。私钥只留在发布方, 默认在插件仓库的 keys/(wujies/wujies-plugins/keys/,已 gitignore),绝不能进仓库。

java -cp wujies-modules/wujies-app-market/target/classes `
    cn.wujies.appmarket.security.PackageSignerMain keygen wujies/wujies-plugins/keys

换钥要同时做三件事:① 重新生成;② 把新 public.pem 贴进 public-key(package.ps1 -SyncPublicKey 可代劳); ③ 重建宿主并重启。旧钥签过的包届时全部作废。详见 签名与发布 与 安全模式。

其它宿主配置

应用市场本身没有独立的文件上传路径配置:安装包走 wujies-common-oss(SysOssService.upload), 对象存储键前缀是代码里的常量 app-market/package/(包)与 app-market/asset/(资产),不可配置。 本机落位路径由 app-market.deploy.* 决定。相关的宿主级上限与临时目录如下。

配置项类型默认值说明
spring.servlet.multipart.max-file-sizestring(容量)yml 显式 100MB单个请求文件大小上限。应用包上传(POST /app/package/parse,multipart)受它约束。
spring.servlet.multipart.max-request-sizestring(容量)yml 显式 1000MB整个请求大小上限。
spring.servlet.multipart.locationstring(路径)yml 未配置(用容器/系统临时目录);容器化部署由 SPRING_SERVLET_MULTIPART_LOCATION: /app/temp 指定上传文件的临时落盘目录。挂在持久卷之外时,进程被 SIGKILL 会留下垃圾文件。
server.undertow.max-http-post-sizestring(容量)yml 显式 1GBUndertow 的 HTTP POST 内容上限。
mybatis-plus.mapperPackagestringyml 显式 cn.wujies.**.mapperMapper 接口扫描包。插件 jar 里若带 Mapper 接口,必须落在这个表达式覆盖的包名内才会被注册。
mybatis-plus.mapperLocationsstringyml 显式 classpath*:mapper/**/*Mapper.xmlMapper XML 资源位置,用宿主类加载器扫描。插件自带的 XML 不靠它:插件 XML 随插件 jar 走,由 PluginMapperRegistrar 在插件加载时只读传入的那个 jar 动态注册进宿主 SqlSessionFactory(它刻意不写 classpath*: —— 那会把宿主的映射文件也捞一遍,撞 Result Maps collection already contains value,宿主直接起不来)。
mybatis-plus.typeAliasesPackagestringyml 显式 cn.wujies.**.domain实体别名扫描包。
spring:
  servlet:
    multipart:
      # 单个文件大小
      max-file-size: 100MB
      # 设置总上传的文件大小
      max-request-size: 1000MB
mybatis-plus:
  # 多包名使用 例如 cn.wujies.**.mapper,org.xxx.**.mapper
  mapperPackage: cn.wujies.**.mapper
  # 对应的 XML 文件位置
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # 实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: cn.wujies.**.domain

容器化部署的覆盖方式

wujies-platform/deploy/1panel/apps/wujies-platform-{server,client}/5.6.3/docker-compose.yml 用环境变量覆盖上面这些配置项。 Spring Boot 的 relaxed binding 会把大写下划线映射成小写点号,所以 APP_MARKET_CATALOG_BASE_URL 就是 app-market.catalog.base-url:

      APP_MARKET_EDITION: official
      APP_MARKET_CATALOG_MODE: local
      APP_MARKET_CATALOG_BASE_URL: ""
      APP_MARKET_CATALOG_TOKEN: ${CATALOG_TOKEN}
      APP_MARKET_DEPLOY_PLUGIN_DIR: /app/plugins-dist
      APP_MARKET_DEPLOY_FRONTEND_DIR: /app/remote-apps
      APP_MARKET_SECURITY_MODE: ${SECURITY_MODE}

这几个覆盖值与本地部署的默认值不同,替换了 yml 里的相对路径:

环境变量容器内取值说明
APP_MARKET_DEPLOY_PLUGIN_DIR/app/plugins-dist容器里的绝对路径。entrypoint.sh 会把它同时传给 -Dapp-market.deploy.plugin-dir,所以 yml 与启动加载器看到的是同一个目录。
APP_MARKET_DEPLOY_FRONTEND_DIR/app/remote-apps后端把前端产物释放到这里;nginx 容器挂同一个卷到 /var/www/remote-apps 并以 alias /var/www/remote-apps/ 对外提供 /remote-apps/。
APP_MARKET_SECURITY_MODE来自安装表单的 SECURITY_MODE注意它会覆盖 yml 里的 enforce。compose 注释说明:首次安装还没有签名公钥,enforce 会拒绝所有包,所以容器化部署默认走 warn。
APP_MARKET_EDITION / CATALOG_MODE / CATALOG_BASE_URL / CATALOG_TOKEN官方版 official+local;用户版 client+remote用户版(1Panel 包 wujies-platform-client)把这三个键设成 client/remote/官方地址,与 yml 默认值相反。
环境变量优先级高于 yml

容器化部署里排查「为什么值和 application.yml 里写的不一样」时,先看 docker-compose.yml 的 environment: 段。APP_MARKET_SECURITY_MODE 是最容易踩的一个:镜像里 yml 是 enforce, 但如果安装表单里 SECURITY_MODE 选了 warn,实际生效的就是 warn。

必须与其它东西对齐的配置项

这一节里的配置改错了不会报错,只会静默失效 —— 表现为「装上了却跑不起来」「升级成功了页面还是老样子」。

plugin-dir ↔ -Dloader.path

app-market.deploy.plugin-dir 与宿主的启动参数 -Dloader.path 必须是同一个目录。 loader.path 只被打包后的 PropertiesLauncher 识别,从 IDE 直接跑主类时会被静默忽略 (PluginClassPathLoader 就是为消灭这个情况而存在的)。

# script/prune/start-host.ps1
$args += "-Dloader.path=$PluginDir"     # 默认 $PluginDir = plugins-dist

两处默认值同为 plugins-dist,都按工作目录解析。另外 PluginClassPathLoader 读的是系统属性 app-market.deploy.plugin-dir(不是 yml),兜底也是 plugins-dist —— 三处默认值必须一致。 实际设置它的地方只有两处:

启动方式谁设置取值
本机脚本script/prune/start-host.ps1-Dloader.path=$PluginDir(默认 plugins-dist),工作目录设为 wujies-platform 根
容器deploy/docker/backend/entrypoint.sh-Dapp-market.deploy.plugin-dir="${PLUGIN_DIR:-/app/plugins-dist}",不传 loader.path
# application.yml(相对路径)
app-market.deploy.plugin-dir: <dir>

# 系统属性兜底(PluginClassPathLoader.java 同名属性,默认 plugins-dist)
-Dapp-market.deploy.plugin-dir=<dir>

# 本机启动(script/prune/start-host.ps1)
-Dloader.path=<dir>

script/prune/prune-plugins.ps1 / .sh 也各自硬编码同一个 plugins-dist 作为推断默认值;清理脚本与启动脚本 指向不同目录时会清理错地方。

loader.path 不递归子目录

它只加载指定目录根下的 jar —— 所以插件目录必须扁平,不能把 jar 放进子目录(迁移脚本落在 migrations/<appCode>/ 子目录里是安全的,它们不是 jar)。

至于"同一个 appCode 两个版本并存":两个加载时机都按 active-plugins.txt 选版本(2026-10-02 起), 启动期是 PluginClassPathLoader.listJars、运行期是 PluginRuntimeManager —— 不在清单里的残留 jar 会被刻意跳过。"两份同样的 mapper XML → Result Maps collection already contains value → 服务直接起不来" 只在 active-plugins.txt 缺失或为空(此时不过滤)时复现。script/prune/start-host.ps1 仍先跑 prune-plugins.ps1,是为了清磁盘与回收被占用的 jar 句柄。

容器化部署下根本没有 -Dloader.path

-Dloader.path 只在 script/prune/start-host.ps1 这一条本机启动路径上被设置。 deploy/docker/backend/entrypoint.sh 走的是另一条路,只传插件目录属性:

exec java $JAVA_OPTS -Dapp-market.deploy.plugin-dir="${PLUGIN_DIR:-/app/plugins-dist}" -jar /app/app.jar

所以容器里插件的加载完全依赖 PluginRuntimeManager(它既在启动期加载、也支持运行期热加载)。 这也意味着 plugin-runtime.enabled 在容器里不能被关掉 —— 关掉就既没有 loader.path 也没有插件运行时,插件不会被加载。

顺带一提:wujies-admin/pom.xml 里配了 <layout>ZIP</layout>,只有这个布局才认 -Dloader.path; 默认的 JarLauncher 会静默忽略它。

frontend-dir ↔ 宿主 /remote-apps/<appCode>/ 的物理路径

AppRuntimeController 拼出的入口是 /remote-apps/<appCode>/<文件名>,前端用动态 import() 加载它。 这个 URL 前缀由三处共同约定:

  1. AppRuntimeController 的常量 REMOTE_APP_URL_PREFIX = "/remote-apps/";
  2. 前端静态服务插件 plus-ui/vite/plugins/appMarketStatic.ts —— 把 URL 前缀 /remote-apps/ 映射到 磁盘目录 plus-ui/app-market-dist/(dev 中间件直接返回文件,build 时整目录复制进 dist/);
  3. 后端落位目录 app-market.deploy.frontend-dir,yml 当前值是 ../wujies-admin/app-market-dist/remote-apps (注解兜底 ../plus-ui/app-market-dist/remote-apps,与 yml 不一致)。

三者的相对位置关系:后端工作目录是 wujies-platform/,所以 ../wujies-admin/app-market-dist/remote-apps 指向工作区顶层那个 wujies-admin/ 目录;而 plus-ui 的 dev server / 构建只认自己仓库根下的 app-market-dist/(appMarketStatic.ts 里 path.resolve(root, 'app-market-dist'))。 两者是独立物理目录,"后端落位成功"与"浏览器能取到"可能对不上 —— 详见 仓库与目录结构。

这个物理目录随环境变化,代码里没有单一真相:

环境/remote-apps/<appCode>/ 的物理位置由谁提供
本机 devplus-ui/app-market-dist/remote-apps/<appCode>/appMarketStatic.ts 在 Vite 内部中间件之前注册静态服务,直接返回文件
本机 build整棵 app-market-dist/ 被复制进 Vite outDir(默认 dist/)→ dist/remote-apps/<appCode>/同上,writeBundle 阶段 fs.cpSync
容器宿主卷 ./data/remote-apps;后端容器挂到 /app/remote-apps、nginx 容器挂到 /var/www/remote-appsnginx location ^~ /remote-apps/ { alias /var/www/remote-apps/; }(^~ 必需,否则被末尾正则段抢走 → 404,见 前端部署与 nginx 坑 3)

plus-ui/vite.config.ts 的 server.proxy 没有 /remote-apps 规则(只有 VITE_APP_BASE_API → 后端端口), 所以 dev 下这个 URL 靠上面第 2 条的中间件、不是代理。

前后端分离部署必须改这一项

单体部署下 frontend-dir 就是本地目录,前后端在同一台机器上。前后端分离部署时它必须改成一个共享/分发位置 (容器化部署就是靠共享卷做到的),否则后端把产物释放到自己的磁盘,前端服务读的是另一份 —— 表现为「安装成功、菜单点开是远程视图不可用」。

c-frontend-dir ↔ 宿主 /apps-c/<appCode>/ 的物理路径

C 端(PC / H5)是整站,不是运行期模块 —— 它自带 index.html 与路由,由一个独立的 URL 前缀对外提供。应用包里的 frontend-c/ 整棵树被释放到 app-market.deploy.c-frontend-dir/<appCode>/:

环境/apps-c/<appCode>/ 的物理位置由谁提供
本机 devplus-ui/app-market-dist/apps-c/<appCode>/同一个 appMarketStatic.ts(/apps-c/ 已在它的 URL 前缀列表里)
本机 builddist/apps-c/<appCode>/writeBundle 的 fs.cpSync(apps-c 就在被复制的 app-market-dist/ 里)
容器宿主卷 ./data/apps-c;后端挂 /app/apps-c、nginx 挂 /var/www/apps-cnginx location ^~ /apps-c/ { alias /var/www/apps-c/; }

为什么与 /remote-apps/ 分两个根:托管方式不同。那份是被宿主 import() 的 ESM 模块 (只要 index.js 一个入口),这份是一个站点(要 index.html、可能的 assets/*.js)。 混在一个目录里,<appCode> 会撞车。

nginx 里这两个 location 都必须写 ^~

nginx 的匹配顺序是「= 精确 → ^~ 前缀 → 正则 → 普通前缀」。 配置里有一条 location ~* \.(js|css|png|…)$,它会抢在普通前缀 location 之前命中 /remote-apps/x/index.js 与 /apps-c/x/assets/index-abc.js,然后按 root(/usr/share/nginx/html) 去找文件 → 一律 404,表现是插件页面白屏 / C 端打不开。 加了 ^~ 的前缀 location 才会「命中即停止,不再看正则」。

远程产物目录名必须等于 appCode

outDir(wujies/wujies-plugins/frontend/<code>-app/vite.config.ts)、package.ps1 的 -FrontendDist 默认值、 后端落位目录、AppRuntimeController 拼出的 URL —— 四处是同一个约定。历史上 demo-app 的 outDir 写成了 remote-apps/demo(少个 -app),后果是库里两条 remote:demo-app/demo/* 菜单永远解析不出来。 (该应用现已从应用目录里去掉,示例应用改由 hello 承担,见 示例应用。)

前端侧的构建期变量

后端 app-market.* 之外,前端还有两个影响远程应用解析的构建期变量。

配置项类型默认值说明
VITE_APP_MARKET_REMOTE_APPSstring(code=entry 用逗号分隔)未找到 —— .env.development / .env.production / .env.docker 三个文件里都没有定义它静态远程应用清单。bootstrap.ts 的解析器静态清单优先,取不到才去问后端 GET /app/runtime/remote-apps;dev 环境另有内置的 demo-app / gallery 两条兜底(demo-app 已从应用目录去掉,那条兜底已失效),生产环境兜底为空。
VITE_APP_MARKET_EDITIONenum official / client三个 env 文件里都被注释掉版本形态的构建期兜底。运行时以服务端 GET /app/edition 为准,这个变量只在取不到时回落(默认 official)。写死成 client 会制造第二个真相。
# plus-ui/.env.development 第 41 行(.env.production / .env.docker 同理)
# VITE_APP_MARKET_EDITION = 'client'

plus-ui/.gitignore 忽略了 app-market-dist/ —— 那是构建产物,不进版本管理。

security.public-key:启动期读入,改了必须重建 + 重启

PackageSecurityInspector 与 SignatureReadinessService 都用 @Value 注入它,只在应用启动时解析一次。 运行时改 yml 无效,改源码目录里的 yml 也无效(application.yml 被打进 wujies-admin.jar)。

正确顺序:改 yml → 重新 mvn -pl wujies-admin -am package → 重启进程。

package.ps1 -SyncPublicKey 会把新公钥写进 application.yml 的 public-key: 块,但不会重建宿主 —— 它会提醒这一点。没重启就发布,表现为「刚用新私钥签的包被 enforce 拒了」。

不能只写在 yml 里的配置

配置项为什么正确做法
app-market.plugin-runtime.enabledPluginClassPathLoader 在 Spring 之前执行,读不到 yml,只能读系统属性。两个读取方必须看到同一个值。用 JVM 参数 -Dapp-market.plugin-runtime.enabled=false 退回旧行为。
app-market.deploy.plugin-dir同上:PluginClassPathLoader 用 System.getProperty(...) 解析插件目录。用 -Dapp-market.deploy.plugin-dir=<dir>(或 start-host.ps1 -PluginDir)。
只在 yml 里关掉 plugin-runtime.enabled 会让插件完全不加载

启动加载器读系统属性时兜底是 true,所以它让路;而 Spring 起来后 PluginRuntimeManager 读到 yml 的 false,于是也不加载。两边都不干活 —— 插件彻底不加载,PluginRuntimeManager 会就此打一条醒目告警:

插件运行时被配置为关闭(app-market.plugin-runtime.enabled = false)。注意:插件启动器读的是系统属性, 两者必须一致 —— 若要退回旧的 loader.path 行为,请用 JVM 参数 -Dapp-market.plugin-runtime.enabled=false, 而不要只改 yml,否则插件不会被加载。

正确的两种组合只有:默认(两边都按 true 走插件运行时),或显式 -D...=false(两边都退回 loader.path)。

security.excludes 里与目录接口有关的一条

security:
  excludes:
    # 应用目录服务接口:服务间调用,没有本机会话。
    # 已由 AppCatalogApiController 用共享令牌(app-market.catalog.token)兜底。
    - /app/catalog/**

把 /app/catalog/** 从排除列表里删掉,会让远程目录模式(catalog.mode=remote)立刻失效 —— 调用方没有本机会话,拿不到 token 就会被 Sa-Token 拦下。

security.tenant.excludes 里与应用市场相关的表

application.yml 的 security.tenant.excludes 列出不参与多租户隔离的表。其中与应用市场相关的有 6 张:

  tenant:
    excludes:
      # 应用市场:应用目录属于平台级数据,不按租户隔离
      # (带 tenant_id 的 app_install / app_install_task / app_subscription / app_audit_log /
      #   app_schema_history 不在此列,交由多租户插件自动隔离)
      - app_info
      - app_version
      - app_category
      - app_version_menu
      - app_dependency
      - app_plan
表数据性质为什么被排除
app_info应用主数据(编码、名称、上架状态、home_path)目录 = 平台级数据,全平台一份。多租户下同一款应用只能有一行,否则「上架一次要同步 N 遍」。
app_version应用版本(含 package_url / 校验和 / 清单)同上:版本是应用的属性,不是某个租户的资产。
app_category应用分类市场页的筛选维度,必须全局统一,否则每个租户看到不同的分类树。
app_version_menu版本 → 菜单的归属映射(menu_id + menu_key)它映射的目标 sys_menu 本身就在排除列表里(菜单是全局行、不分租户),映射表跟着全局。
app_dependency应用依赖(app_id → dep_app_code + 版本约束)表结构里没有 tenant_id,只描述应用之间的关系,是目录的一部分。
app_plan应用套餐(价格、有效期、配额)表结构里没有 tenant_id,由平台方定义;哪个租户买了哪个套餐记在 app_subscription(带 tenant_id,不排除)。

相对的,以下表带 tenant_id 且刻意不排除,由多租户插件自动加上租户条件: app_install(租户应用安装实例)、app_install_task(安装任务与日志)、app_subscription(订阅)、 app_audit_log(审计)、app_schema_history(迁移记账)。

这个划分是目录中心化的前提

app-market.catalog.mode=remote 能把目录搬到另一台机器,靠的就是这几张表不带租户参数 —— 目录接口 /app/catalog/** 因此不需要在请求里传租户。反过来,安装实例数据(app_install / app_install_task / app_schema_history)始终留在各自部署本地,不参与集中。 详见 数据模型 与 目录与版本形态。

未找到的项

以下内容在本仓库中未找到可配置项,因此本页不列出(列出来就是在编造默认值):

  • 应用包解压上限(单条目 256MB、条目总数 5000):是 AppPackageDeployService / PackageDigest 里的 private static final 常量,不可通过配置调整。
  • 对象存储键前缀 app-market/package/ 与 app-market/asset/:AppMarketConstants 里的常量,不可配置。
  • 应用市场专用的本地上传目录:未找到(落位路径由 app-market.deploy.* 决定,上传走 OSS; 临时目录用的是 Spring 自己的 spring.servlet.multipart.location,宿主 yml 里没设置)。
  • Spring profile 级的 app-market.* 覆盖:未找到(application-dev.yml / application-prod.yml 均无 app-market 出现)。容器化部署的环境变量覆盖见上一节,不属于 profile。
  • security.excludes 中其它与应用市场相关的条目:未找到(只有 /app/catalog/** 一条)。

相关阅读

目录