fix: синхронизации лекций
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 9s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 1m3s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 25s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 7s

This commit is contained in:
2026-05-12 00:44:27 +03:00
parent 9b28a09253
commit 860964e3c2
4 changed files with 272 additions and 19 deletions
@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using UniVerse.Application.DTOs.Sync;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Entities;
@@ -26,15 +27,58 @@ public class ScheduleSyncService : IScheduleSyncService
try
{
var events = await _modeus.SearchEventsAsync(request);
foreach (var ev in events.Events)
foreach (var ev in events.EventItems)
{
if (string.IsNullOrWhiteSpace(ev.Id) || string.IsNullOrWhiteSpace(ev.Name))
{
skipped++;
continue;
}
var courseUnitId = GetHrefId(ev.Links?.CourseUnitRealization?.Href);
var courseUnit = events.Embedded?.CourseUnitRealizations?
.FirstOrDefault(c => c.Id == courseUnitId);
var courseExternalId = courseUnit?.Id ?? ev.TypeId ?? ev.Id;
var courseName = courseUnit?.Name ?? ev.Name;
var location = await UpsertEventLocationAsync(events, ev.Id);
var maxEnrollments = events.Embedded?.EventTeams?
.FirstOrDefault(team => team.EventId == ev.Id)?.Size ?? 0;
var startsAt = EnsureUtc(ev.StartsAt);
var endsAt = EnsureUtc(ev.EndsAt);
var existing = await _db.Lectures.FirstOrDefaultAsync(l => l.ExternalId == ev.Id);
if (existing != null) { updated++; existing.StartsAt = ev.StartsAt; existing.EndsAt = ev.EndsAt; existing.UpdatedAt = DateTime.UtcNow; }
if (existing != null)
{
existing.Title = ev.Name;
existing.Description = ev.Description;
existing.StartsAt = startsAt;
existing.EndsAt = endsAt;
existing.LocationId = location?.Id;
existing.MaxEnrollments = maxEnrollments;
existing.UpdatedAt = DateTime.UtcNow;
updated++;
}
else
{
var course = await _db.Courses.FirstOrDefaultAsync(c => c.ExternalId == ev.TypeId);
if (course == null) { course = new Course { Name = ev.Name, ExternalId = ev.TypeId, IsSynced = true }; _db.Courses.Add(course); await _db.SaveChangesAsync(); }
_db.Lectures.Add(new Lecture { CourseId = course.Id, Title = ev.Name, ExternalId = ev.Id, StartsAt = ev.StartsAt, EndsAt = ev.EndsAt });
var course = await _db.Courses.FirstOrDefaultAsync(c => c.ExternalId == courseExternalId);
if (course == null)
{
course = new Course { Name = courseName, ExternalId = courseExternalId, IsSynced = true };
_db.Courses.Add(course);
await _db.SaveChangesAsync();
}
_db.Lectures.Add(new Lecture
{
CourseId = course.Id,
LocationId = location?.Id,
Title = ev.Name,
Description = ev.Description,
ExternalId = ev.Id,
StartsAt = startsAt,
EndsAt = endsAt,
MaxEnrollments = maxEnrollments
});
created++;
}
}
@@ -46,6 +90,9 @@ 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,
@@ -53,10 +100,14 @@ public class ScheduleSyncService : IScheduleSyncService
updated,
skipped,
[
$"specialtyCode={request.SpecialtyCode ?? "<empty>"}",
"size=900",
$"requestJson={BuildScheduleRequestJson(request)}",
$"specialtyCode=[{string.Join(", ", specialtyCodes)}]",
$"timeMin={request.TimeMin:O}",
$"timeMax={request.TimeMax:O}",
$"typeId=[{string.Join(", ", request.TypeId ?? [])}]"
request.TypeId is { Count: > 0 }
? $"typeId=[{string.Join(", ", request.TypeId)}]"
: "typeId=<omitted>"
]));
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result);
return result;
@@ -129,6 +180,48 @@ public class ScheduleSyncService : IScheduleSyncService
public Task<SyncStatusDto> GetLastSyncStatusAsync() => Task.FromResult(_lastStatus);
private async Task<Location?> UpsertEventLocationAsync(ModeusEventsResponse events, string eventId)
{
var roomId = events.Embedded?.EventRooms?
.Select(eventRoom => new
{
EventId = GetHrefId(eventRoom.Links?.Event?.Href),
RoomId = GetHrefId(eventRoom.Links?.Room?.Href)
})
.FirstOrDefault(link => link.EventId == eventId)
?.RoomId;
if (string.IsNullOrWhiteSpace(roomId))
return null;
var room = events.Embedded?.Rooms?.FirstOrDefault(item => item.Id == roomId);
if (room == null || string.IsNullOrWhiteSpace(room.Name))
return null;
var existing = await _db.Locations.FirstOrDefaultAsync(location => location.ExternalId == room.Id);
if (existing != null)
{
existing.Name = room.Name;
existing.Room = room.NameShort;
existing.Building = room.Building?.Name ?? room.Building?.NameShort;
existing.Address = room.Building?.Address;
return existing;
}
var location = new Location
{
Name = room.Name,
Room = room.NameShort,
Building = room.Building?.Name ?? room.Building?.NameShort,
Address = room.Building?.Address,
ExternalId = room.Id
};
_db.Locations.Add(location);
await _db.SaveChangesAsync();
return location;
}
private static IReadOnlyList<string> BuildErrorDetails(
Exception exception,
string stage,
@@ -154,4 +247,43 @@ public class ScheduleSyncService : IScheduleSyncService
details.AddRange(context);
return details;
}
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,
["timeMin"] = request.TimeMin,
["timeMax"] = request.TimeMax,
["specialtyCode"] = specialtyCodes
};
if (request.TypeId is { Count: > 0 })
body["typeId"] = request.TypeId;
return JsonSerializer.Serialize(body);
}
private static string? GetHrefId(string? href)
{
if (string.IsNullOrWhiteSpace(href))
return null;
var index = href.LastIndexOf('/');
return index >= 0 && index < href.Length - 1
? href[(index + 1)..]
: href;
}
private static DateTime EnsureUtc(DateTime value) =>
value.Kind switch
{
DateTimeKind.Utc => value,
DateTimeKind.Local => value.ToUniversalTime(),
_ => DateTime.SpecifyKind(value, DateTimeKind.Local).ToUniversalTime()
};
}