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 logger; private readonly IWeekendDayService weekendDayService; public TemplateDistributor_old( ILogger logger, IWeekendDayService weekendDayService ) { this.logger = logger; this.weekendDayService = weekendDayService; } /// /// Распределяет все шаблоны на указанный период, учитывая план нагрузки. /// Возвращает список всех шаблонов с обновлёнными датами выполнения. /// public async Task> DistributeTemplatesAsync( DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List 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(); 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; } /// /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки. /// Возвращает обновлённый шаблон с новой датой выполнения. /// public async Task GetValidNextRunForTemplateAsync( DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List 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; } /// /// Строит план работ на основе списка шаблонов /// private async Task> BuildPlanAsync( DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List templates, HashSet weekends, List workDays) { var totalWorks = templates.Count; var totalDays = workDays.Count; if (totalDays == 0) return new List(); var interval = (double)totalDays / totalWorks; var plan = new List(); 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(); } /// /// Ищет следующую дату в плане, которая не занята /// private DateOnly? FindNextAvailableDateNotInPlan(List workDays, DateOnly afterDate, List plan) { var planSet = new HashSet(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; } /// /// Ищет следующую доступную дату после указанной, если не находит — ищет назад. /// private DateOnly? FindNextAvailableDate(List 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; } /// /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. /// private List GetWorkDays(DateOnly start, DateOnly end, HashSet weekends) { var result = new List(); var date = start; while (date <= end) { var dayOfWeek = date.DayOfWeek; if (!weekends.Contains(date)) { result.Add(date); } date = date.AddDays(1); } return result; } /// /// Получить список выходных дней /// private async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate) { var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); return weekends.ToHashSet(); } } }