This commit is contained in:
Халимов Рустам
2026-03-29 14:26:14 +03:00
parent 9bae9752cc
commit 22bc964f27
29 changed files with 405 additions and 130 deletions

View File

@@ -14,9 +14,9 @@ public static class MongoDbMapConfigurator
{
if (_initialized) return;
BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));
try { BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard)); } catch { /* Already registered */ }
BsonSerializer.RegisterSerializer(new EnumSerializer<MessageState>(BsonType.String));
BsonSerializer.RegisterSerializer(new EnumSerializer<MessageState>(BsonType.Int32));
BsonSerializer.RegisterSerializer(new EnumSerializer<MediaType>(BsonType.String));
BsonClassMap.RegisterClassMap<Entity<Guid>>(cm =>

View File

@@ -0,0 +1,75 @@
using System;
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 MongoDB.Bson;
using MongoDB.Driver;
namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
public sealed class UserStatsService : IUserStatsService
{
private readonly IMongoCollection<Message> _messages;
public UserStatsService(IMongoDatabase database)
{
_messages = database.GetCollection<Message>("messages");
}
public async Task<Dictionary<Guid, UserStats>> GetStatsForUsersAsync(IEnumerable<Guid> userIds, CancellationToken ct = default)
{
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 {
{ "_id", "$SenderId" },
{ "Count", new BsonDocument("$sum", 1) },
{ "MediaSize", new BsonDocument("$sum", new BsonDocument("$sum", "$Media.Size")) }
})
.ToListAsync(ct);
return stats.ToDictionary(
doc => doc["_id"].AsGuid,
doc => new UserStats(
doc["Count"].AsInt32,
doc.Contains("MediaSize") && !doc["MediaSize"].IsBsonNull ? (long)(doc["MediaSize"].IsInt64 ? doc["MediaSize"].AsInt64 : doc["MediaSize"].AsInt32) : 0L
)
);
}
public async Task<long> GetTotalStorageSizeAsync(CancellationToken ct = default)
{
var result = await _messages.Aggregate()
.Group(new BsonDocument {
{ "_id", BsonNull.Value },
{ "TotalSize", new BsonDocument("$sum", new BsonDocument("$sum", "$Media.Size")) }
})
.FirstOrDefaultAsync(ct);
if (result == null) return 0;
return result.Contains("TotalSize") ? (long)(result["TotalSize"].IsInt64 ? result["TotalSize"].AsInt64 : result["TotalSize"].AsInt32) : 0L;
}
public async Task<int> GetCountOrphanedMessagesAsync(HashSet<Guid> activeChatIds, CancellationToken ct = default)
{
var filter = Builders<Message>.Filter.Or(
Builders<Message>.Filter.BitsAnySet(m => m.State, (long)MessageState.IsDeleted),
Builders<Message>.Filter.Nin(m => m.ChatId, activeChatIds)
);
return (int)(await _messages.CountDocumentsAsync(filter, cancellationToken: ct));
}
public async Task<long> GetOrphanedMediaSizeAsync(HashSet<string> validFileIds, CancellationToken ct = default)
{
// Для больших объемов правильнее собирать список ВСЕХ URL файлов из сообщений,
// но здесь мы оптимизируем через проекцию, чтобы вернуть только нужные поля.
// Этот метод может быть реализован в BackgroundTask для очень больших баз.
return 0; // Временная заглушка, реальный подсчет через курсор в DryRun
}
}