Compare commits
3
Commits
01bab77900
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ca42581e6 | ||
|
|
672d7c40b4 | ||
|
|
c590a8a6c7 |
No files matched your search
@@ -10,4 +10,8 @@ public interface IPlugin
|
|||||||
|
|
||||||
// Регистрация маршрутов (Minimal API) плагина после Build()
|
// Регистрация маршрутов (Minimal API) плагина после Build()
|
||||||
void MapEndpoints(IEndpointRouteBuilder endpoints);
|
void MapEndpoints(IEndpointRouteBuilder endpoints);
|
||||||
|
|
||||||
|
// Инвалидация или обновление внутренних кэшей плагина.
|
||||||
|
Task InvalidateCacheAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||||
|
Task.CompletedTask;
|
||||||
}
|
}
|
||||||
+1
-1
Submodule ModeusSchedule.MSAuth updated: 89316e247f...2c4fccb22d.
@@ -86,13 +86,18 @@
|
|||||||
Обновление выполняет Quartz job [UpdateEmployeesJob](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Jobs/UpdateEmployeesJob.cs):
|
Обновление выполняет Quartz job [UpdateEmployeesJob](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Jobs/UpdateEmployeesJob.cs):
|
||||||
|
|
||||||
- ходит в `Modeus`;
|
- ходит в `Modeus`;
|
||||||
- загружает большой список сотрудников;
|
- загружает список людей из Modeus и проверяет полноту результата;
|
||||||
|
- сохраняет общий индекс людей с признаками студента и сотрудника в `data/people.json`;
|
||||||
- группирует записи по ФИО;
|
- группирует записи по ФИО;
|
||||||
- сохраняет результат на диск;
|
- сохраняет результат на диск;
|
||||||
|
- после успешного обновления предлагает всем плагинам инвалидировать свои кэши;
|
||||||
- повторяет попытки при временных сбоях.
|
- повторяет попытки при временных сбоях.
|
||||||
|
|
||||||
Если сервис ещё не инициализирован, `GET /api/schedule/searchemployee` возвращает `503`.
|
Если сервис ещё не инициализирован, `GET /api/schedule/searchemployee` возвращает `503`.
|
||||||
|
|
||||||
|
`ModeusPeopleService` предоставляет плагинам последний полный снимок людей и восстанавливает его при запуске. Новая версия файла заменяет предыдущую атомарно;
|
||||||
|
ошибка загрузки или записи сохраняет рабочий снимок. Для всего прохода страниц выделено пять минут. Контракт поиска преподавателей не меняется, студенты в его ответ не добавляются.
|
||||||
|
|
||||||
### 5. JWT lifecycle
|
### 5. JWT lifecycle
|
||||||
|
|
||||||
Quartz job [UpdateJwtJob](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Jobs/UpdateJWTJob.cs):
|
Quartz job [UpdateJwtJob](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Jobs/UpdateJWTJob.cs):
|
||||||
@@ -131,7 +136,11 @@ Quartz job [UpdateJwtJob](/home/serega404/Рабочий стол/SfeduSchedule/
|
|||||||
На старте хост ищет все `*.plugin.dll` внутри `data/Plugins` и загружает найденные сборки как плагины. Плагин должен реализовывать интерфейс [IPlugin](/home/serega404/Рабочий стол/SfeduSchedule/ModeusSchedule.Abstractions/IPlugin.cs), чтобы:
|
На старте хост ищет все `*.plugin.dll` внутри `data/Plugins` и загружает найденные сборки как плагины. Плагин должен реализовывать интерфейс [IPlugin](/home/serega404/Рабочий стол/SfeduSchedule/ModeusSchedule.Abstractions/IPlugin.cs), чтобы:
|
||||||
|
|
||||||
- зарегистрировать свои сервисы в DI через `ConfigureServices`;
|
- зарегистрировать свои сервисы в DI через `ConfigureServices`;
|
||||||
- добавить minimal API маршруты через `MapEndpoints`.
|
- добавить minimal API маршруты через `MapEndpoints`;
|
||||||
|
- опционально обновить или очистить свои кэши через `InvalidateCacheAsync`.
|
||||||
|
|
||||||
|
`InvalidateCacheAsync` имеет default-реализацию, поэтому плагины, собранные до добавления этого метода,
|
||||||
|
продолжают загружаться. Ошибка инвалидации одного плагина логируется и не мешает вызвать остальные.
|
||||||
|
|
||||||
MVC-контроллеры из плагинов тоже подключаются автоматически через `ApplicationPart`.
|
MVC-контроллеры из плагинов тоже подключаются автоматически через `ApplicationPart`.
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,18 @@ using SfeduSchedule.Services;
|
|||||||
|
|
||||||
namespace SfeduSchedule.Jobs;
|
namespace SfeduSchedule.Jobs;
|
||||||
|
|
||||||
// TODO: Обновляет список сотрудников из modeus и сохраняет в локальный файл
|
|
||||||
// TODO: Нужно вынести функционал обновления сюда, а в сервисе оставить только загрузку с диска
|
|
||||||
// TODO: Нужно настроить выполнение задачи на часов 10 утра каждый день
|
|
||||||
// TODO: Нужно обработать событие когда данные о сотрудниках запрашиваются до первого обновления
|
|
||||||
|
|
||||||
|
[DisallowConcurrentExecution]
|
||||||
public class UpdateEmployeesJob(
|
public class UpdateEmployeesJob(
|
||||||
ILogger<UpdateEmployeesJob> logger,
|
ILogger<UpdateEmployeesJob> logger,
|
||||||
ModeusEmployeeService employeeService,
|
ModeusEmployeeService employeeService,
|
||||||
ModeusService modeusService) : IJob
|
ModeusService modeusService,
|
||||||
|
ModeusPeopleService peopleService,
|
||||||
|
PluginCacheInvalidator pluginCacheInvalidator) : IJob
|
||||||
{
|
{
|
||||||
private const int MaxAttempts = 5; // Максимальное число попыток
|
private const int MaxAttempts = 5; // Максимальное число попыток
|
||||||
private const int DelaySeconds = 50; // Задержка между попытками в секундах
|
private const int DelaySeconds = 50; // Задержка между попытками в секундах
|
||||||
private const int TimeoutSeconds = 60; // Таймаут для каждого запроса в секундах
|
private const int TimeoutSeconds = 300; // Таймаут полной загрузки всех страниц в секундах
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext jobContext)
|
public async Task Execute(IJobExecutionContext jobContext)
|
||||||
{
|
{
|
||||||
@@ -28,16 +27,18 @@ public class UpdateEmployeesJob(
|
|||||||
logger.LogInformation("Попытка {Attempt}/{MaxAttempts} получения списка сотрудников из Modeus", attempt,
|
logger.LogInformation("Попытка {Attempt}/{MaxAttempts} получения списка сотрудников из Modeus", attempt,
|
||||||
MaxAttempts);
|
MaxAttempts);
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutSeconds));
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(jobContext.CancellationToken);
|
||||||
|
cts.CancelAfter(TimeSpan.FromSeconds(TimeoutSeconds));
|
||||||
|
|
||||||
var employees = await modeusService.GetEmployeesAsync(cts.Token);
|
var people = await modeusService.GetPeopleAsync(cts.Token);
|
||||||
|
var employees = ModeusService.GetEmployees(people);
|
||||||
if (employees.Count == 0)
|
if (employees.Count == 0)
|
||||||
{
|
{
|
||||||
logger.LogWarningHere("Не удалось получить список сотрудников из Modeus.");
|
logger.LogWarningHere("Не удалось получить список сотрудников из Modeus.");
|
||||||
|
|
||||||
if (attempt == MaxAttempts)
|
if (attempt == MaxAttempts)
|
||||||
{
|
{
|
||||||
logger.LogError("Достигнуто максимальное число попыток получения JWT");
|
logger.LogError("Достигнуто максимальное число попыток загрузки людей Modeus");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,18 +46,24 @@ public class UpdateEmployeesJob(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await peopleService.SetAsync(people, cts.Token);
|
||||||
await employeeService.SetEmployees(employees);
|
await employeeService.SetEmployees(employees);
|
||||||
logger.LogInformationHere($"Получено {employees.Count} сотрудников из Modeus.");
|
logger.LogInformationHere($"Получено {employees.Count} сотрудников из Modeus.");
|
||||||
|
await pluginCacheInvalidator.InvalidateAsync(jobContext.CancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (jobContext.CancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Таймаут при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt,
|
logger.LogWarning(ex, "Таймаут при загрузке людей Modeus (попытка {Attempt}/{MaxAttempts})", attempt,
|
||||||
MaxAttempts);
|
MaxAttempts);
|
||||||
|
|
||||||
if (attempt == MaxAttempts)
|
if (attempt == MaxAttempts)
|
||||||
{
|
{
|
||||||
logger.LogError("Достигнут лимит по таймаутам при запросе JWT");
|
logger.LogError("Достигнут лимит по таймаутам при загрузке людей Modeus");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +75,7 @@ public class UpdateEmployeesJob(
|
|||||||
|
|
||||||
if (attempt == MaxAttempts)
|
if (attempt == MaxAttempts)
|
||||||
{
|
{
|
||||||
logger.LogError("Достигнуто максимальное число попыток из-за ошибок при запросе JWT");
|
logger.LogError("Достигнуто максимальное число попыток из-за ошибок при загрузке людей Modeus");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.HttpOverrides;
|
|||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
using Microsoft.Identity.Web;
|
using Microsoft.Identity.Web;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
using ModeusSchedule.Abstractions;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
using Quartz.Listener;
|
using Quartz.Listener;
|
||||||
@@ -84,6 +85,8 @@ builder.Services.AddSingleton<ModeusHttpClient>();
|
|||||||
builder.Services.AddSingleton<ModeusEmployeeService>();
|
builder.Services.AddSingleton<ModeusEmployeeService>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ModeusEmployeeService>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ModeusEmployeeService>());
|
||||||
builder.Services.AddSingleton<ModeusService>();
|
builder.Services.AddSingleton<ModeusService>();
|
||||||
|
builder.Services.AddSingleton<ModeusPeopleService>();
|
||||||
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ModeusPeopleService>());
|
||||||
builder.Services.AddHttpClient("authClient");
|
builder.Services.AddHttpClient("authClient");
|
||||||
|
|
||||||
builder.Services.AddAuthentication()
|
builder.Services.AddAuthentication()
|
||||||
@@ -133,11 +136,14 @@ foreach (var p in loadedPlugins)
|
|||||||
|
|
||||||
// DI из плагина
|
// DI из плагина
|
||||||
p.Instance.ConfigureServices(builder.Services);
|
p.Instance.ConfigureServices(builder.Services);
|
||||||
|
builder.Services.AddSingleton<IPlugin>(p.Instance);
|
||||||
|
|
||||||
// Подключаем контроллеры плагина
|
// Подключаем контроллеры плагина
|
||||||
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(p.Assembly));
|
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(p.Assembly));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<PluginCacheInvalidator>();
|
||||||
|
|
||||||
var updateJwtJob = new JobKey("UpdateJWTJob");
|
var updateJwtJob = new JobKey("UpdateJWTJob");
|
||||||
|
|
||||||
builder.Services.AddQuartz(q =>
|
builder.Services.AddQuartz(q =>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SfeduSchedule.Services;
|
||||||
|
|
||||||
|
public static class AtomicJsonFile
|
||||||
|
{
|
||||||
|
public static async Task WriteAsync<T>(string path, T value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
var temporaryPath = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using (var stream = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
await JsonSerializer.SerializeAsync(stream, value, cancellationToken: cancellationToken);
|
||||||
|
await stream.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
File.Move(temporaryPath, path, overwrite: true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(temporaryPath))
|
||||||
|
File.Delete(temporaryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@ public class ModeusHttpClient
|
|||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
_logger.LogErrorHere(ex, "SearchPersonAsync: Deserialization failed.");
|
_logger.LogErrorHere(ex, "SearchPersonAsync: Deserialization failed.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SfeduSchedule.Services;
|
||||||
|
|
||||||
|
public sealed record ModeusPerson(string PersonId, string FullName, bool IsStudent, bool IsEmployee);
|
||||||
|
|
||||||
|
public sealed record ModeusPeopleSnapshot(DateTimeOffset UpdatedAt, ImmutableArray<ModeusPerson> Persons);
|
||||||
|
|
||||||
|
/// <summary>Последний полностью загруженный список людей Modeus. Не публикуется через API.</summary>
|
||||||
|
public sealed class ModeusPeopleService(ILogger<ModeusPeopleService> logger) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly string _path = Path.Combine(AppContext.BaseDirectory, AppConsts.DataFolderName, "people.json");
|
||||||
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||||
|
private ModeusPeopleSnapshot? _snapshot;
|
||||||
|
|
||||||
|
public ModeusPeopleSnapshot? Snapshot => Volatile.Read(ref _snapshot);
|
||||||
|
|
||||||
|
public async Task SetAsync(ModeusPeopleSnapshot snapshot, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Validate(snapshot);
|
||||||
|
await _writeLock.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await AtomicJsonFile.WriteAsync(_path, snapshot, cancellationToken);
|
||||||
|
Volatile.Write(ref _snapshot, snapshot);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_writeLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!File.Exists(_path)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var stream = File.OpenRead(_path);
|
||||||
|
var snapshot = await JsonSerializer.DeserializeAsync<ModeusPeopleSnapshot>(stream,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
Validate(snapshot);
|
||||||
|
Volatile.Write(ref _snapshot, snapshot);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Не удалось восстановить индекс людей Modeus; ожидается полная загрузка.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
private static void Validate(ModeusPeopleSnapshot? snapshot)
|
||||||
|
{
|
||||||
|
if (snapshot is null || snapshot.UpdatedAt == default || snapshot.Persons.IsDefaultOrEmpty ||
|
||||||
|
snapshot.Persons.Any(p => p is null || !Guid.TryParse(p.PersonId, out _)) ||
|
||||||
|
snapshot.Persons.Select(p => p.PersonId).Distinct(StringComparer.OrdinalIgnoreCase).Count() != snapshot.Persons.Length)
|
||||||
|
throw new InvalidDataException("Неполный или некорректный индекс людей Modeus.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Ical.Net;
|
using Ical.Net;
|
||||||
@@ -185,98 +186,71 @@ public class ModeusService(
|
|||||||
return serializedCalendar;
|
return serializedCalendar;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<string, (string, List<string>)>> GetEmployeesAsync(CancellationToken cancellationToken = default)
|
/// <summary>Получить полный список людей одним проходом всех страниц Modeus.</summary>
|
||||||
|
public async Task<ModeusPeopleSnapshot> GetPeopleAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var searchPersonResponse =
|
const int pageSize = 1000;
|
||||||
await modeusHttpClient.SearchPersonAsync(new ModeusSearchPersonRequest() { Size = 38000 }, cancellationToken);
|
var persons = new Dictionary<string, SfeduSchedule.DTO.Responses.Person>(StringComparer.OrdinalIgnoreCase);
|
||||||
if (searchPersonResponse == null)
|
var students = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var employees = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
int? expectedCount = null;
|
||||||
|
int? expectedPages = null;
|
||||||
|
|
||||||
|
// Modeus нумерует страницы с 1; page=0 является повтором первой страницы.
|
||||||
|
for (var page = 1; ; page++)
|
||||||
{
|
{
|
||||||
logger.LogErrorHere("persons is null");
|
var response = await modeusHttpClient.SearchPersonAsync(
|
||||||
return [];
|
new ModeusSearchPersonRequest(Size: pageSize, Page: page), cancellationToken)
|
||||||
|
?? throw new HttpRequestException($"Не удалось загрузить страницу {page} людей Modeus.");
|
||||||
|
if (response.Page is null || response.Embedded?.Persons is null ||
|
||||||
|
response.Embedded.Students is null || response.Embedded.Employees is null ||
|
||||||
|
response.Page.Number != page || response.Page.TotalElements is not > 0 ||
|
||||||
|
response.Page.TotalPages is not > 0 or > 10000)
|
||||||
|
throw new InvalidDataException("Некорректная пагинация или неполный ответ поиска людей Modeus.");
|
||||||
|
|
||||||
|
expectedCount ??= response.Page.TotalElements;
|
||||||
|
expectedPages ??= response.Page.TotalPages;
|
||||||
|
if (expectedCount != response.Page.TotalElements || expectedPages != response.Page.TotalPages)
|
||||||
|
throw new InvalidDataException("Список людей Modeus изменился во время загрузки; требуется повтор.");
|
||||||
|
|
||||||
|
var countBefore = persons.Count;
|
||||||
|
foreach (var person in response.Embedded.Persons)
|
||||||
|
{
|
||||||
|
if (person is null || !Guid.TryParse(person.Id, out _))
|
||||||
|
throw new InvalidDataException("Некорректный personId в ответе Modeus.");
|
||||||
|
persons[person.Id] = person;
|
||||||
|
}
|
||||||
|
foreach (var student in response.Embedded.Students)
|
||||||
|
if (!string.IsNullOrWhiteSpace(student?.PersonId)) students.Add(student.PersonId);
|
||||||
|
foreach (var employee in response.Embedded.Employees)
|
||||||
|
if (!string.IsNullOrWhiteSpace(employee?.PersonId)) employees.Add(employee.PersonId);
|
||||||
|
|
||||||
|
if (persons.Count == countBefore)
|
||||||
|
throw new InvalidDataException("Пустая или повторяющаяся страница людей Modeus.");
|
||||||
|
if (page >= expectedPages) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var persons = searchPersonResponse.Embedded.Persons;
|
if (persons.Count != expectedCount || students.Any(id => !persons.ContainsKey(id)) ||
|
||||||
var employees = searchPersonResponse.Embedded.Employees;
|
employees.Any(id => !persons.ContainsKey(id)))
|
||||||
|
throw new InvalidDataException("Не удалось получить полный список людей Modeus.");
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
return new ModeusPeopleSnapshot(DateTimeOffset.UtcNow,
|
||||||
var personsById = new Dictionary<string, SfeduSchedule.DTO.Responses.Person>();
|
persons.Values.Select(p => new ModeusPerson(p.Id, (p.FullName ?? string.Empty).Trim(),
|
||||||
foreach (var p in persons)
|
students.Contains(p.Id), employees.Contains(p.Id))).ToImmutableArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, (string, List<string>)>> GetEmployeesAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> GetEmployees(await GetPeopleAsync(cancellationToken));
|
||||||
|
|
||||||
|
public static Dictionary<string, (string, List<string>)> GetEmployees(ModeusPeopleSnapshot snapshot)
|
||||||
|
{
|
||||||
|
var employees = new Dictionary<string, (string, List<string>)>(StringComparer.Ordinal);
|
||||||
|
foreach (var person in snapshot.Persons.Where(p => p.IsEmployee))
|
||||||
{
|
{
|
||||||
var id = p?.Id;
|
var name = string.IsNullOrWhiteSpace(person.FullName) ? person.PersonId : person.FullName;
|
||||||
if (!string.IsNullOrEmpty(id))
|
employees.TryAdd(name, (person.PersonId, []));
|
||||||
{
|
|
||||||
personsById[id] = p;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return employees;
|
||||||
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
|
#region Проксирование методов из ModeusHttpClient
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using ModeusSchedule.Abstractions;
|
||||||
|
|
||||||
|
namespace SfeduSchedule.Services;
|
||||||
|
|
||||||
|
public sealed class PluginCacheInvalidator(
|
||||||
|
IEnumerable<IPlugin> plugins,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<PluginCacheInvalidator> logger)
|
||||||
|
{
|
||||||
|
public async Task InvalidateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var plugin in plugins)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var scope = scopeFactory.CreateAsyncScope();
|
||||||
|
await plugin.InvalidateCacheAsync(scope.ServiceProvider, cancellationToken);
|
||||||
|
logger.LogInformation("Кэш плагина {PluginName} инвалидирован", plugin.Name);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Не удалось инвалидировать кэш плагина {PluginName}", plugin.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ services:
|
|||||||
- Sentry:Dsn=${SENTRY_DSN:-}
|
- Sentry:Dsn=${SENTRY_DSN:-}
|
||||||
- SENTRY_TRACES_SAMPLE_RATE=${SENTRY_TRACES_SAMPLE_RATE:-1}
|
- SENTRY_TRACES_SAMPLE_RATE=${SENTRY_TRACES_SAMPLE_RATE:-1}
|
||||||
- SENTRY_PROFILES_SAMPLE_RATE=${SENTRY_PROFILES_SAMPLE_RATE:-1}
|
- SENTRY_PROFILES_SAMPLE_RATE=${SENTRY_PROFILES_SAMPLE_RATE:-1}
|
||||||
|
- TZ=Europe/Moscow
|
||||||
volumes:
|
volumes:
|
||||||
- data:/app/data
|
- data:/app/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ services:
|
|||||||
- AUTH_API_KEY=${AUTH_API_KEY}
|
- AUTH_API_KEY=${AUTH_API_KEY}
|
||||||
- SENTRY_TRACES_SAMPLE_RATE=${SENTRY_TRACES_SAMPLE_RATE:-1}
|
- SENTRY_TRACES_SAMPLE_RATE=${SENTRY_TRACES_SAMPLE_RATE:-1}
|
||||||
- SENTRY_PROFILES_SAMPLE_RATE=${SENTRY_PROFILES_SAMPLE_RATE:-1}
|
- SENTRY_PROFILES_SAMPLE_RATE=${SENTRY_PROFILES_SAMPLE_RATE:-1}
|
||||||
|
- TZ=Europe/Moscow
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
Reference in new issue
Block a user