8.1 ABP实时服务 - 通知系统

8.1.1 简介

在系统中,通知是用来告知用户特定事件的。ABP提供了一个基于实时通知的基础设施 pub/sub.

8.1.2 发送模式

有两种方法来发送通知给用户:

  • 用户 订阅 一个特定的通知类型。当我们发布这个类型的通知时,该通知会被投递给所有的订阅用户。这就是 pub/sub 模式。

  • 我们能直接的发送通知给目标用户。

8.1.3 通知类型

通知类型也有两种:

  • 常规通知 是任意类型的通知。例如:"如果某个用户发送给我一个交友请求,那么通知我。"

  • 实体通知 是被关联到一个指定的实体。"如果某个用户评论了这个照片,那么通知我。",这是一个基于实体的通知,因为它被关联到一个特定的照片实体。用户可以得到对某些照片评论的通知,而不是所有照片的。

8.1.4 通知数据

通常来说通知中包含了通知数据。例如:"如果某个用户发送给我一个友好的请求,那么通知我。",该通知可以有两个数据属性:发送者的用户名(那个用户发送了这个交友请求)和请求信息(用户发送请求的具体信息)。很显然,这个通知的数据类型是和通知类型紧密耦合的。不同的通知类型有不同的数据类型。

通知数据是可选的。有些通知不需要数据。ABP预定义了一些通知数据类型,这些类型适合于大多数情况下。MessageNotificationData 能够用于一些简单的消息,而 LocalizableMessageNotificationData 可以用于本地化和参数化的通知消息。在后面的部分我们会看到一些有用的例子。

8.1.5 通知的程度

ABP有5种程度的通知等级,它被定义在枚举类型 NotificationSeverity 中: Info, Success, Warn, Error 以及 Fatal。 默认是 Info。很显然,这个通知的数据类型是和通知类型紧密耦合的。不同的通知类型有不同的数据类型。

#####关于通知持久化 了解更多请看 Notification Store

8.1.6 订阅通知

INotificationSubscriptionManager 接口中提供了订阅通知的API,例如:

public class MyService : ITransientDependency
{
    private readonly INotificationSubscriptionManager _notificationSubscriptionManager;

    public MyService(INotificationSubscriptionManager notificationSubscriptionManager)
    {
        _notificationSubscriptionManager = notificationSubscriptionManager;
    }

    //订阅常规通知
    public async Task Subscribe_SentFrendshipRequest(int? tenantId, long userId)
    {
        await _notificationSubscriptionManager.SubscribeAsync(tenantId, userId, "SentFrendshipRequest");    
    }

    //订阅实体通知
    public async Task Subscribe_CommentPhoto(int? tenantId, long userId, Guid photoId)
    {
        await _notificationSubscriptionManager.SubscribeAsync(tenantId, userId, "CommentPhoto", new EntityIdentifier(typeof(Photo), photoId));   
    }
}

首先,我们 注入 INotificationSubscriptionManager 接口。第一个方法是定义 常规通知,用户想得到某些用户发来的交友请求的通知。第二个方法是订阅与 特定实体(Photo) 有关的通知。用户想得到某些用户对特定的照片写了评论的通知。

每个通知的类型应该有唯一的名字(就像例子中的 SentFrendshipRequest 和 CommentPhoto 一样)。

INotificationSubscriptionManager 还有其它的方法来管理订阅,如: UnsubscribeAsync, IsSubscribedAsync, GetSubscriptionsAsync等等。

8.1.7 发布通知

INotificationPublisher 被用来发布通知,例如:

public class MyService : ITransientDependency
{
    private readonly INotificationPublisher _notiticationPublisher;

    public MyService(INotificationPublisher notiticationPublisher)
    {
        _notiticationPublisher = notiticationPublisher;
    }

    //发送常规通知给特定用户
    public async Task Publish_SentFrendshipRequest(string senderUserName, string friendshipMessage, long targetUserId)
    {
        await _notiticationPublisher.PublishAsync("SentFrendshipRequest", new SentFrendshipRequestNotificationData(senderUserName, friendshipMessage), userIds: new[] { targetUserId });
    }

    //发送实体通知给特定用户
    public async Task Publish_CommentPhoto(string commenterUserName, string comment, Guid photoId, long photoOwnerUserId)
    {
        await _notiticationPublisher.PublishAsync("CommentPhoto", new CommentPhotoNotificationData(commenterUserName, comment), new EntityIdentifier(typeof(Photo), photoId), userIds: new[] { photoOwnerUserId });
    }

