项目结构
这一页解决什么问题
接手这个项目时,真正会让人走错路的不是"目录很多",而是三件事:
- 代码不在一个仓库里 —— 后端、插件、宿主前端各自是独立 git 仓库,改一个功能常常要动两个;
- 应用不是平台的模块 —— 每个应用的实体、Mapper、XML、Service、controller、迁移脚本、 清单都在插件仓库里,平台里找不到它的源码;
- 同名目录指的不是同一个东西 —— 顶层
wujies-admin/和wujies-platform/wujies-admin/是两回事,plus-ui/package.json里的name又写着wujies-admin。
下面按"仓库 → 后端 → 插件 → 前端 → 脚本 → 配置"的顺序给出当前真实布局, 以及每条结论的查证方式。本页对应的是当前工作区,不是历史文档里的旧布局。
三个仓库,没有共同 git 根
| 仓库 | 角色 | 技术栈 | 顶层标志物 |
|---|---|---|---|
wujies-platform/ | 后端宿主 + 平台 SQL | Java 17 / Spring Boot 3.5.15 | pom.xml(revision 5.6.2)、wujies-admin/ |
wujies/wujies-plugins/ | 插件本体 + 插件前端工程 + 打包器 | Maven(wujies-plugins-parent)+ pnpm workspace | package.ps1、pnpm-workspace.yaml、frontend/ |
plus-ui/ | 宿主前端 | Vue 3.5 / Vite 6.3 | vite.config.ts、src/、app-market-dist/ |
三个目录各自执行 git rev-parse --show-toplevel,会得到各自的路径 ——
它们没有共同 git 根,需要分别提交。
这个坑已经误导过两次排查,读任何路径前先按"有没有 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/(含开发问题记录、教程、方案)不在任何 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。
两条不能违反的配套约定:
插件 jar 里所有平台依赖(Spring、MyBatis、wujies-common-*、
wujies-common-plugin-sdk,以及兄弟插件构件)一律 <scope>provided</scope>。
打进插件会出现"同一个接口两个 Class 对象",注入与类型判断全部失效。
wujies/wujies-plugins/pom.xml 的 dependencyManagement 已经统一把 scope 定成 provided。
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/)—— 见下。
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 mallwujies-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>/ 远程应用产物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| 前端工程 | 前端产物 | |
|---|---|---|
| 在哪 | 插件仓库 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)为准。
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 XML | wujies-app-market/.../plugin/PluginMapperRegistrar.java |
以下是已知没有核实到底的,别当结论用:
wujies-platform/docs/removed/里归档的 portal / openapi 是否曾经被 链接进 reactor(本页只验证了它们现在不在任何<modules>里);frontend/build/vite.ts中outDir除WUJIES_FRONTEND_DIST之外的分支 没有逐个走查,只确认了resolveOutDir()的默认回落是工程自己的dist/;- 两个
app-market-dist的文件内容没有逐文件比对,只核对到目录层面的差异。