using Dpz.Core.Entity.Base.PublicStruct;
using Microsoft.AspNetCore.SignalR;
namespace Dpz.Core.Web.Library.Hub;
/// <summary>
/// 群组聊天客户端接口
/// </summary>
public interface IGroupChatClient
{
Task OnMessageReceived(GroupChatMessage message);
Task OnUserJoined(VmUserInfo user);
Task OnUserLeft(VmUserInfo user);
Task OnHistoryLoaded(List<GroupChatMessage> messages, bool hasMore);
Task OnError(string error);
Task OnDraw(object data);
Task OnDrawingUserChanged(VmUserInfo? user);
}
/// <summary>
/// 群组聊天Hub
/// </summary>
public class GroupChatHub(
IChatRecordService chatRecordService,
ILogger<GroupChatHub> logger,
IFusionCache fusionCache
) : Hub<IGroupChatClient>
{
private const string GroupId = WebToolsExtensions.DefaultGroupId;
private const string DrawingUserKey = "GroupChat:DrawingUser";
/// <summary>
/// 加入群组
/// </summary>
public async Task<VmUserInfo?> JoinGroup()
{
var sessionId = Context
.GetHttpContext()
?.Request.Cookies[Program.AuthorizeCookieName + ".SessionId"];
if (string.IsNullOrWhiteSpace(sessionId))
{
await Clients.Caller.OnError("需要SessionId才能加入群聊");
return null;
}
VmUserInfo user;
// 检查是否已登录
if (Context.User?.Authenticated == true)
{
user = Context.User.RequiredUserInfo;
}
else
{
var cacheKey = $"GroupChat:AnonymousUser:{sessionId}";
user = await fusionCache.GetOrSetAsync(
cacheKey,
async _ =>
{
var displayName = await AssignNameAsync();
return new VmUserInfo
{
Id = sessionId,
Name = displayName,
Avatar =
$"https://cravatar.cn/avatar/{sessionId.GenerateHashMd5()}?d=wavatar",
Sign = $"匿名用户 - {displayName}",
LastAccessTime = DateTime.Now,
Key = "",
};
},
TimeSpan.FromDays(7)
);
}
await Groups.AddToGroupAsync(Context.ConnectionId, GroupId);
await Clients.Group(GroupId).OnUserJoined(user);
// 如果当前有人在画图,通知新加入的用户
var drawingUser = await fusionCache.TryGetAsync<VmUserInfo>(DrawingUserKey);
if (drawingUser.HasValue)
{
await Clients.Caller.OnDrawingUserChanged(drawingUser.Value);
}
logger.LogInformation("用户 {UserId} ({UserName}) 加入群组", user.Id, user.Name);
return user;
}
/// <summary>
/// 发送消息
/// </summary>
public async Task SendMessage(string message)
{
if (string.IsNullOrWhiteSpace(message))
{
await Clients.Caller.OnError("消息内容不能为空");
return;
}
var user = await GetCurrentUserAsync();
if (user == null)
{
return;
}
// 保存聊天记录
await chatRecordService.SaveGroupRecordAsync(user, GroupId, message);
// 广播消息
var messageData = new GroupChatMessage
{
UserId = user.Id,
UserName = user.Name,
Avatar = user.Avatar,
Message = message,
SendTime = DateTime.Now,
};
await Clients.Group(GroupId).OnMessageReceived(messageData);
logger.LogInformation(
"用户 {UserId} ({UserName}) 发送消息: {Message}",
user.Id,
user.Name,
message
);
}
/// <summary>
/// 请求画图权限
/// </summary>
public async Task RequestDrawingAccess()
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return;
}
// 使用 IFusionCache 实现简单的 分布式锁
var currentDrawingUser = await fusionCache.TryGetAsync<VmUserInfo>(DrawingUserKey);
if (!currentDrawingUser.HasValue)
{
// 30分钟超时自动释放
await fusionCache.SetAsync(DrawingUserKey, user, TimeSpan.FromMinutes(30));
await Clients.Group(GroupId).OnDrawingUserChanged(user);
}
else if (currentDrawingUser.Value.Id == user.Id)
{
// 已经是自己了
await Clients.Caller.OnDrawingUserChanged(user);
}
else
{
await Clients.Caller.OnError(
$"当前用户 {currentDrawingUser.Value.Name} 正在画图,请稍后再试"
);
}
}
/// <summary>
/// 释放画图权限
/// </summary>
public async Task ReleaseDrawingAccess()
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return;
}
var currentDrawingUser = await fusionCache.TryGetAsync<VmUserInfo>(DrawingUserKey);
if (currentDrawingUser.HasValue && currentDrawingUser.Value.Id == user.Id)
{
await fusionCache.RemoveAsync(DrawingUserKey);
await Clients.Group(GroupId).OnDrawingUserChanged(null);
}
}
/// <summary>
/// 发送画图数据
/// </summary>
public async Task SendDrawingData(object data)
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return;
}
var currentDrawingUser = await fusionCache.TryGetAsync<VmUserInfo>(DrawingUserKey);
if (currentDrawingUser.HasValue && currentDrawingUser.Value.Id == user.Id)
{
await Clients.OthersInGroup(GroupId).OnDraw(data);
}
}
/// <summary>
/// 获取历史记录
/// </summary>
public async Task GetHistory(int pageIndex = 1, int pageSize = 50)
{
if (await GetCurrentUserAsync() == null)
{
return;
}
// 限制最多1000条
var maxPageSize = 1000;
var actualPageSize = Math.Min(pageSize, maxPageSize);
var actualPageIndex = pageIndex;
var records = await chatRecordService.GetGroupRecordAsync(
GroupId,
actualPageIndex,
actualPageSize
);
List<GroupChatMessage> messages = [];
foreach (var item in records)
{
if (item.Sender == null)
{
continue;
}
var message = new GroupChatMessage
{
UserId = item.Sender.Id,
UserName = item.Sender.Name,
Avatar = item.Sender.Avatar,
Message = item.Message,
SendTime = item.SendTime,
};
messages.Add(message);
}
// 计算是否还有更多记录
var hasMore = records.CurrentPageIndex < records.TotalPageCount;
await Clients.Caller.OnHistoryLoaded(messages, hasMore);
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var user = await GetCurrentUserAsync();
if (user != null)
{
await Clients.Group(GroupId).OnUserLeft(user);
logger.LogInformation("用户 {UserId} ({UserName}) 离开群组", user.Id, user.Name);
// 如果是画图用户断开连接,释放画图权限
var drawingUser = await fusionCache.TryGetAsync<VmUserInfo>(DrawingUserKey);
if (drawingUser.HasValue && drawingUser.Value.Id == user.Id)
{
await fusionCache.RemoveAsync(DrawingUserKey);
await Clients.Group(GroupId).OnDrawingUserChanged(null);
}
}
await base.OnDisconnectedAsync(exception);
}
private async Task<VmUserInfo?> GetCurrentUserAsync()
{
var sessionId = Context
.GetHttpContext()
?.Request.Cookies[Program.AuthorizeCookieName + ".SessionId"];
if (string.IsNullOrWhiteSpace(sessionId))
{
await Clients.Caller.OnError("需要SessionId");
return null;
}
if (Context.User?.Authenticated == true)
{
return Context.User.RequiredUserInfo;
}
var cacheKey = $"GroupChat:AnonymousUser:{sessionId}";
var anonymousUser = await fusionCache.TryGetAsync<VmUserInfo>(cacheKey);
if (!anonymousUser.HasValue)
{
await Clients.Caller.OnError("用户信息不存在,请重新加入群组");
return null;
}
return anonymousUser.Value;
}
private async Task<string> AssignNameAsync()
{
for (var i = 0; i < 5; i++)
{
var candidate = GenerateBaseName();
var nameKey = $"GroupChat:TakenName:{candidate}";
var isTaken = await fusionCache.TryGetAsync<string>(nameKey);
if (!isTaken.HasValue)
{
await fusionCache.SetAsync(nameKey, "taken", TimeSpan.FromDays(7));
return candidate;
}
}
var baseName = GenerateBaseName();
var suffix = Random.Shared.Next(1000, 9999);
var finalName = $"{baseName}{suffix}";
// 占位
await fusionCache.SetAsync(
$"GroupChat:TakenName:{finalName}",
"taken",
TimeSpan.FromDays(7)
);
return finalName;
string GenerateBaseName()
{
var adjList = Adjectives.AdjectiveList;
var nameList = AnonymousNames.NameList;
var adj = adjList[Random.Shared.Next(adjList.Count)];
var name = nameList[Random.Shared.Next(nameList.Count)];
return $"{adj}de{name}";
}
}
}
评论加载中...