Compare commits

..

3 Commits

Author SHA1 Message Date
46bdc07910 feat(employees): Внедрил фоновое обновление списка сотрудников
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 1m12s
Реализовал новую фоновую задачу для периодического получения данных о сотрудниках из Modeus.
2026-02-01 08:36:19 +03:00
46c50dc8e2 feat: Ввел персистентное хранение сотрудников
Реализовал загрузку списка сотрудников с диска при запуске службы.
Добавил методы для сохранения и загрузки сотрудников в файл.
2026-02-01 08:35:24 +03:00
c3535cafe9 refactor: Переработал управление константами и путями к данным 2026-02-01 08:34:07 +03:00
10 changed files with 224 additions and 114 deletions

View File

@@ -8,5 +8,4 @@ public static class GlobalConsts
public static readonly JsonSerializerOptions JsonSerializerOptions = new() public static readonly JsonSerializerOptions JsonSerializerOptions = new()
{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
public static string JwtFilePath { get; set; } = "data/jwt.txt";
} }

View File

@@ -0,0 +1,30 @@
namespace SfeduSchedule;
public static class AppConsts
{
// Quartz Jobs Cron expressions
public const string UpdateJwtCronEnv = "UPDATE_JWT_CRON";
public const string UpdateEmployeeCronEnv = "UPDATE_EMPLOYEES_CRON";
// Modeus
public const string PreinstalledJwtTokenEnv = "TOKEN";
public const string ModeusUrlEnv = "MODEUS_URL";
public const string ModeusDefaultUrl = "https://sfedu.modeus.org/";
// Telegram
public const string TgChatIdEnv = "TG_CHAT_ID";
public const string TgTokenEnv = "TG_TOKEN";
// RateLimiter
public const string PermitLimitEnv = "PERMIT_LIMIT";
public const string TimeLimitEnv = "TIME_LIMIT";
// MS Auth
public const string AuthUrlEnv = "AUTH_URL";
public const string AuthApiKeyEnv = "AUTH_API_KEY";
// File paths
public const string JwtFileName = "jwt.txt";
public const string EmployeesFileName = "employees.json";
public const string DataFolderName = "Data";
}

View File

