1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
| // Hubs/ChatHub.cs
[Authorize] // 需要身份认证
public class ChatHub(IChatMessageRepository msgRepo, ILogger<ChatHub> logger)
: Hub
{
// 内存用户字典(生产环境应使用 Redis)
private static readonly ConcurrentDictionary<string, OnlineUser> _onlineUsers = new();
// ── 加入房间 ────────────────────────────────────────────────
public async Task JoinRoom(string roomName)
{
var userName = Context.User!.Identity!.Name!;
var user = new OnlineUser(Context.ConnectionId, userName, roomName, DateTime.UtcNow);
_onlineUsers[Context.ConnectionId] = user;
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
// 推送历史消息(最近 50 条)
var history = await msgRepo.GetRecentAsync(roomName, 50);
await Clients.Caller.SendAsync("LoadHistory", history);
// 通知房间内其他人
await Clients.Group(roomName).SendAsync("UserJoined", new
{
userName,
joinedAt = user.JoinedAt,
onlineCount = GetRoomUserCount(roomName)
});
// 发送系统消息
await SendSystemMessage(roomName, $"{userName} 加入了房间");
logger.LogInformation("{User} joined room {Room}", userName, roomName);
}
// ── 离开房间 ────────────────────────────────────────────────
public async Task LeaveRoom(string roomName)
{
await HandleUserLeft(roomName);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
}
// ── 发送消息 ────────────────────────────────────────────────
public async Task SendMessage(string roomName, string content)
{
var userName = Context.User!.Identity!.Name!;
// 内容过滤(XSS 防护)
content = HtmlEncoder.Default.Encode(content.Trim());
if (string.IsNullOrEmpty(content) || content.Length > 2000) return;
var message = new ChatMessage(
Id: Guid.NewGuid().ToString("N"),
RoomName: roomName,
UserName: userName,
Content: content,
SentAt: DateTime.UtcNow
);
// 持久化到数据库
await msgRepo.SaveAsync(message);
// 广播给房间内所有人
await Clients.Group(roomName).SendAsync("ReceiveMessage", message);
}
// ── 私聊 ─────────────────────────────────────────────────────
public async Task SendPrivateMessage(string targetUserName, string content)
{
content = HtmlEncoder.Default.Encode(content.Trim());
var senderName = Context.User!.Identity!.Name!;
// 找到目标用户的所有连接(同一用户可能多设备在线)
var targetConnections = _onlineUsers.Values
.Where(u => u.UserName == targetUserName)
.Select(u => u.ConnectionId)
.ToList();
if (!targetConnections.Any())
{
await Clients.Caller.SendAsync("Error", $"{targetUserName} 不在线");
return;
}
var message = new ChatMessage(
Id: Guid.NewGuid().ToString("N"),
RoomName: "private",
UserName: senderName,
Content: content,
SentAt: DateTime.UtcNow
);
// 同时发给对方和自己
await Clients.Clients(targetConnections).SendAsync("ReceivePrivateMessage", message);
await Clients.Caller.SendAsync("ReceivePrivateMessage", message);
}
// ── 打字指示器 ───────────────────────────────────────────────
public async Task SendTyping(string roomName, bool isTyping)
{
var userName = Context.User!.Identity!.Name!;
// 广播给房间内除自己以外的人
await Clients.OthersInGroup(roomName).SendAsync("UserTyping", new
{
userName,
isTyping
});
}
// ── 获取在线用户列表 ─────────────────────────────────────────
public Task<IEnumerable<string>> GetOnlineUsers(string roomName)
{
var users = _onlineUsers.Values
.Where(u => u.RoomName == roomName)
.Select(u => u.UserName)
.Distinct();
return Task.FromResult(users);
}
// ── 连接断开 ─────────────────────────────────────────────────
public override async Task OnDisconnectedAsync(Exception? exception)
{
if (_onlineUsers.TryRemove(Context.ConnectionId, out var user))
{
await HandleUserLeft(user.RoomName);
}
await base.OnDisconnectedAsync(exception);
}
// ── 私有方法 ─────────────────────────────────────────────────
private async Task HandleUserLeft(string roomName)
{
var userName = Context.User?.Identity?.Name ?? "未知用户";
await Clients.Group(roomName).SendAsync("UserLeft", new
{
userName,
onlineCount = GetRoomUserCount(roomName) - 1
});
await SendSystemMessage(roomName, $"{userName} 离开了房间");
}
private async Task SendSystemMessage(string roomName, string content)
{
var sysMsg = new ChatMessage(
Id: Guid.NewGuid().ToString("N"),
RoomName: roomName,
UserName: "系统",
Content: content,
SentAt: DateTime.UtcNow,
Type: MessageType.System
);
await Clients.Group(roomName).SendAsync("ReceiveMessage", sysMsg);
}
private int GetRoomUserCount(string roomName)
=> _onlineUsers.Values.Count(u => u.RoomName == roomName);
}
|