SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/ Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue, контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер), Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог). Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0, тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
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; тесты передают фиксированные «часы», эталон FakeDiscoveryStore). По умолчанию — <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;
|
||||
}
|
||||
Reference in New Issue
Block a user