using System; using System.Collections.Generic; using System.Linq; namespace Knot.Modules.Messaging.Domain; public class PollMessage : Message { public override string Type => "poll"; public override string? Content { get; protected set; } // Вопрос опроса public bool IsAnonymous { get; protected set; } public bool AllowMultipleAnswers { get; protected set; } public bool IsClosed { get; protected set; } public DateTime? ExpiresAt { get; protected set; } private List _options = new(); public IReadOnlyCollection Options => _options.AsReadOnly(); private List _votes = new(); public IReadOnlyCollection Votes => _votes.AsReadOnly(); // Инфраструктурный конструктор private PollMessage() : base() { } public PollMessage( Guid id, Guid chatId, Guid senderId, string question, List options, bool isAnonymous, bool allowMultipleAnswers, DateTime? expiresAt, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported) : base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) { Content = question; IsAnonymous = isAnonymous; AllowMultipleAnswers = allowMultipleAnswers; ExpiresAt = expiresAt; IsClosed = false; foreach (var optionText in options) { _options.Add(new PollOption(Guid.NewGuid(), Id, optionText)); } if (!isImported) { // Можно добавить специфичное событие для опроса // RaiseDomainEvent(new PollCreatedDomainEvent(Id, ChatId, SenderId, question)); } } public void Vote(Guid userId, List optionIds) { if (IsClosed) throw new InvalidOperationException("Poll is closed"); if (ExpiresAt.HasValue && DateTime.UtcNow > ExpiresAt.Value) { Close(); throw new InvalidOperationException("Poll has expired"); } if (optionIds == null || !optionIds.Any()) throw new ArgumentException("At least one option must be selected"); if (!AllowMultipleAnswers && optionIds.Count > 1) throw new ArgumentException("Multiple answers are not allowed"); // Проверяем существование опций foreach (var oid in optionIds) { if (!_options.Any(o => o.Id == oid)) throw new ArgumentException($"Option {oid} not found"); } // Удаляем старые голоса пользователя _votes.RemoveAll(v => v.UserId == userId); // Добавляем новые голоса foreach (var oid in optionIds) { _votes.Add(new PollVote(Id, userId, oid, DateTime.UtcNow)); } // Обновляем статистику в опциях RecalculateResults(); } public void Close() { IsClosed = true; } private void RecalculateResults() { int totalVotesCount = _votes.Select(v => v.UserId).Distinct().Count(); foreach (var option in _options) { int optionVotes = _votes.Count(v => v.OptionId == option.Id); double percentage = totalVotesCount > 0 ? (double)optionVotes / totalVotesCount * 100 : 0; option.UpdateResults(optionVotes, (int)Math.Round(percentage)); } } public override void Delete() { // При удалении опроса можно очистить голоса или оставить для истории (зависит от политики) base.Delete(); } } public class PollOption { public Guid Id { get; private set; } public Guid PollId { get; private set; } public string Text { get; private set; } // Денормализованные данные для быстрой отдачи public int VotesCount { get; private set; } public int Percentage { get; private set; } private PollOption() { } // EF public PollOption(Guid id, Guid pollId, string text) { Id = id; PollId = pollId; Text = text; } public void UpdateResults(int count, int percentage) { VotesCount = count; Percentage = percentage; } } public class PollVote { public Guid PollId { get; private set; } public Guid UserId { get; private set; } public Guid OptionId { get; private set; } public DateTime VotedAt { get; private set; } private PollVote() { } // EF public PollVote(Guid pollId, Guid userId, Guid optionId, DateTime votedAt) { PollId = pollId; UserId = userId; OptionId = optionId; VotedAt = votedAt; } }