@@ -44,10 +44,14 @@ public class ScheduleController(ModeusService modeusService, ModeusEmployeeServi
/// <response code="200">Возвращает список сотрудников с их GUID</response> /// <response code="200">Возвращает список сотрудников с их GUID</response>
/// <response code="404">Сотрудник не найден</response> /// <response code="404">Сотрудник не найден</response>
/// <response code="401">Неавторизованный</response> /// <response code="401">Неавторизованный</response>
/// <response code="503">Сервис сотрудников не инициализирован</response>
[HttpGet] [HttpGet]
[Route("searchemployee")] [Route("searchemployee")]
public async Task<IActionResult> SearchEmployees([Required][MinLength(1)] string fullname) public async Task<IActionResult> SearchEmployees([Required][MinLength(1)] string fullname)
{ {
if (!modeusEmployeeService.IsInitialized())
return StatusCode(503, "Сервис сотрудников не инициализирован, попробуйте позже.");
var employees = await modeusEmployeeService.GetEmployees(fullname, 10); var employees = await modeusEmployeeService.GetEmployees(fullname, 10);
if (employees.Count == 0) if (employees.Count == 0)
return NotFound(); return NotFound();

View File

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

View File

@@ -8,8 +8,7 @@ public class UpdateJwtJob(
IConfiguration configuration, IConfiguration configuration,
ILogger<UpdateJwtJob> logger, ILogger<UpdateJwtJob> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
ModeusHttpClient modeusHttpClient, ModeusHttpClient modeusHttpClient) : IJob
ModeusService modeusService) : IJob
{ {
private const int MaxAttempts = 5; // Максимальное число попыток private const int MaxAttempts = 5; // Максимальное число попыток
private const int DelaySeconds = 20; // Задержка между попытками в секундах private const int DelaySeconds = 20; // Задержка между попытками в секундах
@@ -19,8 +18,8 @@ public class UpdateJwtJob(
{ {
logger.LogInformation("Начало выполнения UpdateJwtJob"); logger.LogInformation("Начало выполнения UpdateJwtJob");
var authUrl = configuration["AUTH_URL"] ?? "http://msauth:8080/auth/ms"; var authUrl = configuration[AppConsts.AuthUrlEnv] ?? "http://msauth:8080/auth/ms";
var apiKey = configuration["AUTH_API_KEY"] ?? string.Empty; var apiKey = configuration[AppConsts.AuthApiKeyEnv] ?? string.Empty;
var client = httpClientFactory.CreateClient("authClient"); var client = httpClientFactory.CreateClient("authClient");
client.Timeout = TimeSpan.FromSeconds(TimeoutSeconds + 10); client.Timeout = TimeSpan.FromSeconds(TimeoutSeconds + 10);
@@ -72,7 +71,7 @@ public class UpdateJwtJob(
configuration["TOKEN"] = body.Jwt; configuration["TOKEN"] = body.Jwt;
modeusHttpClient.SetToken(body.Jwt); modeusHttpClient.SetToken(body.Jwt);
await File.WriteAllTextAsync(GlobalConsts.JwtFilePath, await File.WriteAllTextAsync(Path.Combine(Path.Combine(AppContext.BaseDirectory, AppConsts.DataFolderName), AppConsts.JwtFileName),
body.Jwt + "\n" + DateTime.Now.ToString("O"), cts.Token); body.Jwt + "\n" + DateTime.Now.ToString("O"), cts.Token);
logger.LogInformation("JWT успешно обновлён"); logger.LogInformation("JWT успешно обновлён");
return; return;

View File

@@ -11,6 +11,8 @@ using Microsoft.OpenApi.Models;
using ModeusSchedule.Abstractions; using ModeusSchedule.Abstractions;
using Prometheus; using Prometheus;
using Quartz; using Quartz;
using Quartz.Listener;
using Quartz.Impl.Matchers;
using SfeduSchedule; using SfeduSchedule;
using SfeduSchedule.Auth; using SfeduSchedule.Auth;
using SfeduSchedule.Jobs; using SfeduSchedule.Jobs;
@@ -24,29 +26,24 @@ var builder = WebApplication.CreateBuilder(args);
#region Работа с конфигурацией #region Работа с конфигурацией
var configuration = builder.Configuration; var configuration = builder.Configuration;
var preinstalledJwtToken = configuration["TOKEN"]; var preinstalledJwtToken = configuration[AppConsts.PreinstalledJwtTokenEnv];
var tgChatId = configuration["TG_CHAT_ID"]; configuration[AppConsts.ModeusUrlEnv] ??= AppConsts.ModeusDefaultUrl;
var tgToken = configuration["TG_TOKEN"]; configuration[AppConsts.UpdateJwtCronEnv] ??= "0 0 4 ? * *";
var updateJwtCron = configuration["UPDATE_JWT_CRON"] ?? "0 0 4 ? * *"; configuration[AppConsts.UpdateEmployeeCronEnv] ??= "0 0 6 ? * *";
// Если не указана TZ, ставим Europe/Moscow // Если не указана TZ, ставим Europe/Moscow
if (string.IsNullOrEmpty(configuration["TZ"])) configuration["TZ"] ??= "Europe/Moscow";
configuration["TZ"] = "Europe/Moscow";
if (string.IsNullOrEmpty(configuration["MODEUS_URL"])) var permitLimit = int.TryParse(configuration[AppConsts.PermitLimitEnv], out var parsedPermitLimit) ? parsedPermitLimit : 40;
configuration["MODEUS_URL"] = "https://sfedu.modeus.org/"; var timeLimit = int.TryParse(configuration[AppConsts.TimeLimitEnv], out var parsedTimeLimit) ? parsedTimeLimit : 10;
var permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40;
var timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10;
#endregion #endregion
#region Работа с папкой данных #region Работа с папкой данных
// Создать папку data если не существует // Создать папку data если не существует
var dataDirectory = Path.Combine(AppContext.BaseDirectory, "data"); var dataDirectory = Path.Combine(AppContext.BaseDirectory, AppConsts.DataFolderName);
if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory); 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"); var pluginsPath = Path.Combine(dataDirectory, "Plugins");
@@ -63,7 +60,7 @@ builder.Logging.AddConsole(options => options.FormatterName = "CustomConsoleForm
.AddConsoleFormatter<ConsoleFormatter, ConsoleFormatterOptions>(); .AddConsoleFormatter<ConsoleFormatter, ConsoleFormatterOptions>();
builder.Logging.AddFilter("Quartz", LogLevel.Warning); 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 => builder.Logging.AddTelegram(options =>
{ {
options.FormatterConfiguration = new X.Extensions.Logging.Telegram.Base.Configuration.FormatterConfiguration options.FormatterConfiguration = new X.Extensions.Logging.Telegram.Base.Configuration.FormatterConfiguration
@@ -71,8 +68,8 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
IncludeException = true, IncludeException = true,
IncludeProperties = true, IncludeProperties = true,
}; };
options.ChatId = tgChatId; options.ChatId = configuration[AppConsts.TgChatIdEnv]!;
options.AccessToken = tgToken; options.AccessToken = configuration[AppConsts.TgTokenEnv]!;
options.FormatterConfiguration.UseEmoji = true; options.FormatterConfiguration.UseEmoji = true;
options.FormatterConfiguration.ReadableApplicationName = "Modeus Schedule Proxy"; options.FormatterConfiguration.ReadableApplicationName = "Modeus Schedule Proxy";
options.LogLevel = new Dictionary<string, LogLevel> options.LogLevel = new Dictionary<string, LogLevel>
@@ -149,23 +146,35 @@ foreach (var p in loadedPlugins)
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(p.Assembly)); 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<UpdateJwtJob>(opts => opts.WithIdentity(jobKey)); q.AddJob<UpdateJwtJob>(opts => opts.WithIdentity(updateJwtJob));
if (string.IsNullOrEmpty(preinstalledJwtToken))
{
q.AddTrigger(opts => opts
.ForJob(updateJwtJob)
.WithIdentity("UpdateJWTJob-trigger")
.WithCronSchedule(configuration["UPDATE_JWT_CRON"]!)
);
}
q.AddJob<UpdateEmployeesJob>(opts => opts.WithIdentity(nameof(UpdateEmployeesJob)));
q.AddTrigger(opts => opts q.AddTrigger(opts => opts
.ForJob(jobKey) .ForJob(nameof(UpdateEmployeesJob))
.WithIdentity("UpdateJWTJob-trigger") .WithIdentity("UpdateEmployeesJob-trigger")
.WithCronSchedule(updateJwtCron) .WithCronSchedule(configuration[AppConsts.UpdateEmployeeCronEnv]!)
); );
// после успешного выполнения UpdateJwtJob сразу выполняем UpdateEmployeesJob
var chainListener = new JobChainingJobListener("chain");
chainListener.AddJobChainLink(updateJwtJob, new JobKey(nameof(UpdateEmployeesJob)));
q.AddJobListener(chainListener, GroupMatcher<JobKey>.GroupEquals(JobKey.DefaultGroup));
}); });
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = false);
}
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => builder.Services.AddSwaggerGen(options =>
@@ -258,45 +267,48 @@ app.UseForwardedHeaders();
// Корреляция логов по запросам // Корреляция логов по запросам
app.UseMiddleware<CorrelationIdMiddleware>(); app.UseMiddleware<CorrelationIdMiddleware>();
if (string.IsNullOrEmpty(preinstalledJwtToken))
{
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler();
// Проверить существование файла jwt.txt var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
if (File.Exists(GlobalConsts.JwtFilePath)) 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("Обнаружена прошлая сессия"); logger.LogInformation("Дата истечения токена: {ExpirationDate}", expirationDate);
var lines = await File.ReadAllLinesAsync(GlobalConsts.JwtFilePath); if (expirationDate.AddHours(23) > DateTime.Now)
if (lines.Length > 1 && DateTime.TryParse(lines[1], out var expirationDate))
{ {
logger.LogInformation("Дата истечения токена: {ExpirationDate}", expirationDate); var token = lines[0];
if (expirationDate.AddHours(23) > DateTime.Now) logger.LogInformation("Используем существующий токен");
{ configuration["TOKEN"] = token;
var token = lines[0]; refreshJwt = false;
logger.LogInformation("Используем существующий токен");
configuration["TOKEN"] = token;
}
else
{
logger.LogInformation("Токен истек или скоро истечет, выполняем обновление токена");
await scheduler.TriggerJob(jobKey);
}
} }
else else
{ logger.LogInformation("Токен истек или скоро истечет, выполняем обновление токена");
logger.LogInformation(
"Файл jwt.txt не содержит дату истечения или она некорректна, выполняем обновление токена");
await scheduler.TriggerJob(jobKey);
}
} }
else else
{ logger.LogInformation(
await scheduler.TriggerJob(jobKey); "Файл jwt.txt не содержит дату истечения или она некорректна, выполняем обновление токена");
}
} }
// Обновить JWT если нужно, либо сразу запустить обновление сотрудников
if (refreshJwt)
await scheduler.TriggerJob(updateJwtJob);
else else
logger.LogInformation("Используем предустановленный токен из конфигурации"); await scheduler.TriggerJob(new JobKey(nameof(UpdateEmployeesJob)));
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();

