345 lines
19 KiB
C#
345 lines
19 KiB
C#
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_old //: ITemplateDistributor
|
||
{
|
||
private readonly ILogger<TemplateDistributor_old> logger;
|
||
private readonly IWeekendDayService weekendDayService;
|
||
|
||
public TemplateDistributor_old(
|
||
ILogger<TemplateDistributor_old> logger,
|
||
IWeekendDayService weekendDayService
|
||
)
|
||
{
|
||
this.logger = logger;
|
||
this.weekendDayService = weekendDayService;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Распределяет все шаблоны на указанный период, учитывая план нагрузки.
|
||
/// Возвращает список всех шаблонов с обновлёнными датами выполнения.
|
||
/// </summary>
|
||
public async Task<List<TemplateNextRunDto>> DistributeTemplatesAsync(
|
||
DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List<TemplateNextRunDto> allTemplates)
|
||
{
|
||
logger.LogInformation("Начинаем распределение {Count} шаблонов с {StartDate} по {EndDate}", allTemplates.Count, startDate, endDate);
|
||
|
||
// Получаем список выходных/праздничных дней
|
||
var weekends = await GetWeekendsAsync(startDate, endDate);
|
||
|
||
// Получаем список рабочих дней (исключая выходные и праздники)
|
||
var workDays = GetWorkDays(startDate, endDate, weekends);
|
||
|
||
logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count);
|
||
|
||
if (workDays.Count == 0)
|
||
{
|
||
logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate);
|
||
return allTemplates;
|
||
}
|
||
|
||
var totalWorks = allTemplates.Count;
|
||
var totalDays = workDays.Count;
|
||
|
||
//-------------
|
||
|
||
// Вычисляем интервал между работами: сколько "дней" приходится на одну работу
|
||
var interval = (double)totalDays / totalWorks;
|
||
|
||
// Создаём новый список шаблонов с обновлёнными датами
|
||
var updatedTemplates = new List<TemplateNextRunDto>();
|
||
|
||
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];
|
||
|
||
// Формируем дату выполнения: дата + время из referenceDate
|
||
var nextRun = new DateTimeOffset(date.Year, date.Month, date.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
|
||
//------------------
|
||
// Проверяем, не раньше ли дата, чем referenceDate (не включая саму referenceDate)
|
||
if (nextRun < referenceDate)
|
||
{
|
||
logger.LogWarning("Шаблон {TemplateId} имеет дату {NextRun}, которая раньше referenceDate {ReferenceDate}. Переносим на ближайший рабочий день после referenceDate (включая её).", allTemplates[i].Id, nextRun, referenceDate);
|
||
|
||
// Ищем ближайший день после referenceDate (включая его)
|
||
var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(referenceDate.DateTime), date);
|
||
if (newDate.HasValue)
|
||
{
|
||
nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
}
|
||
else
|
||
{
|
||
// Если не нашли — используем первый рабочий день
|
||
logger.LogWarning("Не удалось найти дату после referenceDate для шаблона {TemplateId}. Используем первый рабочий день.", allTemplates[i].Id);
|
||
var firstDay = workDays[0];
|
||
nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
}
|
||
}
|
||
//------------------
|
||
|
||
var updatedTemplate = new TemplateNextRunDto
|
||
{
|
||
Id = allTemplates[i].Id,
|
||
NextRun = nextRun
|
||
};
|
||
|
||
updatedTemplates.Add(updatedTemplate);
|
||
|
||
logger.LogDebug("Шаблон {TemplateId} назначен на {NextRun}", updatedTemplate.Id, updatedTemplate.NextRun);
|
||
}
|
||
|
||
logger.LogInformation("Распределение завершено. Возвращаем {Count} записей.", updatedTemplates.Count);
|
||
|
||
return updatedTemplates;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки.
|
||
/// Возвращает обновлённый шаблон с новой датой выполнения.
|
||
/// </summary>
|
||
public async Task<TemplateNextRunDto> GetValidNextRunForTemplateAsync(
|
||
DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List<TemplateNextRunDto> allTemplates)
|
||
{
|
||
logger.LogDebug("Запрос актуальной даты для шаблона {TemplateId}", targetTemplate.Id);
|
||
|
||
// Получаем список выходных/праздничных дней
|
||
var weekends = await GetWeekendsAsync(startDate, endDate);
|
||
|
||
// Получаем список рабочих дней
|
||
var workDays = GetWorkDays(startDate, endDate, weekends);
|
||
|
||
logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count);
|
||
|
||
if (workDays.Count == 0)
|
||
{
|
||
logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate);
|
||
|
||
// Возвращаем дату, равную referenceDate, если не нашли рабочих дней
|
||
logger.LogWarning("Используем referenceDate как дату выполнения для шаблона {TemplateId}", targetTemplate.Id);
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = referenceDate };
|
||
}
|
||
|
||
var now = DateTimeOffset.UtcNow;
|
||
|
||
// Проверяем, устарела ли дата
|
||
if (targetTemplate.NextRun.HasValue && targetTemplate.NextRun.Value < now)
|
||
{
|
||
logger.LogDebug("Дата шаблона {TemplateId} ({Date}) устарела", targetTemplate.Id, targetTemplate.NextRun);
|
||
|
||
// Строим план на основе оставшихся шаблонов (без устаревшего)
|
||
var remainingTemplates = allTemplates.Where(t => t.Id != targetTemplate.Id && t.NextRun.HasValue).ToList();
|
||
|
||
// Вычисляем план для оставшихся
|
||
var plan = await BuildPlanAsync(startDate, endDate, referenceDate, remainingTemplates, weekends, workDays);
|
||
|
||
// Находим следующую дату в плане, не занятую
|
||
var newDate = FindNextAvailableDateNotInPlan(workDays, DateOnly.FromDateTime(now.DateTime), plan);
|
||
if (newDate.HasValue)
|
||
{
|
||
var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun);
|
||
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun };
|
||
}
|
||
else
|
||
{
|
||
logger.LogWarning("Не удалось найти новую дату для устаревшего шаблона {TemplateId} по плану. Используем ближайшую дату.", targetTemplate.Id);
|
||
|
||
// Используем ближайшую дату после now, не в плане
|
||
var fallbackDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(now.DateTime), DateOnly.FromDateTime(now.DateTime));
|
||
if (fallbackDate.HasValue)
|
||
{
|
||
var nextRun = new DateTimeOffset(fallbackDate.Value.Year, fallbackDate.Value.Month, fallbackDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
logger.LogDebug("Шаблон {TemplateId} перенесен на резервную дату {NewDate}", targetTemplate.Id, nextRun);
|
||
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun };
|
||
}
|
||
else
|
||
{
|
||
logger.LogWarning("Не удалось найти резервную дату для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id);
|
||
|
||
var firstDay = workDays[0];
|
||
var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun };
|
||
}
|
||
}
|
||
}
|
||
|
||
// Если дата не устарела, но не соответствует плану — переносим
|
||
var currentPlan = await BuildPlanAsync(startDate, endDate, referenceDate, allTemplates, weekends, workDays);
|
||
var expectedDate = currentPlan.FirstOrDefault(d => d == DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime));
|
||
if (expectedDate == default)
|
||
{
|
||
logger.LogDebug("Дата шаблона {TemplateId} ({Date}) не соответствует плану. Переносим на ближайшую дату в плане.", targetTemplate.Id, targetTemplate.NextRun);
|
||
|
||
var newDate = FindNextAvailableDateNotInPlan(workDays, DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime), currentPlan);
|
||
if (newDate.HasValue)
|
||
{
|
||
var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun);
|
||
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun };
|
||
}
|
||
else
|
||
{
|
||
logger.LogWarning("Не удалось найти новую дату для шаблона {TemplateId} по плану. Используем ближайшую дату.", targetTemplate.Id);
|
||
|
||
// Используем ближайшую дату, не в плане
|
||
var fallbackDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime), DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime));
|
||
if (fallbackDate.HasValue)
|
||
{
|
||
var nextRun = new DateTimeOffset(fallbackDate.Value.Year, fallbackDate.Value.Month, fallbackDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
logger.LogDebug("Шаблон {TemplateId} перенесен на резервную дату {NewDate}", targetTemplate.Id, nextRun);
|
||
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun };
|
||
}
|
||
else
|
||
{
|
||
logger.LogWarning("Не удалось найти резервную дату для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id);
|
||
|
||
var firstDay = workDays[0];
|
||
var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun };
|
||
}
|
||
}
|
||
}
|
||
|
||
logger.LogDebug("Шаблон {TemplateId} имеет актуальную дату: {NextRun}", targetTemplate.Id, targetTemplate.NextRun);
|
||
|
||
// Проверяем, не раньше ли дата, чем referenceDate (не включая саму referenceDate)
|
||
if (targetTemplate.NextRun < referenceDate)
|
||
{
|
||
logger.LogWarning("Шаблон {TemplateId} имеет дату {NextRun}, которая раньше referenceDate {ReferenceDate}. Переносим на ближайшую дату.", targetTemplate.Id, targetTemplate.NextRun, referenceDate);
|
||
|
||
var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(referenceDate.DateTime), DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime));
|
||
if (newDate.HasValue)
|
||
{
|
||
var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun);
|
||
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun };
|
||
}
|
||
else
|
||
{
|
||
logger.LogWarning("Не удалось найти дату после referenceDate для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id);
|
||
|
||
var firstDay = workDays[0];
|
||
var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
|
||
|
||
return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun };
|
||
}
|
||
}
|
||
|
||
return targetTemplate;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Строит план работ на основе списка шаблонов
|
||
/// </summary>
|
||
private async Task<List<DateOnly>> BuildPlanAsync(
|
||
DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List<TemplateNextRunDto> templates, HashSet<DateOnly> weekends, List<DateOnly> workDays)
|
||
{
|
||
var totalWorks = templates.Count;
|
||
var totalDays = workDays.Count;
|
||
|
||
if (totalDays == 0) return new List<DateOnly>();
|
||
|
||
var interval = (double)totalDays / totalWorks;
|
||
var plan = new List<DateOnly>();
|
||
|
||
for (int i = 0; i < totalWorks; i++)
|
||
{
|
||
var dayIndex = (int)Math.Round(i * interval);
|
||
if (dayIndex >= totalDays) dayIndex = totalDays - 1;
|
||
|
||
var date = workDays[dayIndex];
|
||
plan.Add(date);
|
||
}
|
||
|
||
return plan.OrderBy(d => d).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ищет следующую дату в плане, которая не занята
|
||
/// </summary>
|
||
private DateOnly? FindNextAvailableDateNotInPlan(List<DateOnly> workDays, DateOnly afterDate, List<DateOnly> plan)
|
||
{
|
||
var planSet = new HashSet<DateOnly>(plan);
|
||
|
||
// Ищем вперед
|
||
foreach (var day in workDays)
|
||
{
|
||
if (day > afterDate && !planSet.Contains(day))
|
||
{
|
||
return day;
|
||
}
|
||
}
|
||
|
||
// Ищем назад
|
||
foreach (var day in workDays.AsEnumerable().Reverse())
|
||
{
|
||
if (day <= afterDate && !planSet.Contains(day))
|
||
{
|
||
return day;
|
||
}
|
||
}
|
||
|
||
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 List<DateOnly> GetWorkDays(DateOnly start, DateOnly end, HashSet<DateOnly> weekends)
|
||
{
|
||
var result = new List<DateOnly>();
|
||
var date = start;
|
||
|
||
while (date <= end)
|
||
{
|
||
var dayOfWeek = date.DayOfWeek;
|
||
if (!weekends.Contains(date))
|
||
{
|
||
result.Add(date);
|
||
}
|
||
date = date.AddDays(1);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить список выходных дней
|
||
/// </summary>
|
||
private async Task<HashSet<DateOnly>> GetWeekendsAsync(DateOnly startDate, DateOnly endDate)
|
||
{
|
||
var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();
|
||
return weekends.ToHashSet();
|
||
}
|
||
}
|
||
} |