Halo Plugin Dishes — 开发文档
本文面向在本仓库内二次开发、调试与打包的贡献者与维护者。
1. 环境与前置条件
| 项目 | 说明 |
|---|---|
| Halo | >=2.23.0 |
| JDK | 21 |
| 构建 | Gradle Wrapper(./gradlew / gradlew.bat)。 |
控制台 UI(ui/) | Node.js + pnpm |
前台(web-frontend/) | 同上,独立 Vite 工程。 |
插件版本号:根目录 gradle.properties 的 version=(构建产物与发布标签建议与此一致)。
2. 仓库结构总览
halo-plugin-dishes/
├── build.gradle # 根工程:Java + Halo 插件打包,串联 ui / web-frontend 产物拷贝
├── settings.gradle # 多模块:root + ui + web-frontend
├── gradle.properties # version=...
├── src/main/java/com/dishes/ # 后端 Java 源码
├── src/main/resources/
│ ├── plugin.yaml # 插件元数据(名称、requires、链接等)
│ └── templates/dishes.html # 前台入口 Thymeleaf:注入 CSRF、品牌文案、静态资源路径
├── ui/ # Halo 控制台插件(Vue 3 + @halo-dev/ui-plugin-bundler-kit)
├── web-frontend/ # 家庭点菜前台 SPA(Vue 3 + Vite + Tailwind)
└── README.md # 用户向:安装、部署、Nginx、排错
2.1 构建产物如何进 Jar
根 build.gradle 中大致逻辑:
processUiResources:ui子模块dist→build/resources/main/console(控制台插件静态资源)。processPublicFrontendResources:web-frontend/build/dist→build/resources/main/static/dishes-frontend(前台 SPA)。classes依赖上述任务,保证打 Jar 前前端已构建。
因此:改完 ui 或 web-frontend 后,完整发布仍需跑根目录 ./gradlew build(除非你只调试单独前端工程)。
3. 本地开发与调试
3.1 一键构建插件包
在项目根目录:
./gradlew clean build
# Windows
.\gradlew.bat clean build
产物:build/libs/ 下可安装的插件 Jar。
3.2 使用 Halo 插件开发环境(推荐)
根工程使用 run.halo.plugin.devtools(见 build.gradle)。具体任务名与用法以 Halo 插件开发文档 与当前 devtools 版本为准,常见流程包括:
- 启动本地 Halo(或 Docker 容器)加载当前插件;
- 修改 Java 后热加载或重启按 Halo/devtools 行为执行;
- 控制台路由挂载在 Halo Console,前缀见下文「控制台插件路由」。
3.3 单独调试 ui/(控制台)
cd ui
pnpm install
pnpm dev # Vite watch,输出到 dist,供 Gradle 拷贝或由 devtools 消费
pnpm build # 生产构建
pnpm exec vue-tsc --build # 类型检查
技术栈要点:@halo-dev/ui-plugin-bundler-kit 提供 viteConfig;入口 src/index.ts 通过 definePlugin 注册路由与侧边菜单。
3.4 单独调试 web-frontend/(前台 SPA)
cd web-frontend
pnpm install
pnpm dev # 默认端口见 vite.config.ts(示例 5173)
pnpm build
vite.config.ts 已为本地 pnpm dev 配置代理:
/apis/plugins/dishes→ 本地 Halo(示例8090)/plugins/dishes/public→ 同上
按需修改 target 为你的 Halo 地址。环境变量说明见 §7。
4. 后端架构(Java)
4.1 分层与职责(概念)
flowchart TB
subgraph api [HTTP]
Admin[AdminApiController]
Public[PublicApiController]
Router[SiteRouter]
end
subgraph facade [应用服务]
AdminF[AdminFacadeService]
PublicF[PublicFacadeService]
SettingsSvc[DishesSettingsService]
end
subgraph domain [领域与持久化]
Store[DishesStore / PersistentDishesStore]
Finder[PersistentDishesFinder]
DomainSvc[PersistentDishesDomainService]
end
subgraph ext [扩展模型]
HaloClient[ReactiveExtensionClient]
Scheme[ExtensionSchemeRegistry]
end
Admin --> AdminF
Public --> PublicF
Router --> SettingsSvc
AdminF --> Store
AdminF --> Finder
PublicF --> Store
SettingsSvc --> HaloClient
Store --> HaloClient
Finder --> HaloClient
Scheme --> Store
api.*:@RestController,对外 JSON;统一Envelope<T>包装;业务异常由ApiExceptionHandler转为 HTTP 200 +ok: false(与 Halo 控制台 axios 习惯一致)。注意:部分接口返回Mono<...>(WebFlux),导入备份等路径需在响应式链路上处理异常。service.*:Facade 聚合管理端/前台用例;admin/*细分订单、设置、备份等;publics/*访问令牌、域名白名单、通知等。domain.*:DishesStore为抽象端口;生产环境PersistentDishesStore通过ReactiveExtensionClient读写自定义 Extension;PersistentDishesFinder封装 list/fetch/nextId;PersistentDishesDomainService承载校验与规范化(如 slug、菜品字段)。extension.*:Halo Extension 定义(@GVK),对应 Console/API 中的持久化实体。ExtensionSchemeRegistry:注册扩展 Scheme + 索引;在索引异常场景下可rebuildAll()(需慎用,备份清空等路径会配合等待索引收敛)。
4.2 插件入口
DishesPlugin:继承 HaloBasePlugin,在start()中注册扩展方案并做一次 list/fetch 自检,确保索引可用。
4.3 自定义 Extension 一览(核心)
| 类型 | 用途 |
|---|---|
DishCategory | 菜品分类 |
Dish | 菜品 |
MealOrder | 点餐订单 |
DishesSettings | 插件全局设置(访问模式、前台路径、Logo、品牌文案、白名单、通知等) |
DishesSequence | (若存在)与发号/序列相关扩展,以源码为准 |
GVK、group/version 以各类 @GVK 注解为准。
4.4 HTTP 路由约定
管理端 API(需登录控制台会话 / 插件权限):
- 基础路径:
/apis/plugins/dishes/admin - 定义于
AdminApiController - 控制台 axios 客户端:
ui/src/api/client.ts中baseURL: '/apis/plugins/dishes/admin'
前台公共 API(匿名访问策略 + 可选密码 + 域名白名单):
- 同时映射:
/apis/plugins/dishes/public与/plugins/dishes/public(见PublicApiController) - 独立部署时 README 强调浏览器走
/plugins/dishes/public/...并经 同站点 Nginx 反代,避免跨域与 Cookie 问题
前台页面(HTML + SPA):
SiteRouter:RouterFunction,匹配配置的前台路径(默认/dishes),渲染templates/dishes.html- Thymeleaf 注入:CSRF、
title/brandTitle/brandSubtitle、publicBasePath、publicLogoUrl等;前台 Vue 从#appdata 属性 或window.__DISHES_*读取
4.5 数据备份
- 实现类:
service.admin.AdminBackupService - 导出:ZIP +
manifest.json(格式键、版本号见该类常量);菜品仅image_url,不含图片二进制。 - 导入:覆盖式——先清空订单、菜品、分类再重建;清空实现需考虑 WebFlux + 扩展列表索引滞后(多轮删除、短暂等待、必要时
rebuildAll()),详见源码注释。 - 控制器入口:
AdminApiController,POST导入使用FilePart+DataBufferUtils,勿使用 ServletMultipartFile。
5. 控制台插件前端(ui/)
5.1 路由与菜单
- 入口:
src/index.ts - 根路径:
/dishes(挂在 Halo ConsoleRoot下) - 子路由示例:
/dishes/dishes(菜品)、/dishes/orders(记录)、/dishes/settings(设置)
5.2 API 调用
- 封装:
src/api/client.ts - 备份导出:
Accept需兼容application/zip;备份导入:FormData,勿手写multipart/form-data的 Content-Type(边界 boundary 由运行时生成)。
5.3 与 Halo UI 组件库
使用 @halo-dev/components、@halo-dev/ui-shared 等,与 Halo 主版本对齐(见 ui/package.json 版本区间)。
6. 前台 SPA(web-frontend/)
6.1 职责
家庭用户点菜界面:首页、点菜页、路由 router、与 PublicApiController 对话。
6.2 品牌与标题
- Thymeleaf
dishes.html将站点标题与顶栏文案写入#app的data-*。 - 逻辑封装见
src/utils/publicBranding.ts;main.ts中router.afterEach组合document.title。
6.3 构建 base 与资源路径
vite.config.ts:base由VITE_ASSET_BASE推导;默认与插件静态目录/plugins/dishes/assets/dishes-frontend/对齐。dishes.html中 CSS/JS 使用该前缀下的app.js、assets/index.css。
7. 环境变量(web-frontend/)
构建或 pnpm dev 时通过 .env.development / .env.production(或被 loadEnv 加载的文件)设置。
| 变量 | 含义 |
|---|---|
VITE_ASSET_BASE | 静态资源 base;独立部署见 README。 |
VITE_PUBLIC_BASE | 前台路由 history 的 base(子路径部署)。 |
VITE_MEDIA_ORIGIN | 独立域名时,把 /upload/... 等资源解析到 Halo 主站根地址,避免图片 404。 |
VITE_API_PREFIX | (可选)自定义公共 API 前缀时需与 Nginx、后端映射一致,见 README。 |
8. 测试与质量
./gradlew test
前端:
cd ui && pnpm exec vue-tsc --build && pnpm test:unit
cd web-frontend && pnpm build # 内置 vue-tsc + vite build
9. 代码风格与约定
- 根目录
.editorconfig:缩进、换行等 IDE 统一配置。 - Java:UTF-8,release 21。
- 业务错误:抛出
BusinessException+BusinessErrorCode;新增错误码时同步控制台getApiErrorMessage的映射(如有)。
10. 常见开发问题(FAQ)
Q:改了控制台页面,Jar 里没变?
A:确认已执行 ui 的 build,且根目录 ./gradlew build 拷贝了最新 dist。
Q:前台 404 或资源路径不对?
A:核对 VITE_ASSET_BASE、vite 的 base 与 dishes.html 中的静态路径是否一致。
Q:公共 API 403 / CSRF?
A:Thymeleaf 页面会注入 CSRF;独立静态托管时需按 README 先 GET 触发 Cookie,并避免反代剥离 Set-Cookie。
Q:备份导入 415 / 500?
A:控制台须使用 multipart/form-data(boundary 自动生成);服务端须使用 WebFlux FilePart 读取文件,详见 AdminApiController 与 importBackupZip。
11. 延伸阅读
如在文档中发现与源码不一致之处,以 当前分支源码 为准,并欢迎提交 PR 修正本文。