Wujies 无界平台
开始使用

项目结构

这一页解决什么问题

接手这个项目时,真正会让人走错路的不是"目录很多",而是三件事:

  1. 代码不在一个仓库里 —— 后端、插件、宿主前端各自是独立 git 仓库,改一个功能常常要动两个;
  2. 应用不是平台的模块 —— 每个应用的实体、Mapper、XML、Service、controller、迁移脚本、 清单都在插件仓库里,平台里找不到它的源码;
  3. 同名目录指的不是同一个东西 —— 顶层 wujies-admin/ 和 wujies-platform/wujies-admin/ 是两回事,plus-ui/package.json 里的 name 又写着 wujies-admin。

下面按"仓库 → 后端 → 插件 → 前端 → 脚本 → 配置"的顺序给出当前真实布局, 以及每条结论的查证方式。本页对应的是当前工作区,不是历史文档里的旧布局。

三个仓库,没有共同 git 根

仓库角色技术栈顶层标志物
wujies-platform/后端宿主 + 平台 SQLJava 17 / Spring Boot 3.5.15pom.xml(revision 5.6.2)、wujies-admin/
wujies/wujies-plugins/插件本体 + 插件前端工程 + 打包器Maven(wujies-plugins-parent)+ pnpm workspacepackage.ps1、pnpm-workspace.yaml、frontend/
plus-ui/宿主前端Vue 3.5 / Vite 6.3vite.config.ts、src/、app-market-dist/

三个目录各自执行 git rev-parse --show-toplevel,会得到各自的路径 —— 它们没有共同 git 根,需要分别提交。

命名陷阱:wujies-admin 是三样东西

这个坑已经误导过两次排查,读任何路径前先按"有没有 pom.xml / package.json"判一下:

你看到的写法实际指判据
wujies-platform/wujies-admin/后端启动模块(Java,产物 wujies-admin.jar)有 pom.xml、src/main
工作区顶层的 wujies-admin/前端产物落位目录,里面只有 app-market-dist/没有 package.json、没有 src/
plus-ui/宿主前端仓库有 vite.config.ts、src/;package.json 的 name = wujies-admin(混淆的根源)

