92 lines
3.7 KiB
C#
92 lines
3.7 KiB
C#
using BuildFlow.Platform.Application.Users.DTOs;
|
||
using BuildFlow.Platform.Application.Users.Queries;
|
||
using IM.Application.Chat.DTOs;
|
||
using IM.Application.Chat.Queries;
|
||
using IM.Domain.Common;
|
||
using IM.Infrastructure.Persistence;
|
||
using MediatR;
|
||
|
||
namespace IM.Application.Chat.Handlers;
|
||
|
||
/// <summary>
|
||
/// 用户会话列表查询处理器。聚合最后消息与未读数。
|
||
/// </summary>
|
||
public class GetConversationsQueryHandler(
|
||
ImDbContext db,
|
||
IMediator mediator,
|
||
IUnreadCounter unreadCounter)
|
||
: IRequestHandler<GetConversationsQuery, List<ConversationDto>>
|
||
{
|
||
public async Task<List<ConversationDto>> Handle(GetConversationsQuery request, CancellationToken ct)
|
||
{
|
||
// 一次性 Include 参与者与群组导航属性,避免 N+1 查询
|
||
var participants = await db.ConversationParticipants
|
||
.Where(p => p.UserId == request.UserId)
|
||
.Include(p => p.ConversationEntity)
|
||
.ThenInclude(c => c.Participants)
|
||
.Include(p => p.ConversationEntity)
|
||
.ThenInclude(c => c.GroupEntity)
|
||
.Where(p => !p.ConversationEntity.IsDeleted)
|
||
// 排序:置顶优先(PinnedAt 倒序),其次按最后消息时间倒序
|
||
.OrderByDescending(p => p.IsPinned)
|
||
.ThenByDescending(p => p.PinnedAt)
|
||
.ThenByDescending(p => p.ConversationEntity.LastMessageAt)
|
||
.ToListAsync(ct);
|
||
|
||
// 私聊会话需解析"对方"用户以展示头像/昵称;批量收集去重后一次性查询 Platform,避免逐会话往返。
|
||
var otherUserIds = participants
|
||
.Where(p => p.ConversationEntity.Type == Domain.Enums.ConversationTypeEnum.Private)
|
||
.Select(p => p.ConversationEntity.Participants
|
||
.FirstOrDefault(op => op.UserId != request.UserId)?.UserId)
|
||
.Where(id => id.HasValue)
|
||
.Select(id => id!.Value)
|
||
.Distinct()
|
||
.ToList();
|
||
|
||
var users = otherUserIds.Count > 0
|
||
? await mediator.Send(new GetUserSummariesQuery(otherUserIds), ct)
|
||
: new Dictionary<Guid, UserSummaryDto>();
|
||
|
||
var redisUnread = await unreadCounter.GetByUserAsync(
|
||
request.UserId,
|
||
participants.Select(p => p.ConversationId),
|
||
ct);
|
||
|
||
return participants.Select(p =>
|
||
{
|
||
string? otherUserName = null;
|
||
string? otherUserAvatar = null;
|
||
Guid? otherUserId = null;
|
||
|
||
if (p.ConversationEntity.Type == Domain.Enums.ConversationTypeEnum.Private)
|
||
{
|
||
var otherParticipant = p.ConversationEntity.Participants
|
||
.FirstOrDefault(op => op.UserId != request.UserId);
|
||
if (otherParticipant is not null)
|
||
{
|
||
otherUserId = otherParticipant.UserId;
|
||
var userInfo = users.GetValueOrDefault(otherParticipant.UserId);
|
||
otherUserName = userInfo?.Username;
|
||
otherUserAvatar = userInfo?.Avatar;
|
||
}
|
||
}
|
||
|
||
return new ConversationDto(
|
||
p.ConversationEntity.Id,
|
||
p.ConversationEntity.Type,
|
||
p.ConversationEntity.LastMessageContent,
|
||
p.ConversationEntity.LastMessageAt,
|
||
redisUnread.TryGetValue(p.ConversationId, out var unreadCount) ? unreadCount : p.UnreadCount,
|
||
p.ConversationEntity.GroupEntity?.Name,
|
||
p.ConversationEntity.GroupEntity?.Avatar,
|
||
p.ConversationEntity.GroupEntity?.Id,
|
||
otherUserId,
|
||
otherUserName,
|
||
otherUserAvatar,
|
||
p.IsPinned,
|
||
p.IsMuted,
|
||
p.Remark);
|
||
}).ToList();
|
||
}
|
||
}
|