daily_digest/Worker.cs
2025-04-12 00:27:03 +07:00

267 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}
}