feat(distributor): реализована логика распределения всех шаблонов в методе DistributeTemplatesAsync
This commit is contained in:
14
PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs
Normal file
14
PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace PARR.DAL.NextRunServices.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Модель, результат распределения шаблона
|
||||
/// </summary>
|
||||
public class TemplateNextRunResultDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public DateTimeOffset NextRun { get; set; }
|
||||
|
||||
public DateTimeOffset? NextRunOld { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
using PARR.DAL.Models;
|
||||
using PARR.DAL.NextRunServices.Models;
|
||||
using PARR.DAL.NextRunServices.Models;
|
||||
|
||||
namespace PARR.DAL.NextRunServices.Subservices
|
||||
{
|
||||
internal interface ITemplateDistributor
|
||||
//todo: public -> internal!!!
|
||||
public interface ITemplateDistributor
|
||||
{
|
||||
Task<List<TemplateNextRunDto>> GetValidNextRunAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, TimeSpan executionTime);
|
||||
/// <summary>
|
||||
/// Распределяет все шаблоны на указанный период, учитывая план нагрузки.
|
||||
/// Возвращает список всех шаблонов с обновлёнными датами выполнения.
|
||||
/// </summary>
|
||||
/// <param name="startDate">Дата начала распределения</param>
|
||||
/// <param name="duration">Длительность распределения, дней</param>
|
||||
/// <param name="referenceDate">ReferenceDate</param>
|
||||
/// <param name="allTemplates">Спиоск шаблонов для распределения</param>
|
||||
/// <returns></returns>
|
||||
Task<List<TemplateNextRunResultDto>> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List<TemplateNextRunDto> allTemplates);
|
||||
|
||||
Task<List<TemplateNextRunDto>> GetValidNextRunsAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime);
|
||||
|
||||
Task<List<TemplateNextRunDto>> ReassignOutdatedRunsAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime);
|
||||
|
||||
Task<List<TemplateNextRunDto>> RebalanceTemplatesAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime);
|
||||
|
||||
Task<List<TemplateNextRunDto>> ScheduleTemplatesAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime);
|
||||
/// <summary>
|
||||
/// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки.
|
||||
/// Возвращает обновлённый шаблон с новой датой выполнения.
|
||||
/// </summary>
|
||||
Task<TemplateNextRunResultDto> GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List<TemplateNextRunDto> allTemplates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,13 @@ namespace PARR.DAL.NextRunServices.Subservices
|
||||
private readonly ILogger<TemplateDistributor> logger;
|
||||
private readonly IWeekendDayService weekendDayService;
|
||||
|
||||
/// <summary>
|
||||
/// Смещение nextRun на несколько лет вперед, если не смог его рассчитать
|
||||
/// </summary>
|
||||
private const int offsetYearsIfErrorCalcNextRun = 5;
|
||||
|
||||
public TemplateDistributor(
|
||||
ILogger<TemplateDistributor> logger,
|
||||
ILogger<TemplateDistributor> logger,
|
||||
IWeekendDayService weekendDayService
|
||||
)
|
||||
{
|
||||
@@ -19,289 +24,417 @@ namespace PARR.DAL.NextRunServices.Subservices
|
||||
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();
|
||||
public Task<TemplateNextRunResultDto> GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List<TemplateNextRunDto> allTemplates)
|
||||
{
|
||||
//TODO:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<List<TemplateNextRunResultDto>> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List<TemplateNextRunDto> allTemplates)
|
||||
{
|
||||
var _startDate = GetStartPeriodDate(startDate, referenceDate);
|
||||
var _endDate = GetEndPeriodDate(_startDate, duration);
|
||||
|
||||
logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}", allTemplates.Count, duration, _startDate, _endDate, referenceDate);
|
||||
|
||||
// Получаем список выходных/праздничных дней
|
||||
var weekends = await GetWeekendsAsync(_startDate, _endDate);
|
||||
logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count);
|
||||
|
||||
foreach (var weekend in weekends)
|
||||
logger.LogDebug("Не рабочий день: {weekend}", weekend);
|
||||
|
||||
// Получаем список рабочих дней (исключая выходные и праздники)
|
||||
var workDays = GetWorkDays(startDate, endDate, weekends);
|
||||
var workDays = GetWorkDays(_startDate, _endDate, weekends);
|
||||
|
||||
logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count);
|
||||
logger.LogDebug("Найдено рабочих дней: {Count}", workDays.Count);
|
||||
|
||||
foreach (var workDay in workDays)
|
||||
logger.LogDebug("Рабочий день: {workDay}", workDay);
|
||||
|
||||
// Проверяем, есть ли хотя бы один рабочий день
|
||||
if (workDays.Count == 0)
|
||||
{
|
||||
logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate);
|
||||
return new List<TemplateNextRunDto>();
|
||||
}
|
||||
logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun);
|
||||
|
||||
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)
|
||||
// смещаем все nextRun вперед
|
||||
var result = allTemplates.Select(t => new TemplateNextRunResultDto
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} с датой {OldDate} устарел", template.Id, template.NextRun);
|
||||
Id = t.Id,
|
||||
NextRun = t.NextRun.HasValue
|
||||
// если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun
|
||||
? CreateNextRun(t.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate)
|
||||
// если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun
|
||||
: CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
|
||||
NextRunOld = t.NextRun
|
||||
}).OrderBy(t => t.NextRun).ToList();
|
||||
|
||||
var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(now.DateTime), DateOnly.FromDateTime(template.NextRun.Value.Date));
|
||||
if (newDate.HasValue)
|
||||
foreach (var item in result)
|
||||
logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id}, nextRun: {NextRun}, nextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld);
|
||||
|
||||
logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", result.Count, result.Count(t => t.NextRun == t.NextRunOld), result.Count(t => t.NextRun != t.NextRunOld));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
var totalWorks = allTemplates.Count;
|
||||
var totalDays = workDays.Count;
|
||||
|
||||
// интервал между шаблонами
|
||||
var interval = GetInterval(totalDays, totalWorks);
|
||||
|
||||
// строим план, дни на которые должны быть назанчены шаблоны
|
||||
var plan = GetPlan(totalWorks, totalDays, workDays);
|
||||
|
||||
|
||||
// Подсчитываем, сколько работ должно быть на каждый день
|
||||
var expectedDailyLoad = new Dictionary<DateOnly, int>();
|
||||
|
||||
foreach (var day in plan)
|
||||
expectedDailyLoad[day] = expectedDailyLoad.GetValueOrDefault(day, 0) + 1;
|
||||
|
||||
#region Группируем шаблоны по дням, на которые они уже назначены
|
||||
|
||||
var templatesByDay = new Dictionary<DateOnly, List<TemplateNextRunDto>>();
|
||||
|
||||
foreach (var template in allTemplates)
|
||||
{
|
||||
if (template.NextRun.HasValue)
|
||||
{
|
||||
var day = DateOnly.FromDateTime(template.NextRun.Value.Date);
|
||||
if (!templatesByDay.ContainsKey(day))
|
||||
{
|
||||
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 });
|
||||
templatesByDay[day] = new List<TemplateNextRunDto>();
|
||||
}
|
||||
else
|
||||
templatesByDay[day].Add(template);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Подсчитываем текущую нагрузку на каждый день
|
||||
|
||||
var currentDailyLoad = new Dictionary<DateOnly, int>();
|
||||
|
||||
foreach (var day in workDays)
|
||||
{
|
||||
currentDailyLoad[day] = 0;
|
||||
}
|
||||
|
||||
foreach (var kvp in templatesByDay)
|
||||
{
|
||||
var day = kvp.Key;
|
||||
var count = kvp.Value.Count;
|
||||
currentDailyLoad[day] = count;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Определяем, какие шаблоны нужно оставить, а какие — переназначить
|
||||
|
||||
var templatesToKeep = new List<TemplateNextRunResultDto>();
|
||||
var templatesToReassign = new List<TemplateNextRunDto>();
|
||||
|
||||
foreach (var kvp in templatesByDay)
|
||||
{
|
||||
var day = kvp.Key;
|
||||
var templates = kvp.Value;
|
||||
|
||||
// Если день — нерабочий, то все шаблоны нужно переназначить
|
||||
if (!workDays.Contains(day))
|
||||
{
|
||||
foreach (var template in templates)
|
||||
{
|
||||
logger.LogWarning("Не удалось найти новую дату для шаблона {TemplateId}", template.Id);
|
||||
templatesToReassign.Add(template);
|
||||
logger.LogDebug("Шаблон {TemplateId} назначен на нерабочий день {NextRun} — будет переназначен.", template.Id, template.NextRun);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var expectedCount = expectedDailyLoad.GetValueOrDefault(day, 0);
|
||||
var currentCount = templates.Count;
|
||||
|
||||
if (currentCount <= expectedCount)
|
||||
{
|
||||
// Все шаблоны в этом дне можно оставить
|
||||
foreach (var template in templates)
|
||||
{
|
||||
templatesToKeep.Add(new TemplateNextRunResultDto
|
||||
{
|
||||
Id = template.Id,
|
||||
NextRun = template.NextRun.Value,
|
||||
NextRunOld = template.NextRun
|
||||
});
|
||||
logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Оставляем только expectedCount штук, остальные — переназначить
|
||||
for (int i = 0; i < expectedCount; i++)
|
||||
{
|
||||
var template = templates[i];
|
||||
templatesToKeep.Add(new TemplateNextRunResultDto
|
||||
{
|
||||
Id = template.Id,
|
||||
NextRun = template.NextRun.Value,
|
||||
NextRunOld = template.NextRun
|
||||
});
|
||||
logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun);
|
||||
}
|
||||
|
||||
for (int i = expectedCount; i < currentCount; i++)
|
||||
{
|
||||
var template = templates[i];
|
||||
templatesToReassign.Add(template);
|
||||
logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. нагрузка в день превышена.", template.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("Переназначение завершено. Изменено {Count} записей.", result.Count);
|
||||
#endregion
|
||||
|
||||
return result;
|
||||
}
|
||||
#region Добавляем шаблоны, у которых не было даты — тоже нужно распределить
|
||||
|
||||
|
||||
/// <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)
|
||||
foreach (var template in allTemplates)
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} имеет актуальную дату {NextRun}", template.Id, nextRun);
|
||||
return new List<TemplateNextRunDto> { new TemplateNextRunDto { Id = template.Id, NextRun = nextRun.Value } };
|
||||
if (!template.NextRun.HasValue)
|
||||
{
|
||||
templatesToReassign.Add(template);
|
||||
logger.LogDebug("Шаблон {TemplateId} не имел даты — будет распределён.", template.Id);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogWarning("Не удалось получить актуальную дату для шаблона {TemplateId}", template.Id);
|
||||
return new List<TemplateNextRunDto>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает список актуальных дат выполнения для всех переданных шаблонов.
|
||||
/// </summary>
|
||||
public async Task<List<TemplateNextRunDto>> GetValidNextRunsAsync(List<TemplateNextRunDto> templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime)
|
||||
{
|
||||
logger.LogDebug("Запрос актуальных дат для {Count} шаблонов", templates.Count);
|
||||
var updatedTemplates = new List<TemplateNextRunResultDto>(templatesToKeep);
|
||||
|
||||
// Получаем список выходных/праздничных дней
|
||||
var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet();
|
||||
|
||||
var result = new List<TemplateNextRunDto>();
|
||||
|
||||
foreach (var template in templates)
|
||||
foreach (var template in templatesToReassign)
|
||||
{
|
||||
var nextRun = await GetValidNextRunInternalAsync(template, startDate, endDate, weekends, executionTime);
|
||||
var nextRun = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDate, expectedDailyLoad);
|
||||
if (nextRun.HasValue)
|
||||
{
|
||||
result.Add(new TemplateNextRunDto { Id = template.Id, NextRun = nextRun.Value });
|
||||
}
|
||||
}
|
||||
var day = DateOnly.FromDateTime(nextRun.Value.Date);
|
||||
currentDailyLoad[day]++;
|
||||
|
||||
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)
|
||||
updatedTemplates.Add(new TemplateNextRunResultDto
|
||||
{
|
||||
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;
|
||||
Id = template.Id,
|
||||
NextRun = nextRun.Value,
|
||||
NextRunOld = template.NextRun
|
||||
});
|
||||
|
||||
logger.LogDebug("Шаблону {TemplateId} назначен NextRun: {NextRun}, NextRunOld: {NextRunOld}", template.Id, nextRun, template.NextRun);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("Не удалось назначить дату для шаблона {TemplateId}", template.Id);
|
||||
|
||||
// Возвращаем с ошибкой
|
||||
updatedTemplates.Add(new TemplateNextRunResultDto
|
||||
{
|
||||
Id = template.Id,
|
||||
NextRun = CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
|
||||
NextRunOld = template.NextRun
|
||||
});
|
||||
}
|
||||
// Дата валидна и не устарела
|
||||
logger.LogDebug("Шаблон {TemplateId} имеет действительную дату: {NextRun}", template.Id, template.NextRun);
|
||||
return template.NextRun;
|
||||
}
|
||||
|
||||
// Дата недействительна (выходной или вне периода) — ищем ближайшую
|
||||
var closest = FindClosestValidDate(workDays, scheduledDate);
|
||||
if (closest.HasValue)
|
||||
#endregion
|
||||
|
||||
|
||||
updatedTemplates = updatedTemplates.OrderBy(t => t.NextRun).ToList();
|
||||
|
||||
logger.LogDebug("--- Результат распределения: ---");
|
||||
foreach (var item in updatedTemplates)
|
||||
logger.LogDebug("Шаблон {templateId}, NextRun: {NextRun}, NextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld);
|
||||
|
||||
logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", updatedTemplates.Count, updatedTemplates.Count(t => t.NextRun == t.NextRunOld), updatedTemplates.Count(t => t.NextRun != t.NextRunOld));
|
||||
|
||||
return updatedTemplates;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad
|
||||
/// </summary>
|
||||
/// <param name="currentDailyLoad"></param>
|
||||
/// <param name="workDays"></param>
|
||||
/// <param name="referenceDate"></param>
|
||||
/// <param name="expectedDailyLoad"></param>
|
||||
/// <returns></returns>
|
||||
private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary<DateOnly, int> currentDailyLoad, List<DateOnly> workDays, DateTimeOffset referenceDate, Dictionary<DateOnly, int> expectedDailyLoad)
|
||||
{
|
||||
foreach (var day in workDays)
|
||||
{
|
||||
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;
|
||||
var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
|
||||
var current = currentDailyLoad.GetValueOrDefault(day, 0);
|
||||
|
||||
if (current < expected)
|
||||
{
|
||||
return CreateNextRun(day, referenceDate);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogWarning("Не удалось найти подходящую дату для шаблона {TemplateId}", template.Id);
|
||||
return null;
|
||||
// Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план
|
||||
foreach (var day in workDays)
|
||||
{
|
||||
var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
|
||||
var current = currentDailyLoad.GetValueOrDefault(day, 0);
|
||||
|
||||
if (current < expected)
|
||||
{
|
||||
return CreateNextRun(day, referenceDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Если все дни уже загружены по плану — ищем день с минимальной нагрузкой
|
||||
var minLoad = currentDailyLoad.Values.Min();
|
||||
var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key;
|
||||
|
||||
return CreateNextRun(minDay, referenceDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получить план: дни, на которые должны быть назначены шаблоны
|
||||
/// </summary>
|
||||
/// <param name="totalWorks"></param>
|
||||
/// <param name="totalDays"></param>
|
||||
/// <param name="workDays"></param>
|
||||
/// <returns></returns>
|
||||
private List<DateOnly> GetPlan(int totalWorks, int totalDays, List<DateOnly> workDays)
|
||||
{
|
||||
var interval = GetInterval(totalDays, totalWorks);
|
||||
|
||||
var plan = new List<DateOnly>();
|
||||
|
||||
for (int i = 0; i < totalWorks; i++)
|
||||
{
|
||||
#region Вынести в метод
|
||||
// Вычисляем индекс дня, на который нужно назначить i-ю работу
|
||||
//var dayIndex = (int)Math.Round(i * interval);
|
||||
var dayIndex = (int)Math.Floor(i * interval);
|
||||
|
||||
// Защита от возможного выхода за границы массива из-за округления
|
||||
if (dayIndex >= totalDays)
|
||||
dayIndex = totalDays - 1;
|
||||
#endregion
|
||||
|
||||
var date = workDays[dayIndex];
|
||||
plan.Add(date);
|
||||
}
|
||||
|
||||
return plan;
|
||||
// return new HashSet<DateOnly>(plan);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Ищет следующую доступную дату после указанной, если не находит — ищет назад.
|
||||
/// Рассчитать интервал между шаблонами
|
||||
/// </summary>
|
||||
private DateOnly? FindNextAvailableDate(List<DateOnly> workDays, DateOnly afterDate, DateOnly originalDate)
|
||||
/// <param name="totalDays">Кол-во рабочих дней</param>
|
||||
/// <param name="totalWorks">Кол-во шаблонов</param>
|
||||
/// <returns></returns>
|
||||
private double GetInterval(int totalDays, int totalWorks)
|
||||
{
|
||||
var future = workDays.FirstOrDefault(d => d > afterDate);
|
||||
var interval = (double)totalDays / totalWorks;
|
||||
|
||||
if (future != default)
|
||||
return future;
|
||||
|
||||
var past = workDays.Where(t => t >= originalDate).OrderByDescending(t => t).FirstOrDefault();
|
||||
|
||||
return past != default ? (DateOnly?)past : null;
|
||||
return interval;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Ищет ближайшую дату (вперед или назад) к заданной, среди доступных рабочих дней.
|
||||
/// Создать NextRun (из DateTimeOffset + refDate)
|
||||
/// </summary>
|
||||
private DateOnly? FindClosestValidDate(List<DateOnly> workDays, DateOnly targetDate)
|
||||
/// <param name="date">Откуда взять дату</param>
|
||||
/// <param name="refDate">Откуда взять время</param>
|
||||
/// <returns></returns>
|
||||
private DateTimeOffset CreateNextRun(DateTimeOffset date, DateTimeOffset refDate)
|
||||
{
|
||||
var future = workDays.FirstOrDefault(t => t >= targetDate);
|
||||
return CreateNextRun(DateOnly.FromDateTime(date.Date), refDate);
|
||||
}
|
||||
|
||||
if (future != default)
|
||||
return future;
|
||||
/// <summary>
|
||||
/// Создать NextRun (из DateOnly + refDate)
|
||||
/// </summary>
|
||||
/// <param name="date"></param>
|
||||
/// <param name="refDate"></param>
|
||||
/// <returns></returns>
|
||||
private DateTimeOffset CreateNextRun(DateOnly date, DateTimeOffset refDate)
|
||||
{
|
||||
return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
var past = workDays.Where(t => t <= targetDate).OrderByDescending(t => t).FirstOrDefault();
|
||||
|
||||
return past != default ? (DateOnly?)past : null;
|
||||
/// <summary>
|
||||
/// Получить дату начала распределения относительно StartDate, ReferenceDate и текущей даты
|
||||
/// </summary>
|
||||
/// <param name="startDate"></param>
|
||||
/// <param name="referenceDate"></param>
|
||||
/// <returns></returns>
|
||||
private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate)
|
||||
{
|
||||
// дата начала не может быть раньше referenceDate
|
||||
// и не может быть раньше чем сегодня
|
||||
|
||||
var dateStart = startDate;
|
||||
|
||||
// если dateStart < чем refDate, то берем refDate
|
||||
if (dateStart < DateOnly.FromDateTime(referenceDate.Date))
|
||||
dateStart = DateOnly.FromDateTime(referenceDate.Date);
|
||||
|
||||
// если dateStart < чем текущая дата, то берем ьекущую дату
|
||||
if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date))
|
||||
dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date);
|
||||
|
||||
return dateStart;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получить дату конца периода распределения
|
||||
/// </summary>
|
||||
/// <param name="startDate"></param>
|
||||
/// <param name="duration"></param>
|
||||
/// <returns></returns>
|
||||
private DateOnly GetEndPeriodDate(DateOnly startDate, int duration)
|
||||
{
|
||||
// duration — количество дней, включая startDate
|
||||
// поэтому вычитаем 1, чтобы получить сдвиг
|
||||
|
||||
return startDate.AddDays(duration - 1);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получить список выходных дней
|
||||
/// </summary>
|
||||
/// <param name="startDate"></param>
|
||||
/// <param name="endDate"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<HashSet<DateOnly>> GetWeekendsAsync(DateOnly startDate, DateOnly endDate)
|
||||
{
|
||||
var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();
|
||||
|
||||
return weekends.ToHashSet();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники.
|
||||
/// </summary>
|
||||
/// <param name="start"></param>
|
||||
/// <param name="end"></param>
|
||||
/// <param name="weekends"></param>
|
||||
/// <returns></returns>
|
||||
private List<DateOnly> GetWorkDays(DateOnly start, DateOnly end, HashSet<DateOnly> weekends)
|
||||
{
|
||||
var result = new List<DateOnly>();
|
||||
@@ -309,6 +442,7 @@ namespace PARR.DAL.NextRunServices.Subservices
|
||||
|
||||
while (date <= end)
|
||||
{
|
||||
var dayOfWeek = date.DayOfWeek;
|
||||
if (!weekends.Contains(date))
|
||||
{
|
||||
result.Add(date);
|
||||
@@ -318,5 +452,6 @@ namespace PARR.DAL.NextRunServices.Subservices
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
345
PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs
Normal file
345
PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user