跳到主要内容

通知系统

CmsKit 的通知基于 CAP 事件总线 + SignalR 实时推送 + 数据库持久化:互动动作发生时只发一条事件,真正的落库与推送由消费方异步完成。用户可在「全部 / 赞 / 评论 / 关注 / @我 / 系统」之间切换,并查看每类未读数量。

为什么这样设计:事件驱动,而非同步写

通知是典型的「写多、可被延迟、失败可重试」场景。如果每次点赞都同步写通知表 + 推 SignalR,请求链路会被拖慢,且「一个博主被一万次点赞」会瞬间放大写压力。

CmsKit 的做法(CommentService / UserLikeService / UserSubscribeService / ShortMsgService 等)是只调用:

await capBus.PublishAsync(CreateNotificationReq.CreateOrCancelAsync, new CreateNotificationReq { ... });

真正的处理在消费方 NotificationService.CreateOrCancelAsync 完成——请求方不等待通知写入,事件由 CAP 保证至少一次投递并可重试。

消息语义:创建即撤销

CreateNotificationReqIsCancel 字段,同一条消息既能「创建通知」也能「撤销通知」:

  • 给文章点赞 → 发一条创建消息,作者收到「X 赞了你的文章」;
  • 取消点赞 → 发一条 IsCancel=true 的同结构消息,对应通知被移除。

这样天然幂等、天然去重:重复点赞/取消不会留下脏数据,也不用各业务方自己记「我有没有发过这条通知」。

领域关系

从代码可验证:

  • 生产者解耦CommentService 在评论创建时按 SubjectType(文章/沸点/标签/分类)映射出 NotificationSubjectType,发 NotificationAction.Comment 事件;UserLikeService / UserSubscribeService 同理发 Like / Subscribe 事件。
  • 消费方NotificationService.CreateOrCancelAsync 负责落库(InsertAsync 或按表达式 DeleteAsync)与后置推送(AfterCreateNotification)。
  • 推送:经 NotificationHub/hubs/notifications),RedisConnectionManager 做多实例连接跟踪——推送时按 UserId 取出在线 ConnectionId 列表逐连接发送,保证多副本部署下也能精准触达。

通知类别

通知按业务动作分 6 类,由 NotificationSearchType 枚举定义:

枚举值数值含义对应动作
All-1全部通知
UserLike0别人点赞你的文章或评论
UserComment1评论别人评论你的文章或回复你的评论
UserSubscribeUser2关注别人关注了你
Mention3@我别人在内容中 @ 提及你
System4系统系统通知(定时发布成功、运营等)

列表查询用 NotificationSearchType 过滤;管理端可传 ?userId= 以指定用户视角查询(需 CmsKit.Notifications.Manage 权限)。

与内容的关联

通知永远指向「谁对什么内容做了什么」:

  • SubjectTypeNotificationSubjectType:Article / ShortMsg / Tag / Classify / None)+ SubjectId 指向被作用的内容。
  • NotificationRespUserId 是接收者,UserInfoId 是触发者。

例如定时发布成功时,CmsKitJobService.ScheduledPublishAsync 会给作者发一条 Action=SystemSubjectType=Article 的通知——这是通知系统被后台作业直接使用的例子。

实时推送(服务端)

服务端发送通知分两步——落库与推送分离

// 1) 持久化(含创建/撤销语义,可由 CAP 消费方异步完成)
await _notificationService.CreateOrCancelAsync(new CreateNotificationReq
{
Action = NotificationAction.Like,
SubjectType = NotificationSubjectType.Article,
SubjectId = articleId,
NotificationRespUserId = article.AuthorId,
UserInfoId = currentUserId,
IsCancel = false
});

// 2) 实时推送(仅在线时,由 NotificationPushService 经 SignalR 发出)
await _notificationPushService.PushNotificationAsync(new CreateNotificationRequest
{
Action = NotificationAction.Like,
SubjectType = NotificationSubjectType.Article,
SubjectId = articleId,
UserId = article.AuthorId,
FromUserId = currentUserId
});

注意分工:INotificationService 负责落库与查询;INotificationPushServiceNotificationPushService负责 SignalR 推送,不做持久化——避免通知被重复入库。

相关文档