Структура
This commit is contained in:
+28
-37
@@ -1,15 +1,10 @@
|
||||
using MediatR;
|
||||
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик доменного события отправки сообщения.
|
||||
/// Отправляет уведомление через SignalR всем участникам чата.
|
||||
/// </summary>
|
||||
public sealed class MessageSentDomainEventHandler : INotificationHandler<MessageSentDomainEvent>
|
||||
{
|
||||
private readonly IMessageNotifier _hubContext;
|
||||
@@ -39,7 +34,6 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
? new { Id = userInfo.Id, Username = userInfo.Username, DisplayName = userInfo.DisplayName, Avatar = userInfo.Avatar }
|
||||
: new { Id = message.SenderId, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null };
|
||||
|
||||
// Fetch forwarded from info if exists
|
||||
object? forwardedFromObj = null;
|
||||
if (message.ForwardedFromId.HasValue)
|
||||
{
|
||||
@@ -49,7 +43,6 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
: new { Id = message.ForwardedFromId.Value, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null };
|
||||
}
|
||||
|
||||
// Fetch reply info if exists
|
||||
object? replyToObj = null;
|
||||
if (message.ReplyToId.HasValue)
|
||||
{
|
||||
@@ -62,7 +55,7 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
Id = replyMsg.Id,
|
||||
Content = replyMsg.Content,
|
||||
Quote = message.Quote,
|
||||
media = replyMsg.Media.Select(rm => new { rm.Id, rm.Type, rm.Url }).ToList(),
|
||||
media = replyMsg.Media.Select(rm => new { rm.Type, rm.Url }).ToList(),
|
||||
Sender = replySenderInfo != null
|
||||
? new { Id = replySenderInfo.Id, Username = replySenderInfo.Username, DisplayName = replySenderInfo.DisplayName }
|
||||
: new { Id = replyMsg.SenderId, Username = "unknown", DisplayName = "Unknown" }
|
||||
@@ -70,34 +63,32 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем сообщение в "комнату" чата.
|
||||
await _hubContext.NotifyNewMessageAsync(notification.ChatId, new
|
||||
{
|
||||
id = message.Id,
|
||||
chatId = message.ChatId,
|
||||
senderId = message.SenderId,
|
||||
content = message.Content,
|
||||
type = message.Type,
|
||||
createdAt = message.CreatedAt,
|
||||
forwardedFromId = message.ForwardedFromId,
|
||||
forwardedFrom = forwardedFromObj,
|
||||
replyToId = message.ReplyToId,
|
||||
replyTo = replyToObj,
|
||||
quote = message.Quote,
|
||||
media = message.Media.Select(m => new
|
||||
{
|
||||
id = message.Id,
|
||||
chatId = message.ChatId,
|
||||
senderId = message.SenderId,
|
||||
content = message.Content,
|
||||
type = message.Type,
|
||||
createdAt = message.CreatedAt,
|
||||
forwardedFromId = message.ForwardedFromId,
|
||||
forwardedFrom = forwardedFromObj,
|
||||
replyToId = message.ReplyToId,
|
||||
replyTo = replyToObj,
|
||||
quote = message.Quote,
|
||||
media = message.Media.Select(m => new
|
||||
{
|
||||
id = m.Id,
|
||||
type = m.Type,
|
||||
url = m.Url,
|
||||
filename = m.Filename,
|
||||
size = m.Size
|
||||
}).ToList(),
|
||||
sender = senderObj,
|
||||
readBy = new List<object>(),
|
||||
storyId = message.StoryId,
|
||||
storyMediaUrl = message.StoryMediaUrl,
|
||||
storyMediaType = message.StoryMediaType
|
||||
}, cancellationToken);
|
||||
type = m.Type,
|
||||
url = m.Url,
|
||||
filename = m.FileId,
|
||||
size = m.Size
|
||||
}).ToList(),
|
||||
sender = senderObj,
|
||||
readBy = new List<object>(),
|
||||
storyId = message.StoryId,
|
||||
storyMediaUrl = message.StoryMediaUrl,
|
||||
storyMediaType = message.StoryMediaType
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-7
@@ -1,5 +1,6 @@
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using MongoDB.Driver;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Persistence;
|
||||
|
||||
@@ -14,7 +15,7 @@ public sealed class MessageReactionRepository : IMessageReactionRepository
|
||||
_reactions = mongoDatabase.GetCollection<MessageReaction>("message_reactions");
|
||||
_mediator = mediator;
|
||||
_messageRepository = messageRepository;
|
||||
|
||||
|
||||
// Ensure index for fast querying by message
|
||||
var indexKeysDefinition = Builders<MessageReaction>.IndexKeys.Ascending(r => r.MessageId);
|
||||
_reactions.Indexes.CreateOne(new CreateIndexModel<MessageReaction>(indexKeysDefinition));
|
||||
@@ -22,17 +23,14 @@ public sealed class MessageReactionRepository : IMessageReactionRepository
|
||||
|
||||
public async Task AddAsync(MessageReaction reaction, CancellationToken cancellationToken)
|
||||
{
|
||||
// Уникальный индекс или фильтр, чтобы не дублировать
|
||||
var filter = Builders<MessageReaction>.Filter.And(
|
||||
Builders<MessageReaction>.Filter.Eq(r => r.MessageId, reaction.MessageId),
|
||||
Builders<MessageReaction>.Filter.Eq(r => r.UserId, reaction.UserId),
|
||||
Builders<MessageReaction>.Filter.Eq(r => r.Emoji, reaction.Emoji)
|
||||
);
|
||||
|
||||
// Используем ReplaceOptions.IsUpsert = true для идемпотентности (нет гонок)
|
||||
|
||||
await _reactions.ReplaceOneAsync(filter, reaction, new ReplaceOptions { IsUpsert = true }, cancellationToken);
|
||||
|
||||
// Уведомляем систему (для Федерации)
|
||||
var message = await _messageRepository.GetByIdAsync(reaction.MessageId, cancellationToken);
|
||||
if (message != null)
|
||||
{
|
||||
@@ -50,7 +48,6 @@ public sealed class MessageReactionRepository : IMessageReactionRepository
|
||||
|
||||
await _reactions.DeleteOneAsync(filter, cancellationToken);
|
||||
|
||||
// Уведомляем систему (для Федерации)
|
||||
var message = await _messageRepository.GetByIdAsync(messageId, cancellationToken);
|
||||
if (message != null)
|
||||
{
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver.Linq;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using System.Text.RegularExpressions;
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Persistence;
|
||||
|
||||
public sealed class MessageRepository : IMessageRepository
|
||||
{
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
private readonly Knot.Modules.Messaging.Application.Abstractions.IChatAccessProvider _chatAccessProvider;
|
||||
private readonly IChatAccessProvider _chatAccessProvider;
|
||||
private readonly MediatR.IMediator _mediator;
|
||||
|
||||
public MessageRepository(IMongoDatabase mongoDatabase, Knot.Modules.Messaging.Application.Abstractions.IChatAccessProvider chatAccessProvider, MediatR.IMediator mediator)
|
||||
public MessageRepository(IMongoDatabase mongoDatabase, IChatAccessProvider chatAccessProvider, MediatR.IMediator mediator)
|
||||
{
|
||||
_messages = mongoDatabase.GetCollection<Message>("messages");
|
||||
_chatAccessProvider = chatAccessProvider;
|
||||
@@ -28,7 +29,7 @@ public sealed class MessageRepository : IMessageRepository
|
||||
// Publish domain events manualy for mongo entities
|
||||
var events = message.GetDomainEvents().ToList();
|
||||
message.ClearDomainEvents();
|
||||
|
||||
|
||||
// This runs synchronously or without waiting, better to run async but Add is void
|
||||
// In this implementation setting, fire and forget or wrap sync
|
||||
foreach (var domainEvent in events)
|
||||
@@ -65,7 +66,7 @@ public sealed class MessageRepository : IMessageRepository
|
||||
{
|
||||
var builder = Builders<Message>.Filter;
|
||||
var filter = builder.Eq(m => m.ChatId, chatId);
|
||||
|
||||
|
||||
if (cursor.HasValue)
|
||||
{
|
||||
filter &= builder.Lt(m => m.CreatedAt, cursor.Value);
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization;
|
||||
using MongoDB.Bson.Serialization.Serializers;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
|
||||
|
||||
@@ -15,7 +15,7 @@ public static class MongoDbMapConfigurator
|
||||
if (_initialized) return;
|
||||
|
||||
try { BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard)); } catch { /* Already registered */ }
|
||||
|
||||
|
||||
BsonSerializer.RegisterSerializer(new EnumSerializer<MessageState>(BsonType.Int32));
|
||||
BsonSerializer.RegisterSerializer(new EnumSerializer<MediaType>(BsonType.String));
|
||||
|
||||
@@ -45,7 +45,7 @@ public static class MongoDbMapConfigurator
|
||||
cm.AutoMap();
|
||||
cm.MapField("_media").SetElementName("Media");
|
||||
cm.SetDiscriminator("MediaMessage");
|
||||
cm.UnmapProperty(c => c.Caption); // Avoid DB duplication, Content is already saved
|
||||
cm.UnmapProperty(c => c.Caption);
|
||||
});
|
||||
|
||||
BsonClassMap.RegisterClassMap<StoryMessage>(cm =>
|
||||
@@ -69,7 +69,7 @@ public static class MongoDbMapConfigurator
|
||||
BsonClassMap.RegisterClassMap<PollOption>(cm => cm.AutoMap());
|
||||
BsonClassMap.RegisterClassMap<PollVote>(cm => cm.AutoMap());
|
||||
|
||||
|
||||
|
||||
BsonClassMap.RegisterClassMap<Media>(cm =>
|
||||
{
|
||||
cm.AutoMap();
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
|
||||
@@ -24,7 +24,6 @@ public sealed class UserStatsService : IUserStatsService
|
||||
var userGuidList = userIds.ToList();
|
||||
if (!userGuidList.Any()) return new Dictionary<Guid, UserStats>();
|
||||
|
||||
// Эффективная агрегация: считаем количество и сумму Media.Size
|
||||
var stats = await _messages.Aggregate()
|
||||
.Match(Builders<Message>.Filter.In(m => m.SenderId, userGuidList))
|
||||
.Group(new BsonDocument {
|
||||
@@ -67,9 +66,6 @@ public sealed class UserStatsService : IUserStatsService
|
||||
|
||||
public async Task<long> GetOrphanedMediaSizeAsync(HashSet<string> validFileIds, CancellationToken ct = default)
|
||||
{
|
||||
// Для больших объемов правильнее собирать список ВСЕХ URL файлов из сообщений,
|
||||
// но здесь мы оптимизируем через проекцию, чтобы вернуть только нужные поля.
|
||||
// Этот метод может быть реализован в BackgroundTask для очень больших баз.
|
||||
return 0; // Временная заглушка, реальный подсчет через курсор в DryRun
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user