2025-04-12 00:27:03 +07:00
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 ;
2025-04-12 01:35:29 +07:00
// Список времен запуска в формате TimeSpan (представляют ЛОКАЛЬНОЕ время ЦЕЛЕВОГО часового пояса)
private readonly List < TimeSpan > _runTimesLocal ;
private readonly TimeZoneInfo _targetTimeZone ; // Целевой часовой пояс пользователя
2025-04-12 00:27:03 +07:00
private readonly IServiceProvider _serviceProvider ;
public Worker ( ILogger < Worker > logger ,
2025-04-12 01:35:29 +07:00
IOptions < SchedulingSettings > schedulingSettings , // Используем обновленный SchedulingSettings
2025-04-12 00:27:03 +07:00
IServiceProvider serviceProvider )
{
_logger = logger ;
_serviceProvider = serviceProvider ;
2025-04-12 01:35:29 +07:00
_runTimesLocal = new List < TimeSpan > ( ) ; // Инициализируем список
2025-04-12 00:27:03 +07:00
2025-04-12 01:35:29 +07:00
// Получаем настройки планировщика (или используем дефолтные, если секция отсутствует)
var settings = schedulingSettings . Value ? ? new SchedulingSettings ( ) ;
var timeStrings = settings . RunAtTimesLocal ; // Список строк времени
string targetTimeZoneId = settings . TargetTimeZoneId ; // ID часового пояса
2025-04-12 00:27:03 +07:00
2025-04-12 01:35:29 +07:00
// 1. Определяем целевой часовой пояс
try
{
_targetTimeZone = TimeZoneInfo . FindSystemTimeZoneById ( targetTimeZoneId ) ;
_logger . LogInformation ( "Целевой часовой пояс установлен: {TimeZoneId} ({DisplayName})" , _targetTimeZone . Id , _targetTimeZone . DisplayName ) ;
}
catch ( TimeZoneNotFoundException )
{
_logger . LogError ( "Часовой пояс с ID '{TimeZoneId}' не найден в системе! Будет использовано локальное время сервера (TimeZoneInfo.Local)." , targetTimeZoneId ) ;
_targetTimeZone = TimeZoneInfo . Local ; // Откат к локальному времени сервера
}
catch ( InvalidTimeZoneException ex )
{
_logger . LogError ( ex , "Ошибка при загрузке данных часового пояса '{TimeZoneId}'. Будет использовано локальное время сервера." , targetTimeZoneId ) ;
_targetTimeZone = TimeZoneInfo . Local ;
}
catch ( Exception ex ) // Ловим другие возможные ошибки при работе с TimeZoneInfo
{
_logger . LogError ( ex , "Неожиданная ошибка при определении часового пояса '{TimeZoneId}'. Будет использовано локальное время сервера." , targetTimeZoneId ) ;
_targetTimeZone = TimeZoneInfo . Local ;
}
// 2. Парсим В Р Е М Е Н А ЗАПУСКА (они всегда в целевом часовом поясе)
if ( timeStrings ! = null )
2025-04-12 00:27:03 +07:00
{
foreach ( var timeStr in timeStrings )
{
2025-04-12 01:35:29 +07:00
// Используем ParseExact для строгого формата "HH:mm:ss"
2025-04-12 00:27:03 +07:00
if ( TimeSpan . TryParseExact ( timeStr ? . Trim ( ) , @"hh\:mm\:ss" , CultureInfo . InvariantCulture , out var timeSpan ) )
{
2025-04-12 01:35:29 +07:00
_runTimesLocal . Add ( timeSpan ) ;
2025-04-12 00:27:03 +07:00
}
else
{
_logger . LogWarning ( "Н е удалось распознать время '{TimeString}' в настройках Scheduling:RunAtTimesLocal. Используйте формат HH:mm:ss." , timeStr ) ;
}
}
2025-04-12 01:35:29 +07:00
// Убираем дубликаты и сортируем
_runTimesLocal = _runTimesLocal . Distinct ( ) . OrderBy ( t = > t ) . ToList ( ) ;
2025-04-12 00:27:03 +07:00
}
2025-04-12 01:35:29 +07:00
// 3. Проверка и логирование времен запуска
if ( ! _runTimesLocal . Any ( ) )
2025-04-12 00:27:03 +07:00
{
2025-04-12 01:35:29 +07:00
_logger . LogError ( "В конфигурации не указано ни одного корректного времени запуска (RunAtTimesLocal)! Worker не сможет автоматически запускаться." ) ;
// Здесь Н Е Т добавления времени по умолчанию, чтобы требовать явную конфигурацию
2025-04-12 00:27:03 +07:00
}
2025-04-12 01:35:29 +07:00
_logger . LogInformation ( "Worker инициализирован. Дайджест будет запускаться ежедневно в (по времени {TargetTimeZoneId}): {RunTimes}" ,
_targetTimeZone . Id ,
_runTimesLocal . Any ( ) ? string . Join ( ", " , _runTimesLocal . Select ( t = > t . ToString ( @"hh\:mm\:ss" ) ) ) : "Н Е Т В Р Е М Е Н ЗАПУСКА" ) ;
2025-04-12 00:27:03 +07:00
}
// Основной метод, который выполняется в фоне
protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
{
2025-04-12 01:35:29 +07:00
_logger . LogInformation ( "Worker запущен в: {time} UTC" , DateTimeOffset . UtcNow ) ; // Логгируем UTC время запуска
2025-04-12 00:27:03 +07:00
2025-04-12 01:35:29 +07:00
if ( ! _runTimesLocal . Any ( ) )
2025-04-12 00:27:03 +07:00
{
_logger . LogError ( "Нет сконфигурированных времен запуска. Worker завершает работу." ) ;
return ; // Останавливаем выполнение, если нет времен
}
while ( ! stoppingToken . IsCancellationRequested )
{
2025-04-12 01:35:29 +07:00
// Рассчитываем задержку до ближайшего времени запуска в целевом часовом поясе
TimeSpan delay = CalculateDelayUntilNearestRunInTargetZone ( _runTimesLocal , _targetTimeZone ) ;
2025-04-12 00:27:03 +07:00
2025-04-12 01:35:29 +07:00
// Логируем время следующего запуска в UTC и в целевой зоне
var nextRunUtc = DateTime . UtcNow . Add ( delay ) ;
var nextRunTargetZone = TimeZoneInfo . ConvertTimeFromUtc ( nextRunUtc , _targetTimeZone ) ;
_logger . LogInformation ( "Следующий запуск дайджеста запланирован через: {Delay} (в {TargetTimeLocal} {TargetZoneId})" ,
delay , nextRunTargetZone . ToString ( "yyyy-MM-dd HH:mm:ss" ) , _targetTimeZone . Id ) ;
2025-04-12 00:27:03 +07:00
try
{
2025-04-12 01:35:29 +07:00
// Ожидаем до следующего времени запуска
2025-04-12 00:27:03 +07:00
await Task . Delay ( delay , stoppingToken ) ;
}
catch ( OperationCanceledException )
{
_logger . LogInformation ( "Запрос на остановку получен во время ожидания." ) ;
break ; // Выходим из цикла while
}
2025-04-12 01:35:29 +07:00
if ( stoppingToken . IsCancellationRequested ) break ; // Проверяем еще раз
2025-04-12 00:27:03 +07:00
// --- Выполнение основной задачи ---
2025-04-12 01:35:29 +07:00
_logger . LogInformation ( "Запуск процесса создания дайджеста в: {time} UTC" , DateTimeOffset . UtcNow ) ;
2025-04-12 00:27:03 +07:00
using ( var scope = _serviceProvider . CreateScope ( ) )
{
2025-04-12 01:35:29 +07:00
// --- Получаем сервисы ---
2025-04-12 00:27:03 +07:00
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 ;
2025-04-12 01:35:29 +07:00
const int NewsFetchLimit = 50 ;
string? newsSummary = null ;
2025-04-12 00:27:03 +07:00
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 ( "С б о р всех базовых данных завершен." ) ;
2025-04-12 01:35:29 +07:00
// --- 1.5 Генерация саммари ---
2025-04-12 00:27:03 +07:00
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." ) ;
}
}
2025-04-12 01:35:29 +07:00
catch ( OperationCanceledException ) { _logger . LogInformation ( "Операция суммаризации была отменена." ) ; }
catch ( Exception ex ) { _logger . LogError ( ex , "Ошибка во время вызова сервиса суммаризации." ) ; }
2025-04-12 00:27:03 +07:00
}
else
{
_logger . LogInformation ( "Нет текстов новостей для суммаризации." ) ;
}
2025-04-12 01:35:29 +07:00
// --- 2. Формирование текста ---
2025-04-12 00:27:03 +07:00
_logger . LogInformation ( "Формирование текста дайджеста..." ) ;
string digestText = digestBuilder . BuildDigestText ( currencyData , cryptoData , weatherData , newsSummary ) ;
_logger . LogDebug ( "Сформированный текст получен от DigestBuilder." ) ;
2025-04-12 01:35:29 +07:00
// --- 3. Отправка ---
2025-04-12 00:27:03 +07:00
if ( ! string . IsNullOrWhiteSpace ( digestText ) )
{
_logger . LogInformation ( "Отправка дайджеста..." ) ;
bool sent = await telegramSender . SendMessageAsync ( digestText , ParseMode . Markdown , stoppingToken ) ;
2025-04-12 01:35:29 +07:00
if ( ! sent ) { _logger . LogWarning ( "Н е удалось инициировать отправку дайджеста." ) ; }
2025-04-12 00:27:03 +07:00
}
2025-04-12 01:35:29 +07:00
else { _logger . LogWarning ( "Текст дайджеста пуст, отправка не производится." ) ; }
2025-04-12 00:27:03 +07:00
}
catch ( OperationCanceledException )
{
_logger . LogInformation ( "Запрос на остановку получен во время выполнения основной задачи." ) ;
2025-04-12 01:35:29 +07:00
break ;
2025-04-12 00:27:03 +07:00
}
catch ( Exception ex )
2025-04-12 01:35:29 +07:00
{ // Ловим КРИТИЧЕСКИЕ ошибки цикла задачи
2025-04-12 00:27:03 +07:00
_logger . LogError ( ex , "Произошла КРИТИЧЕСКАЯ ошибка во время выполнения задачи создания дайджеста." ) ;
2025-04-12 01:35:29 +07:00
try { await telegramSender . SendErrorMessageAsync ( $"Критическая ошибка в Worker: {ex.Message}" , CancellationToken . None ) ; }
catch ( Exception sendEx ) { _logger . LogError ( sendEx , "Н е удалось отправить сообщение о КРИТИЧЕСКОЙ ошибке." ) ; }
// Н е делаем Delay, чтобы быстрее перейти к следующему плановому запуску
2025-04-12 00:27:03 +07:00
}
} // Конец scope
_logger . LogInformation ( "Процесс создания дайджеста завершен. Расчет следующего запуска." ) ;
2025-04-12 01:35:29 +07:00
// Короткая пауза после выполнения, чтобы не запустить случайно дважды в одну секунду
try { await Task . Delay ( TimeSpan . FromSeconds ( 1 ) , stoppingToken ) ; }
catch ( OperationCanceledException ) { break ; }
2025-04-12 00:27:03 +07:00
} // Конец while
_logger . LogInformation ( "Worker останавливается." ) ;
}
2025-04-12 01:35:29 +07:00
// Метод для расчета задержки до ближайшего времени запуска в целевом часовом поясе
private TimeSpan CalculateDelayUntilNearestRunInTargetZone ( List < TimeSpan > runTimesLocal , TimeZoneInfo targetZone )
2025-04-12 00:27:03 +07:00
{
2025-04-12 01:35:29 +07:00
// Получаем ТЕКУЩЕЕ время в ЦЕЛЕВОМ часовом поясе
DateTime targetZoneNow = TimeZoneInfo . ConvertTimeFromUtc ( DateTime . UtcNow , targetZone ) ;
DateTime targetZoneToday = targetZoneNow . Date ;
TimeSpan targetZoneCurrentTimeOfDay = targetZoneNow . TimeOfDay ;
_logger . LogDebug ( "Текущее время в целевой зоне ({TargetZoneId}): {TargetNow:yyyy-MM-dd HH:mm:ss}" , targetZone . Id , targetZoneNow ) ;
// Ищем ближайшее время запуска СЕГОДНЯ (в целевой зоне), которое >= текущего времени
TimeSpan ? nextRunTimeToday = runTimesLocal
. Where ( t = > t > = targetZoneCurrentTimeOfDay )
2025-04-12 00:27:03 +07:00
. Cast < TimeSpan ? > ( )
2025-04-12 01:35:29 +07:00
. FirstOrDefault ( ) ; // Используем FirstOrDefault, т.к. список отсортирован
2025-04-12 00:27:03 +07:00
2025-04-12 01:35:29 +07:00
DateTime nextRunTargetZoneDateTime ; // Время следующего запуска в целевой зоне
2025-04-12 00:27:03 +07:00
if ( nextRunTimeToday . HasValue )
{
2025-04-12 01:35:29 +07:00
// Планируем на сегодня (в целевой зоне)
nextRunTargetZoneDateTime = targetZoneToday + nextRunTimeToday . Value ;
_logger . LogDebug ( "Ближайшее время запуска сегодня (в целевой зоне): {RunTime:yyyy-MM-dd HH:mm:ss}" , nextRunTargetZoneDateTime ) ;
2025-04-12 00:27:03 +07:00
}
else
{
2025-04-12 01:35:29 +07:00
// Планируем на З А В Т Р А (в целевой зоне), используя самое раннее время
var earliestTime = runTimesLocal . First ( ) ; // Гарантированно есть и отсортировано
nextRunTargetZoneDateTime = targetZoneToday . AddDays ( 1 ) + earliestTime ;
_logger . LogDebug ( "В с е времена запуска сегодня (в целевой зоне) прошли. Ближайшее время завтра: {RunTime:yyyy-MM-dd HH:mm:ss}" , nextRunTargetZoneDateTime ) ;
2025-04-12 00:27:03 +07:00
}
2025-04-12 01:35:29 +07:00
// Переводим рассчитанное время следующего запуска из целевой зоны обратно в UTC
DateTime nextRunUtc = TimeZoneInfo . ConvertTimeToUtc ( nextRunTargetZoneDateTime , targetZone ) ;
// Рассчитываем задержку от ТЕКУЩЕГО времени UTC
TimeSpan delay = nextRunUtc - DateTime . UtcNow ;
2025-04-12 00:27:03 +07:00
2025-04-12 01:35:29 +07:00
_logger . LogDebug ( "Рассчитанное время следующего запуска UTC: {NextRunUtc:yyyy-MM-dd HH:mm:ss}Z" , nextRunUtc ) ;
// Если задержка отрицательная (уже опоздали), возвращаем Zero для немедленного запуска.
2025-04-12 00:27:03 +07:00
return delay < TimeSpan . Zero ? TimeSpan . Zero : delay ;
}
public override Task StopAsync ( CancellationToken cancellationToken )
{
_logger . LogInformation ( "Worker StopAsync вызван." ) ;
return base . StopAsync ( cancellationToken ) ;
}
}
}