通知系统
CmsKit 的通知基于 CAP 事件总线 + SignalR 实时推送 + 数据库持久化:互动动作发生时只发一条事件,真正的落库与推送由消费方异步完成。用户可在「全部 / 赞 / 评论 / 关注 / @我 / 系统」之间切换,并查看每类未读数量。
为什么这样设计:事件驱动,而非同步写
通知是典型的「写多、可被延迟、失败可重试」场景。如果每次点赞都同步写通知表 + 推 SignalR,请求链路会被拖慢,且「一个博主被一万次点赞」会瞬间放大写压力。
CmsKit 的做法(CommentService / UserLikeService / UserSubscribeService / ShortMsgService 等)是只调用:
await capBus.PublishAsync(CreateNotificationReq.CreateOrCancelAsync, new CreateNotificationReq { ... });
真正的处理在消费方 NotificationService.CreateOrCancelAsync 完成——请求方不等待通知写入,事件由 CAP 保证至少一次投递并可重试。
消息语义:创建即撤销
CreateNotificationReq 带 IsCancel 字段,同一条消息既能「创建通知」也能「撤销通知」:
- 给文章点赞 → 发一条创建消息,作者收到「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 | 全部通知 | — |
UserLike | 0 | 赞 | 别人点赞你的文章或评论 |
UserComment | 1 | 评论 | 别人评论你的文章或回复你的评论 |
UserSubscribeUser | 2 | 关注 | 别人关注了你 |
Mention | 3 | @我 | 别人在内容中 @ 提及你 |
System | 4 | 系统 | 系统通知(定时发布成功、运营等) |
列表查询用 NotificationSearchType 过滤;管理端可传 ?userId= 以指定用户视角查询(需 CmsKit.Notifications.Manage 权限)。
与内容的关联
通知永远指向「谁对什么内容做了什么」:
SubjectType(NotificationSubjectType:Article / ShortMsg / Tag / Classify / None)+SubjectId指向被作用的内容。NotificationRespUserId是接收者,UserInfoId是触发者。
例如定时发布成功时,CmsKitJobService.ScheduledPublishAsync 会给作者发一条 Action=System、SubjectType=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负责落库与查询;INotificationPushService(NotificationPushService)只负责 SignalR 推送,不做持久化——避免通知被重复入库。