From 46bdc07910cd81291f7d34f8e1bc95314c00fe2a Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Sun, 1 Feb 2026 08:36:19 +0300 Subject: [PATCH] =?UTF-8?q?feat(employees):=20=D0=92=D0=BD=D0=B5=D0=B4?= =?UTF-8?q?=D1=80=D0=B8=D0=BB=20=D1=84=D0=BE=D0=BD=D0=BE=D0=B2=D0=BE=D0=B5?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D1=81=D0=BE=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализовал новую фоновую задачу для периодического получения данных о сотрудниках из Modeus. --- SfeduSchedule/Jobs/UpdateEmployeesJob.cs | 78 ++++++++++++++ SfeduSchedule/Program.cs | 126 +++++++++++++---------- 2 files changed, 147 insertions(+), 57 deletions(-) create mode 100644 SfeduSchedule/Jobs/UpdateEmployeesJob.cs diff --git a/SfeduSchedule/Jobs/UpdateEmployeesJob.cs b/SfeduSchedule/Jobs/UpdateEmployeesJob.cs new file mode 100644 index 0000000..0c98d23 --- /dev/null +++ b/SfeduSchedule/Jobs/UpdateEmployeesJob.cs @@ -0,0 +1,78 @@ +using Quartz; +using SfeduSchedule.Logging; +using SfeduSchedule.Services; + +namespace SfeduSchedule.Jobs; + +// TODO: Обновляет список сотрудников из modeus и сохраняет в локальный файл +// TODO: Нужно вынести функционал обновления сюда, а в сервисе оставить только загрузку с диска +// TODO: Нужно настроить выполнение задачи на часов 10 утра каждый день +// TODO: Нужно обработать событие когда данные о сотрудниках запрашиваются до первого обновления + +public class UpdateEmployeesJob( + ILogger logger, + ModeusEmployeeService employeeService, + ModeusService modeusService) : IJob +{ + private const int MaxAttempts = 5; // Максимальное число попыток + private const int DelaySeconds = 50; // Задержка между попытками в секундах + private const int TimeoutSeconds = 60; // Таймаут для каждого запроса в секундах + + public async Task Execute(IJobExecutionContext jobContext) + { + logger.LogInformation("Начало выполнения UpdateEmployeesJob"); + + for (var attempt = 1; attempt <= MaxAttempts; attempt++) + try + { + logger.LogInformation("Попытка {Attempt}/{MaxAttempts} получения списка сотрудников из Modeus", attempt, + MaxAttempts); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutSeconds)); + + var employees = await modeusService.GetEmployeesAsync(cts.Token); + if (employees.Count == 0) + { + logger.LogWarningHere("Не удалось получить список сотрудников из Modeus."); + + if (attempt == MaxAttempts) + { + logger.LogError("Достигнуто максимальное число попыток получения JWT"); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken); + continue; + } + + await employeeService.SetEmployees(employees); + logger.LogInformationHere($"Получено {employees.Count} сотрудников из Modeus."); + return; + } + catch (OperationCanceledException ex) + { + logger.LogWarning(ex, "Таймаут при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt, + MaxAttempts); + + if (attempt == MaxAttempts) + { + logger.LogError("Достигнут лимит по таймаутам при запросе JWT"); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken); + } + catch (Exception ex) + { + logger.LogErrorHere(ex, "Ошибка при загрузке сотрудников из Modeus."); + + if (attempt == MaxAttempts) + { + logger.LogError("Достигнуто максимальное число попыток из-за ошибок при запросе JWT"); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken); + } + } +} \ No newline at end of file diff --git a/SfeduSchedule/Program.cs b/SfeduSchedule/Program.cs index 4876180..8b63336 100644 --- a/SfeduSchedule/Program.cs +++ b/SfeduSchedule/Program.cs @@ -11,6 +11,8 @@ using Microsoft.OpenApi.Models; using ModeusSchedule.Abstractions; using Prometheus; using Quartz; +using Quartz.Listener; +using Quartz.Impl.Matchers; using SfeduSchedule; using SfeduSchedule.Auth; using SfeduSchedule.Jobs; @@ -24,29 +26,24 @@ var builder = WebApplication.CreateBuilder(args); #region Работа с конфигурацией var configuration = builder.Configuration; -var preinstalledJwtToken = configuration["TOKEN"]; -var tgChatId = configuration["TG_CHAT_ID"]; -var tgToken = configuration["TG_TOKEN"]; -var updateJwtCron = configuration["UPDATE_JWT_CRON"] ?? "0 0 4 ? * *"; - +var preinstalledJwtToken = configuration[AppConsts.PreinstalledJwtTokenEnv]; +configuration[AppConsts.ModeusUrlEnv] ??= AppConsts.ModeusDefaultUrl; +configuration[AppConsts.UpdateJwtCronEnv] ??= "0 0 4 ? * *"; +configuration[AppConsts.UpdateEmployeeCronEnv] ??= "0 0 6 ? * *"; // Если не указана TZ, ставим Europe/Moscow -if (string.IsNullOrEmpty(configuration["TZ"])) - configuration["TZ"] = "Europe/Moscow"; +configuration["TZ"] ??= "Europe/Moscow"; -if (string.IsNullOrEmpty(configuration["MODEUS_URL"])) - configuration["MODEUS_URL"] = "https://sfedu.modeus.org/"; - -var permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40; -var timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10; +var permitLimit = int.TryParse(configuration[AppConsts.PermitLimitEnv], out var parsedPermitLimit) ? parsedPermitLimit : 40; +var timeLimit = int.TryParse(configuration[AppConsts.TimeLimitEnv], out var parsedTimeLimit) ? parsedTimeLimit : 10; #endregion #region Работа с папкой данных // Создать папку data если не существует -var dataDirectory = Path.Combine(AppContext.BaseDirectory, "data"); +var dataDirectory = Path.Combine(AppContext.BaseDirectory, AppConsts.DataFolderName); if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory); -GlobalConsts.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt"); +var jwtFilePath = Path.Combine(dataDirectory, AppConsts.JwtFileName); // Создать подкаталог для плагинов var pluginsPath = Path.Combine(dataDirectory, "Plugins"); @@ -63,7 +60,7 @@ builder.Logging.AddConsole(options => options.FormatterName = "CustomConsoleForm .AddConsoleFormatter(); builder.Logging.AddFilter("Quartz", LogLevel.Warning); -if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken)) +if (!string.IsNullOrEmpty(configuration[AppConsts.TgChatIdEnv]) && !string.IsNullOrEmpty(configuration[AppConsts.TgTokenEnv])) builder.Logging.AddTelegram(options => { options.FormatterConfiguration = new X.Extensions.Logging.Telegram.Base.Configuration.FormatterConfiguration @@ -71,8 +68,8 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken)) IncludeException = true, IncludeProperties = true, }; - options.ChatId = tgChatId; - options.AccessToken = tgToken; + options.ChatId = configuration[AppConsts.TgChatIdEnv]!; + options.AccessToken = configuration[AppConsts.TgTokenEnv]!; options.FormatterConfiguration.UseEmoji = true; options.FormatterConfiguration.ReadableApplicationName = "Modeus Schedule Proxy"; options.LogLevel = new Dictionary @@ -149,23 +146,35 @@ foreach (var p in loadedPlugins) mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(p.Assembly)); } -var jobKey = new JobKey("UpdateJWTJob"); +var updateJwtJob = new JobKey("UpdateJWTJob"); -if (string.IsNullOrEmpty(preinstalledJwtToken)) -{ - builder.Services.AddQuartz(q => +builder.Services.AddQuartz(q => { - q.AddJob(opts => opts.WithIdentity(jobKey)); + q.AddJob(opts => opts.WithIdentity(updateJwtJob)); + if (string.IsNullOrEmpty(preinstalledJwtToken)) + { + q.AddTrigger(opts => opts + .ForJob(updateJwtJob) + .WithIdentity("UpdateJWTJob-trigger") + .WithCronSchedule(configuration["UPDATE_JWT_CRON"]!) + ); + } + + q.AddJob(opts => opts.WithIdentity(nameof(UpdateEmployeesJob))); q.AddTrigger(opts => opts - .ForJob(jobKey) - .WithIdentity("UpdateJWTJob-trigger") - .WithCronSchedule(updateJwtCron) + .ForJob(nameof(UpdateEmployeesJob)) + .WithIdentity("UpdateEmployeesJob-trigger") + .WithCronSchedule(configuration[AppConsts.UpdateEmployeeCronEnv]!) ); + + // после успешного выполнения UpdateJwtJob сразу выполняем UpdateEmployeesJob + var chainListener = new JobChainingJobListener("chain"); + chainListener.AddJobChainLink(updateJwtJob, new JobKey(nameof(UpdateEmployeesJob))); + q.AddJobListener(chainListener, GroupMatcher.GroupEquals(JobKey.DefaultGroup)); }); - builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); -} +builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = false); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => @@ -258,45 +267,48 @@ app.UseForwardedHeaders(); // Корреляция логов по запросам app.UseMiddleware(); -if (string.IsNullOrEmpty(preinstalledJwtToken)) -{ - var schedulerFactory = app.Services.GetRequiredService(); - var scheduler = await schedulerFactory.GetScheduler(); - // Проверить существование файла jwt.txt - if (File.Exists(GlobalConsts.JwtFilePath)) +var schedulerFactory = app.Services.GetRequiredService(); +var scheduler = await schedulerFactory.GetScheduler(); + +var refreshJwt = true; + +// Если есть предустановленный токен, используем его +if (!string.IsNullOrEmpty(preinstalledJwtToken)) +{ + logger.LogInformation("Используем предустановленный токен из конфигурации"); + configuration["TOKEN"] = preinstalledJwtToken; + refreshJwt = false; +} + +// Проверить существование файла jwt.txt +if (File.Exists(jwtFilePath) && refreshJwt) +{ + logger.LogInformation("Обнаружена прошлая сессия"); + var lines = await File.ReadAllLinesAsync(jwtFilePath); + if (lines.Length > 1 && DateTime.TryParse(lines[1], out var expirationDate)) { - logger.LogInformation("Обнаружена прошлая сессия"); - var lines = await File.ReadAllLinesAsync(GlobalConsts.JwtFilePath); - if (lines.Length > 1 && DateTime.TryParse(lines[1], out var expirationDate)) + logger.LogInformation("Дата истечения токена: {ExpirationDate}", expirationDate); + if (expirationDate.AddHours(23) > DateTime.Now) { - logger.LogInformation("Дата истечения токена: {ExpirationDate}", expirationDate); - if (expirationDate.AddHours(23) > DateTime.Now) - { - var token = lines[0]; - logger.LogInformation("Используем существующий токен"); - configuration["TOKEN"] = token; - } - else - { - logger.LogInformation("Токен истек или скоро истечет, выполняем обновление токена"); - await scheduler.TriggerJob(jobKey); - } + var token = lines[0]; + logger.LogInformation("Используем существующий токен"); + configuration["TOKEN"] = token; + refreshJwt = false; } else - { - logger.LogInformation( - "Файл jwt.txt не содержит дату истечения или она некорректна, выполняем обновление токена"); - await scheduler.TriggerJob(jobKey); - } + logger.LogInformation("Токен истек или скоро истечет, выполняем обновление токена"); } else - { - await scheduler.TriggerJob(jobKey); - } + logger.LogInformation( + "Файл jwt.txt не содержит дату истечения или она некорректна, выполняем обновление токена"); } + +// Обновить JWT если нужно, либо сразу запустить обновление сотрудников +if (refreshJwt) + await scheduler.TriggerJob(updateJwtJob); else - logger.LogInformation("Используем предустановленный токен из конфигурации"); + await scheduler.TriggerJob(new JobKey(nameof(UpdateEmployeesJob))); app.UseSwagger(); app.UseSwaggerUI();