feat: add session capacity waitlist
This commit is contained in:
@@ -2,7 +2,6 @@ using Dapper;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Rendering;
|
||||
|
||||
@@ -18,7 +17,7 @@ public sealed record JoinSessionCommand(
|
||||
int MessageId);
|
||||
|
||||
// DTOs for AOT compilation
|
||||
internal sealed record JoinSessionBatchDto(Guid BatchId, string Title);
|
||||
internal sealed record JoinSessionBatchDto(Guid BatchId, string Title, int? MaxPlayers);
|
||||
|
||||
public sealed class JoinSessionHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
@@ -29,6 +28,7 @@ public sealed class JoinSessionHandler(
|
||||
{
|
||||
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
||||
await using var transaction = await connection.BeginTransactionAsync(ct);
|
||||
var transactionCommitted = false;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -41,12 +41,68 @@ public sealed class JoinSessionHandler(
|
||||
new { TgId = command.TelegramUserId, Name = command.DisplayName, Username = command.TelegramUsername },
|
||||
transaction);
|
||||
|
||||
// 2. Добавляем в участники сессии (статус Pending, так как за 24 часа нужно будет финальное подтверждение)
|
||||
// 2. Блокируем сессию на время расчета мест, чтобы параллельные нажатия не переполнили состав.
|
||||
var batchInfo = await connection.QuerySingleOrDefaultAsync<JoinSessionBatchDto>(
|
||||
@"SELECT batch_id as BatchId, title as Title, max_players as MaxPlayers
|
||||
FROM sessions
|
||||
WHERE id = @SessionId
|
||||
FOR UPDATE",
|
||||
new { command.SessionId },
|
||||
transaction);
|
||||
|
||||
if (batchInfo is null)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var existingRegistrationStatus = await connection.ExecuteScalarAsync<string?>(
|
||||
"""
|
||||
SELECT sp.registration_status
|
||||
FROM session_participants sp
|
||||
WHERE sp.session_id = @SessionId
|
||||
AND sp.player_id = @PlayerId
|
||||
AND sp.is_gm = false
|
||||
""",
|
||||
new { command.SessionId, PlayerId = playerId },
|
||||
transaction);
|
||||
|
||||
if (existingRegistrationStatus is not null)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
var alreadyText = existingRegistrationStatus == ParticipantRegistrationStatus.Waitlisted
|
||||
? "Вы уже в листе ожидания!"
|
||||
: "Вы уже записаны!";
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, alreadyText, cancellationToken: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var activeParticipants = await connection.ExecuteScalarAsync<int>(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM session_participants
|
||||
WHERE session_id = @SessionId
|
||||
AND is_gm = false
|
||||
AND registration_status = @Active
|
||||
""",
|
||||
new { command.SessionId, Active = ParticipantRegistrationStatus.Active },
|
||||
transaction);
|
||||
|
||||
var registrationStatus = SessionCapacityRules.DecideJoinStatus(batchInfo.MaxPlayers, activeParticipants);
|
||||
|
||||
// 3. Добавляем в основной состав или лист ожидания. RSVP остается Pending до финального подтверждения.
|
||||
var inserted = await connection.ExecuteAsync(
|
||||
@"INSERT INTO session_participants (session_id, player_id, is_gm, rsvp_status)
|
||||
VALUES (@SessionId, @PlayerId, false, 'Pending')
|
||||
@"INSERT INTO session_participants (session_id, player_id, is_gm, rsvp_status, registration_status)
|
||||
VALUES (@SessionId, @PlayerId, false, @Pending, @RegistrationStatus)
|
||||
ON CONFLICT (session_id, player_id) DO NOTHING;",
|
||||
new { SessionId = command.SessionId, PlayerId = playerId },
|
||||
new
|
||||
{
|
||||
command.SessionId,
|
||||
PlayerId = playerId,
|
||||
Pending = RsvpStatus.Pending,
|
||||
RegistrationStatus = registrationStatus
|
||||
},
|
||||
transaction);
|
||||
|
||||
if (inserted == 0)
|
||||
@@ -56,26 +112,28 @@ public sealed class JoinSessionHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Получаем batch_id по session_id
|
||||
var batchInfo = await connection.QuerySingleAsync<JoinSessionBatchDto>(
|
||||
@"SELECT batch_id as BatchId, title as Title FROM sessions WHERE id = @SessionId",
|
||||
new { command.SessionId }, transaction);
|
||||
|
||||
// Загружаем весь батч для перерисовки
|
||||
var batchSessions = await connection.QueryAsync<SessionBatchDto>(
|
||||
@"SELECT id as SessionId, scheduled_at as ScheduledAt, status as Status FROM sessions WHERE batch_id = @BatchId ORDER BY scheduled_at",
|
||||
@"SELECT id as SessionId, scheduled_at as ScheduledAt, status as Status, max_players as MaxPlayers
|
||||
FROM sessions
|
||||
WHERE batch_id = @BatchId
|
||||
ORDER BY scheduled_at",
|
||||
new { BatchId = batchInfo.BatchId }, transaction);
|
||||
|
||||
var batchParticipants = await connection.QueryAsync<ParticipantBatchDto>(
|
||||
@"SELECT sp.session_id as SessionId, p.display_name as DisplayName, p.telegram_username as TelegramUsername
|
||||
@"SELECT sp.session_id as SessionId,
|
||||
p.display_name as DisplayName,
|
||||
p.telegram_username as TelegramUsername,
|
||||
sp.registration_status as RegistrationStatus
|
||||
FROM session_participants sp
|
||||
JOIN players p ON sp.player_id = p.id
|
||||
JOIN sessions s ON sp.session_id = s.id
|
||||
WHERE s.batch_id = @BatchId AND sp.is_gm = false
|
||||
ORDER BY sp.responded_at ASC, p.created_at ASC",
|
||||
ORDER BY sp.registration_status ASC, sp.created_at ASC, sp.responded_at ASC, p.created_at ASC",
|
||||
new { BatchId = batchInfo.BatchId }, transaction);
|
||||
|
||||
await transaction.CommitAsync(ct);
|
||||
transactionCommitted = true;
|
||||
|
||||
// 4. Перерисовываем сообщение
|
||||
var renderResult = SessionBatchRenderer.Render(batchInfo.Title, batchSessions.ToList(), batchParticipants.ToList());
|
||||
@@ -88,13 +146,23 @@ public sealed class JoinSessionHandler(
|
||||
replyMarkup: renderResult.Markup,
|
||||
cancellationToken: ct);
|
||||
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Вы успешно записаны!", cancellationToken: ct);
|
||||
var callbackText = registrationStatus == ParticipantRegistrationStatus.Waitlisted
|
||||
? "Основной состав заполнен. Вы добавлены в лист ожидания."
|
||||
: "Вы успешно записаны!";
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, callbackText, cancellationToken: ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка при добавлении игрока к сессии");
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Произошла ошибка при регистрации.", cancellationToken: ct);
|
||||
if (!transactionCommitted)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
}
|
||||
|
||||
var errorText = transactionCommitted
|
||||
? "Регистрация сохранена, но не удалось обновить сообщение расписания."
|
||||
: "Произошла ошибка при регистрации.";
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, errorText, cancellationToken: ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user