Compare commits
11 Commits
ba54deab11
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f08d6af4c7 | |||
| 46bdc07910 | |||
| 46c50dc8e2 | |||
| c3535cafe9 | |||
| a9c0bb1d52 | |||
| 0b9e6d3c95 | |||
| fa8418aedb | |||
| 50ca622b3e | |||
| ed9717df07 | |||
| 39201e561f | |||
| 98705d6bbd |
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine3.22 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine3.22 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ./SfeduSchedule ./SfeduSchedule
|
||||
@@ -7,7 +7,7 @@ WORKDIR /src/SfeduSchedule
|
||||
RUN dotnet restore "SfeduSchedule.csproj"
|
||||
RUN dotnet publish "SfeduSchedule.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||
EXPOSE 8080
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace ModeusSchedule.Abstractions.DTO;
|
||||
namespace ModeusSchedule.Abstractions.DTO.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// DTO для запроса расписания в Modeus.
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SfeduSchedule.DTO.Requests;
|
||||
|
||||
public record ModeusSearchPersonRequest(
|
||||
[property: JsonPropertyName("fullName")]
|
||||
string FullName = "",
|
||||
|
||||
[property: JsonPropertyName("sort")]
|
||||
string Sort = "+fullName",
|
||||
|
||||
[property: JsonPropertyName("size")]
|
||||
int Size = 10,
|
||||
|
||||
[property: JsonPropertyName("page")]
|
||||
int Page = 0
|
||||
);
|
||||
@@ -0,0 +1,84 @@
|
||||
// Auto-generated by https://json2csharp.com/
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SfeduSchedule.DTO.Responses;
|
||||
|
||||
// SearchEmployeesResponse myDeserializedClass = JsonSerializer.Deserialize<SearchEmployeesResponse>(myJsonResponse);
|
||||
|
||||
public record Embedded(
|
||||
[property: JsonPropertyName("persons")]
|
||||
IReadOnlyList<Person> Persons,
|
||||
[property: JsonPropertyName("employees")]
|
||||
IReadOnlyList<Employee> Employees,
|
||||
[property: JsonPropertyName("students")]
|
||||
IReadOnlyList<Student> Students
|
||||
);
|
||||
|
||||
public record Employee(
|
||||
[property: JsonPropertyName("id")] string Id,
|
||||
[property: JsonPropertyName("personId")]
|
||||
string PersonId,
|
||||
[property: JsonPropertyName("groupId")]
|
||||
string GroupId,
|
||||
[property: JsonPropertyName("groupName")]
|
||||
string GroupName,
|
||||
[property: JsonPropertyName("dateIn")] string DateIn,
|
||||
[property: JsonPropertyName("dateOut")]
|
||||
string DateOut
|
||||
);
|
||||
|
||||
public record Links(
|
||||
[property: JsonPropertyName("self")] Self Self
|
||||
);
|
||||
|
||||
public record Page(
|
||||
[property: JsonPropertyName("size")] int? Size,
|
||||
[property: JsonPropertyName("totalElements")]
|
||||
int? TotalElements,
|
||||
[property: JsonPropertyName("totalPages")]
|
||||
int? TotalPages,
|
||||
[property: JsonPropertyName("number")] int? Number
|
||||
);
|
||||
|
||||
public record Person(
|
||||
[property: JsonPropertyName("lastName")]
|
||||
string LastName,
|
||||
[property: JsonPropertyName("firstName")]
|
||||
string FirstName,
|
||||
[property: JsonPropertyName("middleName")]
|
||||
string MiddleName,
|
||||
[property: JsonPropertyName("fullName")]
|
||||
string FullName,
|
||||
[property: JsonPropertyName("_links")] Links Links,
|
||||
[property: JsonPropertyName("id")] string Id
|
||||
);
|
||||
|
||||
public record ModeusSearchPersonResponse(
|
||||
[property: JsonPropertyName("_embedded")]
|
||||
Embedded Embedded,
|
||||
[property: JsonPropertyName("page")] Page Page
|
||||
);
|
||||
|
||||
public record Self(
|
||||
[property: JsonPropertyName("href")] string Href
|
||||
);
|
||||
|
||||
public record Student(
|
||||
[property: JsonPropertyName("id")] string Id,
|
||||
[property: JsonPropertyName("personId")]
|
||||
string PersonId,
|
||||
[property: JsonPropertyName("flowId")] string FlowId,
|
||||
[property: JsonPropertyName("flowCode")]
|
||||
string FlowCode,
|
||||
[property: JsonPropertyName("specialtyCode")]
|
||||
string SpecialtyCode,
|
||||
[property: JsonPropertyName("specialtyName")]
|
||||
string SpecialtyName,
|
||||
[property: JsonPropertyName("specialtyProfile")]
|
||||
string SpecialtyProfile,
|
||||
[property: JsonPropertyName("learningStartDate")]
|
||||
DateTime? LearningStartDate,
|
||||
[property: JsonPropertyName("learningEndDate")]
|
||||
DateTime? LearningEndDate
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SfeduSchedule.DTO.Responses;
|
||||
|
||||
public record SearchPersonResponse(
|
||||
IReadOnlyList<SearchPerson> Persons
|
||||
);
|
||||
|
||||
public record SearchPerson(
|
||||
[property: JsonPropertyName("name")]
|
||||
string Name,
|
||||
[property: JsonPropertyName("person_id")]
|
||||
string PersonId
|
||||
);
|
||||
@@ -8,5 +8,4 @@ public static class GlobalConsts
|
||||
public static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
|
||||
|
||||
public static string JwtFilePath { get; set; } = "data/jwt.txt";
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<OutputType>Library</OutputType>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SfeduSchedule.Plugin.Sample.plugin</AssemblyName>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- Кладём зависимости плагина рядом со сборкой, чтобы загрузчик их нашёл -->
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
|
||||
30
SfeduSchedule/AppConsts.cs
Normal file
30
SfeduSchedule/AppConsts.cs
Normal 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";
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using ModeusSchedule.Abstractions;
|
||||
using ModeusSchedule.Abstractions.DTO;
|
||||
using ModeusSchedule.Abstractions.DTO.Requests;
|
||||
using SfeduSchedule.Services;
|
||||
|
||||
namespace SfeduSchedule.Controllers;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using ModeusSchedule.Abstractions.DTO;
|
||||
using ModeusSchedule.Abstractions.DTO.Requests;
|
||||
using SfeduSchedule.DTO.Responses;
|
||||
using SfeduSchedule.Services;
|
||||
|
||||
namespace SfeduSchedule.Controllers;
|
||||
@@ -10,13 +13,13 @@ namespace SfeduSchedule.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/schedule")]
|
||||
[EnableRateLimiting("throttle")]
|
||||
public class ScheduleController(ModeusService modeusService, ILogger<ScheduleController> logger) : ControllerBase
|
||||
public class ScheduleController(ModeusService modeusService, ModeusEmployeeService modeusEmployeeService, ILogger<ScheduleController> logger) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Получить GUID пользователя по полному имени. (требуется авторизация)
|
||||
/// Получить GUID пользователя по полному имени. (включая студентов, требуется авторизация)
|
||||
/// </summary>
|
||||
/// <param name="fullname">Полное имя пользователя.</param>
|
||||
/// <returns>GUID пользователя.</returns>
|
||||
/// <returns>GUID пользователя</returns>
|
||||
/// <response code="200">Возвращает GUID пользователя</response>
|
||||
/// <response code="404">Пользователь не найден</response>
|
||||
/// <response code="401">Неавторизованный</response>
|
||||
@@ -32,6 +35,33 @@ public class ScheduleController(ModeusService modeusService, ILogger<ScheduleCon
|
||||
|
||||
return Ok(guid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск сотрудников по имени. (преподавателей)
|
||||
/// </summary>
|
||||
/// <param name="fullname">ФИО Сотрудника</param>
|
||||
/// <returns>Список сотрудников (до 10 записей)</returns>
|
||||
/// <response code="200">Возвращает список сотрудников с их GUID</response>
|
||||
/// <response code="404">Сотрудник не найден</response>
|
||||
/// <response code="401">Неавторизованный</response>
|
||||
/// <response code="503">Сервис сотрудников не инициализирован</response>
|
||||
[HttpGet]
|
||||
[Route("searchemployee")]
|
||||
public async Task<IActionResult> SearchEmployees([Required][MinLength(1)] string fullname)
|
||||
{
|
||||
if (!modeusEmployeeService.IsInitialized())
|
||||
return StatusCode(503, "Сервис сотрудников не инициализирован, попробуйте позже.");
|
||||
|
||||
var employees = await modeusEmployeeService.GetEmployees(fullname, 10);
|
||||
if (employees.Count == 0)
|
||||
return NotFound();
|
||||
|
||||
var persons = new List<SearchPerson>(employees.Count);
|
||||
foreach (var employee in employees)
|
||||
persons.Add(new SearchPerson(Name: employee.Key, PersonId: employee.Value.Item1));
|
||||
|
||||
return Ok(new SearchPersonResponse(Persons: persons).Persons);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить расписание в формате ICS по пользовательскому запросу.
|
||||
|
||||
78
SfeduSchedule/Jobs/UpdateEmployeesJob.cs
Normal file
78
SfeduSchedule/Jobs/UpdateEmployeesJob.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,7 @@ public class UpdateJwtJob(
|
||||
IConfiguration configuration,
|
||||
ILogger<UpdateJwtJob> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ModeusHttpClient modeusHttpClient,
|
||||
ModeusService modeusService) : IJob
|
||||
ModeusHttpClient modeusHttpClient) : IJob
|
||||
{
|
||||
private const int MaxAttempts = 5; // Максимальное число попыток
|
||||
private const int DelaySeconds = 20; // Задержка между попытками в секундах
|
||||
@@ -19,8 +18,8 @@ public class UpdateJwtJob(
|
||||
{
|
||||
logger.LogInformation("Начало выполнения UpdateJwtJob");
|
||||
|
||||
var authUrl = configuration["AUTH_URL"] ?? "http://msauth:8080/auth/ms";
|
||||
var apiKey = configuration["AUTH_API_KEY"] ?? string.Empty;
|
||||
var authUrl = configuration[AppConsts.AuthUrlEnv] ?? "http://msauth:8080/auth/ms";
|
||||
var apiKey = configuration[AppConsts.AuthApiKeyEnv] ?? string.Empty;
|
||||
|
||||
var client = httpClientFactory.CreateClient("authClient");
|
||||
client.Timeout = TimeSpan.FromSeconds(TimeoutSeconds + 10);
|
||||
@@ -72,7 +71,7 @@ public class UpdateJwtJob(
|
||||
|
||||
configuration["TOKEN"] = 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);
|
||||
logger.LogInformation("JWT успешно обновлён");
|
||||
return;
|
||||
|
||||
@@ -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,11 +60,16 @@ builder.Logging.AddConsole(options => options.FormatterName = "CustomConsoleForm
|
||||
.AddConsoleFormatter<ConsoleFormatter, ConsoleFormatterOptions>();
|
||||
|
||||
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.ChatId = tgChatId;
|
||||
options.AccessToken = tgToken;
|
||||
options.FormatterConfiguration = new X.Extensions.Logging.Telegram.Base.Configuration.FormatterConfiguration
|
||||
{
|
||||
IncludeException = true,
|
||||
IncludeProperties = true,
|
||||
};
|
||||
options.ChatId = configuration[AppConsts.TgChatIdEnv]!;
|
||||
options.AccessToken = configuration[AppConsts.TgTokenEnv]!;
|
||||
options.FormatterConfiguration.UseEmoji = true;
|
||||
options.FormatterConfiguration.ReadableApplicationName = "Modeus Schedule Proxy";
|
||||
options.LogLevel = new Dictionary<string, LogLevel>
|
||||
@@ -87,6 +89,8 @@ builder.Services.AddHttpClient("modeus", client =>
|
||||
client.BaseAddress = new Uri(configuration["MODEUS_URL"]!);
|
||||
});
|
||||
builder.Services.AddSingleton<ModeusHttpClient>();
|
||||
builder.Services.AddSingleton<ModeusEmployeeService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ModeusEmployeeService>());
|
||||
builder.Services.AddSingleton<ModeusService>();
|
||||
builder.Services.AddHttpClient("authClient");
|
||||
|
||||
@@ -142,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<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
|
||||
.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<JobKey>.GroupEquals(JobKey.DefaultGroup));
|
||||
});
|
||||
|
||||
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||
}
|
||||
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = false);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
@@ -251,44 +267,49 @@ app.UseForwardedHeaders();
|
||||
// Корреляция логов по запросам
|
||||
app.UseMiddleware<CorrelationIdMiddleware>();
|
||||
|
||||
if (string.IsNullOrEmpty(preinstalledJwtToken))
|
||||
{
|
||||
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
|
||||
// Проверить существование файла jwt.txt
|
||||
if (File.Exists(GlobalConsts.JwtFilePath))
|
||||
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
|
||||
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
|
||||
await scheduler.TriggerJob(new JobKey(nameof(UpdateEmployeesJob)));
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
|
||||
80
SfeduSchedule/Services/ModeusEmployeeService.cs
Normal file
80
SfeduSchedule/Services/ModeusEmployeeService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Quartz;
|
||||
using SfeduSchedule.Jobs;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SfeduSchedule.Services;
|
||||
|
||||
public class ModeusEmployeeService(ISchedulerFactory schedulerFactory)
|
||||
: IHostedService
|
||||
{
|
||||
private Dictionary<string, (string, List<string>)> _employees = [];
|
||||
private Task? _backgroundTask;
|
||||
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)
|
||||
{
|
||||
return _employees
|
||||
.Where(e => e.Key.Contains(fullname, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(size)
|
||||
.ToDictionary(e => e.Key, e => e.Value);
|
||||
}
|
||||
|
||||
public bool IsInitialized()
|
||||
{
|
||||
return _employees.Count > 0;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_backgroundTask = Task.Run(async () =>
|
||||
{
|
||||
// Загрузка с диска
|
||||
await LoadEmployeesFromDisk();
|
||||
}, _cts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cts is null || _backgroundTask is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cts.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAny(_backgroundTask, Task.Delay(TimeSpan.FromSeconds(5), cancellationToken));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using ModeusSchedule.Abstractions;
|
||||
using ModeusSchedule.Abstractions.DTO;
|
||||
using ModeusSchedule.Abstractions.DTO.Requests;
|
||||
using SfeduSchedule.DTO.Requests;
|
||||
using SfeduSchedule.DTO.Responses;
|
||||
using SfeduSchedule.Logging;
|
||||
|
||||
namespace SfeduSchedule.Services;
|
||||
@@ -34,11 +38,10 @@ public class ModeusHttpClient
|
||||
|
||||
public async Task<string?> GetScheduleAsync(ModeusScheduleRequest msr)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post,
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post,
|
||||
$"schedule-calendar-v2/api/calendar/events/search?tz={_configuration["TZ"]!}");
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions),
|
||||
Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
request.Content = JsonContent.Create(msr, options: GlobalConsts.JsonSerializerOptions);
|
||||
using var response = await _httpClient.SendAsync(request);
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogErrorHere($"Неуспешный статус при получении расписания: {response.StatusCode}, Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
@@ -49,34 +52,69 @@ public class ModeusHttpClient
|
||||
|
||||
public async Task<List<Attendees>> GetAttendeesAsync(Guid eventId)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get,
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get,
|
||||
$"schedule-calendar-v2/api/calendar/events/{eventId}/attendees");
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
using var response = await _httpClient.SendAsync(request);
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogErrorHere($"Неуспешный статус при получении расписания: {response.StatusCode}, eventId: {eventId}");
|
||||
}
|
||||
List<Attendees>? attendees;
|
||||
try
|
||||
{
|
||||
attendees = Attendees.FromJson(await response.Content.ReadAsStringAsync());
|
||||
return attendees;
|
||||
await using var contentStream = await response.Content.ReadAsStreamAsync();
|
||||
var attendees = await JsonSerializer.DeserializeAsync<List<Attendees>>(
|
||||
contentStream,
|
||||
GlobalConsts.JsonSerializerOptions);
|
||||
return attendees ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogErrorHere(ex, "Deserialization failed.");
|
||||
}
|
||||
|
||||
return new List<Attendees>();
|
||||
return [];
|
||||
}
|
||||
|
||||
public async Task<ModeusSearchPersonResponse?> SearchPersonAsync(ModeusSearchPersonRequest modeusSearchPersonRequest, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post,
|
||||
$"schedule-calendar-v2/api/people/persons/search");
|
||||
request.Content = JsonContent.Create(modeusSearchPersonRequest, options: GlobalConsts.JsonSerializerOptions);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
var requestMs = stopwatch.ElapsedMilliseconds;
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogErrorHere($"Неуспешный статус при получении расписания: {response.StatusCode}, Поле ФИО: \"{modeusSearchPersonRequest.FullName}\"");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var deserializeStartMs = stopwatch.ElapsedMilliseconds;
|
||||
await using var contentStream = await response.Content.ReadAsStreamAsync();
|
||||
var content = await JsonSerializer.DeserializeAsync<ModeusSearchPersonResponse>(
|
||||
contentStream,
|
||||
GlobalConsts.JsonSerializerOptions, cancellationToken);
|
||||
var groupMs = stopwatch.ElapsedMilliseconds - deserializeStartMs;
|
||||
|
||||
_logger.LogInformationHere($"SearchPersonAsync: Request time: {requestMs} ms, Deserialization time: {groupMs} ms, Total time: {stopwatch.ElapsedMilliseconds} ms.");
|
||||
|
||||
return content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogErrorHere(ex, "SearchPersonAsync: Deserialization failed.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<string?> SearchRoomsAsync(RoomSearchRequest requestDto)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/campus/rooms/search");
|
||||
request.Content =
|
||||
new StringContent(JsonSerializer.Serialize(requestDto, GlobalConsts.JsonSerializerOptions),
|
||||
Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/campus/rooms/search");
|
||||
request.Content = JsonContent.Create(requestDto, options: GlobalConsts.JsonSerializerOptions);
|
||||
using var response = await _httpClient.SendAsync(request);
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogErrorHere($"Неуспешный статус при получении расписания: {response.StatusCode}, Request: {JsonSerializer.Serialize(requestDto, GlobalConsts.JsonSerializerOptions)}");
|
||||
@@ -85,43 +123,4 @@ public class ModeusHttpClient
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
public async Task<string?> GetGuidAsync(string fullName)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/people/persons/search");
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(new
|
||||
{
|
||||
fullName,
|
||||
sort = "+fullName",
|
||||
size = 10,
|
||||
page = 0
|
||||
}), Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
_logger.LogInformationHere($"Ответ получен: {response.StatusCode}");
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogErrorHere($"Неуспешный статус при получении расписания: {response.StatusCode}, Name: {fullName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
string? personId;
|
||||
try
|
||||
{
|
||||
personId = JsonDocument.Parse(json).RootElement
|
||||
.GetProperty("_embedded")
|
||||
.GetProperty("persons")[0]
|
||||
.GetProperty("id")
|
||||
.GetString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogWarningHere($"Не удалось получить идентификатор пользователя. FullName={fullName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return personId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Ical.Net;
|
||||
using Ical.Net.CalendarComponents;
|
||||
@@ -5,33 +6,25 @@ using Ical.Net.DataTypes;
|
||||
using Ical.Net.Serialization;
|
||||
using ModeusSchedule.Abstractions;
|
||||
using ModeusSchedule.Abstractions.DTO;
|
||||
using ModeusSchedule.Abstractions.DTO.Requests;
|
||||
using SfeduSchedule.DTO.Requests;
|
||||
using SfeduSchedule.DTO.Responses;
|
||||
using SfeduSchedule.Logging;
|
||||
|
||||
namespace SfeduSchedule.Services;
|
||||
|
||||
public class ModeusService
|
||||
public class ModeusService(
|
||||
ILogger<ModeusService> logger,
|
||||
IConfiguration configuration,
|
||||
ModeusHttpClient modeusHttpClient)
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
private readonly ILogger<ModeusService> _logger;
|
||||
private readonly ModeusHttpClient _modeusHttpClient;
|
||||
|
||||
public ModeusService(
|
||||
ILogger<ModeusService> logger,
|
||||
IConfiguration configuration,
|
||||
ModeusHttpClient modeusHttpClient)
|
||||
{
|
||||
_modeusHttpClient = modeusHttpClient;
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<Schedule?> GetScheduleJsonAsync(ModeusScheduleRequest msr)
|
||||
{
|
||||
var schedule = await GetScheduleAsync(msr);
|
||||
if (schedule == null)
|
||||
{
|
||||
_logger.LogErrorHere($"schedule is null. {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
logger.LogErrorHere(
|
||||
$"schedule is null. {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -42,16 +35,20 @@ public class ModeusService
|
||||
switch (scheduleJson)
|
||||
{
|
||||
case null:
|
||||
_logger.LogErrorHere($"scheduleJson is null. Schedule: {schedule}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
logger.LogErrorHere(
|
||||
$"scheduleJson is null. Schedule: {schedule}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
break;
|
||||
case { Embedded: null }:
|
||||
_logger.LogErrorHere($"scheduleJson.Embedded is null. Response: {schedule}\nscheduleJson: {scheduleJson}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
logger.LogErrorHere(
|
||||
$"scheduleJson.Embedded is null. Response: {schedule}\nscheduleJson: {scheduleJson}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
break;
|
||||
case { Embedded.Events: null }:
|
||||
_logger.LogErrorHere($"scheduleJson.Embedded.Events is null. Response: {schedule}\nscheduleJson: {scheduleJson}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
logger.LogErrorHere(
|
||||
$"scheduleJson.Embedded.Events is null. Response: {schedule}\nscheduleJson: {scheduleJson}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
break;
|
||||
case { Embedded.Events.Length: 0 }:
|
||||
_logger.LogWarningHere($"scheduleJson.Embedded.Events is empty. Embedded: {scheduleJson.Embedded}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
logger.LogWarningHere(
|
||||
$"scheduleJson.Embedded.Events is empty. Embedded: {scheduleJson.Embedded}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
break;
|
||||
default:
|
||||
return scheduleJson;
|
||||
@@ -59,7 +56,8 @@ public class ModeusService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogErrorHere($"Deserialization failed. Schedule: {schedule}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}\n Exception: {ex}");
|
||||
logger.LogErrorHere(
|
||||
$"Deserialization failed. Schedule: {schedule}\n Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}\n Exception: {ex}");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -70,12 +68,13 @@ public class ModeusService
|
||||
var scheduleJson = await GetScheduleJsonAsync(msr);
|
||||
if (scheduleJson == null)
|
||||
{
|
||||
_logger.LogErrorHere($"scheduleJson is null after deserialization. Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
logger.LogErrorHere(
|
||||
$"scheduleJson is null after deserialization. Request: {JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var calendar = new Calendar();
|
||||
calendar.AddTimeZone(new VTimeZone(_configuration["TZ"]!));
|
||||
calendar.AddTimeZone(new VTimeZone(configuration["TZ"]!));
|
||||
|
||||
foreach (var e in scheduleJson.Embedded.Events)
|
||||
{
|
||||
@@ -91,7 +90,7 @@ public class ModeusService
|
||||
{
|
||||
var eventRoomsLink = eventLocation.Links.EventRooms.Value;
|
||||
var eventRoomsHref = eventRoomsLink.Self?.Href
|
||||
?? eventRoomsLink.SelfArray?.FirstOrDefault()?.Href;
|
||||
?? eventRoomsLink.SelfArray?.FirstOrDefault()?.Href;
|
||||
if (string.IsNullOrWhiteSpace(eventRoomsHref))
|
||||
continue;
|
||||
|
||||
@@ -176,37 +175,150 @@ public class ModeusService
|
||||
Summary = (string.IsNullOrEmpty(shortNameCourse) ? "" : shortNameCourse + " / ") + e.Name,
|
||||
Description = e.NameShort + (string.IsNullOrEmpty(roomName) ? "" : $"\nАудитория: {roomName}") +
|
||||
(string.IsNullOrEmpty(teachersNames) ? "" : $"\nПреподаватели: {teachersNames}"),
|
||||
Start = new CalDateTime(e.StartsAtLocal, _configuration["TZ"]!),
|
||||
End = new CalDateTime(e.EndsAtLocal, _configuration["TZ"]!)
|
||||
Start = new CalDateTime(e.StartsAtLocal, configuration["TZ"]!),
|
||||
End = new CalDateTime(e.EndsAtLocal, configuration["TZ"]!)
|
||||
});
|
||||
}
|
||||
|
||||
var serializer = new CalendarSerializer();
|
||||
var serializedCalendar = serializer.SerializeToString(calendar);
|
||||
_logger.LogInformationHere($"serialized calendar created. Length: {serializedCalendar?.Length ?? 0}");
|
||||
logger.LogInformationHere($"serialized calendar created. Length: {serializedCalendar?.Length ?? 0}");
|
||||
return serializedCalendar;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, (string, List<string>)>> GetEmployeesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var searchPersonResponse =
|
||||
await modeusHttpClient.SearchPersonAsync(new ModeusSearchPersonRequest() { Size = 38000 }, cancellationToken);
|
||||
if (searchPersonResponse == null)
|
||||
{
|
||||
logger.LogErrorHere("persons is null");
|
||||
return [];
|
||||
}
|
||||
|
||||
var persons = searchPersonResponse.Embedded.Persons;
|
||||
var employees = searchPersonResponse.Embedded.Employees;
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var personsById = new Dictionary<string, SfeduSchedule.DTO.Responses.Person>();
|
||||
foreach (var p in persons)
|
||||
{
|
||||
var id = p?.Id;
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
{
|
||||
personsById[id] = p;
|
||||
}
|
||||
}
|
||||
|
||||
var mapMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
var groupStartMs = stopwatch.ElapsedMilliseconds;
|
||||
var grouped =
|
||||
new Dictionary<string, (string PersonId, List<string> Positions)>(employees.Count,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
|
||||
foreach (var e in employees)
|
||||
{
|
||||
if (e == null) continue;
|
||||
var personId = e.PersonId ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(personId)) continue;
|
||||
|
||||
var fullName = personId;
|
||||
if (personsById.TryGetValue(personId, out var person))
|
||||
{
|
||||
var name = (person.FullName ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
fullName = name;
|
||||
}
|
||||
}
|
||||
|
||||
var position = (e.GroupName ?? string.Empty).Trim();
|
||||
|
||||
static string FormatDateRange(string? dateIn, string? dateOut)
|
||||
{
|
||||
var start = (dateIn ?? string.Empty).Trim();
|
||||
if (string.IsNullOrEmpty(start)) start = "?";
|
||||
|
||||
var end = (dateOut ?? string.Empty).Trim();
|
||||
if (string.IsNullOrEmpty(end)) end = "по наст.вр.";
|
||||
|
||||
return $"{start}–{end}";
|
||||
}
|
||||
|
||||
var dateRange = FormatDateRange(e.DateIn, e.DateOut);
|
||||
var positionWithDates = string.IsNullOrEmpty(position) ? $"({dateRange})" : $"{position} ({dateRange})";
|
||||
|
||||
if (!grouped.TryGetValue(fullName, out var entry))
|
||||
{
|
||||
entry = (personId, new List<string>());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.PersonId) || entry.PersonId == personId)
|
||||
{
|
||||
entry.PersonId = personId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry.Positions.Contains(positionWithDates))
|
||||
{
|
||||
entry.Positions.Add(positionWithDates);
|
||||
}
|
||||
|
||||
grouped[fullName] = entry;
|
||||
}
|
||||
|
||||
var groupMs = stopwatch.ElapsedMilliseconds - groupStartMs;
|
||||
var totalMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
logger.LogInformationHere(
|
||||
$"GetEmployeesAsync timing: mapPersons={mapMs}ms, groupEmployees={groupMs}ms, total={totalMs}ms");
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
#region Проксирование методов из ModeusHttpClient
|
||||
|
||||
public async Task<string?> SearchRoomsAsync(RoomSearchRequest request)
|
||||
{
|
||||
return await _modeusHttpClient.SearchRoomsAsync(request);
|
||||
return await modeusHttpClient.SearchRoomsAsync(request);
|
||||
}
|
||||
|
||||
public async Task<string?> GetScheduleAsync(ModeusScheduleRequest msr)
|
||||
{
|
||||
return await _modeusHttpClient.GetScheduleAsync(msr);
|
||||
return await modeusHttpClient.GetScheduleAsync(msr);
|
||||
}
|
||||
|
||||
public async Task<string?> GetGuidAsync(string fullname)
|
||||
{
|
||||
return await _modeusHttpClient.GetGuidAsync(fullname);
|
||||
var searchPersonResponse = await modeusHttpClient.SearchPersonAsync(new ModeusSearchPersonRequest
|
||||
{ FullName = fullname, Page = 0, Size = 10, Sort = "+fullName" });
|
||||
if (searchPersonResponse == null)
|
||||
{
|
||||
logger.LogErrorHere($"Не удалось получить ответ от Modeus при поиске пользователя. FullName={fullname}");
|
||||
return null;
|
||||
}
|
||||
|
||||
string? personId;
|
||||
try
|
||||
{
|
||||
personId = searchPersonResponse.Embedded.Persons[0].Id;
|
||||
return personId;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarningHere(
|
||||
$"Не удалось получить идентификатор пользователя. FullName={fullname}. Ответ Modeus: {JsonSerializer.Serialize(searchPersonResponse, GlobalConsts.JsonSerializerOptions)}. Exception: {exception}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Attendees>> GetAttendeesAsync(Guid eventId)
|
||||
{
|
||||
return await _modeusHttpClient.GetAttendeesAsync(eventId);
|
||||
return await modeusHttpClient.GetAttendeesAsync(eventId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
@@ -10,11 +10,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ical.Net" Version="5.1.2"/>
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.1"/>
|
||||
<PackageReference Include="Ical.Net" Version="5.2.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="4.3.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
<PackageReference Include="X.Extensions.Logging.Telegram" Version="2.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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}}"]
|
||||
}
|
||||
|
||||
###
|
||||
Reference in New Issue
Block a user