View File

@@ -1,13 +1,16 @@
using SfeduSchedule.Logging; using Quartz;
using SfeduSchedule.Jobs;
using System.Text.Json;
namespace SfeduSchedule.Services; namespace SfeduSchedule.Services;
public class ModeusEmployeeService(ILogger<ModeusEmployeeService> logger, ModeusService modeusService) public class ModeusEmployeeService(ISchedulerFactory schedulerFactory)
: IHostedService : IHostedService
{ {
private Dictionary<string, (string, List<string>)> _employees = []; private Dictionary<string, (string, List<string>)> _employees = [];
private Task? _backgroundTask; private Task? _backgroundTask;
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
private readonly string _employeesFilePath = Path.Combine(Path.Combine(AppContext.BaseDirectory, AppConsts.DataFolderName), AppConsts.EmployeesFileName);
public async Task<Dictionary<string, (string, List<string>)>> GetEmployees(string fullname, int size = 10) public async Task<Dictionary<string, (string, List<string>)>> GetEmployees(string fullname, int size = 10)
{ {
@@ -16,35 +19,19 @@ public class ModeusEmployeeService(ILogger<ModeusEmployeeService> logger, Modeus
.Take(size) .Take(size)
.ToDictionary(e => e.Key, e => e.Value); .ToDictionary(e => e.Key, e => e.Value);
} }
public bool IsInitialized()
{
return _employees.Count > 0;
}
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_backgroundTask = Task.Run(async () => _backgroundTask = Task.Run(async () =>
{ {
try // Загрузка с диска
{ await LoadEmployeesFromDisk();
await Task.Delay(TimeSpan.FromSeconds(15), _cts.Token);
var employees = await modeusService.GetEmployeesAsync();
if (employees.Count == 0)
{
logger.LogWarningHere("Не удалось получить список сотрудников из Modeus.");
}
else
{
_employees = employees;
logger.LogInformationHere($"Получено {employees.Count} сотрудников из Modeus.");
}
}
catch (OperationCanceledException)
{
// ignore
}
catch (Exception ex)
{
logger.LogErrorHere(ex, "Ошибка при загрузке сотрудников из Modeus.");
}
}, _cts.Token); }, _cts.Token);
return Task.CompletedTask; return Task.CompletedTask;
@@ -68,4 +55,26 @@ public class ModeusEmployeeService(ILogger<ModeusEmployeeService> logger, Modeus
// ignore // ignore
} }
} }
public async Task SetEmployees(Dictionary<string, (string, List<string>)> employees)
{
_employees = employees;
await SaveEmployeesToDisk();
}
private async Task LoadEmployeesFromDisk()
{
if (File.Exists(_employeesFilePath))
{
var json = await File.ReadAllTextAsync(_employeesFilePath);
_employees = JsonSerializer.Deserialize<Dictionary<string, (string, List<string>)>>(json) ?? new Dictionary<string, (string, List<string>)>();
}
}
private async Task SaveEmployeesToDisk()
{
var json = JsonSerializer.Serialize(_employees, new JsonSerializerOptions { WriteIndented = false });
await File.WriteAllTextAsync(_employeesFilePath, json);
}
} }

