using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Extensions; using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.RobotRepositories; using PARR.Domain.DTOs.RobotMetrics; using PARR.Domain.Enums; using PARR.Domain.Exceptions; namespace PARR.Core.Services.RobotMetrics { internal class RobotMetricsService : IRobotMetricsService { private readonly IRobotConfigurationSnapshotRepository _snapshotRepository; private readonly IRobotConfigurationRepository _configurationRepository; private readonly ILogger _logger; public RobotMetricsService( IRobotConfigurationSnapshotRepository snapshotRepository, IRobotConfigurationRepository configurationRepository, ILogger logger ) { _snapshotRepository = snapshotRepository; _configurationRepository = configurationRepository; _logger = logger; } public async Task> GetFilteredMetricsAsync(MetricFilter filter, CancellationToken cancellationToken) { // ------- Правильность расчетов этого метода доконца не проверена ------- if (filter == null) throw new AppValidationException("Фильтр не может быть пустым."); if (filter.IntervalMinutes < 1) throw new AppValidationException("Интервал группировки не может быть меньше 1 минуты."); var query = _snapshotRepository.Get().AsNoTracking(); if (filter.RobotCode.HasValue) query = query.Where(t => t.RobotCode == (int)filter.RobotCode.Value); if (filter.RobotStatusCode.HasValue) query = query.Where(t => t.RobotStatusCode == (int)filter.RobotStatusCode.Value); if (filter.TaskStatusCode.HasValue) query = query.Where(t => t.TaskStatusCode == (int)filter.TaskStatusCode); // Если даты не переданы, берем последние 24 часа по умолчанию var dateFrom = filter.DateFrom ?? DateTimeOffset.UtcNow.AddDays(-1); query = query.Where(s => s.DateCreated >= dateFrom); if (filter.DateTo.HasValue) query = query.Where(s => s.DateCreated <= filter.DateTo.Value); var dbData = await query.ToListAsync(cancellationToken); var dbGrouped = dbData .GroupBy(s => RoundToInterval(s.DateCreated, filter.IntervalMinutes)) .ToDictionary(t => t.Key, t => t.ToList()); var dateTo = filter.DateTo ?? DateTimeOffset.UtcNow; var result = GenerateTimeGrid(dateFrom, dateTo, filter.IntervalMinutes) .Select(time => new FilteredChartPoint( Timestamp: time, //Count: dbGrouped.TryGetValue(time, out var points) ? points.Max(x => x.Count) : 0 Count: dbGrouped.TryGetValue(time, out var points) ? points.GroupBy(x => x.DateCreated) // Группируем по точной минуте снапшота .Select(g => g.Sum(x => x.Count)) // Складываем всё, что подошли под фильтр в эту минуту .MaxOrDefault() // Берем максимальный пик за весь интервал (например, за час) : 0 // Если снапшотов не было — честный ноль )).ToList(); return result; } public async Task> GetRobotStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken) { ValidateRobot(robotCode); CalculatePeriodDates(period, out var fromDate, out var intervalMinutes); // История из снапшотов var snapshots = await _snapshotRepository.Get() .AsNoTracking() .Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate) .ToListAsync(cancellationToken); var dbGrouped = snapshots .GroupBy(t => RoundToInterval(t.DateCreated, intervalMinutes)) .ToDictionary(t => t.Key, t => t.ToList()); // Генерим сетку значений, если значений нет, вставляем нули var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes) .Select(time => dbGrouped.TryGetValue(time, out var points) ? new RobotStatusChartPoint( Timestamp: time, // группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал Wait: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).MaxOrDefault(x => x.Count), points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait) .GroupBy(x => x.DateCreated) .Select(t => t.Sum(x => x.Count)) .MaxOrDefault(), InProgress: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).MaxOrDefault(x => x.Count), points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress) .GroupBy(x => x.DateCreated) .Select(g => g.Sum(x => x.Count)) .MaxOrDefault(), Error: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).MaxOrDefault(x => x.Count) points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error) .GroupBy(x => x.DateCreated) .Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота .MaxOrDefault(), Complete: points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete) .GroupBy(x => x.DateCreated) .Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота .MaxOrDefault() ) : new RobotStatusChartPoint(time, Wait: 0, InProgress: 0, Error: 0, Complete: 0) ).ToList(); // Последнее значение в конце графика из реальной таблицы var liveRaw = await _configurationRepository .Get() .AsNoTracking() .Where(t => t.RobotCode == (int)robotCode) .GroupBy(t => t.RobotStatusCode) .Select(g => new { RobotStatusCode = g.Key, Count = g.Count() }) .ToListAsync(cancellationToken); history.Add(new RobotStatusChartPoint( Timestamp: DateTimeOffset.UtcNow, Wait: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).Sum(x => x.Count), InProgress: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).Sum(x => x.Count), Error: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).Sum(x => x.Count), Complete: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete).Sum(x => x.Count) )); return history; } public async Task> GetTaskStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken) { ValidateRobot(robotCode); CalculatePeriodDates(period, out var fromDate, out var intervalMinutes); // История из снапшотов var snapshots = await _snapshotRepository.Get() .AsNoTracking() .Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate) .ToListAsync(cancellationToken); var dbGrouped = snapshots .GroupBy(s => RoundToInterval(s.DateCreated, intervalMinutes)) .ToDictionary(t => t.Key, t => t.ToList()); var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes) .Select(time => dbGrouped.TryGetValue(time, out var points) ? new TaskStatusChartPoint( Timestamp: time, // группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал Creating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).MaxOrDefault(x => x.Count), points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating) .GroupBy(x => x.DateCreated) .Select(g => g.Sum(x => x.Count)) .MaxOrDefault(), Updating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).MaxOrDefault(x => x.Count), points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating) .GroupBy(x => x.DateCreated) .Select(g => g.Sum(x => x.Count)) .MaxOrDefault(), Ok: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).MaxOrDefault(x => x.Count) points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok) .GroupBy(x => x.DateCreated) .Select(g => g.Sum(x => x.Count)) .MaxOrDefault() ) : new TaskStatusChartPoint(time, Creating: 0, Updating: 0, Ok: 0) ).ToList(); // Живой текущий кадр в конец графика var liveRaw = await _configurationRepository .Get() .AsNoTracking() .Where(t => t.RobotCode == (int)robotCode) .GroupBy(t => t.TaskStatusCode) .Select(g => new { TaskStatusCode = g.Key, Count = g.Count() }) .ToListAsync(cancellationToken); history.Add(new TaskStatusChartPoint( Timestamp: DateTimeOffset.UtcNow, Creating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).Sum(x => x.Count), Updating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).Sum(x => x.Count), Ok: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).Sum(x => x.Count) )); return history; } private void ValidateRobot(RobotsEnum robotCode) { if (!Enum.IsDefined(typeof(RobotsEnum), robotCode)) throw new NotFoundException($"Робот с кодом {robotCode} не найден в системе."); } private void CalculatePeriodDates(ChartPeriod period, out DateTimeOffset fromDate, out int intervalMinutes) { switch (period) { case ChartPeriod.TwoHours: fromDate = DateTimeOffset.UtcNow.AddHours(-2); intervalMinutes = 2; break; case ChartPeriod.TwentyFourHours: fromDate = DateTimeOffset.UtcNow.AddDays(-1); intervalMinutes = 30; break; case ChartPeriod.SevenDays: fromDate = DateTimeOffset.UtcNow.AddDays(-7); intervalMinutes = 60; break; default: throw new AppValidationException("Указан неподдерживаемый период времени."); } } private DateTimeOffset RoundToInterval(DateTimeOffset dt, int intervalMinutes) { var minutes = (dt.Minute / intervalMinutes) * intervalMinutes; return new DateTimeOffset(dt.Year, dt.Month, dt.Day, dt.Hour, minutes, 0, dt.Offset); } /// /// Генератор сетки времени /// /// /// /// /// private IEnumerable GenerateTimeGrid(DateTimeOffset fromDate, DateTimeOffset toDate, int intervalMinutes) { var startTime = RoundToInterval(fromDate, intervalMinutes); var endTime = RoundToInterval(toDate, intervalMinutes); for (var time = startTime; time <= endTime; time = time.AddMinutes(intervalMinutes)) { yield return time; } } } }