@@ -29,6 +29,255 @@ namespace PARR.DAL.NextRunServices.Subservices
this . weekendDayService = weekendDayService ;
}
public async Task < TemplateNextRunResultDto > GetNextRunForTemplateAsync ( DateOnly startDate , int duration , DateTimeOffset referenceDate , TimeSpan ? offset , TemplateNextRunDto targetTemplate , List < TemplateNextRunDto > allTemplates , bool excludeWeekends , bool isNew )
{
logger . LogInformation ( "Начинаем проверку/расчёт NextRun для шаблона {templateId}, nextRun={nextRun}. isNew={isNew}, offset={offset}, referenceDate={referenceDate}" , targetTemplate . Id , targetTemplate . NextRun , isNew , offset , referenceDate ) ;
// 1. Определяем целевой часовой пояс
var targetOffset = offset ? ? TimeSpan . Zero ;
var referenceDateInTargetTz = referenceDate . ToOffset ( targetOffset ) ;
logger . LogDebug ( "Целевой часовой пояс: {offset}. ReferenceDate в целевом поясе: {refDate}" , targetOffset , referenceDateInTargetTz ) ;
// 2. Рассчитываем период распределения в целевом поясе
var _startDate = GetStartPeriodDate ( startDate , referenceDateInTargetTz , targetOffset ) ;
var _endDate = GetEndPeriodDate ( _startDate , duration ) ;
logger . LogInformation ( "Период распределения: {_startDate} - {_endDate} (в поясе {offset})" , _startDate , _endDate , targetOffset ) ;
// 3. Получаем список рабочих дней в периоде
var workDays = await GetWorkDaysAsync ( _startDate , _endDate , excludeWeekends ) ;
if ( workDays . Count = = 0 )
{
logger . LogWarning ( "Нет рабочих дней в периоде {_startDate} - {_endDate} (пояс {offset}). Возвращаем ошибку с о смещением." , _startDate , _endDate , targetOffset ) ;
return FillTemplateResultWithOffsetError ( targetTemplate , _startDate , referenceDateInTargetTz , targetOffset ) ;
}
#region 4. С т р о и м п л а н н а г р у з к и и р а с с ч и т ы в а е м т е к у щ у ю н а г р у з к у
logger . LogDebug ( "Строим план распределения для шаблона {templateId}. В с е г о шаблонов: {totalTemplates}" , targetTemplate . Id , allTemplates . Count ) ;
// Исключаем текущий шаблон из расчёта
var templatesForPlan = allTemplates . Where ( t = > t . Id ! = targetTemplate . Id ) . ToList ( ) ;
logger . LogDebug ( "Исключили текущий шаблон из расчёта. Осталось {count} шаблонов для плана" , templatesForPlan . Count ) ;
// Определяем тип плана (обычный или зеркальный)
bool useMirrorPlan = false ;
// Определяем тип плана на основе контекста
if ( ! targetTemplate . NextRun . HasValue )
{
// Нет текущего NextRun — тип плана зависит от isNew
useMirrorPlan = ! isNew ;
logger . LogDebug ( "Нет текущего NextRun. Тип плана: {planType} (isNew={isNew})" , useMirrorPlan ? "зеркальный" : "обычный" , isNew ) ;
}
else
{
// Есть текущий NextRun — проверяем, в периоде ли он
var _nextRunInTargetTz = targetTemplate . NextRun . Value . ToOffset ( targetOffset ) ;
var _nextRunDate = DateOnly . FromDateTime ( _nextRunInTargetTz . Date ) ;
bool _isInPeriod = _nextRunDate > = _startDate & & _nextRunDate < = _endDate ;
bool _isWorkDay = workDays . Contains ( _nextRunDate ) ;
if ( _isInPeriod & & ! _isWorkDay )
{
// В периоде, но нерабочий день — обычный план
useMirrorPlan = false ;
logger . LogDebug ( "NextRun в периоде, но на нерабочем дне. Используем обычный план." ) ;
}
else if ( ! _isInPeriod )
{
// Н е в периоде — тип плана зависит от isNew
useMirrorPlan = ! isNew ;
logger . LogDebug ( "NextRun вне периода. Тип плана: {planType} (isNew={isNew})" , useMirrorPlan ? "зеркальный" : "обычный" , isNew ) ;
}
else
{
// В периоде и рабочий день — будем проверять актуальность
useMirrorPlan = false ;
logger . LogDebug ( "NextRun в периоде и на рабочем дне. Используем обычный план для проверки." ) ;
}
}
// Строим план распределения
List < DateOnly > plan ;
if ( useMirrorPlan )
{
// Зеркальный план
plan = GetDistributionPlan ( templatesForPlan . Count + 1 , workDays , mirror : true ) ;
logger . LogDebug ( "Построен зеркальный план для {count} шаблонов" , templatesForPlan . Count + 1 ) ;
}
else
{
// Обычный план
plan = GetDistributionPlan ( templatesForPlan . Count + 1 , workDays ) ;
logger . LogDebug ( "Построен обычный план для {count} шаблонов" , templatesForPlan . Count + 1 ) ;
}
// Считаем ожидаемую нагрузку из плана
var expectedDailyLoad = plan . GroupBy ( d = > d ) . ToDictionary ( g = > g . Key , g = > g . Count ( ) ) ;
logger . LogDebug ( "Ожидаемая нагрузка по плану: {loadCount} дней" , expectedDailyLoad . Count ) ;
foreach ( var kvp in expectedDailyLoad . OrderBy ( x = > x . Key ) )
logger . LogDebug ( " {date}: ожидаемо {count} шаблонов" , kvp . Key , kvp . Value ) ;
// Считаем текущую нагрузку (только шаблоны внутри периода)
var currentDailyLoad = BuildCurrentLoad ( templatesForPlan , workDays , _startDate , _endDate , targetOffset ) ;
logger . LogDebug ( "Текущая нагрузка (без учёта текущего шаблона): {loadCount} дней" , currentDailyLoad . Count ) ;
foreach ( var kvp in currentDailyLoad . OrderBy ( x = > x . Key ) )
logger . LogDebug ( " {date}: текущая {current}, ожидаемая {expected}" , kvp . Key , kvp . Value , expectedDailyLoad . GetValueOrDefault ( kvp . Key , 0 ) ) ;
#endregion
// 5. Случай: нет текущего NextRun
if ( ! targetTemplate . NextRun . HasValue )
{
logger . LogInformation ( "Шаблон {templateId} не имеет текущего NextRun. Ищем новое место..." , targetTemplate . Id ) ;
return HandleNoNextRun (
targetTemplate ,
workDays ,
currentDailyLoad ,
expectedDailyLoad ,
referenceDateInTargetTz ,
targetOffset ,
isNew ) ;
}
#region 6. С л у ч а й : е с т ь т е к у щ и й NextRun — п р о в е р я е м а к т у а л ь н о с т ь
logger . LogDebug ( "Шаблон {templateId} имеет текущий NextRun: {nextRun}. Проверяем актуальность..." , targetTemplate . Id , targetTemplate . NextRun . Value ) ;
var nextRunInTargetTz = targetTemplate . NextRun . Value . ToOffset ( targetOffset ) ;
var nextRunDate = DateOnly . FromDateTime ( nextRunInTargetTz . Date ) ;
logger . LogDebug ( "NextRun в целевом поясе {offset}: {nextRunTarget} (дата: {date})" , targetOffset , nextRunInTargetTz , nextRunDate ) ;
// Проверка 1: В периоде?
var isInPeriod = nextRunDate > = _startDate & & nextRunDate < = _endDate ;
logger . LogDebug ( "Проверка 1 (в периоде): {result} ({date} ∈ [{start}, {end}])" , isInPeriod , nextRunDate , _startDate , _endDate ) ;
// Проверка 2: Рабочий день?
var isWorkDay = workDays . Contains ( nextRunDate ) ;
logger . LogDebug ( "Проверка 2 (рабочий день): {result}" , isWorkDay ? "да" : "нет" ) ;
// Проверка 3: В будущем?
var nextRunMoment = new DateTimeOffset (
nextRunDate . Year , nextRunDate . Month , nextRunDate . Day ,
nextRunInTargetTz . Hour , nextRunInTargetTz . Minute , nextRunInTargetTz . Second ,
targetOffset ) ;
var isFuture = nextRunMoment > = DateTimeOffset . UtcNow ;
logger . LogDebug ( "Проверка 3 (в будущем): {result} ({moment} >= {now})" , isFuture , nextRunMoment , DateTimeOffset . UtcNow ) ;
// Проверка 4: Время совпадает с referenceDate?
var hasMatchingTime = nextRunInTargetTz . Hour = = referenceDateInTargetTz . Hour & &
nextRunInTargetTz . Minute = = referenceDateInTargetTz . Minute & &
nextRunInTargetTz . Second = = referenceDateInTargetTz . Second ;
logger . LogDebug ( "Проверка 4 (время совпадает): {result} (NextRun: {nextRunHour}:{nextRunMin}:{nextRunSec} vs Reference: {refHour}:{refMin}:{refSec})" ,
hasMatchingTime ,
nextRunInTargetTz . Hour , nextRunInTargetTz . Minute , nextRunInTargetTz . Second ,
referenceDateInTargetTz . Hour , referenceDateInTargetTz . Minute , referenceDateInTargetTz . Second ) ;
// СЛУЧАЙ А : Дата актуальна, но время не совпадает → корректируем ТОЛЬКО время
if ( isInPeriod & & isWorkDay & & isFuture & & ! hasMatchingTime )
{
logger . LogDebug ( "Дата NextRun шаблона {templateId} актуальна, но время не совпадает. Корректируем время с {oldTime} на {newTime} (пояс {offset})." ,
targetTemplate . Id ,
$"{nextRunInTargetTz.Hour:D2}:{nextRunInTargetTz.Minute:D2}:{nextRunInTargetTz.Second:D2}" ,
$"{referenceDateInTargetTz.Hour:D2}:{referenceDateInTargetTz.Minute:D2}:{referenceDateInTargetTz.Second:D2}" ,
targetOffset ) ;
// Сохраняем ту же дату, но меняем время на время из referenceDate
var correctedNextRunInTargetTz = new DateTimeOffset (
nextRunDate . Year , nextRunDate . Month , nextRunDate . Day ,
referenceDateInTargetTz . Hour ,
referenceDateInTargetTz . Minute ,
referenceDateInTargetTz . Second ,
targetOffset ) ;
//скорректированный момент ещё в будущем?
if ( correctedNextRunInTargetTz > = DateTimeOffset . UtcNow )
{
// Время актуально — возвращаем скорректированный NextRun
var correctedNextRunUtc = correctedNextRunInTargetTz . ToOffset ( TimeSpan . Zero ) ;
logger . LogInformation ( "Скорректированный NextRun для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {nextRunOld}" ,
targetTemplate . Id , correctedNextRunInTargetTz , targetOffset , correctedNextRunUtc , targetTemplate . NextRun ) ;
return new TemplateNextRunResultDto (
Id : targetTemplate . Id ,
NextRun : correctedNextRunUtc ,
NextRunOld : targetTemplate . NextRun ) ;
}
else
{
// Скорректированное время уже прошло — считаем дату НЕАКТУАЛЬНОЙ
logger . LogDebug ( "Скорректированное время {correctedTime} уже прошло (сейчас {now}). Считаем дату неактуальной и передаём в основную логику поиска." ,
correctedNextRunInTargetTz , DateTimeOffset . UtcNow ) ;
// Помечаем дату как неактуальную по времени
isFuture = false ;
// Дальнейшая логика сама вызовет HandleOutdatedNextRun с правильными параметрами
}
}
// СЛУЧАЙ Б: В с е проверки пройдены (включая время) → возвращаем без изменений
if ( isInPeriod & & isWorkDay & & isFuture & & hasMatchingTime )
{
var nextRunUtc = targetTemplate . NextRun . Value . ToOffset ( TimeSpan . Zero ) ;
logger . LogInformation ( "NextRun шаблона {templateId} полностью актуален. Возвращаем без изменений: {nextRunUtc} (UTC)" ,
targetTemplate . Id , nextRunUtc ) ;
return new TemplateNextRunResultDto (
Id : targetTemplate . Id ,
NextRun : nextRunUtc ,
NextRunOld : targetTemplate . NextRun ) ;
}
// СЛУЧАЙ В : Дата неактуальна (не в периоде / нерабочий день / уже прошло) → ищем новое место
logger . LogDebug ( "NextRun шаблона {templateId} неактуален по дате (в периоде={inPeriod}, рабочий={workDay}, будущее={future}). Ищем новое место..." ,
targetTemplate . Id , isInPeriod , isWorkDay , isFuture ) ;
#endregion
// 7. NextRun неактуален — ищем новое место
logger . LogDebug ( "NextRun шаблона {templateId} неактуален (в периоде={inPeriod}, рабочий={workDay}, будущее={future}). Ищем новое место..." , targetTemplate . Id , isInPeriod , isWorkDay , isFuture ) ;
return HandleOutdatedNextRun (
targetTemplate ,
nextRunDate ,
isInPeriod ,
isWorkDay ,
isNew ,
workDays ,
currentDailyLoad ,
expectedDailyLoad ,
referenceDateInTargetTz ,
targetOffset ,
_startDate ,
_endDate ) ;
}
/// <summary>
/// Распределить шаблоны
/// </summary>
/// <param name="startDate"></param>
/// <param name="duration"></param>
/// <param name="referenceDate"></param>
/// <param name="offset"></param>
/// <param name="allTemplates"></param>
/// <param name="excludeWeekends"></param>
/// <returns></returns>
public async Task < List < TemplateNextRunResultDto > > DistributeTemplatesAsync ( DateOnly startDate , int duration , DateTimeOffset referenceDate , TimeSpan ? offset , List < TemplateNextRunDto > allTemplates , bool excludeWeekends )
{
// 1. Определяем целевой часовой пояс
@@ -65,7 +314,7 @@ namespace PARR.DAL.NextRunServices.Subservices
// 5. Строим план распределения
// строим план, дни на которые должны быть назанчены шаблоны
var plan = GetPlan ( allTemplates . Count , workDays ) ;
var plan = GetDistribution Plan ( allTemplates . Count , workDays ) ;
// Подсчитываем, сколько работ должно быть на каждый день
var expectedDailyLoad = plan . GroupBy ( d = > d )
. ToDictionary ( g = > g . Key , g = > g . Count ( ) ) ;
@@ -233,6 +482,383 @@ namespace PARR.DAL.NextRunServices.Subservices
}
/// <summary>
/// Обрабатывает случай, когда у шаблона нет текущего NextRun.
/// </summary>
private TemplateNextRunResultDto HandleNoNextRun ( TemplateNextRunDto targetTemplate , List < DateOnly > workDays , Dictionary < DateOnly , int > currentDailyLoad , Dictionary < DateOnly , int > expectedDailyLoad , DateTimeOffset referenceDateInTargetTz , TimeSpan targetOffset , bool isNew )
{
logger . LogDebug ( "Обработка случая 'нет текущего NextRun' для шаблона {templateId} (isNew={isNew})" , targetTemplate . Id , isNew ) ;
DateOnly ? foundDay = null ;
if ( isNew )
{
// Ищем от начала периода к концу
logger . LogDebug ( "isNew=true: ищем от начала периода к концу" ) ;
foreach ( var day in workDays )
{
if ( currentDailyLoad [ day ] < expectedDailyLoad . GetValueOrDefault ( day , 0 ) )
{
foundDay = day ;
logger . LogDebug ( "Нашли первый не перегруженный день: {day}" , day ) ;
break ;
}
}
if ( ! foundDay . HasValue )
{
// Н е нашли свободного места — берём первый день периода
foundDay = workDays . First ( ) ;
logger . LogWarning ( "В с е дни перегружены. Берём первый день периода: {day}" , foundDay . Value ) ;
}
}
else
{
// Ищем от конца периода к началу
logger . LogDebug ( "isNew=false: ищем от конца периода к началу" ) ;
foreach ( var day in workDays . AsEnumerable ( ) . Reverse ( ) )
{
if ( currentDailyLoad [ day ] < expectedDailyLoad . GetValueOrDefault ( day , 0 ) )
{
foundDay = day ;
logger . LogDebug ( "Нашли первый не перегруженный день: {day}" , day ) ;
break ;
}
}
if ( ! foundDay . HasValue )
{
// Н е нашли свободного места — берём последний день периода
foundDay = workDays . Last ( ) ;
logger . LogWarning ( "В с е дни перегружены. Берём последний день периода: {day}" , foundDay . Value ) ;
}
}
// Формируем результат
var nextRunInTargetTz = new DateTimeOffset (
foundDay . Value . Year , foundDay . Value . Month , foundDay . Value . Day ,
referenceDateInTargetTz . Hour ,
referenceDateInTargetTz . Minute ,
referenceDateInTargetTz . Second ,
targetOffset ) ;
// Корректируем найденный день, чтобы момент времени был в будущем
DateTimeOffset ? adjustedNextRun ;
TemplateNextRunResultDto ? errorResult = null ;
adjustedNextRun = AdjustNextRunToFuture (
foundDay . Value ,
workDays ,
currentDailyLoad ,
expectedDailyLoad ,
referenceDateInTargetTz ,
targetOffset ,
workDays . First ( ) , // startDateForError для случая ошибки
targetTemplate ,
out errorResult ) ;
if ( errorResult ! = null )
return errorResult ;
//var nextRunUtc = nextRunInTargetTz.ToOffset(TimeSpan.Zero);
var nextRunUtc = adjustedNextRun . Value . ToOffset ( TimeSpan . Zero ) ;
logger . LogInformation ( "Найдено новое место для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC)" , targetTemplate . Id , nextRunInTargetTz , targetOffset , nextRunUtc ) ;
return new TemplateNextRunResultDto (
Id : targetTemplate . Id ,
NextRun : nextRunUtc ,
NextRunOld : targetTemplate . NextRun ) ;
}
/// <summary>
/// Обрабатывает случай, когда текущий NextRun неактуален.
/// </summary>
private TemplateNextRunResultDto HandleOutdatedNextRun ( TemplateNextRunDto targetTemplate , DateOnly nextRunDate , bool isInPeriod , bool isWorkDay , bool isNew , List < DateOnly > workDays , Dictionary < DateOnly , int > currentDailyLoad , Dictionary < DateOnly , int > expectedDailyLoad , DateTimeOffset referenceDateInTargetTz , TimeSpan targetOffset , DateOnly periodStart , DateOnly periodEnd )
{
logger . LogDebug ( "Обработка случая 'неактуальный NextRun' для шаблона {templateId}" , targetTemplate . Id ) ;
DateOnly ? foundDay = null ;
if ( isInPeriod & & ! isWorkDay )
{
// СЦЕНАРИЙ А : В периоде, но нерабочий день
logger . LogDebug ( "Сценарий А : в периоде ({date}), но нерабочий день. Ищем слева..." , nextRunDate ) ;
// Берём все рабочие дни СТРОГО ЛЕВЕЕ исходной даты
var leftDays = workDays
. Where ( d = > d < nextRunDate )
. OrderByDescending ( d = > d ) // Ближайший к исходной дате — первый
. ToList ( ) ;
if ( leftDays . Any ( ) )
{
logger . LogDebug ( "Слева найдено {count} рабочих дней. Ищем первый не перегруженный..." , leftDays . Count ) ;
// Ищем первый не перегруженный
foreach ( var day in leftDays )
{
if ( currentDailyLoad [ day ] < expectedDailyLoad . GetValueOrDefault ( day , 0 ) )
{
foundDay = day ;
logger . LogDebug ( "Нашли слева не перегруженный день: {day}" , day ) ;
break ;
}
}
// Если не нашли — берём ближайший слева
if ( ! foundDay . HasValue )
{
foundDay = leftDays . First ( ) ;
logger . LogWarning ( "В с е дни слева перегружены. Берём ближайший: {day}" , foundDay . Value ) ;
}
}
else
{
// Слева нет рабочих дней — берём ближайший справа
var rightDays = workDays
. Where ( d = > d > nextRunDate )
. OrderBy ( d = > d ) // Ближайший к исходной дате — первый
. ToList ( ) ;
if ( rightDays . Any ( ) )
{
foundDay = rightDays . First ( ) ;
logger . LogDebug ( "Слева нет рабочих дней. Берём ближайший справа: {day}" , foundDay . Value ) ;
}
else
{
// Совсем нет рабочих дней — ошибка
logger . LogError ( "Нет рабочих дней ни слева, ни справа от {date}. Возвращаем ошибку." , nextRunDate ) ;
return FillTemplateResultWithOffsetError ( targetTemplate , periodStart , referenceDateInTargetTz , targetOffset ) ;
}
}
}
else if ( ! isInPeriod )
{
// СЦЕНАРИЙ Б: Н е в периоде
logger . LogDebug ( "Сценарий Б: не в периоде ({date}). Ищем в зависимости от isNew..." , nextRunDate ) ;
if ( isNew )
{
// Ищем от начала периода к концу (обычный план)
logger . LogDebug ( "isNew=true: ищем от начала периода к концу" ) ;
foreach ( var day in workDays )
{
if ( currentDailyLoad [ day ] < expectedDailyLoad . GetValueOrDefault ( day , 0 ) )
{
foundDay = day ;
logger . LogDebug ( "Нашли первый не перегруженный день: {day}" , day ) ;
break ;
}
}
if ( ! foundDay . HasValue )
{
foundDay = workDays . First ( ) ;
logger . LogWarning ( "В с е дни перегружены. Берём первый день периода: {day}" , foundDay . Value ) ;
}
}
else
{
// Ищем от конца периода к началу (зеркальный план)
logger . LogDebug ( "isNew=false: ищем от конца периода к началу" ) ;
foreach ( var day in workDays . AsEnumerable ( ) . Reverse ( ) )
{
if ( currentDailyLoad [ day ] < expectedDailyLoad . GetValueOrDefault ( day , 0 ) )
{
foundDay = day ;
logger . LogDebug ( "Нашли первый не перегруженный день: {day}" , day ) ;
break ;
}
}
if ( ! foundDay . HasValue )
{
foundDay = workDays . Last ( ) ;
logger . LogWarning ( "В с е дни перегружены. Берём последний день периода: {day}" , foundDay . Value ) ;
}
}
}
else
{
// Другие случаи (например, время уже прошло)
logger . LogDebug ( "NextRun неактуален по другим причинам. Используем логику в зависимости от isNew..." ) ;
if ( isNew )
{
foreach ( var day in workDays )
{
if ( currentDailyLoad [ day ] < expectedDailyLoad . GetValueOrDefault ( day , 0 ) )
{
foundDay = day ;
break ;
}
}
foundDay ? ? = workDays . First ( ) ;
}
else
{
foreach ( var day in workDays . AsEnumerable ( ) . Reverse ( ) )
{
if ( currentDailyLoad [ day ] < expectedDailyLoad . GetValueOrDefault ( day , 0 ) )
{
foundDay = day ;
break ;
}
}
foundDay ? ? = workDays . Last ( ) ;
}
}
// Формируем результат
var nextRunInTargetTz = new DateTimeOffset (
foundDay . Value . Year , foundDay . Value . Month , foundDay . Value . Day ,
referenceDateInTargetTz . Hour ,
referenceDateInTargetTz . Minute ,
referenceDateInTargetTz . Second ,
targetOffset ) ;
// Корректируем найденный день, чтобы момент времени был в будущем
DateTimeOffset ? adjustedNextRun ;
TemplateNextRunResultDto ? errorResult = null ;
adjustedNextRun = AdjustNextRunToFuture (
foundDay . Value ,
workDays ,
currentDailyLoad ,
expectedDailyLoad ,
referenceDateInTargetTz ,
targetOffset ,
periodStart , // startDateForError для случая ошибки
targetTemplate ,
out errorResult ) ;
if ( errorResult ! = null )
return errorResult ;
//var nextRunUtc = nextRunInTargetTz.ToOffset(TimeSpan.Zero);
var nextRunUtc = adjustedNextRun . Value . ToOffset ( TimeSpan . Zero ) ;
logger . LogInformation ( "Найдено новое место для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {nextRunOld}" , targetTemplate . Id , nextRunInTargetTz , targetOffset , nextRunUtc , targetTemplate . NextRun ) ;
return new TemplateNextRunResultDto (
Id : targetTemplate . Id ,
NextRun : nextRunUtc ,
NextRunOld : targetTemplate . NextRun ) ;
}
/// <summary>
/// Корректирует найденный день, чтобы момент времени был в будущем.
/// Если момент времени уже прошёл, ищет следующий рабочий день в будущем с учётом нагрузки.
/// В случае ошибки (нет будущих рабочих дней) возвращает ошибку через out-параметр.
/// </summary>
private DateTimeOffset ? AdjustNextRunToFuture (
DateOnly foundDay ,
List < DateOnly > workDays ,
Dictionary < DateOnly , int > currentDailyLoad ,
Dictionary < DateOnly , int > expectedDailyLoad ,
DateTimeOffset referenceDateInTargetTz ,
TimeSpan targetOffset ,
DateOnly startDateForError ,
TemplateNextRunDto targetTemplate ,
out TemplateNextRunResultDto ? errorResult )
{
errorResult = null ;
// Создаём момент времени для найденного дня
var nextRunInTargetTz = new DateTimeOffset (
foundDay . Year , foundDay . Month , foundDay . Day ,
referenceDateInTargetTz . Hour ,
referenceDateInTargetTz . Minute ,
referenceDateInTargetTz . Second ,
targetOffset ) ;
// ФИНАЛЬНАЯ ПРОВЕРКА: момент времени должен быть в будущем
if ( nextRunInTargetTz < DateTimeOffset . UtcNow )
{
logger . LogWarning ( "Найденный момент времени {nextRun} уже прошёл (сейчас {now}). Ищем следующий рабочий день в будущем." ,
nextRunInTargetTz , DateTimeOffset . UtcNow ) ;
// Берём все рабочие дни СТРОГО ПОСЛЕ найденного дня
var futureWorkDays = workDays . Where ( d = > d > foundDay ) . ToList ( ) ;
if ( futureWorkDays . Any ( ) )
{
// Ищем первый день с допустимой нагрузкой (текущая < ожидаемая)
var notOverloadedDays = futureWorkDays
. Where ( d = > currentDailyLoad [ d ] < expectedDailyLoad . GetValueOrDefault ( d , 0 ) )
. ToList ( ) ;
var futureDay = notOverloadedDays . Any ( )
? notOverloadedDays . First ( )
: futureWorkDays . First ( ) ;
logger . LogDebug ( "Выбран следующий рабочий день в будущем: {day}" , futureDay ) ;
// Пересоздаём момент времени с новой датой
nextRunInTargetTz = new DateTimeOffset (
futureDay . Year , futureDay . Month , futureDay . Day ,
referenceDateInTargetTz . Hour ,
referenceDateInTargetTz . Minute ,
referenceDateInTargetTz . Second ,
targetOffset ) ;
}
else
{
// Нет будущих рабочих дней — ошибка
logger . LogError ( "Нет будущих рабочих дней после {date} в периоде. Возвращаем ошибку с о смещением." ,
foundDay ) ;
errorResult = FillTemplateResultWithOffsetError ( targetTemplate , startDateForError , referenceDateInTargetTz , targetOffset ) ;
return null ;
}
}
else
{
logger . LogDebug ( "Момент времени {nextRun} актуален (в будущем). Коррекция не требуется." , nextRunInTargetTz ) ;
}
return nextRunInTargetTz ;
}
/// <summary>
/// Строит текущую нагрузку на дни, учитывая ТОЛЬКО шаблоны внутри периода.
/// Шаблоны вне периода игнорируются.
/// </summary>
private Dictionary < DateOnly , int > BuildCurrentLoad ( List < TemplateNextRunDto > templates , List < DateOnly > workDays , DateOnly periodStart , DateOnly periodEnd , TimeSpan targetOffset )
{
var load = workDays . ToDictionary ( day = > day , _ = > 0 ) ;
foreach ( var template in templates )
{
if ( ! template . NextRun . HasValue )
continue ;
// Конвертируем в целевой пояс для определения даты
var templateDate = DateOnly . FromDateTime ( template . NextRun . Value . ToOffset ( targetOffset ) . Date ) ;
// Учитываем ТОЛЬКО шаблоны внутри периода
if ( templateDate > = periodStart & & templateDate < = periodEnd )
{
if ( load . ContainsKey ( templateDate ) )
load [ templateDate ] + + ;
}
}
return load ;
}
/// <summary>
/// Заполнить TemplateNextRunResultDto, если нет рабочих дней с о смещением несколько лет
@@ -302,42 +928,41 @@ namespace PARR.DAL.NextRunServices.Subservices
}
/// <summary>
/// Получить план: дни, на которые должны быть назначены шаблоны
/// Получить план распределения шаблонов по рабочим дням.
/// </summary>
/// <param name="totalWorks"></param>
/// <param name="workDays"></param>
/// <returns></returns >
private List < DateOnly > GetPlan ( int totalWorks , List < DateOnly > workDays )
/// <param name="totalWorks">Общее количество шаблонов для распределения </param>
/// <param name="workDays">Список рабочих дней в периоде </param>
/// <param name="mirror">Если true — строит зеркальный план (справа налево от конца периода), иначе обычный (слева направо от начала периода)</param >
/// <returns>Список дат, на которые должны быть назначены шаблоны</returns>
private List < DateOnly > GetDistributionPlan ( int totalWorks , List < DateOnly > workDays , bool mirror = false )
{
var totalDays = workDays . Count ( ) ;
var interval = GetInterval ( totalDays , totalWorks ) ;
var totalDays = workDays . Count ;
var interval = CalculateDistributionInterval ( 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 ) ;
// Базовый индекс для обычного плана (слева → направо)
var baseIndex = ( int ) Math . Floor ( i * interval ) ;
// Защита от возможного выхода за границы массива из-за округления
if ( dayI ndex > = totalDays )
dayIndex = totalDays - 1 ;
#endregion
// Применяем зеркалирование, если нужно
var i ndex = mirror
? totalDays - 1 - baseIndex // Зеркальный индекс: от конца периода
: baseIndex ; // Обычный индекс: от начала периода
var date = workDays [ dayIndex ] ;
plan . Add ( date ) ;
// Защита от выхода за границы массива
index = Math . Max ( 0 , Math . Min ( index , totalDays - 1 ) ) ;
plan . Add ( workDays [ index ] ) ;
}
logger . LogDebug ( "--- План распределения ---" ) ;
foreach ( var item in plan . GroupBy ( t = > t ) )
// Логгирование
var planType = mirror ? "Зеркальный" : "Обычный" ;
logger . LogDebug ( "--- {planType} план распределения ({totalWorks} шаблонов, {totalDays} р а б . дней) ---" , planType , totalWorks , totalDays ) ;
foreach ( var item in plan . GroupBy ( t = > t ) . OrderBy ( g = > g . Key ) )
logger . LogDebug ( "Дата: {date}, кол-во работ: {workCount}" , item . Key , item . Count ( ) ) ;
return plan ;
@@ -350,11 +975,11 @@ namespace PARR.DAL.NextRunServices.Subservices
/// <param name="totalDays">Кол-во рабочих дней</param>
/// <param name="totalWorks">Кол-во шаблонов</param>
/// <returns></returns>
private double Get Interval( int totalDays , int totalWorks )
private double CalculateDistribution Interval( int totalDays , int totalWorks )
{
var interval = ( double ) totalDays / totalWorks ;
logger . LogDebug ( "Интевал между шаблонами: {interval} " , interval ) ;
logger . LogDebug ( "Интер вал распределения: {interval} (дней на шаблон) " , interval ) ;
return interval ;
}
@@ -490,6 +1115,5 @@ namespace PARR.DAL.NextRunServices.Subservices
return result ;
}
}
}