Files
docs/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/notification.md
Ryan Wang 1cf9b3636f docs: fix some correct document formatting using autocorrect (#493)
Fix some correct document formatting using [autocorrect](https://github.com/huacnlee/autocorrect)

```release-note
None
```
2025-04-23 03:45:09 +00:00

27 KiB
Raw Blame History

title, description
title description
发送和订阅通知 了解如何在插件中发送和订阅通知。

Halo 的通知功能提供了事件驱动的消息提醒机制,让用户能够及时获取系统内的关键事件。 开发者可以根据需求定义事件类型和通知方式(如站内消息、邮件等),并支持个性化的推送策略,提升用户体验和系统可扩展性。

通知系统通过事件机制将关键消息推送给用户。开发者可以自定义通知类型、消息格式和推送方式,主要应用于以下场景:

  • 用户互动:如文章评论、点赞等;
  • 订单和流程提醒:如订单创建、处理完成等;
  • 内容更新:如文章发布、系统公告等。

通知系统工作流程

下图展示了 Halo 通知功能的工作流程,包括事件声明、订阅查找、通知发送等关键步骤。

 graph TD
    A[开发者声明 ReasonType] --> B[业务代码创建 Reason]
    B --> C[查找匹配的 Subscription]
    C --> D{用户订阅了事件吗?}
    D -- 是 --> E[获取用户通知偏好]
    E --> F[查找通知模板]
    F --> G[查找对应的 Notifier]
    G --> H[发送通知]
    D -- 否 --> I[结束,无需通知]
    H --> J[用户接收通知]
    J --> K[用户查看/处理通知]
    Z[用户订阅事件] --> D
  1. 声明事件类型:开发者首先需要声明通知事件的类型 (ReasonType),指定事件的名称、描述和事件所需的属性。
  2. 插件触发事件:当插件中的某个业务操作发生时,插件需要触发相应的事件。
  3. 创建 Reason 实例Halo 根据发送的事件创建一个 Reason 实例,表示具体的事件信息,步骤 2 和 3 可以合并为业务代码创建 Reason
  4. 订阅查找Halo 通知中心会根据事件的类型和属性查找匹配的订阅。
  5. 通知发送:如果找到匹配的订阅,系统根据用户的偏好设置通过不同的通知器(如站内消息、邮件等)发送通知。
  6. 用户接收和处理通知:用户通过预设的通知渠道接收事件通知,并可以在系统中查看或处理这些通知。

通知事件

在 Halo 通知系统中,ReasonTypeReason 是两个核心自定义模型,用于定义通知事件的类别和具体的事件实例。 理解它们的字段对于开发者扩展通知功能至关重要。

下面将详细说明这两个模型的字段及其作用。

ReasonType 模型

ReasonType 是用于定义通知事件类别的模型。每个 ReasonType 都代表一类特定的事件,例如文章评论、新文章发布等。通过 ReasonType,系统可以了解该事件的特定属性和数据结构。 用户个人中心的通知设置页面会根据 ReasonType 的名称和描述展示事件类型和通知方式。

ReasonType 字段说明

字段名 类型 是否必填 说明
apiVersion string API 版本号,定义为 notification.halo.run/v1alpha1
kind string 自定义资源类型,必须为 ReasonType
metadata.name string 事件类别的唯一标识名称,例如 comment
spec.displayName string 事件类别的展示名称,用户界面中显示的事件名称
spec.description string 事件类别的描述,说明此事件的用途和含义
spec.properties array 此事件包含的属性字段,用于定义该类事件应携带的数据

spec.properties 字段

properties 字段用于定义该通知事件需要传递的参数或属性。每个属性都是一个对象,通常包含以下字段:

字段名 类型 是否必填 说明
name string 属性的名称,表示事件数据中的某个字段
type string 属性的数据类型,例如 stringboolean 等,仅用于描述
description string 对该属性的描述,说明其在事件中的作用
optional boolean 该属性是否为可选字段,默认值为 falsefalse 表示必填

properties.type 字段仅用于文档性目的,不会在运行时进行数据类型检查。有了此描述,便于编写通知模板时使用正确的数据类型。

示例: 声明评论事件的 ReasonType

apiVersion: notification.halo.run/v1alpha1
kind: ReasonType
metadata:
  name: comment
spec:
  displayName: "评论事件"
  description: "用户在文章上收到评论时触发。"
  properties:
    - name: postName
      type: string
      description: "文章的名称。"
    - name: commenter
      type: string
      description: "评论者用户名。"
    - name: content
      type: string
      description: "评论内容。"

在这个示例中,ReasonType 定义了一个评论事件,该事件包括三种属性:postName(文章名)、commenter(评论者)和 content(评论内容),这些属性在触发事件时将传递给通知系统。

这一类型的资源声明非常适合放在插件的 resources 目录下,以便插件安装时自动创建。参考 声明自定义模型对象

Reason 模型

Reason 模型用于描述具体的事件实例,它是 ReasonType 的一个实例化,包含触发该事件时的具体数据。 Reason 通常在某个事件发生时创建,例如某篇文章收到评论时生成一个 Reason,记录具体的评论信息。

Reason 字段说明

字段名 类型 是否必填 说明
apiVersion string API 版本号,定义为 notification.halo.run/v1alpha1
kind string 自定义资源类型,必须为 Reason
metadata.name string 该事件实例的唯一标识名称,通常自动生成
spec.reasonType string 引用的 ReasonType 名称,表示该事件实例属于哪个事件类型
spec.author string 事件的触发者或创建者,通常为用户或系统的标识符
spec.subject object 事件的主题,指向该事件所涉及的具体对象(如文章、评论等)
spec.attributes object 包含事件具体数据的键值对,内容与 ReasonType 中定义的属性一致

spec.subject 字段

subject 字段描述了与该事件相关的主体对象,例如,评论事件中的文章对象。subject 通常包含以下字段:

字段名 类型 是否必填 说明
apiVersion string 主题对象的 API 版本号,例如 content.halo.run/v1alpha1
kind string 主题对象的类型,例如 Post 表示文章
name string 主题对象的唯一标识,通常是对象的名称或 ID
title string 主题对象的标题,通常是人类可读的名称
url string 主题对象的访问链接或详情页面的 URL

参考 Reason 自定义模型

spec.attributes 字段

attributes 字段用于存储该事件实例的具体数据。每个键值对表示一个 ReasonType 中定义的属性和其对应的值。

示例: 创建评论事件的 Reason

apiVersion: notification.halo.run/v1alpha1
kind: Reason
metadata:
  name: comment-123
spec:
  reasonType: comment
  author: "访客"
  subject:
    apiVersion: 'content.halo.run/v1alpha1'
    kind: Post
    name: 'post-456'
    title: 'Halo 系统介绍'
    url: 'https://example.com/archives/456'
  attributes:
    postName: "Halo 系统介绍"
    commenter: "访客"
    content: "这是一篇非常有帮助的文章!"

在这个示例中,Reason 表示具体的评论事件。 它关联了 comment 这一 ReasonType并提供了详细的事件信息包括文章subject的标识信息以及评论的内容attributes

通过 ReasonType 和 Reason开发者可以定义和管理各种事件并在事件发生时触发相应的通知逻辑。

通知模板

在 Halo 系统中,通知模板用于定义每种通知类型的展示格式和内容结构。每当触发某个通知事件时,系统会根据事件的类型选择相应的通知模板,并将事件的属性嵌入到模板中生成最终的通知内容。如果未定义通知模板,则系统无法确定通知的具体格式和内容,这可能导致通知发送失败。因此,定义通知模板是实现通知功能的关键步骤。

通知模板的基本结构

通知模板是通过 NotificationTemplate 自定义模型定义的。 每个模板指定了与某个事件类型 (ReasonType) 关联的内容格式,包括通知的标题、正文内容等。 模板内容可以使用属性占位符以便在通知生成时自动填充事件属性。Halo 支持使用 Thymeleaf 模板引擎进行内容渲染。

通知模板字段说明

字段名 类型 是否必填 说明
apiVersion string API 版本号,定义为 notification.halo.run/v1alpha1
kind string 自定义资源类型,必须为 NotificationTemplate
metadata.name string 模板的唯一标识名称
spec.reasonSelector.reasonType string 关联的事件类型 (ReasonType) 名称
spec.reasonSelector.language string 模板语言,固定写为 default
spec.template.title string 通知的标题模板,支持占位符
spec.template.rawBody string 通知的正文模板,应当是纯文本,支持占位符
spec.template.htmlBody string 通知的正文模板,格式为 HTML 的模板,支持占位符

定义通知模板

定义通知模板时,开发者需要指定模板的 reasonSelector,用于与事件类型关联,并在 template 中定义通知标题和内容的格式。

spec.reasonSelector.language 字段用于指定模板的语言,设计支持多语言,但目前 Halo 没有提供保存用户语言偏好的入口,因此只能使用 default

模板内容支持纯文本和 HTML 格式,建议开发者两种内容都提供,以适应不同的通知渠道。比如邮件通知需要 HTML 格式,而短信通知则仅支持纯文本。

示例: 定义评论事件的通知模板

假设我们需要为“新评论”事件定义一个通知模板,该模板包括事件的标题和正文内容:

apiVersion: notification.halo.run/v1alpha1
kind: NotificationTemplate
metadata:
  name: template-new-comment-on-post
spec:
  reasonSelector:
    reasonType: new-comment-on-post
    language: default
  template:
    title: "你的文章 [(${subject.title})] 收到了一条新评论"
    rawBody: |
      评论者 [(${author.name})] 评论了您的文章 [(${subject.title})],内容如下:
      [(${props.comment})]
    htmlBody: |
      <p>评论者 <strong>[(${author.name})]</strong> 评论了您的文章 <a href="[(${subject.url})]">[(${subject.title})]</a>,内容如下:</p>
      <p>[(${props.comment})]</p>

参考 Halo 默认通知模板 YAML

通知模板设计小技巧与最佳实践

合理使用占位符

在通知模板中,使用占位符来动态插入事件数据是提高模板灵活性的关键。占位符的设计应遵循以下规则:

  • 使用清晰的命名:占位符应具有明确的含义,例如 [(${quoteReplyName})] 表示引用回复的名称,让代码更具可读性,建议使用 CamelCase 命名规范,不要使用嵌套对象作为变量如 subject.title
  • 避免嵌套复杂表达式:为了避免通知生成的内容过于复杂,建议在模板中尽量使用简单的占位符。复杂逻辑应在事件触发时处理,尽量保持模板的简洁。
  • 确保属性完整性:占位符应与 ReasonType 中定义的属性一致,避免由于属性缺失导致的通知发送错误。
提供多种格式的内容

在 Halo 中,不同的通知渠道可能支持不同格式的内容,建议模板中同时提供纯文本和 HTML 格式,以便适应各类通知渠道需求。

  • 纯文本格式:适合即时通讯类应用消息和短信通知,尽量简洁明了,关注重点信息。
  • HTML 格式适合富文本展示渠道如邮件通知。HTML 模板可使用简单的样式和链接,帮助用户更好地理解通知内容。
  • 适配不同渠道的内容:对于可能发送到多渠道的通知,可以在不同格式中包含适合该渠道的具体内容,如 HTML 中使用 <a> 标签提供链接,而纯文本只展示简洁的链接地址。
  • 如果在模板中需要用到日期,建议提前将其格式化为带时区的日期字符串,避免在模板中使用复杂的日期格式化。
模板语法

Halo 使用 Thymeleaf 模板引擎来渲染通知模板,开发者可以在模板中使用 Thymeleaf 的语法来处理模板中的逻辑和数据。

  • 对于纯文本如标题和 rawBody,使用 Thymeleaf 的 Textual syntax 语法来引用变量和表达式。 其取值格式为:[(${expression})],例如 [(${title})]
  • 对于 HTML 内容如 htmlBody,使用 Thymeleaf 的 Standard syntax 语法。 其取值格式为:${expression},例如 ${title}

模板渲染与发送

在通知事件触发时Halo 通知中心会查找与该事件 ReasonType 匹配的 NotificationTemplate,并将事件数据填充到模板中生成最终通知内容。 没有定义模板的事件将无法发送通知,因此为每个 ReasonType 定义模板是保证通知发送成功的前提。

可以有多个相同的 reasonSelector 绑定到同一个事件类型,比如 reasonType=new-comment-on-postlanguage=default 的模板存在多个Halo 会选择最近创建的模板。 根据这个特点,插件或主题开发者可以提供自己的通知模板以覆盖默认的通知模板

在生成通知内容时,系统还会提供一些额外的全局属性(如 site.title 等),这些属性可以在模板中直接使用:

字段名 类型 说明
site.title string 站点标题
site.subtitle string 站点副标题
site.logo string 站点 Logo URL
site.url string 站点外部访问地址
subscriber.displayName string 订阅者显示名称
subscriber.id string 订阅者唯一标识符
unsubscribeUrl string 退订地址,用于取消订阅的链接

触发通知事件

在 Halo 系统中,通知功能的核心是通过触发特定的事件,生成相应的 Reason 实例,然后通过系统的通知机制将该事件通知给订阅者。 Halo 提供了 NotificationReasonEmitter 接口,开发者可以通过它轻松触发通知事件,将业务逻辑与通知机制结合起来。

工作机制

NotificationReasonEmitter 的作用是简化事件触发和通知的处理流程,它的主要职责是:

  • 接收业务事件的参数,生成 Reason 实例。
  • Reason 实例与 ReasonType 进行匹配,触发事件。
  • 通过 Halo 的通知系统,将事件推送给订阅了该事件的用户。

定义参考 NotificationReasonEmitter

  • reasonType:事件类型的名称,对应于 ReasonTypemetadata.name 字段。
  • reasonData:事件数据的构建器,用于构建 Reason 实例的属性。

Reason 数据的构建器有以下属性:

public class ReasonPayloadBuilder {
    private Reason.Subject subject;
    private UserIdentity author;
    private Map<String, Object> attributes;
}
  • subject:事件的主体对象,参考 Reason 中的 spec.subject 字段。
  • author:事件的触发者,通常是用户或系统的标识符,这是一个 UserIdentity 对象,如果作者是匿名的则需要传递邮箱地址来构造,比如评论者可能没有关联具体用户而仅仅是邮箱地址。
  • attributes:事件的具体数据,包含了 ReasonType 中定义的属性和对应的值,如果必填字段没有传递则会抛出异常。

使用场景举例:

  • 当用户在博客上发表文章时,触发“新文章发布”事件,通知订阅了该事件的用户。
  • 当用户在文章上发表评论时,触发“评论回复”事件,通知文章作者或订阅了评论的用户。
  • 当管理员处理插件订单时,触发“订单创建”事件,通知管理员新订单的详情。

示例

假设我们需要在用户发布的文章时有新评论时,向订阅了“文章有新评论”事件的用户发送通知。可以通过 NotificationReasonEmitter 实现如下逻辑。

第一步:在你的插件代码中注入 NotificationReasonEmitter

import run.halo.app.notification.NotificationReasonEmitter;

@Service
@RequiredArgsConstructor
public class CommentEventListener {

  private final NotificationReasonEmitter notificationReasonEmitter;
}

第二步:有新评论被创建时触发

import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Post;

@Async
@EventListener(CommentCreatedEvent.class)
public void onNewComment(CommentCreatedEvent event) {
  Comment comment = event.getComment();
  Ref subjectRef = comment.getSpec().getSubjectRef();
  Post post = client.fetch(Post.class, subjectRef.getName()).orElseThrow();
  // 评论的主体是文章,因此构建文章的主体对象
  var reasonSubject = Reason.Subject.builder()
    .apiVersion(post.getApiVersion())
    .kind(post.getKind())
    .name(subjectRef.getName())
    .title(post.getSpec().getTitle())
    .url(postUrl)
    .build();
  // new-comment-on-post 用于表示 ReasonType 的 metadata.name
  notificationReasonEmitter.emit("new-comment-on-post",
    builder -> {
        // 定义了一个类型用于保存评论事件的数据,避免 map 的 key 写错
        var attributes = CommentOnPostReasonData.builder()
            .postName(subjectRef.getName())
            .postOwner(post.getSpec().getOwner())
            .postTitle(post.getSpec().getTitle())
            .postUrl(postUrl)
            .commenter(owner.getDisplayName())
            .content(comment.getSpec().getContent())
            .commentName(comment.getMetadata().getName())
            .build();
        // 将 CommentOnPostReasonData 转换为事件所需的 Map 类型数据
        builder.attributes(toAttributeMap(attributes))
            .author(identityFrom(owner))
            .subject(reasonSubject);
    }).block();
}

public static <T> Map<String, Object> toAttributeMap(T data) {
  Assert.notNull(data, "Reason attributes must not be null");
  return JsonUtils.mapper().convertValue(data, new TypeReference<>() {
  });
}

参考 Halo 评论事件触发

订阅通知

在 Halo 系统中,用户可以通过订阅通知来接收感兴趣的事件,并且可以使用表达式对事件进行过滤,确保只接收到符合条件的通知。 通过这种方式,用户可以精确订阅某些事件,并避免被无关的事件打扰。 订阅机制支持根据事件属性、主体对象和事件发起人来过滤事件,并使用 SpELSpring Expression Language编写过滤表达式。

订阅通知的工作机制

通知订阅机制允许用户订阅各种类型的事件(如文章发布、评论回复等),并且支持通过表达式过滤事件。

订阅系统可以通过以下三种根对象来筛选感兴趣的事件:

  • props事件属性的根对象表示事件携带的具体数据。例如在评论回复事件中属性可以是 {repliedOwner: 'guqing'},可以通过 props.repliedOwner 来访问 repliedOwner 属性。
  • subject事件的主体对象表示事件所关联的核心对象字段同 Reason 中的 spec.subject 字段。例如在“新评论”事件中subject 是评论所属的文章。
  • author事件的发起人表示触发事件的用户标识符字符串类型。例如在评论事件中发起人是评论者。

通过这些对象,订阅者可以编写表达式来对事件进行过滤。

表达式的结果必须为布尔值,用于判断当前事件是否符合订阅者的条件。

表达式过滤规则

使用 SpEL 规范,开发者可以通过以下方式过滤事件:

  • props访问事件的属性。例如props.repliedOwner == 'guqing' 用于筛选事件属性中 repliedOwner 为 guqing 的事件。
  • subject访问事件主体的属性。例如subject.kind == 'Post' 用于筛选主体为 Post 的事件。
  • author访问事件的发起人。例如author == 'guqing' 用于筛选 guqing 发起的评事件。

订阅方式

目前 Halo 没有提供用户界面来订阅通知,订阅均是通过事件发起前由开发者为用户创建的订阅对象来实现的。 因此,开发者需要在插件中实现订阅逻辑,为用户创建订阅对象,需要注意以下几点:

  1. 订阅对象的创建需要在用户订阅事件前完成,否则用户无法接收到通知。
  2. 哪些用户能够接收到通知需要开发者谨慎考虑,确保用户订阅的事件是符合其需求的,避免将无关的事件推送给用户。
  3. 在创建订阅时,同样的逻辑反复执行不会重复创建订阅,因此可以在每次事件触发前订阅。

Halo 提供 NotificationCenter Bean 来帮助开发者创建订阅对象和取消订阅对象。

参考 NotificationCenter Bean

subscribe 方法用于订阅事件,unsubscribe 方法用于取消订阅事件,第一个参数是订阅者,第二个参数是感兴趣的事件。

Subscription.Subscriber

Subscription.Subscriber 具有以下属性:

字段名 类型 是否必填 说明
name string 订阅者的用户名,或通过 UserIdentity 构建的匿名订阅标识

Subscription.InterestReason

Subscription.InterestReason 具有以下属性:

字段名 类型 是否必填 说明
reasonType string 感兴趣的事件类型的名称,对应于 ReasonTypemetadata.name 字段
expression string 订阅事件的过滤表达式,用于过滤感兴趣的事件避免用户接收到不相关的通知

参考 Subscription 自定义模型

示例

以下是一个通过 Java 代码实现订阅文章“新评论”通知的示例。该示例展示了如何使用表达式来筛选事件,只接收特定文章下的评论通知。

// step 0: 依赖注入 NotificationCenter
private final NotificationCenter notificationCenter;

Mono<Void> subscribeNewCommentOnPostNotification(String username) {
  // step1: 创建订阅者对象
  var subscriber = new Subscription.Subscriber();
  subscriber.setName(username);  // 设置订阅者的用户名

  // step2: 创建感兴趣的事件类型对象
  var interestReason = new Subscription.InterestReason();
  // 设置订阅的事件类型为“新评论”
  interestReason.setReasonType(NEW_COMMENT_ON_POST);

  // step3: 使用表达式过滤事件props 是事件属性的根对象
  // 只接收由指定用户创建的文章下的评论事件
  interestReason.setExpression("props.repliedOwner == '%s'".formatted(username));

  // step4: 订阅事件
  return notificationCenter.subscribe(subscriber, interestReason);
}

在这个示例中,我们通过以下步骤实现了订阅:

  1. 创建订阅者对象:通过 Subscription.Subscriber() 实例化一个订阅者对象,并为其设置订阅者的用户名。
  2. 定义感兴趣的事件类型:使用 Subscription.InterestReason() 定义感兴趣的事件类型,这里使用 NEW_COMMENT_ON_POST 表示订阅“新评论”事件。
  3. 使用表达式过滤事件:通过表达式 props.repliedOwner == '%s',订阅者只会接收到被评论者/回复者是他们自己的评论通知。
  4. 执行订阅操作:调用 notificationCenter.subscribe(subscriber, interestReason) 方法完成订阅操作。

参考 Halo 评论订阅

通知器

开发者可以通过 Halo 的通知器机制扩展通知的发送方式。

参考 通知器扩展点