plus-ui/package.json 的 name 字段确实是 wujies-admin,那是名字不是位置。 凡是指向 src/plugins/**、src/views/**、app-market-dist/** 的,都是前端 = plus-ui/。

工作区里还有一个不在任何仓库里的 docs/

工作区根的 docs/(含开发问题记录、教程、方案)不在任何 git 仓库里, 改它不会留下版本历史。要长期保留的内容放到 wujies/wujies-plugins/docs/ 或 wujies-platform/docs/。

后端:wujies-platform/

平台根 pom 只挂四个聚合模块,全反应堆 38 个 pom.xml(含各层 parent):

wujies-platform/
├── wujies-admin/                 启动模块(PluginClassPathLoader 在这里,Spring 之前执行)
├── wujies-common/                基础设施(27 个子模块)
│   ├── wujies-common-core/       PluginServiceProvider + MenuOwnershipProvider(宿主反查插件/菜单归属的 SPI)
│   ├── wujies-common-web/        GlobalExceptionHandler(异常 → 前端的唯一出口)
│   └── wujies-common-plugin-sdk/ 宿主向插件暴露的能力契约(插件唯一应依赖的宿主入口)
├── wujies-modules/
│   ├── wujies-app-market/        ★ 应用市场:目录 + 运行时 + 上架端 + 安全
│   │   ├── catalog/              IAppCatalogService + Local/Remote 实现(目录访问的唯一入口)
│   │   ├── edition/              版本形态开关(official / client)
│   │   ├── plugin/               PluginClassLoader / PluginHandlerMapping
│   │   │                         / PluginRuntimeManager / PluginMapperRegistrar
│   │   ├── security/             PackageDigest / Inspector / SignerMain
│   │   ├── service/impl/         AppInstallServiceImpl(生命周期编排)
│   │   ├── spi/                  AppInstaller(安装器 SPI)
│   │   └── support/              AppTaskRecorder(进度落库)
│   ├── wujies-system/            系统管理(用户/部门/角色/菜单/字典/OSS/消息)
│   ├── wujies-workflow/          工作流
│   └── wujies-plugin-probe/      插件运行时探针(自带 mapper XML 与定时任务的最小样本)
├── wujies-extend/                独立服务:wujies-monitor-admin、wujies-snailjob-server
├── script/
│   ├── prune/                    start-host.ps1 / prune-plugins.ps1 / prune-plugins.sh
│   └── sql/                      platform_init.sql、update/*.sql、archive-upstream/、backup/
├── deploy/                       1Panel 应用模板与构建工具(deploy/tools/*.mjs)
├── plugins-dist/                 ★ 后端插件 jar 落位目录(不递归子目录)
├── dist-packages/                ⚠️ **历史**产物目录(打包器早已不再往这里写,仍在 .gitignore)
└── docs/                         平台侧文档 + docs/removed/(已归档的 wujies-portal、wujies-openapi)

平台里没有 *-api 契约模块,也没有 wujies-job / wujies-generator (定时任务随应用走,宿主只提供 wujies-common-job)。

两条"查证过"的事实,别按旧文档理解
  • wujies-modules/*-api 模块数为 0:在 wujies-platform 下 Get-ChildItem -Recurse -Directory -Filter '*-api'(排除 target)返回空。
  • 已归档的 portal / openapi 不算平台引用:这两个模块的源码在 wujies-platform/docs/removed/,不在任何 <modules> 里、不参与编译。 它们里面仍有 import cn.wujies.gallery.* 这类语句;把这批文件排除之后, 真正参与编译的模块里对 cn.wujies.<app>.* 的引用只剩一处测试 (wujies-app-market/src/test/.../PluginClassLoaderIdentityTest.java,用途是断言 PluginClassLoader.ownerAppCodeOf(...) 能按包名识别归属应用)。

契约随插件走:每个应用是自包含的一个工程

这是整套结构里最需要记住的一条,也是最近一次被推翻的设计。 旧结构是"契约放 *-api 模块(在 reactor 内)+ 实现放 plugins/*"——已废弃。

现在每个应用的全部代码都在插件仓库自己的目录里:

wujies/wujies-plugins/wujies-<app>/
├── app.json                                清单:code / version / assets / menus
├── pom.xml                                 artifactId = wujies-<app>-plugin
├── migrations/                             建表 + 字典 + 站点配置 + 种子数据
└── src/main/
    ├── java/cn/wujies/<app>/
    │   ├── domain/                        实体 + BO + VO
    │   ├── mapper/                        Mapper 接口
    │   ├── service/ + service/impl/       Service 接口与实现
    │   ├── controller/                    接口
    │   ├── open/                          开放接口(/open/**)
    │   ├── listener/                      消息监听
    │   └── task/                          定时任务
    └── resources/
        ├── mapper/<app>/*.xml             ★ Mapper XML 随插件 jar 走
        └── META-INF/spring/...AutoConfiguration.imports

以 wujies-gallery 为例,实体、mapper/*.java 与 resources/mapper/gallery/*.xml 确实同在一个模块下;wujies-hello 是同结构的最小模板,新插件从它复制。

平台对 cn.wujies.<app>.* 的 Java 引用为 0,带来的直接结果是: 新增一个业务应用不需要动平台一行。应用之间互相要契约时,依赖兄弟应用的构件 (同样 <scope>provided</scope>),例如 mall → member / pay / im。

两条不能违反的配套约定:

插件里的平台依赖必须 provided

插件 jar 里所有平台依赖(Spring、MyBatis、wujies-common-*、 wujies-common-plugin-sdk,以及兄弟插件构件)一律 <scope>provided</scope>。 打进插件会出现"同一个接口两个 Class 对象",注入与类型判断全部失效。 wujies/wujies-plugins/pom.xml 的 dependencyManagement 已经统一把 scope 定成 provided。

Mapper XML 靠代码注册,不靠 mapperLocations

XML 随插件 jar 走,由 PluginMapperRegistrar (wujies-app-market/src/main/java/cn/wujies/appmarket/plugin/PluginMapperRegistrar.java) 在插件加载时动态注册进宿主 SqlSessionFactory。

宿主 application.yml 里那行 mapperLocations: classpath*:mapper/**/*Mapper.xml 用宿主类加载器扫 classpath,扫不到插件里的 XML;反过来如果拿它去"补扫", 会把宿主的映射文件再注册一遍,直接撞 Result Maps collection already contains value 导致宿主起不来。注册顺序与清理逻辑的踩坑记录见 开发问题与解决方案.md §2.1。

