Files
parr_api/PARR.Core/Services/RobotSnapshotServices/RobotSnapshotService.cs

241 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AutoMapper;
using Microsoft.EntityFrameworkCore;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.Domain.DTOs.RobotSnapshotDTO;
using PARR.Domain.DTOs.User;
using PARR.Domain.Entities.RobotEntities;
using PARR.Domain.Exceptions;
namespace PARR.Core.Services.RobotSnapshotServices
{
internal class RobotSnapshotService : IRobotSnapshotService
{
private readonly IRobotSnapshotRepository robotSnapshotRepository;
private readonly IUserRepository userRepository;
private readonly IMapper mapper;
public RobotSnapshotService(
IRobotSnapshotRepository robotSnapshotRepository,
IUserRepository userRepository,
IMapper mapper
)
{
this.robotSnapshotRepository = robotSnapshotRepository;
this.userRepository = userRepository;
this.mapper = mapper;
}
public async Task<List<RobotSnapshotItemDto>> GetAsync(RobotSnapshotQuery queryDto)
{
// Просто возвращаем данные из БД, не проверяем что есть промежутки между которыми не было данных
// Т.е это просто чтоб посмотреть что есть в БД
var minutes = queryDto.Minutes > 0 ? queryDto.Minutes : 60 * 12;
var dateEnd = DateTimeOffset.UtcNow;
var dateStart = dateEnd.AddMinutes(-minutes);
var query = robotSnapshotRepository.Get().AsNoTracking()
.Where(t => dateStart <= t.DateCreated && t.DateCreated <= dateEnd);
if (!string.IsNullOrEmpty(queryDto.Ip))
query = query.Where(t => t.Ip == queryDto.Ip);
var groupingSnapshots = (await query.ToListAsync()).GroupBy(t => t.Ip);
var robotIps = groupingSnapshots.Select(t => t.Key).ToHashSet();
var robotList = await userRepository.Get()
.AsNoTracking()
.Where(t => robotIps.Contains(t.Ip))
.ToDictionaryAsync(t => t.Ip);
var result = new List<RobotSnapshotItemDto>();
foreach (var item in groupingSnapshots)
{
robotList.TryGetValue(item.Key, out var robot);
var userDto = new UserBaseDto { Ip = item.Key, Description = robot?.Description ?? string.Empty, Name = robot?.Name ?? string.Empty };
var snapshots = mapper.Map<List<RobotSnapshotDto>>(item).OrderBy(t => t.DateCreated).ToList();
result.Add(new RobotSnapshotItemDto { Robot = userDto, Snapshots = snapshots });
}
return result.OrderBy(t => t.Robot.Name).ThenBy(t => t.Robot.Ip).ToList();
}
public async Task<List<ServerHourlyAnalyticsDto>> GetHourlyAnalyticsAsync(RobotAnalyticsQuery queryDto)
{
var snapshotsQuery = robotSnapshotRepository.Get()
.AsNoTracking()
.Where(t => queryDto.DateStart <= t.DateCreated && t.DateCreated <= queryDto.DateEnd);
// Фильтруем по конкретному IP
if (!string.IsNullOrEmpty(queryDto.Ip))
snapshotsQuery = snapshotsQuery.Where(t => t.Ip == queryDto.Ip);
// Превращает TimeSpan в строку вида "+03:00" или "-05:00"
string formattedOffset = (queryDto.Offset >= TimeSpan.Zero ? "+" : "-") +
queryDto.Offset.ToString(@"hh\:mm");
// Группируем на стороне PostgreSQL с учетом часового пояса пользователя.
// Метод .Add(offset) сдвигает UTC-дату в Postgres на интервал времени пользователя,
// благодаря чему минуты объединяются в правильные локальные "часовые корзины".
var rawGroupedData = await snapshotsQuery
.GroupBy(t => new
{
t.Ip,
// Явно сдвигаем дату и достаем только нужные компоненты
Year = (t.DateCreated + queryDto.Offset).DateTime.Year,
Month = (t.DateCreated + queryDto.Offset).DateTime.Month,
Day = (t.DateCreated + queryDto.Offset).DateTime.Day,
Hour = (t.DateCreated + queryDto.Offset).DateTime.Hour
})
.Select(g => new
{
g.Key.Ip,
// Вытаскиваем компоненты даты из уже смещенного локального времени
LocalYear = g.Key.Year,
LocalMonth = g.Key.Month,
LocalDay = g.Key.Day,
LocalHour = g.Key.Hour,
MaxAllowed = g.Max(t => t.MaxRobots),
MaxTemplates = g.Max(t => t.TemplateRobotsCount),
MaxSchedules = g.Max(t => t.ScheduleRobotsCount),
SumTemplates = g.Sum(t => t.TemplateRobotsCount),
SumSchedules = g.Sum(t => t.ScheduleRobotsCount)
})
.ToListAsync();
if (!rawGroupedData.Any())
return new List<ServerHourlyAnalyticsDto>();
// Получаем информацию о серверах
var serverIps = rawGroupedData.Select(g => g.Ip).ToHashSet();
var serverDictionary = await userRepository.Get()
.AsNoTracking()
.Where(t => serverIps.Contains(t.Ip))
.ToDictionaryAsync(t => t.Ip);
// Группируем полученные агрегаты по серверам уже в памяти веб-сервера
var dataByServers = rawGroupedData.GroupBy(g => g.Ip);
var result = new List<ServerHourlyAnalyticsDto>();
// Принудительно переводим UTC-границы запроса в локальное время пользователя для корректной генерации "дыр"
var localStart = queryDto.DateStart.ToOffset(queryDto.Offset).DateTime;
var localEnd = queryDto.DateEnd.ToOffset(queryDto.Offset).DateTime;
// Округляем начальный час до ровного значения (00 минут, 00 секунд)
var startHour = new DateTime(localStart.Year, localStart.Month, localStart.Day, localStart.Hour, 0, 0);
foreach (var serverGroup in dataByServers)
{
serverDictionary.TryGetValue(serverGroup.Key, out var robotInfo);
var userDto = new UserBaseDto
{
Ip = serverGroup.Key,
Description = robotInfo?.Description ?? string.Empty,
Name = robotInfo?.Name ?? string.Empty
};
// Формируем точки, которые удалось вытащить из базы данных
var hourlyPoints = serverGroup.Select(g => new RobotHourlyPointDto
{
// Возвращаем дату со смещением пользователя, чтобы Angular сразу отображал правильный час
Hour = new DateTimeOffset(g.LocalYear, g.LocalMonth, g.LocalDay, g.LocalHour, 0, 0, TimeSpan.Zero),
MaxAllowedRobots = g.MaxAllowed,
MaxTemplateRobots = g.MaxTemplates,
MaxScheduleRobots = g.MaxSchedules,
// Считаем среднее количество роботов, деля поминутную сумму строго на 60 минут часа
AvgTemplateRobots = Math.Round(g.SumTemplates / 60.0, 1),
AvgScheduleRobots = Math.Round(g.SumSchedules / 60.0, 1)
})
.ToList();
// Алгоритм заполнения пропусков во времени (если сервер был выключен)
var filledPoints = new List<RobotHourlyPointDto>();
var currentHour = startHour;
int lastKnownMax = hourlyPoints.FirstOrDefault()?.MaxAllowedRobots ?? 0;
while (currentHour <= localEnd)
{
// Создаем временную точку со смещением для точного сопоставления
var targetOffsetDateTime = new DateTimeOffset(currentHour, TimeSpan.Zero);
var point = hourlyPoints.FirstOrDefault(p => p.Hour == targetOffsetDateTime);
if (point != null)
{
filledPoints.Add(point);
lastKnownMax = point.MaxAllowedRobots;
}
else
{
// Если за этот час записей в БД нет — значит, сервер был полностью оффлайн.
// Выводим нули, чтобы график в Angular не прерывался, а плавно падал.
filledPoints.Add(new RobotHourlyPointDto
{
Hour = targetOffsetDateTime,
MaxAllowedRobots = lastKnownMax,
MaxTemplateRobots = 0,
AvgTemplateRobots = 0,
MaxScheduleRobots = 0,
AvgScheduleRobots = 0
});
}
// Шагаем строго на +1 час вперед по локальному времени клиента
currentHour = currentHour.AddHours(1);
}
result.Add(new ServerHourlyAnalyticsDto
{
Robot = userDto,
Snapshots = filledPoints.OrderBy(p => p.Hour).ToList()
});
}
return result
.OrderBy(t => t.Robot.Name)
.ThenBy(t => t.Robot.Ip)
.ToList();
}
public async Task<RobotSnapshotItemDto> CreateAsync(CreateRobotSnapshot robotSnapshot)
{
var snapshot = new RobotSnapshot
{
Id = Guid.NewGuid(),
TemplateRobotsCount = robotSnapshot.TemplateRobotsCount,
ScheduleRobotsCount = robotSnapshot.ScheduleRobotsCount,
MaxRobots = robotSnapshot.MaxRobots,
Ip = robotSnapshot.Ip
};
var createdResult = await robotSnapshotRepository.CreateAsync(snapshot);
var commitResult = await robotSnapshotRepository.CommitAsync();
if (!createdResult || !commitResult)
throw new DbErrorException("Ошибка сохранения в БД");
var user = await userRepository.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Ip == robotSnapshot.Ip);
var userDto = new UserBaseDto { Ip = robotSnapshot.Ip, Description = user?.Description ?? string.Empty, Name = user?.Name ?? string.Empty };
var result = new RobotSnapshotItemDto
{
Robot = userDto,
Snapshots = mapper.Map<List<RobotSnapshotDto>>(new List<RobotSnapshot> { snapshot })
};
return result;
}
}
}