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; /// /// Бан-гард авто-вступлений Discovery /// public sealed class DiscoveryBanGuard { private readonly IDiscoveryStore _store; private readonly ISettingsStore _settings; private readonly Func _utcNow; /// /// Создаёт бан-гард. /// /// Хранилище Discovery (счётчик DiscLog за сутки). /// KV-настройки тенанта (discJoinLimit/discFloodDay/discPaused). /// Источник текущего времени (UTC). По умолчанию — . public DiscoveryBanGuard( IDiscoveryStore store, ISettingsStore settings, Func? utcNow = null) { ArgumentNullException.ThrowIfNull(store); ArgumentNullException.ThrowIfNull(settings); _store = store; _settings = settings; _utcNow = utcNow ?? (() => DateTimeOffset.UtcNow); } /// /// Ручной стоп-кран авто-вступлений. /// /// True — вступления на паузе (воркер не делает сетевых шагов). public async Task GlobalPausedAsync(CancellationToken ct) { TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct); return settingsSnapshot.GetBool(SettingsKeys.DiscPaused, SettingsDefaults.DiscPaused); } /// /// Авто-вступления за текущие UTC-сутки. /// /// Число авто-вступлений сегодня (0 — вступлений нет). public Task JoinsTodayAutoAsync(CancellationToken ct) => _store.CountLogEventAsync(DiscoveryLogEvents.JoinAuto, StartOfDayUtc(), ct); /// /// Была ли flood-блокировка в текущие UTC-сутки. /// /// True — фиксировалась сегодня (стоп до конца суток). public async Task FloodTodayAsync(CancellationToken ct) { TenantSettingsSnapshot snapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct); return IsFloodToday(snapshot.GetLong(SettingsKeys.DiscFloodDay, 0), StartOfDayMs()); } /// /// Разрешено ли авто-вступление /// /// True — следующее авто-вступление допустимо. public async Task 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; } /// /// Фиксирует FloodWait /// 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; }