    //发送常规通知给所有的订阅用户并制定通知的严重等级
    public async Task Publish_LowDisk(int remainingDiskInMb)
    {
        //例如:"LowDiskWarningMessage" 英文内容是 -> "Attention! Only {remainingDiskInMb} MBs left on the disk!"
        //注意,磁盘空间仅剩下{remainingDiskInMb} MBs
        var data = new LocalizableMessageNotificationData(new LocalizableString("LowDiskWarningMessage", "MyLocalizationSourceName"));
        data["remainingDiskInMb"] = remainingDiskInMb;

        await _notiticationPublisher.PublishAsync("System.LowDisk", data, severity: NotificationSeverity.Warn);    
    }
}

在第一个例子中,我们对一个用户发布了一条通知。SentFrendshipRequestNotificationData 应该派生自 NotificationData 如下所示:

[Serializable]
public class SentFrendshipRequestNotificationData : NotificationData
{
    public string SenderUserName { get; set; }

    public string FriendshipMessage { get; set; }

    public SentFrendshipRequestNotificationData(string senderUserName, string friendshipMessage)
    {
        SenderUserName = senderUserName;
        FriendshipMessage = friendshipMessage;
    }
}

在第二个例子中,我们发送了一个特定实体的通知给了特定的用户。通知数据类不需要被 序列化 (因为默认被序列化为JSON)。但是还是建议你将特性:Serializable 加在数据类上,因为你可能需要在应用之间移动通知,也可能在将来使用二进制序列化。此外,正如之前声明的那样,通知数据是可选的,而且对于所有的通知可能不是必须的。

注意:如果我们发布通知给特定的用户,那么他们不需要订阅那些通知。

在第三个例子中,我们没有定义一个专门的通知数据类。相反,直接使用了内置的基于字典类型的 LocalizableMessageNotificationData 类,并以 Warn 等级来发布通知。 LocalizableMessageNotificationData 能存储基于字典的任意数据(这是由于自定义通知数据类型是继承 NotificationData 类)。我们在本地化时使用 remainingDiskInMb 作为参数。本地化消息可以包含这些参数(就像例子中的"Attention! Only MBs left on the disk!")。我们将会在客户端看到如何本地化。

8.1.8 用户通知管理

IUserNotificationManager 被用来管理用户通知。有这些方法来为用户 get, update 或 delete 通知。你可以使用它们为你的应用程序来准备一个通知列表页面。

8.1.9 实时通知

当你使用 IUserNotificationManager 来查询通知时,通常我们是想实时推送通知到客户端。

通知系统使用 IRealTimeNotifier 接口发送实时通知给用户。任何类型的实时通信系统都能实现它。它已经被实现在一个独立的 SignalR 包中。在启动模板中已经安装了SignalR。详情请参考集成SignalR

注意:在后台作业中,通信系统异步调用 IRealTimeNotifier。所以,通知可能会延迟一小会才会被发送。

8.1.10 客户端

当实时通知被接受的时候,ABP在客户端触发了一个 全局事件 。你可以像下面一样注册它来获取通知:

abp.event.on('abp.notifications.received', function (userNotification) {
    console.log(userNotification);
});

每次接受到实时通知 abp.notifications.received 事件会被触发。你能够想上面展示的那样注册这个事件来获取通知。详情请参考事件总线。下面是一个传入的JSON格式的"System.LowDisk"示例:

{
    "userId": 2,
    "state": 0,
    "notification": {
        "notificationName": "System.LowDisk",
        "data": {
            "message": {
                "sourceName": "MyLocalizationSourceName",
                "name": "LowDiskWarningMessage"
            },
            "type": "Abp.Notifications.LocalizableMessageNotificationData",
            "properties": {
                "remainingDiskInMb": "42"
            }
        },
        "entityType": null,
        "entityTypeName": null,
        "entityId": null,
        "severity": 0,
        "creationTime": "2016-02-09T17:03:32.13",
        "id": "0263d581-3d8a-476b-8e16-4f6a6f10a632"
    },
    "id": "4a546baf-bf17-4924-b993-32e420a8d468"
}

在这个对象中:

  • userId:当前用户Id。通常你不需要知道这个,因为你知道那个是当前用户。

  • state:枚举类型 UserNotificationState 的值。 0:Unread,1:Read

  • notification:通知详细信息:

    1. notificationName: 通知的唯一名字(发布通知时使用相同的值)。

    2. data:通知数据。在上面例子中,我们使用了 LocalizableMessageNotificationData (在之前的例子中我们使用它来发布的)。

      • message: 本地化信息。在UI端,我们可以使用 sourceName 和 name 来本地化信息。

      • type:通知数据类型。类型的全名称,包含名称空间。当处理这个通知数据的时候,我们可以检查这个类型。

      • properties:自定义属的基于字典类型的属性。

    3. entityType,entityTypeName 和 entityId:实体信息,如果这是一个与实体相关的通知。

    4. severity:枚举类型 NotificationSeverity 的值。0: Info, 1: Success, 2: Warn, 3: Error, 4: Fatal

    5. creationTime:表示通知被创建的时间。

    6. id:通知的id。

  • id:用户通知id。

