2026-06-10 16:48:16 +10:00
|
|
|
|
using AutoMapper;
|
2026-06-11 11:40:12 +10:00
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2026-07-13 15:29:48 +10:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
2026-06-11 11:40:12 +10:00
|
|
|
|
using PARR.Core.Repositories.Interfaces;
|
2026-06-10 16:48:16 +10:00
|
|
|
|
using PARR.Core.Repositories.Interfaces.RobotRepositories;
|
2026-07-13 15:29:48 +10:00
|
|
|
|
using PARR.Core.Services.Snapshots.Interfaces;
|
2026-06-10 16:48:16 +10:00
|
|
|
|
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
2026-06-11 11:40:12 +10:00
|
|
|
|
using PARR.Domain.DTOs.User;
|
2026-06-10 16:48:16 +10:00
|
|
|
|
using PARR.Domain.Entities.RobotEntities;
|
|
|
|
|
|
using PARR.Domain.Exceptions;
|
|
|
|
|
|
|
|
|
|
|
|
namespace PARR.Core.Services.RobotSnapshotServices
|
|
|
|
|
|
{
|
2026-07-13 15:29:48 +10:00
|
|
|
|
internal class RobotSnapshotService : IRobotSnapshotService, ISnapshotProvider
|
2026-06-10 16:48:16 +10:00
|
|
|
|
{
|
2026-06-15 11:00:53 +10:00
|
|
|
|
private readonly IRobotSnapshotRepository _robotSnapshotRepository;
|
|
|
|
|
|
private readonly IUserRepository _userRepository;
|
|
|
|
|
|
private readonly IMapper _mapper;
|
2026-07-13 15:29:48 +10:00
|
|
|
|
private readonly ILogger<RobotSnapshotService> _logger;
|
|
|
|
|
|
private readonly ISnapshotSettings _snapshotSettings;
|
|
|
|
|
|
|
|
|
|
|
|
// Тут любое значение, не используем метод создания снапшота
|
|
|
|
|
|
public TimeSpan Interval => TimeSpan.FromHours(1);
|
|
|
|
|
|
|
|
|
|
|
|
public TimeSpan RetentionPeriod => _snapshotSettings.RobotSnapshotRetentionPeriod;
|
2026-06-10 16:48:16 +10:00
|
|
|
|
|
|
|
|
|
|
public RobotSnapshotService(
|
|
|
|
|
|
IRobotSnapshotRepository robotSnapshotRepository,
|
2026-06-11 11:40:12 +10:00
|
|
|
|
IUserRepository userRepository,
|
2026-07-13 15:29:48 +10:00
|
|
|
|
IMapper mapper,
|
|
|
|
|
|
ILogger<RobotSnapshotService> logger,
|
|
|
|
|
|
ISnapshotSettings snapshotSettings
|
2026-06-10 16:48:16 +10:00
|
|
|
|
)
|
|
|
|
|
|
{
|
2026-06-15 11:00:53 +10:00
|
|
|
|
_robotSnapshotRepository = robotSnapshotRepository;
|
|
|
|
|
|
_userRepository = userRepository;
|
|
|
|
|
|
_mapper = mapper;
|
2026-07-13 15:29:48 +10:00
|
|
|
|
_logger = logger;
|
|
|
|
|
|
_snapshotSettings = snapshotSettings;
|
2026-06-10 16:48:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-11 11:40:12 +10:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-06-15 11:00:53 +10:00
|
|
|
|
var query = _robotSnapshotRepository.Get().AsNoTracking()
|
2026-06-11 11:40:12 +10:00
|
|
|
|
.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();
|
2026-06-15 11:00:53 +10:00
|
|
|
|
var robotList = await _userRepository.Get()
|
2026-06-11 11:40:12 +10:00
|
|
|
|
.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 };
|
2026-06-15 11:00:53 +10:00
|
|
|
|
var snapshots = _mapper.Map<List<RobotSnapshotDto>>(item).OrderBy(t => t.DateCreated).ToList();
|
2026-06-11 11:40:12 +10:00
|
|
|
|
|
|
|
|
|
|
result.Add(new RobotSnapshotItemDto { Robot = userDto, Snapshots = snapshots });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 14:44:39 +10:00
|
|
|
|
return result.OrderBy(t => t.Robot.Name).ThenBy(t => t.Robot.Ip).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<List<ServerHourlyAnalyticsDto>> GetHourlyAnalyticsAsync(RobotAnalyticsQuery queryDto)
|
|
|
|
|
|
{
|
2026-06-15 11:00:53 +10:00
|
|
|
|
var snapshotsQuery = _robotSnapshotRepository.Get()
|
2026-06-11 14:44:39 +10:00
|
|
|
|
.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);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Группируем на стороне 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();
|
2026-06-15 11:00:53 +10:00
|
|
|
|
var serverDictionary = await _userRepository.Get()
|
2026-06-11 14:44:39 +10:00
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.Where(t => serverIps.Contains(t.Ip))
|
|
|
|
|
|
.ToDictionaryAsync(t => t.Ip);
|
|
|
|
|
|
|
2026-06-15 11:00:53 +10:00
|
|
|
|
// Группируем полученные агрегаты по серверам уже в памяти
|
2026-06-11 14:44:39 +10:00
|
|
|
|
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 минут часа
|
2026-06-15 14:39:03 +10:00
|
|
|
|
//todo: тут не всегда нужно делить на 60, если час последний, то там может быть 50 минут или 20 и тп
|
2026-06-11 14:44:39 +10:00
|
|
|
|
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();
|
2026-06-11 11:40:12 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 11:00:53 +10:00
|
|
|
|
public async Task<List<HourlyRobotTypeAnalyticsDto>> GetHourlyAnalyticsByRobotTypeAsync(RobotAnalyticsRobotTypeQuery queryDto)
|
|
|
|
|
|
{
|
|
|
|
|
|
var snapshotsQuery = _robotSnapshotRepository.Get()
|
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.Where(t => queryDto.DateStart <= t.DateCreated && t.DateCreated <= queryDto.DateEnd)
|
|
|
|
|
|
.GroupBy(t => new
|
|
|
|
|
|
{
|
|
|
|
|
|
// Явно сдвигаем дату
|
|
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
IQueryable<RobotSnapshotRobotTypeDbDto> groupedQuery;
|
|
|
|
|
|
|
|
|
|
|
|
switch (queryDto.RobotType)
|
|
|
|
|
|
{
|
|
|
|
|
|
case Domain.Enums.RobotsEnum.TemplateOrder:
|
|
|
|
|
|
groupedQuery = snapshotsQuery.Select(g => new RobotSnapshotRobotTypeDbDto
|
|
|
|
|
|
{
|
|
|
|
|
|
// Вытаскиваем компоненты даты из уже смещенного локального времени
|
|
|
|
|
|
LocalYear = g.Key.Year,
|
|
|
|
|
|
LocalMonth = g.Key.Month,
|
|
|
|
|
|
LocalDay = g.Key.Day,
|
|
|
|
|
|
LocalHour = g.Key.Hour,
|
|
|
|
|
|
|
|
|
|
|
|
// Макс не правильно.
|
|
|
|
|
|
//MaxAllowed = g.Max(t => t.MaxRobots),
|
|
|
|
|
|
Sum = g.Sum(t => t.TemplateRobotsCount)
|
|
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
case Domain.Enums.RobotsEnum.ScheduleOrder:
|
|
|
|
|
|
groupedQuery = snapshotsQuery.Select(g => new RobotSnapshotRobotTypeDbDto
|
|
|
|
|
|
{
|
|
|
|
|
|
// Вытаскиваем компоненты даты из уже смещенного локального времени
|
|
|
|
|
|
LocalYear = g.Key.Year,
|
|
|
|
|
|
LocalMonth = g.Key.Month,
|
|
|
|
|
|
LocalDay = g.Key.Day,
|
|
|
|
|
|
LocalHour = g.Key.Hour,
|
|
|
|
|
|
|
|
|
|
|
|
// Макс не правильно.
|
|
|
|
|
|
//MaxAllowed = g.Max(t => t.MaxRobots),
|
|
|
|
|
|
Sum = g.Sum(t => t.ScheduleRobotsCount)
|
|
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw new ArgumentException("Не обработано значение параметра", nameof(queryDto.RobotType));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var data = await groupedQuery.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (!data.Any())
|
|
|
|
|
|
return new List<HourlyRobotTypeAnalyticsDto>();
|
|
|
|
|
|
|
2026-06-15 14:39:03 +10:00
|
|
|
|
// Фиксируем текущее время в UTC и «округляем» его до начала часа
|
|
|
|
|
|
var nowLocal = DateTimeOffset.UtcNow.ToOffset(queryDto.Offset);
|
|
|
|
|
|
var currentHourLocal = new DateTime(nowLocal.Year, nowLocal.Month, nowLocal.Day, nowLocal.Hour, 0, 0);
|
|
|
|
|
|
|
2026-06-15 11:00:53 +10:00
|
|
|
|
var dbDataDict = data.ToDictionary(
|
|
|
|
|
|
k => new DateTime(k.LocalYear, k.LocalMonth, k.LocalDay, k.LocalHour, 0, 0),
|
2026-06-15 14:39:03 +10:00
|
|
|
|
v =>
|
2026-06-15 11:00:53 +10:00
|
|
|
|
{
|
2026-06-15 14:39:03 +10:00
|
|
|
|
double minutesInHour = 60.0;
|
|
|
|
|
|
var dbLocalHour = new DateTime(v.LocalYear, v.LocalMonth, v.LocalDay, v.LocalHour, 0, 0);
|
|
|
|
|
|
// Строгая проверка: совпадает ли час из базы данных ИМЕННО с текущим часом сегодня?
|
|
|
|
|
|
// Если да, то нужно делить не на 60, а на прошедние минуты
|
|
|
|
|
|
if (dbLocalHour == currentHourLocal)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Если идет первая минута часа, делим на 1, чтобы избежать деления на ноль
|
|
|
|
|
|
minutesInHour = nowLocal.Minute == 0 ? 1.0 : (double)nowLocal.Minute;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new
|
|
|
|
|
|
{
|
|
|
|
|
|
//MaxAllowedRobots = t.MaxAllowed,
|
|
|
|
|
|
AvgRobots = Math.Round(v.Sum / minutesInHour, 1)
|
|
|
|
|
|
};
|
2026-06-15 11:00:53 +10:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Заполняем модель
|
|
|
|
|
|
var statistics = new List<HourlyRobotTypeAnalyticsDto>();
|
|
|
|
|
|
|
|
|
|
|
|
#region Алгоритм заполнения пропусков во времени (если сервер был выключен)
|
|
|
|
|
|
// Превращаем границы в локальное время для генерации сетки часов
|
|
|
|
|
|
var currentHour = queryDto.DateStart.ToOffset(queryDto.Offset).DateTime;
|
|
|
|
|
|
var localEnd = queryDto.DateEnd.ToOffset(queryDto.Offset).DateTime;
|
|
|
|
|
|
|
|
|
|
|
|
// Срезаем минуты/секунды для старта
|
|
|
|
|
|
currentHour = new DateTime(currentHour.Year, currentHour.Month, currentHour.Day, currentHour.Hour, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
while (currentHour <= localEnd)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Ищем значение в словаре. Если нет — ставим 0
|
|
|
|
|
|
dbDataDict.TryGetValue(currentHour, out var statInfo);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Создаем DateTimeOffset для локального времени с правильным смещением
|
|
|
|
|
|
var localDateTimeOffset = new DateTimeOffset(currentHour, queryDto.Offset);
|
|
|
|
|
|
|
|
|
|
|
|
statistics.Add(new HourlyRobotTypeAnalyticsDto
|
|
|
|
|
|
{
|
|
|
|
|
|
// Возвращаем DateTimeOffset с нулевым смещением (как в вашем коде) или с queryDto.Offset
|
|
|
|
|
|
Hour = localDateTimeOffset.ToUniversalTime(),
|
|
|
|
|
|
AvgRobots = statInfo?.AvgRobots ?? 0
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
currentHour = currentHour.AddHours(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
return statistics;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-11 11:40:12 +10:00
|
|
|
|
public async Task<RobotSnapshotItemDto> CreateAsync(CreateRobotSnapshot robotSnapshot)
|
2026-06-10 16:48:16 +10:00
|
|
|
|
{
|
|
|
|
|
|
var snapshot = new RobotSnapshot
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
|
TemplateRobotsCount = robotSnapshot.TemplateRobotsCount,
|
|
|
|
|
|
ScheduleRobotsCount = robotSnapshot.ScheduleRobotsCount,
|
|
|
|
|
|
MaxRobots = robotSnapshot.MaxRobots,
|
|
|
|
|
|
Ip = robotSnapshot.Ip
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-15 11:00:53 +10:00
|
|
|
|
var createdResult = await _robotSnapshotRepository.CreateAsync(snapshot);
|
|
|
|
|
|
var commitResult = await _robotSnapshotRepository.CommitAsync();
|
2026-06-10 16:48:16 +10:00
|
|
|
|
|
|
|
|
|
|
if (!createdResult || !commitResult)
|
|
|
|
|
|
throw new DbErrorException("Ошибка сохранения в БД");
|
|
|
|
|
|
|
2026-06-15 11:00:53 +10:00
|
|
|
|
var user = await _userRepository.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Ip == robotSnapshot.Ip);
|
2026-06-11 11:40:12 +10:00
|
|
|
|
var userDto = new UserBaseDto { Ip = robotSnapshot.Ip, Description = user?.Description ?? string.Empty, Name = user?.Name ?? string.Empty };
|
|
|
|
|
|
|
|
|
|
|
|
var result = new RobotSnapshotItemDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Robot = userDto,
|
2026-06-15 11:00:53 +10:00
|
|
|
|
Snapshots = _mapper.Map<List<RobotSnapshotDto>>(new List<RobotSnapshot> { snapshot })
|
2026-06-11 11:40:12 +10:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
2026-06-10 16:48:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-13 15:29:48 +10:00
|
|
|
|
public Task TakeSnapshotAsync(CancellationToken cancellationToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Метод пустой! Нам не нужно собирать данные по таймеру,
|
|
|
|
|
|
// так как они и так пишутся сюда через контроллер API.
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async Task CleanUpOldSnapshotsAsync(CancellationToken cancellationToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
var thresholdDate = DateTimeOffset.UtcNow - RetentionPeriod;
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("[{ServiceName}] Запуск очистки старых снапшотов. Удаление данных старше {ThresholdDate}", GetType().Name, thresholdDate);
|
|
|
|
|
|
|
|
|
|
|
|
// Удаляем старые записи напрямую в PostgreSQL
|
|
|
|
|
|
var deletedCount = await _robotSnapshotRepository.Get()
|
|
|
|
|
|
.Where(s => s.DateCreated < thresholdDate)
|
|
|
|
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("[{ServiceName}] Очистка завершена. Удалено устаревших строк снапшотов: {Count}", GetType().Name, deletedCount);
|
|
|
|
|
|
}
|
2026-06-10 16:48:16 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|