261 lines
15 KiB
C#
261 lines
15 KiB
C#
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<RobotMetricsService> _logger;
|
||
|
||
public RobotMetricsService(
|
||
IRobotConfigurationSnapshotRepository snapshotRepository,
|
||
IRobotConfigurationRepository configurationRepository,
|
||
ILogger<RobotMetricsService> logger
|
||
)
|
||
{
|
||
_snapshotRepository = snapshotRepository;
|
||
_configurationRepository = configurationRepository;
|
||
_logger = logger;
|
||
}
|
||
|
||
|
||
public async Task<List<FilteredChartPoint>> 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<List<RobotStatusChartPoint>> 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<List<TaskStatusChartPoint>> 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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Генератор сетки времени
|
||
/// </summary>
|
||
/// <param name="fromDate"></param>
|
||
/// <param name="toDate"></param>
|
||
/// <param name="intervalMinutes"></param>
|
||
/// <returns></returns>
|
||
private IEnumerable<DateTimeOffset> 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;
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|