Files
parr_api/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs

323 lines
18 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 Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.DAL.NextRunServices.Models;
using PARR.DAL.Services.Interfaces;
namespace PARR.DAL.NextRunServices.Subservices
{
internal class TemplateDistributor : ITemplateDistributor
{
private readonly ILogger<TemplateDistributor> logger;
private readonly IWeekendDayService weekendDayService;
public TemplateDistributor(
ILogger<TemplateDistributor> logger,
IWeekendDayService weekendDayService
)
{
this.logger = logger;
this.weekendDayService = weekendDayService;
}
/// <summary>
/// Распределяет шаблоны по рабочим дням в заданном диапазоне с равномерной загрузкой.
/// Возвращает список с Id и новой датой выполнения для каждого шаблона.
/// </summary>
public async Task<List<TemplateNextRunDto>> ScheduleTemplatesAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime)
{
logger.LogInformation("Начинаем распределение {Count} шаблонов с {StartDate} по {EndDate}", templates.Count, startDate, endDate);
// Получаем список выходных/праздничных дней в заданном диапазоне
var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet();
// Получаем список рабочих дней (исключая выходные и праздники)
var workDays = GetWorkDays(startDate, endDate, weekends);
logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count);
// Проверяем, есть ли хотя бы один рабочий день
if (workDays.Count == 0)
{
logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate);
return new List<TemplateNextRunDto>();
}
var totalWorks = templates.Count; // Общее количество шаблонов для распределения
var totalDays = workDays.Count; // Общее количество доступных рабочих дней
// Сортируем шаблоны: сначала без даты (NextRun == null), затем с датой
var sortedTemplates = templates.OrderBy(t => t.NextRun.HasValue ? 1 : 0).ToList();
// Вычисляем интервал между работами: сколько "дней" приходится на одну работу
// Это позволяет равномерно распределить работы по всему периоду
// т е например 2 работы распределятся на 30 дней равномерно, не будут идти подряд
var interval = (double)totalDays / totalWorks;
// Проходим по всем шаблонам и распределяем их равномерно по дням
for (int i = 0; i < totalWorks; i++)
{
// Вычисляем индекс дня, на который нужно назначить i-ю работу
// Используем округление, чтобы получить ближайший день
var dayIndex = (int)Math.Round(i * interval);
// Защита от возможного выхода за границы массива из-за округления
if (dayIndex >= totalDays) dayIndex = totalDays - 1;
// Получаем дату из списка рабочих дней по вычисленному индексу
var date = workDays[dayIndex];
// Формируем дату выполнения: дата + заданное время (executionTime)
var nextRun = new DateTimeOffset(date.Year, date.Month, date.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero);
// Назначаем вычисленную дату выполнения шаблону
sortedTemplates[i].NextRun = nextRun;
logger.LogDebug("Шаблон {TemplateId} назначен на {NextRun}", sortedTemplates[i].Id, nextRun);
}
logger.LogInformation("Распределение завершено. Возвращаем {Count} записей.", templates.Count);
// Возвращаем список шаблонов с обновлёнными датами выполнения
return templates.Select(t => new TemplateNextRunDto { Id = t.Id, NextRun = t.NextRun!.Value }).ToList();
}
/// <summary>
/// Обновляет устаревшие даты выполнения шаблонов.
/// Возвращает список шаблонов, чьи даты были изменены.
/// </summary>
public async Task<List<TemplateNextRunDto>> ReassignOutdatedRunsAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime)
{
logger.LogInformation("Начинаем переназначение устаревших дат выполнения");
// Получаем список выходных/праздничных дней
var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet();
var workDays = GetWorkDays(startDate, endDate, weekends).OrderBy(d => d).ToList();
var now = DateTimeOffset.UtcNow;
var result = new List<TemplateNextRunDto>();
foreach (var template in templates)
{
if (template.NextRun.HasValue && template.NextRun.Value < now)
{
logger.LogDebug("Шаблон {TemplateId} с датой {OldDate} устарел", template.Id, template.NextRun);
var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(now.DateTime), DateOnly.FromDateTime(template.NextRun.Value.Date));
if (newDate.HasValue)
{
var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero);
template.NextRun = nextRun;
logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", template.Id, nextRun);
result.Add(new TemplateNextRunDto { Id = template.Id, NextRun = nextRun });
}
else
{
logger.LogWarning("Не удалось найти новую дату для шаблона {TemplateId}", template.Id);
}
}
}
logger.LogInformation("Переназначение завершено. Изменено {Count} записей.", result.Count);
return result;
}
/// <summary>
/// Перераспределяет только те шаблоны, у которых дата недействительна (выходной, вне периода).
/// Возвращает список всех шаблонов с новыми или старыми датами.
/// </summary>
public async Task<List<TemplateNextRunDto>> RebalanceTemplatesAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime)
{
logger.LogInformation("Начинаем перераспределение шаблонов");
// Получаем список выходных/праздничных дней
var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet();
var workDays = GetWorkDays(startDate, endDate, weekends);
var validDates = new HashSet<DateOnly>(workDays);
// Отфильтруем шаблоны с недействительной датой
var unassigned = templates.Where(t => !t.NextRun.HasValue || !validDates.Contains(DateOnly.FromDateTime(t.NextRun.Value.Date))).ToList();
logger.LogDebug("Найдено {Count} шаблонов для переназначения", unassigned.Count);
// Перераспределяем только невалидные
var reassigned = await ScheduleTemplatesAsync(unassigned, startDate, endDate, executionTime);
// Возвращаем все шаблоны с их датами
var result = templates.Select(t => new TemplateNextRunDto { Id = t.Id, NextRun = t.NextRun!.Value }).ToList();
logger.LogInformation("Перераспределение завершено. Возвращаем {Count} записей.", result.Count);
return result;
}
/// <summary>
/// Возвращает актуальную дату выполнения для одного шаблона.
/// Результат — список из одного элемента (если успешно) или пустой.
/// </summary>
public async Task<List<TemplateNextRunDto>> GetValidNextRunAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, TimeSpan executionTime)
{
logger.LogDebug("Запрос актуальной даты для шаблона {TemplateId}", template.Id);
// Получаем список выходных/праздничных дней
var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet();
var nextRun = await GetValidNextRunInternalAsync(template, startDate, endDate, weekends, executionTime);
if (nextRun.HasValue)
{
logger.LogDebug("Шаблон {TemplateId} имеет актуальную дату {NextRun}", template.Id, nextRun);
return new List<TemplateNextRunDto> { new TemplateNextRunDto { Id = template.Id, NextRun = nextRun.Value } };
}
logger.LogWarning("Не удалось получить актуальную дату для шаблона {TemplateId}", template.Id);
return new List<TemplateNextRunDto>();
}
/// <summary>
/// Возвращает список актуальных дат выполнения для всех переданных шаблонов.
/// </summary>
public async Task<List<TemplateNextRunDto>> GetValidNextRunsAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime)
{
logger.LogDebug("Запрос актуальных дат для {Count} шаблонов", templates.Count);
// Получаем список выходных/праздничных дней
var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet();
var result = new List<TemplateNextRunDto>();
foreach (var template in templates)
{
var nextRun = await GetValidNextRunInternalAsync(template, startDate, endDate, weekends, executionTime);
if (nextRun.HasValue)
{
result.Add(new TemplateNextRunDto { Id = template.Id, NextRun = nextRun.Value });
}
}
logger.LogInformation("Получено {Count} актуальных дат выполнения", result.Count);
return result;
}
/// <summary>
/// Внутренний метод для получения актуальной даты выполнения для одного шаблона.
/// </summary>
private async Task<DateTimeOffset?> GetValidNextRunInternalAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, HashSet<DateOnly> weekends, TimeSpan executionTime)
{
var workDays = GetWorkDays(startDate, endDate, weekends);
var today = DateOnly.FromDateTime(DateTime.UtcNow);
// Если у шаблона нет даты, назначаем первую доступную
if (!template.NextRun.HasValue)
{
var firstDay = workDays.FirstOrDefault();
if (firstDay != default)
{
var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero);
logger.LogDebug("Шаблон {TemplateId} получил первую доступную дату: {NextRun}", template.Id, nextRun);
return nextRun;
}
logger.LogWarning("Нет доступных рабочих дней для шаблона {TemplateId}", template.Id);
return null;
}
var scheduledDate = DateOnly.FromDateTime(template.NextRun.Value.Date);
// Если дата валидна и не устарела — возвращаем как есть
if (workDays.Contains(scheduledDate))
{
if (template.NextRun.Value < DateTimeOffset.UtcNow)
{
logger.LogDebug("Дата шаблона {TemplateId} ({Date}) устарела", template.Id, template.NextRun);
var newDate = FindNextAvailableDate(workDays, today, scheduledDate);
if (newDate.HasValue)
{
var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero);
logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", template.Id, nextRun);
return nextRun;
}
logger.LogWarning("Не удалось найти новую дату для устаревшего шаблона {TemplateId}", template.Id);
return null;
}
// Дата валидна и не устарела
logger.LogDebug("Шаблон {TemplateId} имеет действительную дату: {NextRun}", template.Id, template.NextRun);
return template.NextRun;
}
// Дата недействительна (выходной или вне периода) — ищем ближайшую
var closest = FindClosestValidDate(workDays, scheduledDate);
if (closest.HasValue)
{
var nextRun = new DateTimeOffset(closest.Value.Year, closest.Value.Month, closest.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero);
logger.LogDebug("Шаблон {TemplateId} перемещен на ближайшую дату: {NextRun}", template.Id, nextRun);
return nextRun;
}
logger.LogWarning("Не удалось найти подходящую дату для шаблона {TemplateId}", template.Id);
return null;
}
/// <summary>
/// Ищет следующую доступную дату после указанной, если не находит — ищет назад.
/// </summary>
private DateOnly? FindNextAvailableDate(List<DateOnly> workDays, DateOnly afterDate, DateOnly originalDate)
{
var future = workDays.FirstOrDefault(d => d > afterDate);
if (future != default)
return future;
var past = workDays.Where(t => t >= originalDate).OrderByDescending(t => t).FirstOrDefault();
return past != default ? (DateOnly?)past : null;
}
/// <summary>
/// Ищет ближайшую дату (вперед или назад) к заданной, среди доступных рабочих дней.
/// </summary>
private DateOnly? FindClosestValidDate(List<DateOnly> workDays, DateOnly targetDate)
{
var future = workDays.FirstOrDefault(t => t >= targetDate);
if (future != default)
return future;
var past = workDays.Where(t => t <= targetDate).OrderByDescending(t => t).FirstOrDefault();
return past != default ? (DateOnly?)past : null;
}
/// <summary>
/// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники.
/// </summary>
private List<DateOnly> GetWorkDays(DateOnly start, DateOnly end, HashSet<DateOnly> weekends)
{
var result = new List<DateOnly>();
var date = start;
while (date <= end)
{
if (!weekends.Contains(date))
{
result.Add(date);
}
date = date.AddDays(1);
}
return result;
}
}
}