267 lines
17 KiB
C#
267 lines
17 KiB
C#
![]() |
using DailyDigestWorker.Configuration; // Наша конфигурация
|
|||
|
using DailyDigestWorker.Services; // Наши сервисы
|
|||
|
using Microsoft.Extensions.Options; // Для IOptions
|
|||
|
using System.Globalization; // Для TimeSpan.ParseExact и других настроек культуры
|
|||
|
using Telegram.Bot.Types.Enums; // Для ParseMode
|
|||
|
|
|||
|
namespace DailyDigestWorker
|
|||
|
{
|
|||
|
public class Worker : BackgroundService
|
|||
|
{
|
|||
|
private readonly ILogger<Worker> _logger;
|
|||
|
// Храним список времен запуска как TimeSpan
|
|||
|
private readonly List<TimeSpan> _runTimes;
|
|||
|
private readonly IServiceProvider _serviceProvider;
|
|||
|
|
|||
|
// Внедряем зависимости через конструктор
|
|||
|
public Worker(ILogger<Worker> logger,
|
|||
|
IOptions<SchedulingSettings> schedulingSettings,
|
|||
|
IServiceProvider serviceProvider)
|
|||
|
{
|
|||
|
_logger = logger;
|
|||
|
_serviceProvider = serviceProvider;
|
|||
|
_runTimes = new List<TimeSpan>();
|
|||
|
|
|||
|
// Проверяем наличие Value и списка перед использованием
|
|||
|
var timeStrings = schedulingSettings.Value?.RunAtTimesLocal; // Теперь может быть null
|
|||
|
|
|||
|
if (timeStrings != null) // Обрабатываем только если список задан в конфиге
|
|||
|
{
|
|||
|
foreach (var timeStr in timeStrings)
|
|||
|
{
|
|||
|
if (TimeSpan.TryParseExact(timeStr?.Trim(), @"hh\:mm\:ss", CultureInfo.InvariantCulture, out var timeSpan))
|
|||
|
{
|
|||
|
_runTimes.Add(timeSpan);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_logger.LogWarning("Не удалось распознать время '{TimeString}' в настройках Scheduling:RunAtTimesLocal. Используйте формат HH:mm:ss.", timeStr);
|
|||
|
}
|
|||
|
}
|
|||
|
_runTimes = _runTimes.Distinct().OrderBy(t => t).ToList();
|
|||
|
}
|
|||
|
|
|||
|
// Логика обработки пустого или отсутствующего списка остается прежней
|
|||
|
if (!_runTimes.Any())
|
|||
|
{
|
|||
|
_logger.LogError("В конфигурации не указано ни одного корректного времени запуска! Worker не сможет автоматически запускаться.");
|
|||
|
// Важно: убедитесь, что здесь НЕТ строки, добавляющей 8 утра по умолчанию,
|
|||
|
// если вы хотите, чтобы бот НЕ работал без явной конфигурации времени.
|
|||
|
}
|
|||
|
|
|||
|
_logger.LogInformation("Worker инициализирован. Дайджест будет запускаться ежедневно в: {RunTimes}",
|
|||
|
_runTimes.Any() ? string.Join(", ", _runTimes.Select(t => t.ToString(@"hh\:mm\:ss"))) : "НЕТ ВРЕМЕН ЗАПУСКА");
|
|||
|
}
|
|||
|
|
|||
|
// Основной метод, который выполняется в фоне
|
|||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|||
|
{
|
|||
|
_logger.LogInformation("Worker запущен в: {time}", DateTimeOffset.Now);
|
|||
|
|
|||
|
// Проверяем наличие времен запуска перед стартом цикла
|
|||
|
if (!_runTimes.Any())
|
|||
|
{
|
|||
|
_logger.LogError("Нет сконфигурированных времен запуска. Worker завершает работу.");
|
|||
|
return; // Останавливаем выполнение, если нет времен
|
|||
|
}
|
|||
|
|
|||
|
while (!stoppingToken.IsCancellationRequested)
|
|||
|
{
|
|||
|
// Рассчитываем задержку до ближайшего времени запуска
|
|||
|
TimeSpan delay = CalculateDelayUntilNearestRun(_runTimes);
|
|||
|
|
|||
|
_logger.LogInformation("Следующий запуск дайджеста запланирован через: {Delay} (в {TargetTime})",
|
|||
|
delay, DateTime.Now.Add(delay).ToString("yyyy-MM-dd HH:mm:ss"));
|
|||
|
|
|||
|
try
|
|||
|
{
|
|||
|
// Ожидаем до следующего времени запуска, прерывая ожидание, если придет сигнал остановки
|
|||
|
// Если задержка 0 или меньше (опоздали), Task.Delay корректно обработает это и не будет ждать.
|
|||
|
await Task.Delay(delay, stoppingToken);
|
|||
|
}
|
|||
|
catch (OperationCanceledException)
|
|||
|
{
|
|||
|
_logger.LogInformation("Запрос на остановку получен во время ожидания.");
|
|||
|
break; // Выходим из цикла while
|
|||
|
}
|
|||
|
|
|||
|
// Проверяем еще раз перед выполнением задачи, на случай если остановка пришла точно в момент срабатывания
|
|||
|
if (stoppingToken.IsCancellationRequested) break;
|
|||
|
|
|||
|
// --- Выполнение основной задачи ---
|
|||
|
_logger.LogInformation("Запуск процесса создания дайджеста в: {time}", DateTimeOffset.Now);
|
|||
|
using (var scope = _serviceProvider.CreateScope())
|
|||
|
{
|
|||
|
// --- Получаем ВСЕ необходимые сервисы в начале scope ---
|
|||
|
var currencyService = scope.ServiceProvider.GetRequiredService<ICurrencyService>();
|
|||
|
var cryptoService = scope.ServiceProvider.GetRequiredService<ICryptoService>();
|
|||
|
var weatherService = scope.ServiceProvider.GetRequiredService<IWeatherService>();
|
|||
|
var digestBuilder = scope.ServiceProvider.GetRequiredService<IDigestBuilderService>();
|
|||
|
var telegramSender = scope.ServiceProvider.GetRequiredService<ITelegramBotSender>();
|
|||
|
var newsReaderService = scope.ServiceProvider.GetRequiredService<ITelegramChannelReader>();
|
|||
|
var summarizationService = scope.ServiceProvider.GetRequiredService<ISummarizationService>();
|
|||
|
|
|||
|
// Константы для чтения новостей (можно вынести в конфиг)
|
|||
|
const int NewsMaxAgeHours = 24;
|
|||
|
const int NewsFetchLimit = 50; // Сколько последних сообщений проверить
|
|||
|
|
|||
|
string? newsSummary = null; // Инициализируем переменную для саммари
|
|||
|
|
|||
|
try
|
|||
|
{
|
|||
|
// --- 1. Сбор данных ---
|
|||
|
_logger.LogInformation("Начало сбора данных для дайджеста...");
|
|||
|
var currencyTask = currencyService.GetCurrencyRatesAsync(stoppingToken);
|
|||
|
var cryptoTask = cryptoService.GetCryptoRatesAsync(stoppingToken);
|
|||
|
var weatherTask = weatherService.GetWeatherAsync(stoppingToken);
|
|||
|
var newsTask = newsReaderService.GetRecentNewsAsync(NewsMaxAgeHours, NewsFetchLimit, stoppingToken);
|
|||
|
|
|||
|
await Task.WhenAll(currencyTask, cryptoTask, weatherTask, newsTask);
|
|||
|
|
|||
|
var currencyData = await currencyTask;
|
|||
|
var cryptoData = await cryptoTask;
|
|||
|
var weatherData = await weatherTask;
|
|||
|
var rawNewsTexts = await newsTask;
|
|||
|
|
|||
|
_logger.LogInformation("Сбор всех базовых данных завершен.");
|
|||
|
|
|||
|
// --- 1.5 Генерация саммари новостей (если есть новости) ---
|
|||
|
if (rawNewsTexts != null && rawNewsTexts.Any())
|
|||
|
{
|
|||
|
_logger.LogInformation("Получено {NewsCount} текстов новостей. Запуск суммаризации...", rawNewsTexts.Count);
|
|||
|
try
|
|||
|
{
|
|||
|
string combinedNewsText = string.Join("\n\n---\n\n", rawNewsTexts);
|
|||
|
newsSummary = await summarizationService.SummarizeTextAsync(combinedNewsText, stoppingToken);
|
|||
|
|
|||
|
if (newsSummary != null)
|
|||
|
{
|
|||
|
_logger.LogInformation("Саммари новостей успешно получено.");
|
|||
|
_logger.LogDebug("Текст саммари:\n{NewsSummary}", newsSummary);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_logger.LogWarning("Сервис суммаризации вернул null.");
|
|||
|
}
|
|||
|
}
|
|||
|
catch (OperationCanceledException)
|
|||
|
{
|
|||
|
_logger.LogInformation("Операция суммаризации была отменена.");
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
_logger.LogError(ex, "Ошибка во время вызова сервиса суммаризации.");
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_logger.LogInformation("Нет текстов новостей для суммаризации.");
|
|||
|
}
|
|||
|
|
|||
|
// --- 2. Формирование текста дайджеста ---
|
|||
|
_logger.LogInformation("Формирование текста дайджеста...");
|
|||
|
string digestText = digestBuilder.BuildDigestText(currencyData, cryptoData, weatherData, newsSummary);
|
|||
|
_logger.LogDebug("Сформированный текст получен от DigestBuilder.");
|
|||
|
|
|||
|
// --- 3. Отправка дайджеста ---
|
|||
|
if (!string.IsNullOrWhiteSpace(digestText))
|
|||
|
{
|
|||
|
_logger.LogInformation("Отправка дайджеста...");
|
|||
|
bool sent = await telegramSender.SendMessageAsync(digestText, ParseMode.Markdown, stoppingToken);
|
|||
|
if (!sent)
|
|||
|
{
|
|||
|
_logger.LogWarning("Не удалось инициировать отправку дайджеста.");
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_logger.LogWarning("Текст дайджеста пуст, отправка не производится.");
|
|||
|
}
|
|||
|
}
|
|||
|
catch (OperationCanceledException)
|
|||
|
{
|
|||
|
_logger.LogInformation("Запрос на остановку получен во время выполнения основной задачи.");
|
|||
|
break; // Выходим из while
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{ // Ловим ошибки на уровне всего цикла задачи
|
|||
|
_logger.LogError(ex, "Произошла КРИТИЧЕСКАЯ ошибка во время выполнения задачи создания дайджеста.");
|
|||
|
try
|
|||
|
{
|
|||
|
await telegramSender.SendErrorMessageAsync($"Критическая ошибка в Worker: {ex.Message}", CancellationToken.None);
|
|||
|
}
|
|||
|
catch (Exception sendEx)
|
|||
|
{
|
|||
|
_logger.LogError(sendEx, "Не удалось отправить сообщение о КРИТИЧЕСКОЙ ошибке в Telegram.");
|
|||
|
}
|
|||
|
// Не добавляем здесь Delay, чтобы не задерживать следующий цикл после критической ошибки.
|
|||
|
// Worker перейдет к расчету следующего запуска.
|
|||
|
}
|
|||
|
} // Конец scope
|
|||
|
|
|||
|
_logger.LogInformation("Процесс создания дайджеста завершен. Расчет следующего запуска.");
|
|||
|
|
|||
|
// Небольшая пауза (~1 сек) после выполнения, чтобы гарантированно перейти на следующую секунду
|
|||
|
// и избежать повторного срабатывания в ту же минуту, если расчет времени даст очень маленькую задержку.
|
|||
|
try
|
|||
|
{
|
|||
|
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
|
|||
|
}
|
|||
|
catch (OperationCanceledException)
|
|||
|
{
|
|||
|
_logger.LogInformation("Запрос на остановку получен во время короткой паузы после выполнения.");
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
} // Конец while
|
|||
|
|
|||
|
_logger.LogInformation("Worker останавливается.");
|
|||
|
}
|
|||
|
|
|||
|
// Метод для расчета задержки до ближайшего времени запуска из списка
|
|||
|
private TimeSpan CalculateDelayUntilNearestRun(List<TimeSpan> runTimes)
|
|||
|
{
|
|||
|
var now = DateTime.Now;
|
|||
|
var today = now.Date;
|
|||
|
TimeSpan currentTimeOfDay = now.TimeOfDay;
|
|||
|
|
|||
|
// Ищем ближайшее время запуска СЕГОДНЯ, которое еще не прошло (или равно текущему времени с точностью до секунды)
|
|||
|
// Добавляем небольшую дельту (например, 1 секунду) к currentTimeOfDay, чтобы не пропустить запуск,
|
|||
|
// который должен был произойти точно в текущую секунду.
|
|||
|
TimeSpan? nextRunTimeToday = runTimes
|
|||
|
.Where(t => t >= currentTimeOfDay)
|
|||
|
.Cast<TimeSpan?>()
|
|||
|
.FirstOrDefault(); // Список уже отсортирован, берем первое подходящее
|
|||
|
|
|||
|
DateTime nextRunDateTime;
|
|||
|
|
|||
|
if (nextRunTimeToday.HasValue)
|
|||
|
{
|
|||
|
// Если нашли время сегодня, планируем на сегодня
|
|||
|
nextRunDateTime = today + nextRunTimeToday.Value;
|
|||
|
_logger.LogDebug("Ближайшее время запуска сегодня: {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunDateTime);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Если сегодня подходящего времени нет, берем самое раннее время из списка и планируем на ЗАВТРА
|
|||
|
// runTimes гарантированно не пустой и отсортирован (проверено в конструкторе)
|
|||
|
var earliestTimeTomorrow = runTimes.First();
|
|||
|
nextRunDateTime = today.AddDays(1) + earliestTimeTomorrow;
|
|||
|
_logger.LogDebug("Все времена запуска сегодня прошли. Ближайшее время завтра: {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunDateTime);
|
|||
|
}
|
|||
|
|
|||
|
var delay = nextRunDateTime - now;
|
|||
|
|
|||
|
// Если задержка отрицательная или очень маленькая (меньше ~0), возвращаем Zero.
|
|||
|
return delay < TimeSpan.Zero ? TimeSpan.Zero : delay;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
public override Task StopAsync(CancellationToken cancellationToken)
|
|||
|
{
|
|||
|
_logger.LogInformation("Worker StopAsync вызван.");
|
|||
|
// Можно добавить код для корректного завершения (например, Logout для WTelegramClient)
|
|||
|
return base.StopAsync(cancellationToken);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|