96 lines
4.4 KiB
C#
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;
|
|
}
|