插件仓库:wujies/wujies-plugins/

wujies/wujies-plugins/
├── pom.xml                   父 POM(wujies-plugins-parent);<modules> 列出所有后端插件 + sandbox
├── package.ps1 / package.cmd ★ 通用打包器(构建 / 打包 / 签名 / keygen 一个入口)
├── release.ps1               ★ 发布一个版本(读源 app.json 的 version → 打包 → 打 tag → 推 tag)
├── keys/                     签名密钥(已 gitignore)
├── dist-packages/            ⚠️ **历史**产物目录(2026-10-05 起不再写入,仍在 .gitignore)
├── wujies-<app>/             各插件模块(源码见上一节)
├── frontend/                 ★ 插件前端工程(pnpm workspace,`packages: frontend/*-app`)
│   ├── build/vite.ts         defineRemoteApp:共享的构建约定
│   └── <code>-app/           每个应用一个工程
├── sandbox/                  插件接口的开发工具,不进任何应用包
└── docs/                     插件侧文档(有版本历史)

插件模块与前端工程的对应关系只差一个 -app 后缀:wujies/wujies-plugins/wujies-mall 对应 frontend/mall-app,构建产物落成 remote-apps/mall/。

frontend/ 当前有 enterprise / exam / finance / gallery / hello / im / mall / member / pay / quote / social 十一个 -app 工程,与对应的插件模块一一对齐 (wujies-<app>/ ↔ frontend/<app>-app/)。唯一还没有落位产物的是 hello (示例应用,打包后产物落在 remote-apps/hello/)—— 见下。

落位目录里的 demo-app 是个孤儿,hello 则没有产物

plus-ui/app-market-dist/ 是被 gitignore 的本地构建目录,不是仓库内容。 它下面 remote-apps/ 目前有 11 个应用目录:十个真实应用 + 一个 demo-app (顶层 wujies-admin/app-market-dist/remote-apps/ 只有那十个)。

  • demo-app 是 app-market-poc 时代遗留的示例产物,没有对应的插件模块, 并且已经从应用目录里去掉(生成器的 EXCLUDED_APPS,见 示例应用); 它只可能以旧构建残留的形式留在磁盘上,重新构建前端之后就不会再出现;
  • hello 有 frontend/hello-app 工程,但还没有打过包,所以落位目录里还没有它。

所以判断"某个应用是否真的装了 / 有没有前端",不要看目录名: 看 plugins-dist/active-plugins.txt 与包内清单的 assets.frontend。

插件产物怎么进到宿主里

三步,每一步都可以独立查:

步骤谁做落点
打包package.ps1 / release.ps1<插件仓库的上一级>\.plugin-dist\<code>\<version>\<code>-<version>.zip(仓库外;release.ps1 顺带打 tag <code>-v<version>)
部署(安装/升级)AppPackageDeployService后端 jar → app-market.deploy.plugin-dir;前端 → app-market.deploy.frontend-dir/<appCode>/
加载启动期 PluginClassPathLoader / 运行期 PluginRuntimeManager插件 jar 挂进宿主,Mapper / 路由 / 菜单注册

plugins-dist/ 里当前登记 9 个应用(active-plugins.txt 一行一条 appCode=jarFileName):

gallery  quote  enterprise  social  exam  pay  im  member  mall

wujies-hello 与 wujies-finance 不在其中:前者是模板,后者是纯前端应用 (app.json 只写 assets.frontend,包里没有 jar,因此也没有 pom.xml、不出现在父 POM 的 <modules> 里)。

启动期与运行期对"同一 appCode 有两个版本 jar"的判据现在是一致的(2026-10-02 起),prune 脚本因此从"必须"变成"建议":

时机加载者选版本?
运行期(部署 / 热加载)PluginRuntimeManager选:只加载 active-plugins.txt 记的那个
JVM 启动瞬间启动器的 -Dloader.path选:PluginClassPathLoader.listJars 同样按 active-plugins.txt 过滤,不在清单里的 jar 不挂载(启动日志打印「跳过 N 个不在 active-plugins.txt 里的 jar」)

