From ed9717df07d3e20aeb2efeaf7c544c05e068c96d Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Sun, 25 Jan 2026 00:33:09 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20person=20id=20=D1=81=D0=BE=D1=82=D1=80=D1=83=D0=B4=D0=BD?= =?UTF-8?q?=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModeusScheduleRequest.cs} | 2 +- .../DTO/Requests/ModeusSearchPersonRequest.cs | 17 ++ .../Responses/ModeusSearchPersonResponse.cs | 84 +++++++++ .../DTO/Responses/SearchPersonResponse.cs | 14 ++ SfeduSchedule/Controllers/ProxyController.cs | 1 + .../Controllers/ScheduleController.cs | 32 +++- SfeduSchedule/Program.cs | 1 + .../Services/ModeusEmployeeService.cs | 75 ++++++++ SfeduSchedule/Services/ModeusHttpClient.cs | 82 ++++---- SfeduSchedule/Services/ModeusService.cs | 178 ++++++++++++++---- 10 files changed, 409 insertions(+), 77 deletions(-) rename ModeusSchedule.Abstractions/DTO/{ModeusScheduleRequestDTO.cs => Requests/ModeusScheduleRequest.cs} (98%) create mode 100644 ModeusSchedule.Abstractions/DTO/Requests/ModeusSearchPersonRequest.cs create mode 100644 ModeusSchedule.Abstractions/DTO/Responses/ModeusSearchPersonResponse.cs create mode 100644 ModeusSchedule.Abstractions/DTO/Responses/SearchPersonResponse.cs create mode 100644 SfeduSchedule/Services/ModeusEmployeeService.cs diff --git a/ModeusSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs b/ModeusSchedule.Abstractions/DTO/Requests/ModeusScheduleRequest.cs similarity index 98% rename from ModeusSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs rename to ModeusSchedule.Abstractions/DTO/Requests/ModeusScheduleRequest.cs index 7fb1fd1..251cff9 100644 --- a/ModeusSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs +++ b/ModeusSchedule.Abstractions/DTO/Requests/ModeusScheduleRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace ModeusSchedule.Abstractions.DTO; +namespace ModeusSchedule.Abstractions.DTO.Requests; /// /// DTO для запроса расписания в Modeus. diff --git a/ModeusSchedule.Abstractions/DTO/Requests/ModeusSearchPersonRequest.cs b/ModeusSchedule.Abstractions/DTO/Requests/ModeusSearchPersonRequest.cs new file mode 100644 index 0000000..ec62b40 --- /dev/null +++ b/ModeusSchedule.Abstractions/DTO/Requests/ModeusSearchPersonRequest.cs @@ -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 +); \ No newline at end of file diff --git a/ModeusSchedule.Abstractions/DTO/Responses/ModeusSearchPersonResponse.cs b/ModeusSchedule.Abstractions/DTO/Responses/ModeusSearchPersonResponse.cs new file mode 100644 index 0000000..b04e79e --- /dev/null +++ b/ModeusSchedule.Abstractions/DTO/Responses/ModeusSearchPersonResponse.cs @@ -0,0 +1,84 @@ +// Auto-generated by https://json2csharp.com/ + +using System.Text.Json.Serialization; + +namespace SfeduSchedule.DTO.Responses; + +// SearchEmployeesResponse myDeserializedClass = JsonSerializer.Deserialize(myJsonResponse); + +public record Embedded( + [property: JsonPropertyName("persons")] + IReadOnlyList Persons, + [property: JsonPropertyName("employees")] + IReadOnlyList Employees, + [property: JsonPropertyName("students")] + IReadOnlyList 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 +); \ No newline at end of file diff --git a/ModeusSchedule.Abstractions/DTO/Responses/SearchPersonResponse.cs b/ModeusSchedule.Abstractions/DTO/Responses/SearchPersonResponse.cs new file mode 100644 index 0000000..b3e9643 --- /dev/null +++ b/ModeusSchedule.Abstractions/DTO/Responses/SearchPersonResponse.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace SfeduSchedule.DTO.Responses; + +public record SearchPersonResponse( + IReadOnlyList Persons +); + +public record SearchPerson( + [property: JsonPropertyName("name")] + string Name, + [property: JsonPropertyName("person_id")] + string PersonId +); \ No newline at end of file diff --git a/SfeduSchedule/Controllers/ProxyController.cs b/SfeduSchedule/Controllers/ProxyController.cs index c4f4b75..d22670e 100644 --- a/SfeduSchedule/Controllers/ProxyController.cs +++ b/SfeduSchedule/Controllers/ProxyController.cs @@ -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; diff --git a/SfeduSchedule/Controllers/ScheduleController.cs b/SfeduSchedule/Controllers/ScheduleController.cs index 25a5141..cf5ed31 100644 --- a/SfeduSchedule/Controllers/ScheduleController.cs +++ b/SfeduSchedule/Controllers/ScheduleController.cs @@ -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 logger) : ControllerBase +public class ScheduleController(ModeusService modeusService, ModeusEmployeeService modeusEmployeeService, ILogger logger) : ControllerBase { /// - /// Получить GUID пользователя по полному имени. (требуется авторизация) + /// Получить GUID пользователя по полному имени. (включая студентов, требуется авторизация) /// /// Полное имя пользователя. - /// GUID пользователя. + /// GUID пользователя /// Возвращает GUID пользователя /// Пользователь не найден /// Неавторизованный @@ -32,6 +35,29 @@ public class ScheduleController(ModeusService modeusService, ILogger + /// Поиск сотрудников по имени. (преподавателей) + /// + /// ФИО Сотрудника + /// Список сотрудников (до 10 записей) + /// Возвращает список сотрудников с их GUID + /// Сотрудник не найден + /// Неавторизованный + [HttpGet] + [Route("searchemployee")] + public async Task SearchEmployees([Required][MinLength(1)] string fullname) + { + var employees = await modeusEmployeeService.GetEmployees(fullname, 10); + if (employees.Count == 0) + return NotFound(); + + var persons = new List(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); + } /// /// Получить расписание в формате ICS по пользовательскому запросу. diff --git a/SfeduSchedule/Program.cs b/SfeduSchedule/Program.cs index 2047936..d029298 100644 --- a/SfeduSchedule/Program.cs +++ b/SfeduSchedule/Program.cs @@ -92,6 +92,7 @@ builder.Services.AddHttpClient("modeus", client => client.BaseAddress = new Uri(configuration["MODEUS_URL"]!); }); builder.Services.AddSingleton(); +builder.Services.AddHostedService(); builder.Services.AddSingleton(); builder.Services.AddHttpClient("authClient"); diff --git a/SfeduSchedule/Services/ModeusEmployeeService.cs b/SfeduSchedule/Services/ModeusEmployeeService.cs new file mode 100644 index 0000000..0505847 --- /dev/null +++ b/SfeduSchedule/Services/ModeusEmployeeService.cs @@ -0,0 +1,75 @@ +using SfeduSchedule.Logging; + +namespace SfeduSchedule.Services; + +public class ModeusEmployeeService(ILogger logger, ModeusService modeusService) + : IHostedService +{ + private Dictionary)> _employees = []; + private Task? _backgroundTask; + private CancellationTokenSource? _cts; + + public async Task)>> 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 Task StartAsync(CancellationToken cancellationToken) + { + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _backgroundTask = Task.Run(async () => + { + try + { + 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."); + } + + await Task.Delay(TimeSpan.FromSeconds(5), _cts.Token); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + catch (OperationCanceledException) + { + // ignore + } + catch (Exception ex) + { + logger.LogErrorHere(ex, "Ошибка при загрузке сотрудников из Modeus."); + } + }, _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 + } + } +} \ No newline at end of file diff --git a/SfeduSchedule/Services/ModeusHttpClient.cs b/SfeduSchedule/Services/ModeusHttpClient.cs index 73dc8cb..015cb9a 100644 --- a/SfeduSchedule/Services/ModeusHttpClient.cs +++ b/SfeduSchedule/Services/ModeusHttpClient.cs @@ -1,8 +1,12 @@ +using System.Diagnostics; using System.Text; using System.Text.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; @@ -67,7 +71,44 @@ public class ModeusHttpClient _logger.LogErrorHere(ex, "Deserialization failed."); } - return new List(); + return []; + } + + public async Task SearchPersonAsync(ModeusSearchPersonRequest modeusSearchPersonRequest) + { + using var request = new HttpRequestMessage(HttpMethod.Post, + $"schedule-calendar-v2/api/people/persons/search"); + request.Content = new StringContent( + JsonSerializer.Serialize(modeusSearchPersonRequest, GlobalConsts.JsonSerializerOptions), + Encoding.UTF8, "application/json"); + var stopwatch = Stopwatch.StartNew(); + using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + 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( + contentStream, + GlobalConsts.JsonSerializerOptions); + 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 SearchRoomsAsync(RoomSearchRequest requestDto) @@ -85,43 +126,4 @@ public class ModeusHttpClient return await response.Content.ReadAsStringAsync(); } - public async Task 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; - } - } \ No newline at end of file diff --git a/SfeduSchedule/Services/ModeusService.cs b/SfeduSchedule/Services/ModeusService.cs index dc330db..8031f52 100644 --- a/SfeduSchedule/Services/ModeusService.cs +++ b/SfeduSchedule/Services/ModeusService.cs @@ -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 logger, + IConfiguration configuration, + ModeusHttpClient modeusHttpClient) { - private readonly IConfiguration _configuration; - - private readonly ILogger _logger; - private readonly ModeusHttpClient _modeusHttpClient; - - public ModeusService( - ILogger logger, - IConfiguration configuration, - ModeusHttpClient modeusHttpClient) - { - _modeusHttpClient = modeusHttpClient; - _logger = logger; - _configuration = configuration; - } - public async Task 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)>> GetEmployeesAsync() + { + var searchPersonResponse = + await modeusHttpClient.SearchPersonAsync(new ModeusSearchPersonRequest() { Size = 38000 }); + 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(); + 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 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()); + } + 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 SearchRoomsAsync(RoomSearchRequest request) { - return await _modeusHttpClient.SearchRoomsAsync(request); + return await modeusHttpClient.SearchRoomsAsync(request); } public async Task GetScheduleAsync(ModeusScheduleRequest msr) { - return await _modeusHttpClient.GetScheduleAsync(msr); + return await modeusHttpClient.GetScheduleAsync(msr); } public async Task 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> GetAttendeesAsync(Guid eventId) { - return await _modeusHttpClient.GetAttendeesAsync(eventId); + return await modeusHttpClient.GetAttendeesAsync(eventId); } #endregion