1. 环境要求

说明
JDK21(build.gradleJavaLanguageVersion.of(21)
Halo 平台不低于 2.23.0
构建Gradle(推荐使用仓库自带的 gradlew / gradlew.bat
控制台 UI子项目 ui/,包管理为 pnpm

打包命令、产物路径与清理方式见 第 8 节。未列出的工具(如 IDE、数据库)由运行 Halo 实例的方式决定;插件本身不单独起库,数据走 Halo 的 Extension 存储。


2. 仓库结构(从外到内)

  • 根工程build.gradlesettings.gradleinclude 'ui'),主类在 Plugin-Main-Class 中声明为 com.avrinbai.wechatshare.WechatSharePlugin
  • ui/:Halo 控制台插件前端,使用 definePlugin 注册菜单与路由,构建产物由 Gradle 拷入 resources/main/console
  • src/main/java/.../wechatshare/:后端 Spring Bean,与 Halo ReactiveExtensionClientSchemeManager 等集成。
  • 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.namewechat-share。管理端 API 路径常量 WechatShareConstants.ADMIN_API_BASE_PATH 为:

/apis/plugins/wechat-share/admin

与 Halo 惯例一致:/apis/plugins/{插件名}/...。前端 ui/src/api/client.tsbaseURL 必须与之一致,否则控制台请求会 404。

3.2 自定义资源(Extension)

两种 GVK 均在 Java 里用 @GVK 标注,group 为 wechatshare.plugin.halo.run,version v1alpha1

Kind用途
WechatShareCard单张分享卡片,pluralwechatsharecards
WechatShareSettings插件配置单例,metadata 名固定为 default(见 WechatShareSettingsService

业务上通过 spec.sid 对外标识卡片;ExtensionSchemeRegistry 在启动时为 WechatShareCard 注册了 spec.sid 索引,查询 SID 时走索引字段,避免无谓的全表扫描。

启动顺序上,WechatSharePlugin.start() 会:

  1. prepareCardSchemeOnStartup():若已存在 Card 的 Scheme 则先卸载(避免重复注册等问题,具体见类注释与实现);
  2. ensureRegistered():注册 Card / Settings 的 Scheme;
  3. 做一次轻量 list/fetch 预热索引(失败仅打日志)。

停止时 unregisterOnStop() 卸载 Scheme。

3.3 扩展点:additional-webfilter

文件 src/main/resources/extensions/extension-definitions.yamlWechatSharePublicWebFilter 挂到 additional-webfilter。这是 Halo 提供的扩展点:插件可以向全局 Web 过滤器链贡献逻辑。

本插件用它在 GET 请求上拦截:

  • {publicBasePath}/share
  • {publicBasePath}/go

publicBasePath 默认 /wechat-share,可在设置里改;拦截后再交给 WechatShareSiteHandler 生成 HTML 或 302。未匹配的请求原样透传,不占其它路由。


4. 后端分层(按包理解职责)

包 / 类职责
WechatSharePlugin插件入口,BasePlugin 子类,生命周期内注册 Scheme
ExtensionSchemeRegistryScheme 注册、spec.sid 索引、启停时 unregister
extension.*WechatShareCardWechatShareSettings 模型
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 加载等工具

常量集中在 WechatShareConstantsWechatShareCardKindWechatShareCardStates(例如 enabled 为 null 时视为启用)。


5. 管理端 API 一览

根路径:/apis/plugins/wechat-share/admin(生产环境若 Halo 有统一前缀,以实际部署为准)。

方法路径说明
GET/cards卡片列表;响应里带拼好的 shareUrlgoUrl、二维码 data URL 等
POST/cards创建,bodyCardWriteRequest
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

响应封装为 Envelopeokdatamessagecode。前端 client.ts 已按 ok 判断抛错。

站点对外根 URL 存在设置项里:由 Halo ExternalUrlSupplier 提供(控制台「外部访问地址」)。设置里的 publicSiteUrl 字段在模型中保留为兼容,加载/保存时会被清空,逻辑见 WechatShareSettingsService


6. 公开访客路径与分享逻辑要点

WechatShareSiteHandler 中:

  • /share?sid=:渲染落地页;签名 URL 使用当前请求的完整 URL(去掉 hash),供微信 JS-SDK wx.config
  • /go?sid=:校验后 302 到卡片上的 redirectUrl,仅允许 http/https

链接类卡片(WechatShareCardKind.LINK)在二次分享时,分享链接会指向 /go,点开即跳转外链。其它类型更多场景下分享链接仍指向 /share(并可能带 hint=0),避免链路不一致。细节以 share() 内分支为准。


7. 控制台前端(ui/

  • 入口 ui/src/index.ts:使用 @halo-dev/ui-shareddefinePlugin,父路由挂在 ToolsRoot 下,路径 /wechat-share,子路由 cards 对应列表页。
  • HTTP:axios 实例基地址与后端 ADMIN_API_BASE_PATH 对齐。
  • 主要视图:WechatShareCardsView.vue;编辑弹窗、设置面板等在 components/ 下。

本地改 UI 后需要重新执行 UI 构建(或 watch),再由根工程 processUiResourcesui/build/dist 打进插件资源;只改 Java 时可以不构建 UI。


8. 打包、构建与产物

本节约定:仓库根目录为插件工程根(含 build.gradlegradlew / gradlew.bat)。版本号在 gradle.propertiesversion;当前根工程名为 plugin-wechat-share(见 settings.gradle),最终 JAR 一般为 build/libs/plugin-wechat-share-<version>.jar

8.1 前置条件

  • JDK 21:与根工程 java.toolchain 一致;JAVA_HOME 指向 JDK 21 可避免 Gradle 选错版本。
  • pnpmui/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.jsondev 脚本):

cd ui
pnpm install
pnpm run dev

再在另一个终端跑根目录的 gradlew jar(或 IDE 触发构建),把最新 dist 打进插件;也可以根据本地习惯用 Halo devtools 的热更新流程(仍以官方插件开发文档为准)。

8.4 仅编译后端(不产出安装包)

若暂时只改 Java、不想跑完整打包,可只用编译任务缩短反馈(不会替代发布前的完整 jar):

gradlew.bat compileJava

说明:classes / assemble / jar 等任务会走资源与 UI 复制链,耗时更长;发布前务必仍以 jarbuild 为准。

8.5 测试与清理

gradlew.bat test
gradlew.bat clean

仓库根目录附带 clean-artifacts.bat(Windows):删除根目录 build.gradleworkplace,以及 ui\buildui\node_modulesui\.gradle,用于彻底清空本地构建与前端依赖;删除前请关闭占用这些目录的进程(IDE、正在跑的 dev server 等)。

8.6 常见问题

  • 控制台界面仍是旧的:确认本次安装的是新生成的 build/libs/...jar,且打包日志里 ui 已成功构建;若曾只改前端但未执行含 processUiResources 的任务,也会出现旧界面。
  • 命令找不到:在仓库根目录执行 gradlew / gradlew.bat,不要进到 srcui 再执行。

9. 本地调试 Halo 插件

根工程已引入 io.freefair.lombokrun.halo.plugin.devtools。具体启动方式以 Halo 官方插件开发文档为准(例如指定 Halo 版本、工作目录、调试端口)。本仓库 build.gradlehalo { version = '2.23' }plugin.yamlrequires 应对齐,升级 Halo 时记得同步改这两处并做一次回归。


10. 微信相关代码在阅读时的抓手

  • WeChatJsBridgeService:用 AppId/AppSecret 换取 access_token、jsapi_ticket,做 SHA1 签名;内含缓存与连续失败时的短时熔断,避免配置错误时反复打微信接口。
  • WechatSharePageRenderer:按 cardKind 输出不同外壳与内嵌样式,并在签名可用时注入 jweixin 与分享参数。

公众号后台仍需自行配置 JS 接口安全域名 等,这与插件代码无关,但直接影响 wx.config 是否成功。


11. 扩展与修改时的建议

  • 给卡片加字段:同时改 WechatShareCard.SpecCardWriteRequestWechatShareCardService 的校验与持久化逻辑,以及控制台表单与 CardDto 映射;必要时补 Swagger/OpenAPI 注解以便后续生成文档。
  • 新增卡片类型:在 WechatShareCardKind 增加常量并在 normalize 中纳入;WechatSharePageRenderer 增加分支;WechatShareSiteHandler 若影响分享 URL 策略需一并评估。
  • 改公开路径:只改设置里的 publicBasePath 时,记得 Filter 匹配逻辑用的是规范化后的路径(去尾斜杠等),与 WechatShareSettingsService.normalizePath 保持一致。

12. 许可证

与仓库根目录及 plugin.yaml 声明一致:GPL-3.0。分发与二次发布时请遵守协议条款。


13. 参考(官方)

开发与调试请以 Halo 官方文档 中「插件开发」「Extension」「控制台插件」等章节为准;API 路径、扩展点名称随 Halo 大版本可能调整,升级前对照发行说明与本仓库 requires 版本。