⚠️ 唯一的例外是清单读不到(文件缺失或为空)——那时启动器不过滤,退回"目录下所有 jar 全挂"的旧行为, "带着残留重启会直接起不来"只在这种情况下复现。

所以"运行期留着旧 jar"是安全的(残留也不会在下次启动时被挂上);start-host.ps1 每次都先跑 prune-plugins.ps1 是为了清磁盘与回收文件句柄,也是 active-plugins.txt 存在的意义。

前端:plus-ui/(宿主)与插件前端工程(远程应用)

宿主前端不是"应用的容器",它只提供壳:

plus-ui/
├── src/
│   ├── plugins/appmarket/            ★ 应用市场前端运行时
│   │   ├── bootstrap.ts              远程应用入口清单解析(静态清单优先,其余问服务端)
│   │   ├── loader.ts                 运行期 import()、视图索引、路由注册
│   │   ├── runtime.ts                注入给远程应用的宿主运行时
│   │   ├── edition.ts                版本形态(官方版 / 用户版)判定
│   │   ├── appAvailability.ts        应用是否启用(页面守卫用)
│   │   └── menuRefresh.ts            装完/升级后刷新菜单树
│   ├── views/                        宿主自带页面
│   └── router/index.ts               静态路由(含 remote: 视图路由)
├── vite/plugins/appMarketStatic.ts   dev 由它提供 /app-runtime/** 与 /remote-apps/**
├── scripts/                          四个守门脚本 + gen-runtime-shims.mjs
└── app-market-dist/                  本仓库的产物目录(gitignore)
    ├── app-runtime/                  共享依赖垫片(import map 指向它)
    └── remote-apps/<appCode>/        远程应用产物
app-market-dist 有两个,别以为它们是同一个

application.yml 里 app-market.deploy.frontend-dir 当前是:

frontend-dir: ../wujies-admin/app-market-dist/remote-apps

即工作区顶层 wujies-admin/app-market-dist/remote-apps(相对路径,基准是宿主的 工作目录 wujies-platform/)—— 这是后端落位写文件的位置。

而 plus-ui 的 dev server / 构建只认自己仓库根下的 app-market-dist (vite/plugins/appMarketStatic.ts 里 sourceDir = path.resolve(root, 'app-market-dist')) —— 这是浏览器真正取到 JS 的位置。本次核对:

目录顶层 wujies-admin/...plus-ui/...
remote-apps/ 下应用目录10 个11 个(多一个遗留的 demo-app)
app-runtime/(import map 垫片)不存在有

两者是独立物理目录(都不是 junction/symlink)。所以"落位成功"与"页面能打开" 可能对不上;判断某个应用到底装没装,看 plugins-dist/active-plugins.txt, 不要看这两个目录名。上面那行配置是当前真实值, 不要照抄旧文档里的 ../plus-ui/... 写法。

插件前端工程的共同约定只有几条,都在 frontend/build/vite.ts 的 defineRemoteApp: 产物是独立 ESM、宿主组件走 hostSdkComponents 白名单、宿主源码别名走 hostAliases、 共享依赖由宿主的 import map 提供。三个"远程应用专属"的坑(产物目录名必须等于 appCode、 AutoImport 清单必须与宿主逐项一致、垫片少一个具名导出就整块链接失败)分别对应下面三个守门脚本。

守门脚本

plus-ui/scripts/ 下的四个脚本,专门拦"构建期全绿、浏览器里才炸"的问题:

命令检查什么
npm run check:remote模板组件能否解析、白名单名字是否存在、产物里有没有 process.env 残留
npm run check:globals产物里有没有"宿主自动导入宇宙"里未绑定的标识符(ReferenceError 的守门人)
npm run check:runtime远程产物真正 import 的名字与垫片导出对账,有缺口就非零退出
npm run audit:menus库里所有 C 菜单的 component 能否落到真实页面

npm run gen:runtime 生成运行期垫片,已挂在 predev / prebuild:* 上自动执行。

运维脚本

后端只有三个(都在 wujies-platform/script/prune/):

脚本作用
start-host.ps1启动宿主:先清理旧插件 jar(-SkipPrune 可跳过)→ 需要时重新打包 → 用正确的 -Dloader.path / profile / 工作目录启动
prune-plugins.ps1按 active-plugins.txt 清理非当前版本的插件 jar(-DryRun 只报告)
prune-plugins.sh上一项的 Linux 版本

注意脚本的参数默认值是当前事实:-PluginDir plugins-dist、-Profile prod。

常跑的命令与两个必读配置

# 后端:编译 / 打包 / 启动(在 wujies-platform 下)
mvn -B -q compile -DskipTests
mvn -pl wujies-admin -am -B install -DskipTests
powershell -NoProfile -ExecutionPolicy Bypass -File script\prune\start-host.ps1

# 宿主前端(在 plus-ui 下;plus-ui 自身要求 Node >=18.18.0)
npm install
npm run dev

# 插件后端 + 前端打包(在 wujies/wujies-plugins 下;该 workspace 要求 Node >=20,JDK 是硬前提)
mvn -B package -DskipTests
pnpm install
pnpm build
.\package.ps1 -Plugin wujies-gallery -SigningKey keys/private.pem
前端工程与产物目录是两回事,pnpm build 与 package.ps1 落点也不同
前端工程前端产物
在哪插件仓库 frontend/<code>-app宿主仓库 app-market-dist/remote-apps/<code>
package.ps1 参数-FrontendProject(默认 <code>-app)-FrontendDist(默认 app-market-dist/remote-apps/<code>)

package.ps1 在打前端前会把 WUJIES_FRONTEND_DIST 设成 -FrontendDist, 由 frontend/build/vite.ts 的 resolveOutDir() 读走;不设这个变量时 产物落在各工程自己的 frontend/<code>-app/dist/(本次核对:十一个工程都有 dist/)。 也就是说单独跑 pnpm build 不会把产物送进宿主目录 —— 要走宿主得用 package.ps1 或显式设 $env:WUJIES_FRONTEND_DIST。

注释里若还写着 wujies-admin/app-market-poc/<code>-app,那是历史遗留, 以 package.ps1 里 $frontendRoot 的定义(<插件仓库>/frontend)为准。

application.yml 的 profile 占位符是活的

wujies-admin/src/main/resources/application.yml 里写的是:

  profiles:
    active: @profiles.active@
#    active: prod

也就是 Maven 资源过滤占位符生效、硬编码那行被注释掉。 要切 profile 用命令行 -Dspring.profiles.active=prod,或在打包时用 -Dprofiles.active=dev 让占位符展开;不要照抄"占位符被注释掉、只能靠命令行"的旧说法。

怎么自己查证这一页

任何一条结论都可以用不依赖文档的命令复核:

想确认命令(在对应仓库根执行)
有几个仓库git rev-parse --show-toplevel
平台有几个模块(Get-ChildItem -Recurse -Filter pom.xml | Where-Object FullName -notmatch '\\target\\').Count
有没有 *-api 模块Get-ChildItem -Recurse -Directory -Filter '*-api' | Where-Object FullName -notmatch '\\target\\'
插件有哪些wujies/wujies-plugins/pom.xml 的 <modules>,以及 plugins-dist/active-plugins.txt
已部署哪些应用Get-Content plugins-dist/active-plugins.txt
前端工程有哪些Get-ChildItem wujies/wujies-plugins/frontend -Directory
前端产物落哪wujies-admin/src/main/resources/application.yml 的 app-market.deploy
谁在扫 Mapper XMLwujies-app-market/.../plugin/PluginMapperRegistrar.java
本页标明"未验证"的部分

以下是已知没有核实到底的,别当结论用:

  1. wujies-platform/docs/removed/ 里归档的 portal / openapi 是否曾经被 链接进 reactor(本页只验证了它们现在不在任何 <modules> 里);
  2. frontend/build/vite.ts 中 outDir 除 WUJIES_FRONTEND_DIST 之外的分支 没有逐个走查,只确认了 resolveOutDir() 的默认回落是工程自己的 dist/;
  3. 两个 app-market-dist 的文件内容没有逐文件比对,只核对到目录层面的差异。

相关阅读

目录