Files
forkmessager/backend/src/Modules/Messaging/Domain/PollMessage.cs

157 lines
4.8 KiB
C#

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<PollOption> _options = new();
public IReadOnlyCollection<PollOption> Options => _options.AsReadOnly();
private List<PollVote> _votes = new();
public IReadOnlyCollection<PollVote> Votes => _votes.AsReadOnly();
// Инфраструктурный конструктор
private PollMessage() : base() { }
public PollMessage(
Guid id,
Guid chatId,
Guid senderId,
string question,
List<string> 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<Guid> 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;
}
}