CmsKit 实时通知实施指南
📋 概述
CmsKit 的实时通知系统基于 SignalR 实现,支持以下特性:
- ✅ 实时推送:点赞、评论、关注等操作后 3 秒内推送给接收方
- ✅ 多设备支持:同一用户可在多个设备同时接收通知
- ✅ 离线消息:用户不在线时消息保存到数据库,上线后同步
- ✅ 已读同步:单条通知已读状态实时同步到所有设备
- ✅ 未读计数:未读通知数量实时更新
- ✅ 连接鉴权:基于 JWT Token 的连接认证
- ✅ 断线重连:客户端自动重连机制
🏗️ 技术架构
核心组件
-
NotificationHub (
Hubs/NotificationHub.cs)- SignalR 中心,管理用户连接
- 支持一对多推送(单用户多设备)
- 连接/断开事件处理
- JWT Token 鉴权
- 基于 FreeRedis 存储连接状态
-
INotificationClient (
Hubs/INotificationClient.cs)- SignalR 客户端接口定义
- 定义可推送的通知类型
-
NotificationPushService (
Application/Notifications/NotificationPushService.cs)- 通知推送服务
- 持久化 + 实时推送双保险
- 使用 FreeRedis 查询连接状态
-
RedisConnectionManager (
Hubs/RedisConnectionManager.cs)- 基于 FreeRedis 的连接管理器
- 用户连接状态存储(UserId -> ConnectionIds)
- 支持多实例部署
- 连接状态 Redis Key:
signalr:connections:{userId}
数据流程
用户操作(点赞/评论/关注)
↓
应用层创建通知
↓
NotificationService.CreateOrCancelAsync
↓
AfterCreateNotification
↓
NotificationPushService.PushNotificationAsync
↓
┌─────────────────────────────────────┐
│ 1. 持久化到数据库 │
│ 2. 查询 Redis 获取用户连接 │
│ - 从 signalr:connections:{userId} │
│ 3. 通过 SignalR 实时推送 │
│ - 用户在线:立即推送到所有连接 │
│ - 用户离线:保存到数据库 │
└─────────────────────────────────────┘
多实例部署场景:
┌─────────────┐ ┌─────────────┐
│ 实例 A │ │ 实例 B │
│ User1: Conn1│ │ User1: Conn2│
│ Redis 共享连接状态 │◄──────────►│
└─────────────┘ └─────────────┘
技术架构说明
为什么使用 FreeRedis 而不是 SignalR 官方 Backplane?
- 项目统一性:项目已使用 FreeRedis 作为 Redis 客户端,避免引入多个 Redis 库
- 简化依赖:不需要额外安装
Microsoft.AspNetCore.SignalR.StackExchangeRedis - 自定义控制:可以灵活控制连接存储结构和过期策略
- 多实例支持:通过 Redis 共享连接状态,支持水平扩展
Redis 数据结构:
Key: signalr:connections:{userId}
Type: Set
Value: [connectionId1, connectionId2, ...]
TTL: 24 小时(自动过期)
多实例部署工作原理:
- 所有实例共享同一个 Redis
- 用户连接到任意实例时,连接信息都写入 Redis
- 推送通知时,从 Redis 读取该用户的所有连接(可能分布在多个实例)
- 通过 SignalR HubContext 向这些连接推送
🚀 快速开始
1. 后端配置
后端已在 CmsKitModuleStartup.cs 中完成配置:
// ConfigureServices 中
services.AddSignalR();
services.AddScoped<INotificationPushService, NotificationPushService>();
services.AddSingleton<FreeKit.CmsKit.Hubs.RedisConnectionManager>();
// Configure 中
app.MapHub<Hubs.NotificationHub>("/hubs/notifications");
注意事项:
- 确保项目中已配置 FreeRedis 连接
- RedisConnectionManager 会自动使用项目中已注册的 IRedisClient
- 连接状态存储在 Redis 中,支持多实例部署
2. 前端集成示例
2.1 连接 SignalR
import * as signalR from "@microsoft/signalr";
class NotificationClient {
constructor() {
this.connection = null;
this.reconnectInterval = 5000; // 5 秒重连
}
// 建立连接
async connect() {
const token = localStorage.getItem("access_token");
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`/hubs/notifications?access_token=${token}`)
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // 指数退避
.build();
// 注册事件处理
this.registerHandlers();
try {
await this.connection.start();
console.log("SignalR 连接成功");
} catch (err) {
console.error("SignalR 连接失败:", err);
// 触发重连逻辑
setTimeout(() => this.connect(), this.reconnectInterval);
}
}
// 注册事件处理
registerHandlers() {
// 接收新通知
this.connection.on("SendNotificationAsync", (notification) => {
console.log("收到新通知:", notification);
this.showNotificationToast(notification);
this.updateUnreadCount();
});
// 更新未读数量
this.connection.on("UpdateUnreadCountAsync", (count) => {
console.log("未读数量:", count);
this.updateUnreadBadge(count);
});
// 标记通知已读
this.connection.on("MarkNotificationAsReadAsync", (notificationId) => {
console.log("通知已读:", notificationId);
this.markAsRead(notificationId);
});
// 连接关闭
this.connection.onclose(() => {
console.log("SignalR 连接已关闭");
setTimeout(() => this.connect(), this.reconnectInterval);
});
}
// 显示通知弹窗
showNotificationToast(notification) {
// 使用你的 UI 组件库显示通知
// 例如:Element Plus, Ant Design Vue 等
ElNotification({
title: this.getNotificationTitle(notification.notificationType),
message: this.getNotificationMessage(notification),
type: "info",
duration: 4000
});
}
// 更新未读徽章
updateUnreadBadge(count) {
const badge = document.querySelector(".notification-badge");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "block" : "none";
}
}
// 标记已读
markAsRead(notificationId) {
// 更新 UI 中的通知状态
const element = document.querySelector(`[data-notification-id="${notificationId}"]`);
if (element) {
element.classList.add("read");
}
}
// 获取通知标题
getNotificationTitle(type) {
const titles = {
1: "点赞通知",
2: "评论通知",
3: "关注通知"
};
return titles[type] || "新通知";
}
// 获取通知消息
getNotificationMessage(notification) {
return `${notification.userInfo?.nickName || "用户"}: ${this.getMessageContent(notification)}`;
}
getMessageContent(notification) {
switch (notification.notificationType) {
case 1: // UserLikeArticle
return "点赞了您的文章";
case 2: // UserCommentOnArticle
return "评论了您的文章";
case 3: // UserSubscribeUser
return "关注了您";
default:
return "发送了一条消息";
}
}
}
// 使用示例
const notificationClient = new NotificationClient();
notificationClient.connect();
2.2 Vue 3 组合式 API 示例
<template>
<div class="notification-center">
<el-badge :value="unreadCount" :hidden="unreadCount === 0">
<el-button @click="showNotifications">
<i class="el-icon-bell"></i>
</el-button>
</el-badge>
<el-dialog v-model="dialogVisible" title="消息通知">
<el-tabs v-model="activeTab">
<el-tab-pane label="评论" name="comment">
<notification-list :notifications="commentNotifications" />
</el-tab-pane>
<el-tab-pane label="点赞" name="like">
<notification-list :notifications="likeNotifications" />
</el-tab-pane>
<el-tab-pane label="关注" name="subscribe">
<notification-list :notifications="subscribeNotifications" />
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as signalR from '@microsoft/signalr';
import { useNotificationStore } from '@/stores/notification';
const notificationStore = useNotificationStore();
const unreadCount = ref(0);
const dialogVisible = ref(false);
const activeTab = ref('comment');
let connection = null;
onMounted(async () => {
await initSignalR();
await loadNotifications();
});
onUnmounted(() => {
if (connection) {
connection.stop();
}
});
async function initSignalR() {
const token = localStorage.getItem('access_token');
connection = new signalR.HubConnectionBuilder()
.withUrl(`/hubs/notifications?access_token=${token}`)
.withAutomaticReconnect()
.build();
connection.on('SendNotificationAsync', (notification) => {
notificationStore.addNotification(notification);
updateUnreadCount();
showNotificationToast(notification);
});
connection.on('UpdateUnreadCountAsync', (count) => {
unreadCount.value = count;
});
connection.on('MarkNotificationAsReadAsync', (id) => {
notificationStore.markAsRead(id);
});
await connection.start();
console.log('SignalR Connected');
}
async function loadNotifications() {
const response = await fetch('/api/cms/notifications/unread-count');
const data = await response.json();
unreadCount.value = data.userLikeCount + data.userCommentCount + data.userSubscribeUserCount;
}
function updateUnreadCount() {
loadNotifications();
}
function showNotificationToast(notification) {
ElNotification({
title: '新消息',
message: `${notification.userInfo?.nickName} ${getMessage(notification)}`,
type: 'info',
duration: 4000
});
}
function getMessage(notification) {
const messages = {
1: '点赞了您的内容',
2: '评论了您的内容',
3: '关注了您'
};
return messages[notification.notificationType] || '发送了消息';
}
</script>
📊 监控与调试
1. 查看连接状态
// 在 NotificationHub 中已添加日志
logger.LogInformation("用户 {UserId} 连接到通知 Hub,连接 ID: {ConnectionId}", userId, Context.ConnectionId);
2. 测试实时推送
使用 Postman 或浏览器控制台测试:
// 浏览器控制台
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/notifications?access_token=YOUR_TOKEN")
.build();
connection.on("SendNotificationAsync", (data) => {
console.log("收到通知:", data);
});
connection.start();
3. 常见问题排查
问题 1: 连接失败
- 检查 Token 是否有效
- 检查 Hub 路由是否正确 (
/hubs/notifications) - 查看浏览器控制台和服务器日志
问题 2: 通知不推送
- 检查用户是否在线(查看 Hub 日志)
- 检查数据库通知是否保存成功
- 查看
NotificationPushService日志
问题 3: 断线后不重连
- 检查
withAutomaticReconnect配置 - 查看网络状态
- 检查 Token 是否过期
🎯 验收标准
- 点赞后,被点赞用户 3 秒内收到通知
- 评论后,被评论用户 3 秒内收到通知
- 关注后,被关注用户 3 秒内收到通知
- 通知列表实时刷新
- 未读数量实时更新
- 单条通知已读状态实时同步
- 用户离线时消息不丢失(保存到数据库)
- 用户上线后自动同步未读消息
- 支持多设备同时接收通知
- 连接鉴权正常工作
- 断线后自动重连
- 支持多实例部署(基于 FreeRedis 共享连接状态)
📝 注意事项
-
性能优化
- ✅ 已使用 Redis 存储连接状态,支持多实例部署
- 连接信息 24 小时自动过期,避免脏数据积累
- 使用 Redis Set 数据结构,支持快速添加/删除连接
-
安全性
- Token 通过查询参数传递,确保使用 HTTPS
- Hub 已添加
[Authorize]特性进行鉴权
-
容错处理
- 实时推送失败不影响数据库保存
- 用户上线后可查询未读通知
- Redis 连接失败时自动降级(通知仍保存到数据库)
-
扩展性
- 支持自定义通知类型
- 支持批量推送
- 支持通知模板和国际化
- ✅ 支持多实例水平扩展
-
Redis 依赖
- 项目已使用 FreeRedis,无需额外依赖
- Redis 故障时,通知仍会保存到数据库
- 建议使用 Redis 集群提高可用性
🔗 相关文件
Hubs/NotificationHub.cs- SignalR Hub 实现(基于 FreeRedis)Hubs/INotificationClient.cs- 客户端接口Hubs/RedisConnectionManager.cs- Redis 连接管理器Application/Notifications/NotificationPushService.cs- 推送服务Application/Notifications/OfflineNotificationQueue.cs- 离线队列(保留)CmsKitModuleStartup.cs- 模块启动配置