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
{
2025-04-12 10:20:46 +07:00
// Структура для хранения распарсенного расписания (внутренняя)
internal readonly record struct ParsedScheduleEntry ( TimeSpan TimeLocal , string ChannelUsername , DateTime NextRunTargetZone ) ;
2025-04-12 00:27:03 +07:00
public class Worker : BackgroundService
{
private readonly ILogger < Worker > _logger ;
2025-04-12 10:20:46 +07:00
// Храним список пар (Время в целевой зоне, Имя канала)
private readonly List < ( TimeSpan TimeLocal , string ChannelUsername ) > _schedules ;
2025-04-12 01:35:29 +07:00
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 10:20:46 +07:00
_schedules = new List < ( TimeSpan TimeLocal , string ChannelUsername ) > ( ) ; // Инициализируем список
2025-04-12 00:27:03 +07:00
2025-04-12 01:35:29 +07:00
var settings = schedulingSettings . Value ? ? new SchedulingSettings ( ) ;
2025-04-12 10:20:46 +07:00
var scheduleEntries = settings . Schedules ; // Получаем новый список ScheduleEntry
string targetTimeZoneId = settings . TargetTimeZoneId ;
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 ) ;
2025-04-12 10:20:46 +07:00
_targetTimeZone = TimeZoneInfo . Local ;
2025-04-12 01:35:29 +07:00
}
catch ( InvalidTimeZoneException ex )
{
_logger . LogError ( ex , "Ошибка при загрузке данных часового пояса '{TimeZoneId}'. Будет использовано локальное время сервера." , targetTimeZoneId ) ;
_targetTimeZone = TimeZoneInfo . Local ;
}
2025-04-12 10:20:46 +07:00
catch ( Exception ex )
2025-04-12 01:35:29 +07:00
{
_logger . LogError ( ex , "Неожиданная ошибка при определении часового пояса '{TimeZoneId}'. Будет использовано локальное время сервера." , targetTimeZoneId ) ;
_targetTimeZone = TimeZoneInfo . Local ;
}
2025-04-12 10:20:46 +07:00
// 2. Парсим новое расписание из конфигурации
if ( scheduleEntries ! = null )
2025-04-12 00:27:03 +07:00
{
2025-04-12 10:20:46 +07:00
foreach ( var entry in scheduleEntries )
2025-04-12 00:27:03 +07:00
{
2025-04-12 10:20:46 +07:00
if ( entry ! = null & &
! string . IsNullOrWhiteSpace ( entry . ChannelUsername ) & &
TimeSpan . TryParseExact ( entry . TimeLocal ? . Trim ( ) , @"hh\:mm\:ss" , CultureInfo . InvariantCulture , out var timeSpan ) )
2025-04-12 00:27:03 +07:00
{
2025-04-12 10:20:46 +07:00
// Добавляем валидную пару (время, канал) в список
_schedules . Add ( ( timeSpan , entry . ChannelUsername . Trim ( ) ) ) ; // Trim на случай лишних пробелов
2025-04-12 00:27:03 +07:00
}
else
{
2025-04-12 10:20:46 +07:00
_logger . LogWarning ( "Некорректная или неполная запись в расписании Scheduling:Schedules: {@ScheduleEntry}. Запись проигнорирована." , entry ) ;
2025-04-12 00:27:03 +07:00
}
}
2025-04-12 10:20:46 +07:00
// Убираем дубликаты и сортируем по времени
_schedules = _schedules
. GroupBy ( s = > s . TimeLocal ) // Группируем по времени, чтобы избежать дублей времени
. Select ( g = > g . First ( ) ) // Берем первую запись для каждого времени
. OrderBy ( s = > s . TimeLocal ) // Сортируем по времени
. ToList ( ) ;
2025-04-12 00:27:03 +07:00
}
2025-04-12 10:20:46 +07:00
// 3. Проверка и логирование расписания
if ( ! _schedules . Any ( ) )
2025-04-12 00:27:03 +07:00
{
2025-04-12 10:20:46 +07:00
_logger . LogError ( "В конфигурации не указано ни одного корректного расписания (Scheduling:Schedules)! Worker не сможет автоматически запускаться." ) ;
2025-04-12 00:27:03 +07:00
}
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "Worker инициализирован. Расписание запусков (по времени {TargetTimeZoneId}):" , _targetTimeZone . Id ) ;
if ( _schedules . Any ( ) )
{
foreach ( var schedule in _schedules )
{
_logger . LogInformation ( " - {Time} -> Канал: {Channel}" , schedule . TimeLocal . ToString ( @"hh\:mm\:ss" ) , schedule . ChannelUsername ) ;
}
}
else
{
_logger . LogInformation ( " - Н Е Т ЗАПЛАНИРОВАННЫХ ЗАПУСКОВ" ) ;
}
2025-04-12 00:27:03 +07:00
}
// Основной метод, который выполняется в фоне
protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
{
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "Worker запущен в: {time} UTC" , DateTimeOffset . UtcNow ) ;
2025-04-12 00:27:03 +07:00
2025-04-12 10:20:46 +07:00
if ( ! _schedules . Any ( ) )
2025-04-12 00:27:03 +07:00
{
2025-04-12 10:20:46 +07:00
_logger . LogError ( "Нет сконфигурированных расписаний запуска. Worker завершает работу." ) ;
return ; // Останавливаем выполнение, если нет расписаний
2025-04-12 00:27:03 +07:00
}
while ( ! stoppingToken . IsCancellationRequested )
{
2025-04-12 10:20:46 +07:00
// Рассчитываем задержку до ближайшего запуска И получаем задание для этого запуска
( TimeSpan delay , string channelForNextRun , DateTime nextRunTargetZone ) = CalculateDelayAndNextSchedule ( _schedules , _targetTimeZone ) ;
2025-04-12 00:27:03 +07:00
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "Следующий запуск дайджеста из канала '{Channel}' запланирован через: {Delay} (в {TargetTimeLocal} {TargetZoneId})" ,
channelForNextRun , delay , nextRunTargetZone . ToString ( "yyyy-MM-dd HH:mm:ss" ) , _targetTimeZone . Id ) ;
2025-04-12 00:27:03 +07:00
try
{
2025-04-12 10:20:46 +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 10:20:46 +07:00
// --- Определяем задание для ТЕКУЩЕГО запуска ---
// Пересчитываем, чтобы получить актуальное задание на момент пробуждения
2025-04-12 10:46:39 +07:00
string currentChannelUsername = channelForNextRun ;
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "--- Начало цикла обработки дайджеста для канала: [{ChannelUsername}] ({Time} UTC) ---" , currentChannelUsername , DateTimeOffset . UtcNow ) ;
2025-04-12 00:27:03 +07:00
// --- Выполнение основной задачи ---
using ( var scope = _serviceProvider . CreateScope ( ) )
{
2025-04-12 10:20:46 +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 > ( ) ;
2025-04-12 10:20:46 +07:00
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. С б о р данных ---
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "1. Начало с б о р а данных..." ) ;
2025-04-12 00:27:03 +07:00
var currencyTask = currencyService . GetCurrencyRatesAsync ( stoppingToken ) ;
var cryptoTask = cryptoService . GetCryptoRatesAsync ( stoppingToken ) ;
var weatherTask = weatherService . GetWeatherAsync ( stoppingToken ) ;
2025-04-12 10:20:46 +07:00
// Передаем имя канала, актуальное для ЭТОГО запуска
var newsTask = newsReaderService . GetRecentNewsAsync ( currentChannelUsername , NewsMaxAgeHours , NewsFetchLimit , stoppingToken ) ;
2025-04-12 00:27:03 +07:00
await Task . WhenAll ( currencyTask , cryptoTask , weatherTask , newsTask ) ;
var currencyData = await currencyTask ;
var cryptoData = await cryptoTask ;
var weatherData = await weatherTask ;
var rawNewsTexts = await newsTask ;
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "1. С б о р данных завершен." ) ;
2025-04-12 00:27:03 +07:00
2025-04-12 10:20:46 +07:00
// --- 1.5 Суммаризация ---
2025-04-12 00:27:03 +07:00
if ( rawNewsTexts ! = null & & rawNewsTexts . Any ( ) )
{
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "1.5 Получено {NewsCount} текстов из '{Channel}'. Запуск суммаризации..." , rawNewsTexts . Count , currentChannelUsername ) ;
2025-04-12 00:27:03 +07:00
try
{
string combinedNewsText = string . Join ( "\n\n---\n\n" , rawNewsTexts ) ;
2025-04-12 10:20:46 +07:00
// В будущем можно передать currentChannelUsername в SummarizeTextAsync для контекста промпта
2025-04-12 00:27:03 +07:00
newsSummary = await summarizationService . SummarizeTextAsync ( combinedNewsText , stoppingToken ) ;
2025-04-12 10:20:46 +07:00
if ( newsSummary ! = null ) { _logger . LogInformation ( "1.5 Суммаризация успешна." ) ; }
else { _logger . LogWarning ( "1.5 Сервис суммаризации вернул null." ) ; }
2025-04-12 00:27:03 +07:00
}
2025-04-12 10:20:46 +07:00
catch ( OperationCanceledException ) { _logger . LogInformation ( "1.5 Операция суммаризации отменена." ) ; }
catch ( Exception ex ) { _logger . LogError ( ex , "1.5 Ошибка во время вызова сервиса суммаризации." ) ; }
2025-04-12 00:27:03 +07:00
}
else
{
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "1.5 Нет текстов новостей для суммаризации из канала '{Channel}'." , currentChannelUsername ) ;
2025-04-12 00:27:03 +07:00
}
2025-04-12 01:35:29 +07:00
// --- 2. Формирование текста ---
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "2. Формирование текста дайджеста..." ) ;
// Можно передать currentChannelUsername для добавления в заголовок дайджеста
2025-04-12 00:27:03 +07:00
string digestText = digestBuilder . BuildDigestText ( currencyData , cryptoData , weatherData , newsSummary ) ;
2025-04-12 10:20:46 +07:00
_logger . LogDebug ( "2. Сформированный текст получен от DigestBuilder." ) ;
2025-04-12 00:27:03 +07:00
2025-04-12 01:35:29 +07:00
// --- 3. Отправка ---
2025-04-12 00:27:03 +07:00
if ( ! string . IsNullOrWhiteSpace ( digestText ) )
{
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "3. Отправка дайджеста..." ) ;
2025-04-12 00:27:03 +07:00
bool sent = await telegramSender . SendMessageAsync ( digestText , ParseMode . Markdown , stoppingToken ) ;
2025-04-12 10:20:46 +07:00
if ( ! sent ) { _logger . LogWarning ( "3. Н е удалось инициировать отправку дайджеста." ) ; }
2025-04-12 00:27:03 +07:00
}
2025-04-12 10:20:46 +07:00
else { _logger . LogWarning ( "3. Текст дайджеста пуст, отправка не производится." ) ; }
2025-04-12 00:27:03 +07:00
}
catch ( OperationCanceledException )
{
_logger . LogInformation ( "Запрос на остановку получен во время выполнения основной задачи." ) ;
2025-04-12 10:20:46 +07:00
break ; // Выходим из while
2025-04-12 00:27:03 +07:00
}
catch ( Exception ex )
2025-04-12 01:35:29 +07:00
{ // Ловим КРИТИЧЕСКИЕ ошибки цикла задачи
2025-04-12 10:20:46 +07:00
_logger . LogError ( ex , "Произошла КРИТИЧЕСКАЯ ошибка во время выполнения задачи создания дайджеста для канала {Channel}." , currentChannelUsername ) ;
try { await telegramSender . SendErrorMessageAsync ( $"Критическая ошибка в Worker для канала {currentChannelUsername}: {ex.Message}" , CancellationToken . None ) ; }
2025-04-12 01:35:29 +07:00
catch ( Exception sendEx ) { _logger . LogError ( sendEx , "Н е удалось отправить сообщение о КРИТИЧЕСКОЙ ошибке." ) ; }
2025-04-12 00:27:03 +07:00
}
} // Конец scope
2025-04-12 10:20:46 +07:00
_logger . LogInformation ( "--- Завершение цикла обработки дайджеста для канала: [{ChannelUsername}] ---" , currentChannelUsername ) ;
2025-04-12 00:27:03 +07:00
2025-04-12 10:20:46 +07:00
// Короткая пауза после выполнения
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 10:20:46 +07:00
// Метод расчета задержки И получения СЛЕДУЮЩЕГО задания из расписания
private ( TimeSpan Delay , string ChannelUsername , DateTime NextRunTargetZone ) CalculateDelayAndNextSchedule (
List < ( TimeSpan TimeLocal , string ChannelUsername ) > schedules , TimeZoneInfo targetZone )
2025-04-12 00:27:03 +07:00
{
2025-04-12 10:20:46 +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 ;
2025-04-12 10:20:46 +07:00
_logger . LogDebug ( "Расчет след. запуска: Текущее время в зоне {TargetZoneId}: {TargetNow:yyyy-MM-dd HH:mm:ss.fff}" , targetZone . Id , targetZoneNow ) ;
2025-04-12 01:35:29 +07:00
2025-04-12 10:20:46 +07:00
// Ищем первое расписание на СЕГОДНЯ (в целевой зоне), время которого >= текущего времени
var nextScheduleToday = schedules
. Where ( s = > s . TimeLocal > = targetZoneCurrentTimeOfDay )
. Cast < ( TimeSpan TimeLocal , string ChannelUsername ) ? > ( )
. FirstOrDefault ( ) ;
2025-04-12 00:27:03 +07:00
2025-04-12 10:20:46 +07:00
( TimeSpan TimeLocal , string ChannelUsername ) targetSchedule ;
2025-04-12 01:35:29 +07:00
DateTime nextRunTargetZoneDateTime ; // Время следующего запуска в целевой зоне
2025-04-12 00:27:03 +07:00
2025-04-12 10:20:46 +07:00
if ( nextScheduleToday . HasValue )
2025-04-12 00:27:03 +07:00
{
2025-04-12 10:20:46 +07:00
// Нашли запуск на сегодня
targetSchedule = nextScheduleToday . Value ;
nextRunTargetZoneDateTime = targetZoneToday + targetSchedule . TimeLocal ;
_logger . LogDebug ( "Расчет след. запуска: Найден запуск на сегодня ({Channel}) в {RunTime:HH:mm:ss}" , targetSchedule . ChannelUsername , nextRunTargetZoneDateTime ) ;
2025-04-12 00:27:03 +07:00
}
else
{
2025-04-12 10:20:46 +07:00
// В с е запуски сегодня прошли, берем первый запуск из списка на З А В Т Р А
targetSchedule = schedules . First ( ) ; // Гарантированно есть и отсортировано
nextRunTargetZoneDateTime = targetZoneToday . AddDays ( 1 ) + targetSchedule . TimeLocal ;
_logger . LogDebug ( "Расчет след. запуска: Запуски на сегодня прошли. Следующий завтра ({Channel}) в {RunTime:HH:mm:ss}" , targetSchedule . ChannelUsername , nextRunTargetZoneDateTime ) ;
2025-04-12 00:27:03 +07:00
}
2025-04-12 10:20:46 +07:00
// Переводим время следующего запуска из целевой зоны в UTC
2025-04-12 01:35:29 +07:00
DateTime nextRunUtc = TimeZoneInfo . ConvertTimeToUtc ( nextRunTargetZoneDateTime , targetZone ) ;
// Рассчитываем задержку от ТЕКУЩЕГО времени UTC
TimeSpan delay = nextRunUtc - DateTime . UtcNow ;
2025-04-12 00:27:03 +07:00
2025-04-12 10:20:46 +07:00
_logger . LogDebug ( "Расчет след. запуска: Время UTC {NextRunUtc:yyyy-MM-dd HH:mm:ss}Z. Задержка: {Delay}" , nextRunUtc , delay ) ;
2025-04-12 01:35:29 +07:00
2025-04-12 10:20:46 +07:00
// Возвращаем задержку (не меньше нуля), имя канала для этого запуска и время запуска в целевой зоне
return ( delay < TimeSpan . Zero ? TimeSpan . Zero : delay , targetSchedule . ChannelUsername , nextRunTargetZoneDateTime ) ;
2025-04-12 00:27:03 +07:00
}
public override Task StopAsync ( CancellationToken cancellationToken )
{
_logger . LogInformation ( "Worker StopAsync вызван." ) ;
return base . StopAsync ( cancellationToken ) ;
}
}
}