Halo Plugin Dishes — 开发文档

本文面向在本仓库内二次开发、调试与打包的贡献者与维护者。


1. 环境与前置条件

项目说明
Halo>=2.23.0
JDK21
构建Gradle Wrapper./gradlew / gradlew.bat)。
控制台 UI(ui/Node.js + pnpm
前台(web-frontend/同上,独立 Vite 工程。

插件版本号:根目录 gradle.propertiesversion=(构建产物与发布标签建议与此一致)。


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 中大致逻辑:

  • processUiResourcesui 子模块 distbuild/resources/main/console(控制台插件静态资源)。
  • processPublicFrontendResourcesweb-frontend/build/distbuild/resources/main/static/dishes-frontend(前台 SPA)。
  • classes 依赖上述任务,保证打 Jar 前前端已构建。

因此:改完 uiweb-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:继承 Halo BasePlugin,在 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.tsbaseURL: '/apis/plugins/dishes/admin'

前台公共 API(匿名访问策略 + 可选密码 + 域名白名单):

  • 同时映射:/apis/plugins/dishes/public/plugins/dishes/public(见 PublicApiController
  • 独立部署时 README 强调浏览器走 /plugins/dishes/public/... 并经 同站点 Nginx 反代,避免跨域与 Cookie 问题

前台页面(HTML + SPA)

  • SiteRouterRouterFunction,匹配配置的前台路径(默认 /dishes),渲染 templates/dishes.html
  • Thymeleaf 注入:CSRFtitle / brandTitle / brandSubtitlepublicBasePathpublicLogoUrl 等;前台 Vue 从 #app data 属性window.__DISHES_* 读取

4.5 数据备份

  • 实现类:service.admin.AdminBackupService
  • 导出:ZIP + manifest.json(格式键、版本号见该类常量);菜品仅 image_url,不含图片二进制。
  • 导入:覆盖式——先清空订单、菜品、分类再重建;清空实现需考虑 WebFlux + 扩展列表索引滞后(多轮删除、短暂等待、必要时 rebuildAll()),详见源码注释。
  • 控制器入口:AdminApiControllerPOST 导入使用 FilePart + DataBufferUtils,勿使用 Servlet MultipartFile

5. 控制台插件前端(ui/

5.1 路由与菜单

  • 入口:src/index.ts
  • 根路径:/dishes(挂在 Halo Console Root 下)
  • 子路由示例:/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 将站点标题与顶栏文案写入 #appdata-*
  • 逻辑封装见 src/utils/publicBranding.tsmain.tsrouter.afterEach 组合 document.title

6.3 构建 base 与资源路径

  • vite.config.tsbaseVITE_ASSET_BASE 推导;默认与插件静态目录 /plugins/dishes/assets/dishes-frontend/ 对齐。
  • dishes.html 中 CSS/JS 使用该前缀下的 app.jsassets/index.css

7. 环境变量(web-frontend/

构建或 pnpm dev 时通过 .env.development / .env.production(或被 loadEnv 加载的文件)设置。

变量含义
VITE_ASSET_BASE静态资源 base;独立部署见 README。
VITE_PUBLIC_BASE前台路由 historybase(子路径部署)。
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-8release 21
  • 业务错误:抛出 BusinessException + BusinessErrorCode;新增错误码时同步控制台 getApiErrorMessage 的映射(如有)。

10. 常见开发问题(FAQ)

Q:改了控制台页面,Jar 里没变?
A:确认已执行 ui 的 build,且根目录 ./gradlew build 拷贝了最新 dist

Q:前台 404 或资源路径不对?
A:核对 VITE_ASSET_BASEvitebasedishes.html 中的静态路径是否一致。

Q:公共 API 403 / CSRF?
A:Thymeleaf 页面会注入 CSRF;独立静态托管时需按 README 先 GET 触发 Cookie,并避免反代剥离 Set-Cookie

Q:备份导入 415 / 500?
A:控制台须使用 multipart/form-data(boundary 自动生成);服务端须使用 WebFlux FilePart 读取文件,详见 AdminApiControllerimportBackupZip


11. 延伸阅读


如在文档中发现与源码不一致之处,以 当前分支源码 为准,并欢迎提交 PR 修正本文。