当然你不会去记录这个通知。你可以使用通知数据来显示通知信息给用户。例如:

abp.event.on('abp.notifications.received', function (userNotification) {
    if (userNotification.notification.data.type === 'Abp.Notifications.LocalizableMessageNotificationData') {
        var localizedText = abp.localization.localize(
            userNotification.notification.data.message.name,
            userNotification.notification.data.message.sourceName
        );

        $.each(userNotification.notification.data.properties, function (key, value) {
            localizedText = localizedText.replace('{' + key + '}', value);
        });

        alert('New localized notification: ' + localizedText);
    } else if (userNotification.notification.data.type === 'Abp.Notifications.MessageNotificationData') {
        alert('New simple notification: ' + userNotification.notification.data.message);
    }
});

为了能够处理通知数据,我们应该检查数据类型。这个简单的例子展示的是从通知数据中取得消息。对于本地化消息(LocalizableMessageNotificationData),我们要本地化该消息并替换掉参数。对于简单消息(MessageNotificationData),我们直接的取得该消息。当然,在一个真实的项目中,我们不会使用alert函数。我们会使用 abp.notify API来展示漂亮的UI通知。

如果你需要实现上面所展示的逻辑,这里有一个更简单且可伸缩性的方法。当推送通知被接收到的时候,你可以仅使用一行代码来显示 UI通知

abp.event.on('abp.notifications.received', function (userNotification) {
    abp.notifications.showUiNotifyForUserNotification(userNotification);
});

下面展示的是一个 UI 通知 (上面System.LowDisk示例)。

它对于内置的通知数据类型(LocalizableMessageNotificationData 和 MessageNotificationData)可以很好的工作。如果你有自定义的通知数据类型,那么你应该像下面一样使用格式化器来注册这个数据:

abp.notifications.messageFormatters['MyProject.MyNotificationDataType'] = function(userNotification) {
    return ...; //在这是实现格式化数据的逻辑并返回消息
};

因此,showUiNotifyForUserNotification 能对你的数据类型来创建并显示该消息。如果你仅需要格式化消息,你可以直接的使用 abp.notifications.getFormattedMessageFromUserNotification(userNotification),这是由showUiNotifyForUserNotification内部使用的。

当你接受到推送通知的时候,启动模板已经包含了显示UI通知的代码。

8.1.11 存储通知

通知系统使用 INotificationStore 来持久化通知。为了使通知系统正确的工作,我们应该实现这个接口。你可以自己实现它,或者使用已经实现了该接口的 module-zero

8.1.12 通知定义

使用之前,你不需要定义一个通知。你可以使用任何的没有被定义过的通知名字。但是,定义通知名字可以给你带来额外的好处。例如:你可能需要在你的应用程序中调研所有的通知。在这种情况下,我们需要为我们模块 通知提供器(notification provider) ,如下所示:

public class MyAppNotificationProvider : NotificationProvider
{
    public override void SetNotifications(INotificationDefinitionContext context)
    {
        context.Manager.Add(
            new NotificationDefinition(
                "App.NewUserRegistered",
                displayName: new LocalizableString("NewUserRegisteredNotificationDefinition", "MyLocalizationSourceName"),
                permissionDependency: new SimplePermissionDependency("App.Pages.UserManagement")
                )
            );
    }
}

App.NewUserRegistered 是通知的唯一名字。我们定义了一个本地化的 displayName(当我们订阅了该通知时,我们可以在UI上显示它)。最后,我们声明了 App.Pages.UserManagement 权限,只有当用户具有该权限的时候,该通知才会显示给用户。

当然这里还有其它一些参数。你可以在代码中研究它们。对于通知定义只有通知的名字是必须的。

在你定义了如上所述的通知提供器后,我们应该在模块的 PreInitialize 方法中注册它。如下所示:

public class AbpZeroTemplateCoreModule : AbpModule
{
    public override void PreInitialize()
    {
        Configuration.Notifications.Providers.Add<MyAppNotificationProvider>();
    }

    //...
}

最后,你可以在你的应用程序中注入并使用 INotificationDefinitionManager 接口来获取通知定义。