test time correct

This commit is contained in:
Professional 2025-04-12 01:35:29 +07:00
parent 4951a0ce32
commit 9a68a123d1
3 changed files with 111 additions and 106 deletions

View File

@ -2,7 +2,10 @@
{ {
public class SchedulingSettings public class SchedulingSettings
{ {
// Убираем значение по умолчанию. Список будет null, если не задан в конфиге. // Список времен запуска в формате "HH:mm:ss" (это ВАШЕ локальное время)
public List<string>? RunAtTimesLocal { get; set; } public List<string>? RunAtTimesLocal { get; set; }
// Идентификатор целевого часового пояса (IANA ID)
public string TargetTimeZoneId { get; set; } = "Asia/Novokuznetsk"; // Значение по умолчанию
} }
} }

206
Worker.cs
View File

@ -9,57 +9,84 @@ namespace DailyDigestWorker
public class Worker : BackgroundService public class Worker : BackgroundService
{ {
private readonly ILogger<Worker> _logger; private readonly ILogger<Worker> _logger;
// Храним список времен запуска как TimeSpan // Список времен запуска в формате TimeSpan (представляют ЛОКАЛЬНОЕ время ЦЕЛЕВОГО часового пояса)
private readonly List<TimeSpan> _runTimes; private readonly List<TimeSpan> _runTimesLocal;
private readonly TimeZoneInfo _targetTimeZone; // Целевой часовой пояс пользователя
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
// Внедряем зависимости через конструктор
public Worker(ILogger<Worker> logger, public Worker(ILogger<Worker> logger,
IOptions<SchedulingSettings> schedulingSettings, IOptions<SchedulingSettings> schedulingSettings, // Используем обновленный SchedulingSettings
IServiceProvider serviceProvider) IServiceProvider serviceProvider)
{ {
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_runTimes = new List<TimeSpan>(); _runTimesLocal = new List<TimeSpan>(); // Инициализируем список
// Проверяем наличие Value и списка перед использованием // Получаем настройки планировщика (или используем дефолтные, если секция отсутствует)
var timeStrings = schedulingSettings.Value?.RunAtTimesLocal; // Теперь может быть null var settings = schedulingSettings.Value ?? new SchedulingSettings();
var timeStrings = settings.RunAtTimesLocal; // Список строк времени
string targetTimeZoneId = settings.TargetTimeZoneId; // ID часового пояса
if (timeStrings != null) // Обрабатываем только если список задан в конфиге // 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)
{ {
foreach (var timeStr in timeStrings) foreach (var timeStr in timeStrings)
{ {
// Используем ParseExact для строгого формата "HH:mm:ss"
if (TimeSpan.TryParseExact(timeStr?.Trim(), @"hh\:mm\:ss", CultureInfo.InvariantCulture, out var timeSpan)) if (TimeSpan.TryParseExact(timeStr?.Trim(), @"hh\:mm\:ss", CultureInfo.InvariantCulture, out var timeSpan))
{ {
_runTimes.Add(timeSpan); _runTimesLocal.Add(timeSpan);
} }
else else
{ {
_logger.LogWarning("Не удалось распознать время '{TimeString}' в настройках Scheduling:RunAtTimesLocal. Используйте формат HH:mm:ss.", timeStr); _logger.LogWarning("Не удалось распознать время '{TimeString}' в настройках Scheduling:RunAtTimesLocal. Используйте формат HH:mm:ss.", timeStr);
} }
} }
_runTimes = _runTimes.Distinct().OrderBy(t => t).ToList(); // Убираем дубликаты и сортируем
_runTimesLocal = _runTimesLocal.Distinct().OrderBy(t => t).ToList();
} }
// Логика обработки пустого или отсутствующего списка остается прежней // 3. Проверка и логирование времен запуска
if (!_runTimes.Any()) if (!_runTimesLocal.Any())
{ {
_logger.LogError("В конфигурации не указано ни одного корректного времени запуска! Worker не сможет автоматически запускаться."); _logger.LogError("В конфигурации не указано ни одного корректного времени запуска (RunAtTimesLocal)! Worker не сможет автоматически запускаться.");
// Важно: убедитесь, что здесь НЕТ строки, добавляющей 8 утра по умолчанию, // Здесь НЕТ добавления времени по умолчанию, чтобы требовать явную конфигурацию
// если вы хотите, чтобы бот НЕ работал без явной конфигурации времени.
} }
_logger.LogInformation("Worker инициализирован. Дайджест будет запускаться ежедневно в: {RunTimes}", _logger.LogInformation("Worker инициализирован. Дайджест будет запускаться ежедневно в (по времени {TargetTimeZoneId}): {RunTimes}",
_runTimes.Any() ? string.Join(", ", _runTimes.Select(t => t.ToString(@"hh\:mm\:ss"))) : "НЕТ ВРЕМЕН ЗАПУСКА"); _targetTimeZone.Id,
_runTimesLocal.Any() ? string.Join(", ", _runTimesLocal.Select(t => t.ToString(@"hh\:mm\:ss"))) : "НЕТ ВРЕМЕН ЗАПУСКА");
} }
// Основной метод, который выполняется в фоне // Основной метод, который выполняется в фоне
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
_logger.LogInformation("Worker запущен в: {time}", DateTimeOffset.Now); _logger.LogInformation("Worker запущен в: {time} UTC", DateTimeOffset.UtcNow); // Логгируем UTC время запуска
// Проверяем наличие времен запуска перед стартом цикла if (!_runTimesLocal.Any())
if (!_runTimes.Any())
{ {
_logger.LogError("Нет сконфигурированных времен запуска. Worker завершает работу."); _logger.LogError("Нет сконфигурированных времен запуска. Worker завершает работу.");
return; // Останавливаем выполнение, если нет времен return; // Останавливаем выполнение, если нет времен
@ -67,16 +94,19 @@ namespace DailyDigestWorker
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
// Рассчитываем задержку до ближайшего времени запуска // Рассчитываем задержку до ближайшего времени запуска в целевом часовом поясе
TimeSpan delay = CalculateDelayUntilNearestRun(_runTimes); TimeSpan delay = CalculateDelayUntilNearestRunInTargetZone(_runTimesLocal, _targetTimeZone);
_logger.LogInformation("Следующий запуск дайджеста запланирован через: {Delay} (в {TargetTime})", // Логируем время следующего запуска в UTC и в целевой зоне
delay, DateTime.Now.Add(delay).ToString("yyyy-MM-dd HH:mm:ss")); 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);
try try
{ {
// Ожидаем до следующего времени запуска, прерывая ожидание, если придет сигнал остановки // Ожидаем до следующего времени запуска
// Если задержка 0 или меньше (опоздали), Task.Delay корректно обработает это и не будет ждать.
await Task.Delay(delay, stoppingToken); await Task.Delay(delay, stoppingToken);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@ -85,14 +115,13 @@ namespace DailyDigestWorker
break; // Выходим из цикла while break; // Выходим из цикла while
} }
// Проверяем еще раз перед выполнением задачи, на случай если остановка пришла точно в момент срабатывания if (stoppingToken.IsCancellationRequested) break; // Проверяем еще раз
if (stoppingToken.IsCancellationRequested) break;
// --- Выполнение основной задачи --- // --- Выполнение основной задачи ---
_logger.LogInformation("Запуск процесса создания дайджеста в: {time}", DateTimeOffset.Now); _logger.LogInformation("Запуск процесса создания дайджеста в: {time} UTC", DateTimeOffset.UtcNow);
using (var scope = _serviceProvider.CreateScope()) using (var scope = _serviceProvider.CreateScope())
{ {
// --- Получаем ВСЕ необходимые сервисы в начале scope --- // --- Получаем сервисы ---
var currencyService = scope.ServiceProvider.GetRequiredService<ICurrencyService>(); var currencyService = scope.ServiceProvider.GetRequiredService<ICurrencyService>();
var cryptoService = scope.ServiceProvider.GetRequiredService<ICryptoService>(); var cryptoService = scope.ServiceProvider.GetRequiredService<ICryptoService>();
var weatherService = scope.ServiceProvider.GetRequiredService<IWeatherService>(); var weatherService = scope.ServiceProvider.GetRequiredService<IWeatherService>();
@ -101,11 +130,9 @@ namespace DailyDigestWorker
var newsReaderService = scope.ServiceProvider.GetRequiredService<ITelegramChannelReader>(); var newsReaderService = scope.ServiceProvider.GetRequiredService<ITelegramChannelReader>();
var summarizationService = scope.ServiceProvider.GetRequiredService<ISummarizationService>(); var summarizationService = scope.ServiceProvider.GetRequiredService<ISummarizationService>();
// Константы для чтения новостей (можно вынести в конфиг)
const int NewsMaxAgeHours = 24; const int NewsMaxAgeHours = 24;
const int NewsFetchLimit = 50; // Сколько последних сообщений проверить const int NewsFetchLimit = 50;
string? newsSummary = null;
string? newsSummary = null; // Инициализируем переменную для саммари
try try
{ {
@ -122,10 +149,9 @@ namespace DailyDigestWorker
var cryptoData = await cryptoTask; var cryptoData = await cryptoTask;
var weatherData = await weatherTask; var weatherData = await weatherTask;
var rawNewsTexts = await newsTask; var rawNewsTexts = await newsTask;
_logger.LogInformation("Сбор всех базовых данных завершен."); _logger.LogInformation("Сбор всех базовых данных завершен.");
// --- 1.5 Генерация саммари новостей (если есть новости) --- // --- 1.5 Генерация саммари ---
if (rawNewsTexts != null && rawNewsTexts.Any()) if (rawNewsTexts != null && rawNewsTexts.Any())
{ {
_logger.LogInformation("Получено {NewsCount} текстов новостей. Запуск суммаризации...", rawNewsTexts.Count); _logger.LogInformation("Получено {NewsCount} текстов новостей. Запуск суммаризации...", rawNewsTexts.Count);
@ -133,7 +159,6 @@ namespace DailyDigestWorker
{ {
string combinedNewsText = string.Join("\n\n---\n\n", rawNewsTexts); string combinedNewsText = string.Join("\n\n---\n\n", rawNewsTexts);
newsSummary = await summarizationService.SummarizeTextAsync(combinedNewsText, stoppingToken); newsSummary = await summarizationService.SummarizeTextAsync(combinedNewsText, stoppingToken);
if (newsSummary != null) if (newsSummary != null)
{ {
_logger.LogInformation("Саммари новостей успешно получено."); _logger.LogInformation("Саммари новостей успешно получено.");
@ -144,123 +169,100 @@ namespace DailyDigestWorker
_logger.LogWarning("Сервис суммаризации вернул null."); _logger.LogWarning("Сервис суммаризации вернул null.");
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException) { _logger.LogInformation("Операция суммаризации была отменена."); }
{ catch (Exception ex) { _logger.LogError(ex, "Ошибка во время вызова сервиса суммаризации."); }
_logger.LogInformation("Операция суммаризации была отменена.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка во время вызова сервиса суммаризации.");
}
} }
else else
{ {
_logger.LogInformation("Нет текстов новостей для суммаризации."); _logger.LogInformation("Нет текстов новостей для суммаризации.");
} }
// --- 2. Формирование текста дайджеста --- // --- 2. Формирование текста ---
_logger.LogInformation("Формирование текста дайджеста..."); _logger.LogInformation("Формирование текста дайджеста...");
string digestText = digestBuilder.BuildDigestText(currencyData, cryptoData, weatherData, newsSummary); string digestText = digestBuilder.BuildDigestText(currencyData, cryptoData, weatherData, newsSummary);
_logger.LogDebug("Сформированный текст получен от DigestBuilder."); _logger.LogDebug("Сформированный текст получен от DigestBuilder.");
// --- 3. Отправка дайджеста --- // --- 3. Отправка ---
if (!string.IsNullOrWhiteSpace(digestText)) if (!string.IsNullOrWhiteSpace(digestText))
{ {
_logger.LogInformation("Отправка дайджеста..."); _logger.LogInformation("Отправка дайджеста...");
bool sent = await telegramSender.SendMessageAsync(digestText, ParseMode.Markdown, stoppingToken); bool sent = await telegramSender.SendMessageAsync(digestText, ParseMode.Markdown, stoppingToken);
if (!sent) if (!sent) { _logger.LogWarning("Не удалось инициировать отправку дайджеста."); }
{
_logger.LogWarning("Не удалось инициировать отправку дайджеста.");
}
}
else
{
_logger.LogWarning("Текст дайджеста пуст, отправка не производится.");
} }
else { _logger.LogWarning("Текст дайджеста пуст, отправка не производится."); }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
_logger.LogInformation("Запрос на остановку получен во время выполнения основной задачи."); _logger.LogInformation("Запрос на остановку получен во время выполнения основной задачи.");
break; // Выходим из while break;
} }
catch (Exception ex) catch (Exception ex)
{ // Ловим ошибки на уровне всего цикла задачи { // Ловим КРИТИЧЕСКИЕ ошибки цикла задачи
_logger.LogError(ex, "Произошла КРИТИЧЕСКАЯ ошибка во время выполнения задачи создания дайджеста."); _logger.LogError(ex, "Произошла КРИТИЧЕСКАЯ ошибка во время выполнения задачи создания дайджеста.");
try try { await telegramSender.SendErrorMessageAsync($"Критическая ошибка в Worker: {ex.Message}", CancellationToken.None); }
{ catch (Exception sendEx) { _logger.LogError(sendEx, "Не удалось отправить сообщение о КРИТИЧЕСКОЙ ошибке."); }
await telegramSender.SendErrorMessageAsync($"Критическая ошибка в Worker: {ex.Message}", CancellationToken.None); // Не делаем Delay, чтобы быстрее перейти к следующему плановому запуску
}
catch (Exception sendEx)
{
_logger.LogError(sendEx, "Не удалось отправить сообщение о КРИТИЧЕСКОЙ ошибке в Telegram.");
}
// Не добавляем здесь Delay, чтобы не задерживать следующий цикл после критической ошибки.
// Worker перейдет к расчету следующего запуска.
} }
} // Конец scope } // Конец scope
_logger.LogInformation("Процесс создания дайджеста завершен. Расчет следующего запуска."); _logger.LogInformation("Процесс создания дайджеста завершен. Расчет следующего запуска.");
// Небольшая пауза (~1 сек) после выполнения, чтобы гарантированно перейти на следующую секунду // Короткая пауза после выполнения, чтобы не запустить случайно дважды в одну секунду
// и избежать повторного срабатывания в ту же минуту, если расчет времени даст очень маленькую задержку. try { await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); }
try catch (OperationCanceledException) { break; }
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Запрос на остановку получен во время короткой паузы после выполнения.");
break;
}
} // Конец while } // Конец while
_logger.LogInformation("Worker останавливается."); _logger.LogInformation("Worker останавливается.");
} }
// Метод для расчета задержки до ближайшего времени запуска из списка // Метод для расчета задержки до ближайшего времени запуска в целевом часовом поясе
private TimeSpan CalculateDelayUntilNearestRun(List<TimeSpan> runTimes) private TimeSpan CalculateDelayUntilNearestRunInTargetZone(List<TimeSpan> runTimesLocal, TimeZoneInfo targetZone)
{ {
var now = DateTime.Now; // Получаем ТЕКУЩЕЕ время в ЦЕЛЕВОМ часовом поясе
var today = now.Date; DateTime targetZoneNow = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, targetZone);
TimeSpan currentTimeOfDay = now.TimeOfDay; DateTime targetZoneToday = targetZoneNow.Date;
TimeSpan targetZoneCurrentTimeOfDay = targetZoneNow.TimeOfDay;
// Ищем ближайшее время запуска СЕГОДНЯ, которое еще не прошло (или равно текущему времени с точностью до секунды) _logger.LogDebug("Текущее время в целевой зоне ({TargetZoneId}): {TargetNow:yyyy-MM-dd HH:mm:ss}", targetZone.Id, targetZoneNow);
// Добавляем небольшую дельту (например, 1 секунду) к currentTimeOfDay, чтобы не пропустить запуск,
// который должен был произойти точно в текущую секунду. // Ищем ближайшее время запуска СЕГОДНЯ (в целевой зоне), которое >= текущего времени
TimeSpan? nextRunTimeToday = runTimes TimeSpan? nextRunTimeToday = runTimesLocal
.Where(t => t >= currentTimeOfDay) .Where(t => t >= targetZoneCurrentTimeOfDay)
.Cast<TimeSpan?>() .Cast<TimeSpan?>()
.FirstOrDefault(); // Список уже отсортирован, берем первое подходящее .FirstOrDefault(); // Используем FirstOrDefault, т.к. список отсортирован
DateTime nextRunDateTime; DateTime nextRunTargetZoneDateTime; // Время следующего запуска в целевой зоне
if (nextRunTimeToday.HasValue) if (nextRunTimeToday.HasValue)
{ {
// Если нашли время сегодня, планируем на сегодня // Планируем на сегодня (в целевой зоне)
nextRunDateTime = today + nextRunTimeToday.Value; nextRunTargetZoneDateTime = targetZoneToday + nextRunTimeToday.Value;
_logger.LogDebug("Ближайшее время запуска сегодня: {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunDateTime); _logger.LogDebug("Ближайшее время запуска сегодня (в целевой зоне): {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunTargetZoneDateTime);
} }
else else
{ {
// Если сегодня подходящего времени нет, берем самое раннее время из списка и планируем на ЗАВТРА // Планируем на ЗАВТРА (в целевой зоне), используя самое раннее время
// runTimes гарантированно не пустой и отсортирован (проверено в конструкторе) var earliestTime = runTimesLocal.First(); // Гарантированно есть и отсортировано
var earliestTimeTomorrow = runTimes.First(); nextRunTargetZoneDateTime = targetZoneToday.AddDays(1) + earliestTime;
nextRunDateTime = today.AddDays(1) + earliestTimeTomorrow; _logger.LogDebug("Все времена запуска сегодня (в целевой зоне) прошли. Ближайшее время завтра: {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunTargetZoneDateTime);
_logger.LogDebug("Все времена запуска сегодня прошли. Ближайшее время завтра: {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunDateTime);
} }
var delay = nextRunDateTime - now; // Переводим рассчитанное время следующего запуска из целевой зоны обратно в UTC
DateTime nextRunUtc = TimeZoneInfo.ConvertTimeToUtc(nextRunTargetZoneDateTime, targetZone);
// Если задержка отрицательная или очень маленькая (меньше ~0), возвращаем Zero. // Рассчитываем задержку от ТЕКУЩЕГО времени UTC
TimeSpan delay = nextRunUtc - DateTime.UtcNow;
_logger.LogDebug("Рассчитанное время следующего запуска UTC: {NextRunUtc:yyyy-MM-dd HH:mm:ss}Z", nextRunUtc);
// Если задержка отрицательная (уже опоздали), возвращаем Zero для немедленного запуска.
return delay < TimeSpan.Zero ? TimeSpan.Zero : delay; return delay < TimeSpan.Zero ? TimeSpan.Zero : delay;
} }
public override Task StopAsync(CancellationToken cancellationToken) public override Task StopAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("Worker StopAsync вызван."); _logger.LogInformation("Worker StopAsync вызван.");
// Можно добавить код для корректного завершения (например, Logout для WTelegramClient)
return base.StopAsync(cancellationToken); return base.StopAsync(cancellationToken);
} }
} }

View File

@ -27,13 +27,13 @@
// Для Gemini API URL может быть не нужен, если используется клиентская библиотека Google // Для Gemini API URL может быть не нужен, если используется клиентская библиотека Google
}, },
"Scheduling": { "Scheduling": {
// Используем массив строк для указания времени
"RunAtTimesLocal": [ "RunAtTimesLocal": [
"18:14:00", "09:00:00",
"14:00:00", "14:00:00",
"18:00:00", "18:00:00",
"22:00:00" "22:00:00"
] ],
"TargetTimeZoneId": "Asia/Novokuznetsk" // Укажите ваш IANA TimeZone ID
}, },
"GeminiSettings": { "GeminiSettings": {
"ModelName": "gemini-1.5-flash-latest", // <--- ИЗМЕНИТЬ ЗДЕСЬ "ModelName": "gemini-1.5-flash-latest", // <--- ИЗМЕНИТЬ ЗДЕСЬ