Files
Deal/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs
T

96 lines
4.4 KiB
C#

using System.Text.Json;
using Deal.Modules.Discovery.Application.Abstractions;
using Deal.Modules.Discovery.Application.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
namespace Deal.Modules.Discovery.Application.Services;
/// <summary>
/// Бан-гард авто-вступлений Discovery
/// </summary>
public sealed class DiscoveryBanGuard
{
private readonly IDiscoveryStore _store;
private readonly ISettingsStore _settings;
private readonly Func<DateTimeOffset> _utcNow;
/// <summary>
/// Создаёт бан-гард.
/// </summary>
/// <param name="store">Хранилище Discovery (счётчик DiscLog за сутки).</param>
/// <param name="settings">KV-настройки тенанта (discJoinLimit/discFloodDay/discPaused).</param>
/// <param name="utcNow">Источник текущего времени (UTC). По умолчанию — <see cref="DateTimeOffset.UtcNow"/>.</param>
public DiscoveryBanGuard(
IDiscoveryStore store,
ISettingsStore settings,
Func<DateTimeOffset>? utcNow = null)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(settings);
_store = store;
_settings = settings;
_utcNow = utcNow ?? (() => DateTimeOffset.UtcNow);
}
/// <summary>
/// Ручной стоп-кран авто-вступлений.
/// </summary>
/// <returns>True — вступления на паузе (воркер не делает сетевых шагов).</returns>
public async Task<bool> GlobalPausedAsync(CancellationToken ct)
{
TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
return settingsSnapshot.GetBool(SettingsKeys.DiscPaused, SettingsDefaults.DiscPaused);
}
/// <summary>
/// Авто-вступления за текущие UTC-сутки.
/// </summary>
/// <returns>Число авто-вступлений сегодня (0 — вступлений нет).</returns>
public Task<int> JoinsTodayAutoAsync(CancellationToken ct)
=> _store.CountLogEventAsync(DiscoveryLogEvents.JoinAuto, StartOfDayUtc(), ct);
/// <summary>
/// Была ли flood-блокировка в текущие UTC-сутки.
/// </summary>
/// <returns>True — <see cref="NoteFloodAsync"/> фиксировалась сегодня (стоп до конца суток).</returns>
public async Task<bool> FloodTodayAsync(CancellationToken ct)
{
TenantSettingsSnapshot snapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
return IsFloodToday(snapshot.GetLong(SettingsKeys.DiscFloodDay, 0), StartOfDayMs());
}
/// <summary>
/// Разрешено ли авто-вступление
/// </summary>
/// <returns>True — следующее авто-вступление допустимо.</returns>
public async Task<bool> CanAutoJoinAsync(CancellationToken ct)
{
// Один типизированный снимок настроек на вердикт (C30): discJoinLimit/discFloodDay/discPaused
// читаются одним GetAllAsync вместо трёх GetAsync.
TenantSettingsSnapshot snapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
int limit = snapshot.GetInt(SettingsKeys.DiscJoinLimit, SettingsDefaults.DiscJoinLimit);
int joined = await JoinsTodayAutoAsync(ct).ConfigureAwait(false);
bool floodToday = IsFloodToday(snapshot.GetLong(SettingsKeys.DiscFloodDay, 0), StartOfDayMs());
bool paused = snapshot.GetBool(SettingsKeys.DiscPaused, SettingsDefaults.DiscPaused);
return joined < Math.Max(1, limit) && !floodToday && !paused;
}
/// <summary>
/// Фиксирует FloodWait
/// </summary>
public Task NoteFloodAsync(CancellationToken ct)
=> _settings.SetAsync(SettingsKeys.DiscFloodDay, JsonSerializer.Serialize(StartOfDayMs()), ct);
private DateTimeOffset StartOfDayUtc()
{
DateTimeOffset now = _utcNow().ToUniversalTime();
return new DateTimeOffset(now.Year, now.Month, now.Day, 0, 0, 0, TimeSpan.Zero);
}
private long StartOfDayMs() => StartOfDayUtc().ToUnixTimeMilliseconds();
private static bool IsFloodToday(long floodDay, long startOfDayMs)
=> floodDay != 0 && floodDay == startOfDayMs;
}