feat: улучшил синхронизацию лекций
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 10m14s
Frontend CI / build-and-check (push) Failing after 16m12s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 14m7s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 14m59s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m57s
Backend CI / build-and-test (push) Failing after 13m27s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 10m14s
Frontend CI / build-and-check (push) Failing after 16m12s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 14m7s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 14m59s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m57s
Backend CI / build-and-test (push) Failing after 13m27s
This commit is contained in:
@@ -19,10 +19,11 @@ public class SyncController : ControllerBase
|
||||
/// <summary>Запустить синхронизацию расписания лекций из Modeus.</summary>
|
||||
/// <remarks>
|
||||
/// Только Admin. Выполняет upsert лекций и связанных курсов на основе данных
|
||||
/// из внешнего API `schedule.rdcenter.ru`. Поддерживает фильтрацию по специальности,
|
||||
/// периоду и типу занятий.
|
||||
/// из внешнего API `schedule.rdcenter.ru`. Поддерживает фильтрацию по периоду,
|
||||
/// размеру выборки, аудиториям, участникам, реализациям курсов/циклов,
|
||||
/// специальностям, годам набора, профилям, учебным планам и типам занятий.
|
||||
/// </remarks>
|
||||
/// <param name="req">Параметры синхронизации: specialtyCode, timeMin/timeMax, typeId.</param>
|
||||
/// <param name="req">Параметры поиска событий во внешнем сервисе расписания.</param>
|
||||
/// <response code="200">Результат синхронизации: кол-во созданных, обновлённых и пропущенных записей.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
|
||||
@@ -2994,9 +2994,9 @@
|
||||
"Sync"
|
||||
],
|
||||
"summary": "Запустить синхронизацию расписания лекций из Modeus.",
|
||||
"description": "Только Admin. Выполняет upsert лекций и связанных курсов на основе данных\nиз внешнего API `schedule.rdcenter.ru`. Поддерживает фильтрацию по специальности,\nпериоду и типу занятий.\n\n**Required roles:** Admin",
|
||||
"description": "Только Admin. Выполняет upsert лекций и связанных курсов на основе данных\nиз внешнего API `schedule.rdcenter.ru`. Поддерживает фильтрацию по периоду,\nразмеру выборки, аудиториям, участникам, реализациям курсов/циклов,\nспециальностям, годам набора, профилям, учебным планам и типам занятий.\n\n**Required roles:** Admin",
|
||||
"requestBody": {
|
||||
"description": "Параметры синхронизации: specialtyCode, timeMin/timeMax, typeId.",
|
||||
"description": "Параметры поиска событий во внешнем сервисе расписания.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -5725,7 +5725,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"specialtyCode": {
|
||||
"type": "string",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"timeMin": {
|
||||
@@ -5744,6 +5747,61 @@
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"roomId": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"attendeePersonId": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"courseUnitRealizationId": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"cycleRealizationId": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"learningStartYear": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"profileName": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"curriculumId": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
namespace UniVerse.Application.DTOs.Sync;
|
||||
|
||||
public record SyncScheduleRequest(
|
||||
string? SpecialtyCode,
|
||||
IReadOnlyList<string>? SpecialtyCode,
|
||||
DateTime? TimeMin,
|
||||
DateTime? TimeMax,
|
||||
IReadOnlyList<string>? TypeId
|
||||
IReadOnlyList<string>? TypeId,
|
||||
int? Size = null,
|
||||
IReadOnlyList<string>? RoomId = null,
|
||||
IReadOnlyList<string>? AttendeePersonId = null,
|
||||
IReadOnlyList<string>? CourseUnitRealizationId = null,
|
||||
IReadOnlyList<string>? CycleRealizationId = null,
|
||||
IReadOnlyList<int>? LearningStartYear = null,
|
||||
IReadOnlyList<string>? ProfileName = null,
|
||||
IReadOnlyList<string>? CurriculumId = null
|
||||
);
|
||||
|
||||
public record SyncResultDto(
|
||||
|
||||
@@ -24,29 +24,30 @@ public class ModeusApiClient : IModeusApiClient
|
||||
|
||||
public async Task<ModeusEventsResponse> SearchEventsAsync(SyncScheduleRequest request)
|
||||
{
|
||||
const int pageSize = 900;
|
||||
var specialtyCodes = string.IsNullOrWhiteSpace(request.SpecialtyCode)
|
||||
? []
|
||||
: request.SpecialtyCode
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
var typeIds = request.TypeId ?? [];
|
||||
var pageSize = request.Size is > 0 ? request.Size.Value : 900;
|
||||
var body = new Dictionary<string, object?>
|
||||
{
|
||||
["size"] = pageSize,
|
||||
["timeMin"] = request.TimeMin,
|
||||
["timeMax"] = request.TimeMax,
|
||||
["specialtyCode"] = specialtyCodes
|
||||
["timeMax"] = request.TimeMax
|
||||
};
|
||||
|
||||
if (typeIds.Count > 0)
|
||||
body["typeId"] = typeIds;
|
||||
AddNonEmpty(body, "roomId", request.RoomId);
|
||||
AddNonEmpty(body, "attendeePersonId", request.AttendeePersonId);
|
||||
AddNonEmpty(body, "courseUnitRealizationId", request.CourseUnitRealizationId);
|
||||
AddNonEmpty(body, "cycleRealizationId", request.CycleRealizationId);
|
||||
AddNonEmpty(body, "specialtyCode", request.SpecialtyCode);
|
||||
AddNonEmpty(body, "learningStartYear", request.LearningStartYear);
|
||||
AddNonEmpty(body, "profileName", request.ProfileName);
|
||||
AddNonEmpty(body, "curriculumId", request.CurriculumId);
|
||||
AddNonEmpty(body, "typeId", request.TypeId);
|
||||
|
||||
var response = await _http.PostAsJsonAsync("/api/proxy/events/search", body);
|
||||
var requestJson = JsonSerializer.Serialize(body);
|
||||
await EnsureSuccessAsync(response, "Modeus events search",
|
||||
BuildEventsRequestSummary(pageSize, specialtyCodes, request.TimeMin, request.TimeMax, typeIds, requestJson));
|
||||
BuildEventsRequestSummary(requestJson));
|
||||
return await ReadJsonAsync<ModeusEventsResponse>(response, "Modeus events search",
|
||||
BuildEventsRequestSummary(pageSize, specialtyCodes, request.TimeMin, request.TimeMax, typeIds, requestJson))
|
||||
BuildEventsRequestSummary(requestJson))
|
||||
?? new ModeusEventsResponse();
|
||||
}
|
||||
|
||||
@@ -97,16 +98,15 @@ public class ModeusApiClient : IModeusApiClient
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
private static string BuildEventsRequestSummary(
|
||||
int size,
|
||||
IReadOnlyList<string> specialtyCodes,
|
||||
DateTime? timeMin,
|
||||
DateTime? timeMax,
|
||||
IReadOnlyList<string> typeIds,
|
||||
string requestJson)
|
||||
private static string BuildEventsRequestSummary(string requestJson) => $"Request JSON: {requestJson}";
|
||||
|
||||
private static void AddNonEmpty<T>(
|
||||
IDictionary<string, object?> body,
|
||||
string key,
|
||||
IReadOnlyList<T>? values)
|
||||
{
|
||||
var typeFilter = typeIds.Count > 0 ? $"typeId=[{string.Join(", ", typeIds)}]" : "typeId=<omitted>";
|
||||
return $"size={size}, specialtyCode=[{string.Join(", ", specialtyCodes)}], timeMin={timeMin:O}, timeMax={timeMax:O}, {typeFilter}. Request JSON: {requestJson}";
|
||||
if (values is { Count: > 0 })
|
||||
body[key] = values;
|
||||
}
|
||||
|
||||
private static async Task<T?> ReadJsonAsync<T>(HttpResponseMessage response, string operation, string requestSummary)
|
||||
|
||||
@@ -104,9 +104,6 @@ public class ScheduleSyncService : IScheduleSyncService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Schedule sync failed");
|
||||
var specialtyCodes = string.IsNullOrWhiteSpace(request.SpecialtyCode)
|
||||
? []
|
||||
: request.SpecialtyCode.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
var result = new SyncResultDto(created, updated, skipped, ex.Message, BuildErrorDetails(
|
||||
ex,
|
||||
stage,
|
||||
@@ -114,14 +111,9 @@ public class ScheduleSyncService : IScheduleSyncService
|
||||
updated,
|
||||
skipped,
|
||||
[
|
||||
"size=900",
|
||||
$"requestJson={BuildScheduleRequestJson(request)}",
|
||||
$"specialtyCode=[{string.Join(", ", specialtyCodes)}]",
|
||||
$"timeMin={request.TimeMin:O}",
|
||||
$"timeMax={request.TimeMax:O}",
|
||||
request.TypeId is { Count: > 0 }
|
||||
? $"typeId=[{string.Join(", ", request.TypeId)}]"
|
||||
: "typeId=<omitted>"
|
||||
$"timeMax={request.TimeMax:O}"
|
||||
]));
|
||||
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result);
|
||||
return result;
|
||||
@@ -498,24 +490,35 @@ public class ScheduleSyncService : IScheduleSyncService
|
||||
|
||||
private static string BuildScheduleRequestJson(SyncScheduleRequest request)
|
||||
{
|
||||
var specialtyCodes = string.IsNullOrWhiteSpace(request.SpecialtyCode)
|
||||
? []
|
||||
: request.SpecialtyCode.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var body = new Dictionary<string, object?>
|
||||
{
|
||||
["size"] = 900,
|
||||
["size"] = request.Size is > 0 ? request.Size.Value : 900,
|
||||
["timeMin"] = request.TimeMin,
|
||||
["timeMax"] = request.TimeMax,
|
||||
["specialtyCode"] = specialtyCodes
|
||||
["timeMax"] = request.TimeMax
|
||||
};
|
||||
|
||||
if (request.TypeId is { Count: > 0 })
|
||||
body["typeId"] = request.TypeId;
|
||||
AddNonEmpty(body, "roomId", request.RoomId);
|
||||
AddNonEmpty(body, "attendeePersonId", request.AttendeePersonId);
|
||||
AddNonEmpty(body, "courseUnitRealizationId", request.CourseUnitRealizationId);
|
||||
AddNonEmpty(body, "cycleRealizationId", request.CycleRealizationId);
|
||||
AddNonEmpty(body, "specialtyCode", request.SpecialtyCode);
|
||||
AddNonEmpty(body, "learningStartYear", request.LearningStartYear);
|
||||
AddNonEmpty(body, "profileName", request.ProfileName);
|
||||
AddNonEmpty(body, "curriculumId", request.CurriculumId);
|
||||
AddNonEmpty(body, "typeId", request.TypeId);
|
||||
|
||||
return JsonSerializer.Serialize(body);
|
||||
}
|
||||
|
||||
private static void AddNonEmpty<T>(
|
||||
IDictionary<string, object?> body,
|
||||
string key,
|
||||
IReadOnlyList<T>? values)
|
||||
{
|
||||
if (values is { Count: > 0 })
|
||||
body[key] = values;
|
||||
}
|
||||
|
||||
private static string? GetHrefId(string? href)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(href))
|
||||
|
||||
Reference in New Issue
Block a user