View File

@@ -75,13 +75,13 @@ public class ModeusHttpClient
return []; return [];
} }
public async Task<ModeusSearchPersonResponse?> SearchPersonAsync(ModeusSearchPersonRequest modeusSearchPersonRequest) public async Task<ModeusSearchPersonResponse?> SearchPersonAsync(ModeusSearchPersonRequest modeusSearchPersonRequest, CancellationToken cancellationToken = default)
{ {
using var request = new HttpRequestMessage(HttpMethod.Post, using var request = new HttpRequestMessage(HttpMethod.Post,
$"schedule-calendar-v2/api/people/persons/search"); $"schedule-calendar-v2/api/people/persons/search");
request.Content = JsonContent.Create(modeusSearchPersonRequest, options: GlobalConsts.JsonSerializerOptions); request.Content = JsonContent.Create(modeusSearchPersonRequest, options: GlobalConsts.JsonSerializerOptions);
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
var requestMs = stopwatch.ElapsedMilliseconds; var requestMs = stopwatch.ElapsedMilliseconds;
if (response.StatusCode != System.Net.HttpStatusCode.OK) if (response.StatusCode != System.Net.HttpStatusCode.OK)
{ {
@@ -95,7 +95,7 @@ public class ModeusHttpClient
await using var contentStream = await response.Content.ReadAsStreamAsync(); await using var contentStream = await response.Content.ReadAsStreamAsync();
var content = await JsonSerializer.DeserializeAsync<ModeusSearchPersonResponse>( var content = await JsonSerializer.DeserializeAsync<ModeusSearchPersonResponse>(
contentStream, contentStream,
GlobalConsts.JsonSerializerOptions); GlobalConsts.JsonSerializerOptions, cancellationToken);
var groupMs = stopwatch.ElapsedMilliseconds - deserializeStartMs; var groupMs = stopwatch.ElapsedMilliseconds - deserializeStartMs;
_logger.LogInformationHere($"SearchPersonAsync: Request time: {requestMs} ms, Deserialization time: {groupMs} ms, Total time: {stopwatch.ElapsedMilliseconds} ms."); _logger.LogInformationHere($"SearchPersonAsync: Request time: {requestMs} ms, Deserialization time: {groupMs} ms, Total time: {stopwatch.ElapsedMilliseconds} ms.");

View File

@@ -186,10 +186,10 @@ public class ModeusService(
return serializedCalendar; return serializedCalendar;
} }
public async Task<Dictionary<string, (string, List<string>)>> GetEmployeesAsync() public async Task<Dictionary<string, (string, List<string>)>> GetEmployeesAsync(CancellationToken cancellationToken = default)
{ {
var searchPersonResponse = var searchPersonResponse =
await modeusHttpClient.SearchPersonAsync(new ModeusSearchPersonRequest() { Size = 38000 }); await modeusHttpClient.SearchPersonAsync(new ModeusSearchPersonRequest() { Size = 38000 }, cancellationToken);
if (searchPersonResponse == null) if (searchPersonResponse == null)
{ {
logger.LogErrorHere("persons is null"); logger.LogErrorHere("persons is null");

View File

@@ -1,21 +0,0 @@
@SfeduSchedule_HostAddress = http://localhost:5087
###
[Получить расписание по списку GUID]
GET {{SfeduSchedule_HostAddress}}/api/schedule?attendeePersonId={{guid1}}&attendeePersonId={{guid2}}
Accept: application/json
###
[Получить расписание через POST]
POST {{SfeduSchedule_HostAddress}}/api/schedule
Content-Type: application/json
Accept: application/json
{
"maxResults": 500,
"startDate": "2025-08-31T00:00:00Z",
"endDate": "2025-09-20T00:00:00Z",
"attendeePersonId": ["{{guid1}}", "{{guid2}}"]
}
###