1. 环境要求
| 项 | 说明 |
|---|---|
| JDK | 21(build.gradle 中 JavaLanguageVersion.of(21)) |
| Halo 平台 | 不低于 2.23.0 |
| 构建 | Gradle(推荐使用仓库自带的 gradlew / gradlew.bat) |
| 控制台 UI | 子项目 ui/,包管理为 pnpm |
打包命令、产物路径与清理方式见 第 8 节。未列出的工具(如 IDE、数据库)由运行 Halo 实例的方式决定;插件本身不单独起库,数据走 Halo 的 Extension 存储。
2. 仓库结构(从外到内)
- 根工程:
build.gradle、settings.gradle(include 'ui'),主类在Plugin-Main-Class中声明为com.avrinbai.wechatshare.WechatSharePlugin。 ui/:Halo 控制台插件前端,使用definePlugin注册菜单与路由,构建产物由 Gradle 拷入resources/main/console。src/main/java/.../wechatshare/:后端 Spring Bean,与 HaloReactiveExtensionClient、SchemeManager等集成。src/main/resources/plugin.yaml:插件元数据(名称、版本约束、展示信息等)。src/main/resources/extensions/extension-definitions.yaml:向 Halo 声明实现了additional-webfilter扩展点的类,用于挂载公开访问的 Filter(见下文)。src/main/resources/wechat-share/landing/*.css:落地页样式片段,由ShareLandingCss等读取并与 Java 拼接 HTML 时使用(具体加载方式以代码为准)。
熟悉 Halo 插件时,可以把本仓库看成:一个 Extension 定义两种自定义资源 + 一套管理端 REST + 一个对外 WebFilter。
3. Halo 插件侧需要对齐的几件事
3.1 plugin.yaml
metadata.name 为 wechat-share。管理端 API 路径常量 WechatShareConstants.ADMIN_API_BASE_PATH 为:
/apis/plugins/wechat-share/admin
与 Halo 惯例一致:/apis/plugins/{插件名}/...。前端 ui/src/api/client.ts 的 baseURL 必须与之一致,否则控制台请求会 404。
3.2 自定义资源(Extension)
两种 GVK 均在 Java 里用 @GVK 标注,group 为 wechatshare.plugin.halo.run,version v1alpha1:
| Kind | 用途 |
|---|---|
WechatShareCard | 单张分享卡片,plural 为 wechatsharecards |
WechatShareSettings | 插件配置单例,metadata 名固定为 default(见 WechatShareSettingsService) |
业务上通过 spec.sid 对外标识卡片;ExtensionSchemeRegistry 在启动时为 WechatShareCard 注册了 spec.sid 索引,查询 SID 时走索引字段,避免无谓的全表扫描。
启动顺序上,WechatSharePlugin.start() 会:
prepareCardSchemeOnStartup():若已存在 Card 的 Scheme 则先卸载(避免重复注册等问题,具体见类注释与实现);ensureRegistered():注册 Card / Settings 的 Scheme;- 做一次轻量 list/fetch 预热索引(失败仅打日志)。
停止时 unregisterOnStop() 卸载 Scheme。
3.3 扩展点:additional-webfilter
文件 src/main/resources/extensions/extension-definitions.yaml 将 WechatSharePublicWebFilter 挂到 additional-webfilter。这是 Halo 提供的扩展点:插件可以向全局 Web 过滤器链贡献逻辑。
本插件用它在 GET 请求上拦截:
{publicBasePath}/share{publicBasePath}/go
publicBasePath 默认 /wechat-share,可在设置里改;拦截后再交给 WechatShareSiteHandler 生成 HTML 或 302。未匹配的请求原样透传,不占其它路由。
4. 后端分层(按包理解职责)
| 包 / 类 | 职责 |
|---|---|
WechatSharePlugin | 插件入口,BasePlugin 子类,生命周期内注册 Scheme |
ExtensionSchemeRegistry | Scheme 注册、spec.sid 索引、启停时 unregister |
extension.* | WechatShareCard、WechatShareSettings 模型 |
api.AdminWechatShareController | 管理端 JSON API,统一返回 Envelope<T> |
api.CardWriteRequest | 创建/更新卡片的请求体,与前端表单字段对应 |
service.* | 卡片 CRUD、设置读写、微信 JS-SDK 签名、二维码上游调用等 |
web.WechatSharePublicWebFilter | 公开路径入口 |
web.WechatShareSiteHandler | /share 与 /go 的业务处理 |
web.WechatSharePageRenderer | 落地页 HTML 拼接、按 cardKind 分支样式与脚本 |
support.* | URL 规范化、HTML 转义、落地页 CSS 加载等工具 |
常量集中在 WechatShareConstants、WechatShareCardKind、WechatShareCardStates(例如 enabled 为 null 时视为启用)。
5. 管理端 API 一览
根路径:/apis/plugins/wechat-share/admin(生产环境若 Halo 有统一前缀,以实际部署为准)。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /cards | 卡片列表;响应里带拼好的 shareUrl、goUrl、二维码 data URL 等 |
| POST | /cards | 创建,body 为 CardWriteRequest |
| PUT | /cards/{metadataName} | 按 Extension 的 metadata name 更新 |
| PATCH | /cards/{metadataName}/enabled | 启用/停用,body 为 { "enabled": true/false } |
| DELETE | /cards/{metadataName} | 删除 |
| GET | /cards/{metadataName}/share-qrcode | 返回缓存的二维码 Base64(无缓存则报错提示) |
| GET | /settings | 读取设置 |
| PUT | /settings | 保存设置(WechatShareSettings) |
响应封装为 Envelope:ok、data、message、code。前端 client.ts 已按 ok 判断抛错。
站点对外根 URL 不存在设置项里:由 Halo ExternalUrlSupplier 提供(控制台「外部访问地址」)。设置里的 publicSiteUrl 字段在模型中保留为兼容,加载/保存时会被清空,逻辑见 WechatShareSettingsService。
6. 公开访客路径与分享逻辑要点
在 WechatShareSiteHandler 中:
/share?sid=:渲染落地页;签名 URL 使用当前请求的完整 URL(去掉 hash),供微信 JS-SDKwx.config。/go?sid=:校验后 302 到卡片上的redirectUrl,仅允许http/https。
链接类卡片(WechatShareCardKind.LINK)在二次分享时,分享链接会指向 /go,点开即跳转外链。其它类型更多场景下分享链接仍指向 /share(并可能带 hint=0),避免链路不一致。细节以 share() 内分支为准。
7. 控制台前端(ui/)
- 入口
ui/src/index.ts:使用@halo-dev/ui-shared的definePlugin,父路由挂在ToolsRoot下,路径/wechat-share,子路由cards对应列表页。 - HTTP:axios 实例基地址与后端
ADMIN_API_BASE_PATH对齐。 - 主要视图:
WechatShareCardsView.vue;编辑弹窗、设置面板等在components/下。
本地改 UI 后需要重新执行 UI 构建(或 watch),再由根工程 processUiResources 把 ui/build/dist 打进插件资源;只改 Java 时可以不构建 UI。
8. 打包、构建与产物
本节约定:仓库根目录为插件工程根(含 build.gradle、gradlew / gradlew.bat)。版本号在 gradle.properties 的 version;当前根工程名为 plugin-wechat-share(见 settings.gradle),最终 JAR 一般为 build/libs/plugin-wechat-share-<version>.jar。
8.1 前置条件
- JDK 21:与根工程
java.toolchain一致;JAVA_HOME指向 JDK 21 可避免 Gradle 选错版本。 - pnpm:
ui/package.json指定了packageManager(如pnpm@10.x)。建议使用 Corepack(corepack enable)或自行安装同主版本的 pnpm,避免 UI 构建脚本行为不一致。
8.2 一键打包插件 JAR(推荐:含控制台前端)
根目录执行 Gradle Wrapper,无需全局安装 Gradle。
Windows(PowerShell / CMD):
gradlew.bat clean jar
或完整校验(含单元测试等):
gradlew.bat clean build
macOS / Linux:
./gradlew clean jar
# 或
./gradlew clean build
任务关系简述(见根目录 build.gradle):
jar依赖processUiResources;processUiResources会把ui子项目的assemble产出(ui/build/dist)复制到build/resources/main/console,再打入主 JAR;processResources结束后也会finalizedBy processUiResources,因此走完整资源链时同样会带上控制台。
产物路径: 构建成功后,安装 Halo 时上传的文件一般为:
build/libs/plugin-wechat-share-<version>.jar
其中 <version> 与 gradle.properties 一致(例如 1.0.3)。
8.3 仅构建控制台 UI(调试前端)
在 ui 目录首次安装依赖并生产构建(与 Gradle 里 pnpmBuild 一致):
cd ui
pnpm install
pnpm run build
构建输出目录为 ui/build/dist(由 ui/build.gradle 与 Vite 配置决定)。根工程执行 jar / assemble 时会自动依赖 :ui:assemble,一般无需手工复制。
开发时频繁改界面,可用 UI 子项目的 watch(见 ui/package.json 的 dev 脚本):
cd ui
pnpm install
pnpm run dev
再在另一个终端跑根目录的 gradlew jar(或 IDE 触发构建),把最新 dist 打进插件;也可以根据本地习惯用 Halo devtools 的热更新流程(仍以官方插件开发文档为准)。
8.4 仅编译后端(不产出安装包)
若暂时只改 Java、不想跑完整打包,可只用编译任务缩短反馈(不会替代发布前的完整 jar):
gradlew.bat compileJava
说明:classes / assemble / jar 等任务会走资源与 UI 复制链,耗时更长;发布前务必仍以 jar 或 build 为准。
8.5 测试与清理
gradlew.bat test
gradlew.bat clean
仓库根目录附带 clean-artifacts.bat(Windows):删除根目录 build、.gradle、workplace,以及 ui\build、ui\node_modules、ui\.gradle,用于彻底清空本地构建与前端依赖;删除前请关闭占用这些目录的进程(IDE、正在跑的 dev server 等)。
8.6 常见问题
- 控制台界面仍是旧的:确认本次安装的是新生成的
build/libs/...jar,且打包日志里ui已成功构建;若曾只改前端但未执行含processUiResources的任务,也会出现旧界面。 - 命令找不到:在仓库根目录执行
gradlew/gradlew.bat,不要进到src或ui再执行。
9. 本地调试 Halo 插件
根工程已引入 io.freefair.lombok 与 run.halo.plugin.devtools。具体启动方式以 Halo 官方插件开发文档为准(例如指定 Halo 版本、工作目录、调试端口)。本仓库 build.gradle 中 halo { version = '2.23' } 与 plugin.yaml 的 requires 应对齐,升级 Halo 时记得同步改这两处并做一次回归。
10. 微信相关代码在阅读时的抓手
WeChatJsBridgeService:用 AppId/AppSecret 换取 access_token、jsapi_ticket,做 SHA1 签名;内含缓存与连续失败时的短时熔断,避免配置错误时反复打微信接口。WechatSharePageRenderer:按cardKind输出不同外壳与内嵌样式,并在签名可用时注入 jweixin 与分享参数。
公众号后台仍需自行配置 JS 接口安全域名 等,这与插件代码无关,但直接影响 wx.config 是否成功。
11. 扩展与修改时的建议
- 给卡片加字段:同时改
WechatShareCard.Spec、CardWriteRequest、WechatShareCardService的校验与持久化逻辑,以及控制台表单与CardDto映射;必要时补 Swagger/OpenAPI 注解以便后续生成文档。 - 新增卡片类型:在
WechatShareCardKind增加常量并在normalize中纳入;WechatSharePageRenderer增加分支;WechatShareSiteHandler若影响分享 URL 策略需一并评估。 - 改公开路径:只改设置里的
publicBasePath时,记得 Filter 匹配逻辑用的是规范化后的路径(去尾斜杠等),与WechatShareSettingsService.normalizePath保持一致。
12. 许可证
与仓库根目录及 plugin.yaml 声明一致:GPL-3.0。分发与二次发布时请遵守协议条款。
13. 参考(官方)
开发与调试请以 Halo 官方文档 中「插件开发」「Extension」「控制台插件」等章节为准;API 路径、扩展点名称随 Halo 大版本可能调整,升级前对照发行说明与本仓库 requires 版本。