feat(distributor): реализована логика распределения всех шаблонов в методе DistributeTemplatesAsync

This commit is contained in:
Mikhail Trubnikov
2025-12-25 12:07:53 +10:00
parent a7a82b902a
commit 6a8f90c860
7 changed files with 860 additions and 243 deletions

View File

@@ -0,0 +1,345 @@
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();
}
}
}