2025-12-22 17:03:41 +10:00
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 : ITemplateDistributor
{
private readonly ILogger < TemplateDistributor > logger ;
private readonly IWeekendDayService weekendDayService ;
2025-12-25 12:07:53 +10:00
/// <summary>
/// Смещение nextRun на несколько лет вперед, если не смог е г о рассчитать
/// </summary>
private const int offsetYearsIfErrorCalcNextRun = 5 ;
2025-12-22 17:03:41 +10:00
public TemplateDistributor (
2025-12-25 12:07:53 +10:00
ILogger < TemplateDistributor > logger ,
2025-12-22 17:03:41 +10:00
IWeekendDayService weekendDayService
)
{
this . logger = logger ;
this . weekendDayService = weekendDayService ;
}
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
public async Task < TemplateNextRunResultDto > GetValidNextRunForTemplateAsync ( DateOnly startDate , int duration , DateTimeOffset referenceDate , TemplateNextRunDto targetTemplate , List < TemplateNextRunDto > allTemplates , bool excludeWeekends , bool isNew )
2025-12-22 17:03:41 +10:00
{
2026-01-20 15:04:36 +10:00
//todo: allTemplates - пока не используется. Будет использоваться когда каждый раз будем строить план
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
// Пока работает так:
// Получаем список рабочих дней, если работа попадает в эти дни, то считаем дату валидной
// Если нет, то перемещаем на последний день периода распределения или первый в зависимости от параметра isNew
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
var _startDate = GetStartPeriodDate ( startDate , referenceDate ) ;
var _endDate = GetEndPeriodDate ( _startDate , duration ) ;
2025-12-22 17:03:41 +10:00
2026-01-20 15:04:36 +10:00
logger . LogDebug ( "Получаю актуальный nextRun для шаблона {templateId}. Текущий nextRun: {nextRun}. Период {duration} дней, с {_startDate} по {_endDate}. Исключить выходные и праздники: {excludeWeekends}. Для нового шаблона: {isNew}" , targetTemplate . Id , targetTemplate . NextRun , duration , _startDate , _endDate , excludeWeekends , isNew ) ;
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
// Получаем список рабочих дней (исключая выходные и праздники)
var workDays = await GetWorkDaysAsync ( _startDate , _endDate , excludeWeekends ) ;
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
if ( workDays . Count = = 0 )
{
logger . LogWarning ( "Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем шаблон с о смещением: {offsetYearsIfErrorCalcNextRun} лет." , _startDate , _endDate , offsetYearsIfErrorCalcNextRun ) ;
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
// смещаем nextRun вперед
var result = FillTemplateResultWithOffsetError ( targetTemplate , _startDate , referenceDate ) ;
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
logger . LogInformation ( "Получение актуального NextRun завершено, templateId: {templateId}. NextRun {nextRun}. OldNextRun у {oldNextRun} шт." , result . Id , result . NextRun , result . NextRunOld ) ;
return result ;
}
// если isNew, то берем первый день периода
if ( isNew )
2026-01-19 16:33:11 +10:00
{
2026-01-20 15:04:36 +10:00
var result = new TemplateNextRunResultDto
(
Id : targetTemplate . Id ,
NextRun : FindNextAvailableDateIgnoreLoad ( workDays , referenceDate , true ) , //CreateDateTimeOffset(workDays.First(), referenceDate),
NextRunOld : targetTemplate . NextRun
) ;
logger . LogInformation ( "Получение актуального NextRun завершено, templateId: {templateId}. Это было создание нового, поместили е г о в начало периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт." , result . Id , result . NextRun , result . NextRunOld ) ;
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
return result ;
2026-01-19 16:33:11 +10:00
}
2025-12-22 17:03:41 +10:00
2026-01-20 15:04:36 +10:00
// есть ли NextRun и попадает ли он на рабочий день
// проверяем чтоб было не меньше чем UtcNow
//TODO: тут еще сделать проверку нагрузки на день
if ( targetTemplate . NextRun . HasValue & & workDays . Contains ( DateOnly . FromDateTime ( targetTemplate . NextRun . Value . Date ) ) & & targetTemplate . NextRun . Value > DateTimeOffset . UtcNow )
{
var result = new TemplateNextRunResultDto
(
Id : targetTemplate . Id ,
NextRun : CreateDateTimeOffset ( targetTemplate . NextRun . Value , referenceDate ) ,
NextRunOld : targetTemplate . NextRun
) ;
2025-12-25 12:07:53 +10:00
2026-01-20 15:04:36 +10:00
logger . LogInformation ( "Получение актуального NextRun завершено, templateId: {templateId}. NextRun актуальный, возвращаем е г о . NextRun {nextRun}. OldNextRun у {oldNextRun} шт." , result . Id , result . NextRun , result . NextRunOld ) ;
2025-12-22 17:03:41 +10:00
2026-01-20 15:04:36 +10:00
return result ;
}
//TODO: Временный костыль, перемещаем на конец периода
var endPeriodResult = new TemplateNextRunResultDto
(
Id : targetTemplate . Id ,
NextRun : FindNextAvailableDateIgnoreLoad ( workDays , referenceDate , false ) ,
NextRunOld : targetTemplate . NextRun
) ;
logger . LogInformation ( "Получение актуального NextRun завершено, templateId: {templateId}. NextRun перемещен на конец периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт." , endPeriodResult . Id , endPeriodResult . NextRun , endPeriodResult . NextRunOld ) ;
return endPeriodResult ;
}
public async Task < List < TemplateNextRunResultDto > > DistributeTemplatesAsync ( DateOnly startDate , int duration , DateTimeOffset referenceDate , List < TemplateNextRunDto > allTemplates , bool excludeWeekends )
{
var _startDate = GetStartPeriodDate ( startDate , referenceDate ) ;
var _endDate = GetEndPeriodDate ( _startDate , duration ) ;
logger . LogInformation ( "Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}." , allTemplates . Count , duration , _startDate , _endDate , referenceDate , excludeWeekends ) ;
// Получаем список рабочих дней (исключая выходные и праздники)
var workDays = await GetWorkDaysAsync ( _startDate , _endDate , excludeWeekends ) ;
2025-12-22 17:03:41 +10:00
if ( workDays . Count = = 0 )
{
2025-12-25 12:07:53 +10:00
logger . LogWarning ( "Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов с о смещением: {offsetYearsIfErrorCalcNextRun} лет." , _startDate , _endDate , offsetYearsIfErrorCalcNextRun ) ;
// смещаем все nextRun вперед
2026-01-20 15:04:36 +10:00
var result = allTemplates . Select ( t = >
FillTemplateResultWithOffsetError ( t , _startDate , referenceDate )
) . OrderBy ( t = > t . NextRun ) . ToList ( ) ;
2025-12-25 12:07:53 +10:00
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 ;
2025-12-22 17:03:41 +10:00
}
2025-12-25 12:07:53 +10:00
var totalWorks = allTemplates . Count ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
// интервал между шаблонами
2026-01-20 15:04:36 +10:00
var interval = GetInterval ( workDays . Count , totalWorks ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
// строим план, дни на которые должны быть назанчены шаблоны
2026-01-20 15:04:36 +10:00
var plan = GetPlan ( totalWorks , workDays ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
// Подсчитываем, сколько работ должно быть на каждый день
var expectedDailyLoad = new Dictionary < DateOnly , int > ( ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
foreach ( var day in plan )
expectedDailyLoad [ day ] = expectedDailyLoad . GetValueOrDefault ( day , 0 ) + 1 ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
#region Г р у п п и р у е м ш а б л о н ы п о д н я м , н а к о т о р ы е о н и у ж е н а з н а ч е н ы
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
var templatesByDay = new Dictionary < DateOnly , List < TemplateNextRunDto > > ( ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
foreach ( var template in allTemplates )
{
if ( template . NextRun . HasValue )
{
var day = DateOnly . FromDateTime ( template . NextRun . Value . Date ) ;
if ( ! templatesByDay . ContainsKey ( day ) )
{
templatesByDay [ day ] = new List < TemplateNextRunDto > ( ) ;
}
templatesByDay [ day ] . Add ( template ) ;
}
2025-12-22 17:03:41 +10:00
}
2025-12-25 12:07:53 +10:00
#endregion
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
#region П о д с ч и т ы в а е м т е к у щ у ю н а г р у з к у н а к а ж д ы й д е н ь
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
var currentDailyLoad = new Dictionary < DateOnly , int > ( ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
foreach ( var day in workDays )
{
currentDailyLoad [ day ] = 0 ;
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
foreach ( var kvp in templatesByDay )
{
var day = kvp . Key ;
var count = kvp . Value . Count ;
currentDailyLoad [ day ] = count ;
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
#endregion
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
#region О п р е д е л я е м , к а к и е ш а б л о н ы н у ж н о о с т а в и т ь , а к а к и е — п е р е н а з н а ч и т ь
var templatesToKeep = new List < TemplateNextRunResultDto > ( ) ;
var templatesToReassign = new List < TemplateNextRunDto > ( ) ;
foreach ( var kvp in templatesByDay )
2025-12-22 17:03:41 +10:00
{
2025-12-25 12:07:53 +10:00
var day = kvp . Key ;
var templates = kvp . Value ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
// Если день — нерабочий, то все шаблоны нужно переназначить
if ( ! workDays . Contains ( day ) )
{
foreach ( var template in templates )
2025-12-22 17:03:41 +10:00
{
2025-12-25 12:07:53 +10:00
templatesToReassign . Add ( template ) ;
logger . LogDebug ( "Шаблон {TemplateId} назначен на нерабочий день {NextRun} — будет переназначен." , template . Id , template . NextRun ) ;
}
continue ;
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
var expectedCount = expectedDailyLoad . GetValueOrDefault ( day , 0 ) ;
var currentCount = templates . Count ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
if ( currentCount < = expectedCount )
{
// В с е шаблоны в этом дне можно оставить
foreach ( var template in templates )
{
templatesToKeep . Add ( new TemplateNextRunResultDto
2026-01-20 15:04:36 +10:00
(
Id : template . Id ,
NextRun : template . NextRun . Value ,
NextRunOld : template . NextRun
) ) ;
2025-12-25 12:07:53 +10:00
logger . LogDebug ( "Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена." , template . Id , template . NextRun ) ;
2025-12-22 17:03:41 +10:00
}
2025-12-25 12:07:53 +10:00
}
else
{
// Оставляем только expectedCount штук, остальные — переназначить
for ( int i = 0 ; i < expectedCount ; i + + )
{
var template = templates [ i ] ;
templatesToKeep . Add ( new TemplateNextRunResultDto
2026-01-20 15:04:36 +10:00
(
Id : template . Id ,
NextRun : template . NextRun . Value ,
NextRunOld : template . NextRun
) ) ;
2025-12-25 12:07:53 +10:00
logger . LogDebug ( "Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена." , template . Id , template . NextRun ) ;
}
for ( int i = expectedCount ; i < currentCount ; i + + )
2025-12-22 17:03:41 +10:00
{
2025-12-25 12:07:53 +10:00
var template = templates [ i ] ;
templatesToReassign . Add ( template ) ;
logger . LogDebug ( "Шаблон {TemplateId} переназначается, т.к. нагрузка в день превышена." , template . Id ) ;
2025-12-22 17:03:41 +10:00
}
}
}
2025-12-25 12:07:53 +10:00
#endregion
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
#region Д о б а в л я е м ш а б л о н ы , у к о т о р ы х н е б ы л о д а т ы — т о ж е н у ж н о р а с п р е д е л и т ь
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
foreach ( var template in allTemplates )
{
if ( ! template . NextRun . HasValue )
{
templatesToReassign . Add ( template ) ;
logger . LogDebug ( "Шаблон {TemplateId} не имел даты — будет распределён." , template . Id ) ;
}
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
#endregion
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
#region Р а с п р е д е л я е м ш а б л о н ы , к о т о р ы е н у ж н о п е р е н а з н а ч и т ь , у ч и т ы в а я н а г р у з к у
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
var updatedTemplates = new List < TemplateNextRunResultDto > ( templatesToKeep ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
foreach ( var template in templatesToReassign )
{
var nextRun = FindNextAvailableDateWithLoad ( currentDailyLoad , workDays , referenceDate , expectedDailyLoad ) ;
if ( nextRun . HasValue )
{
var day = DateOnly . FromDateTime ( nextRun . Value . Date ) ;
currentDailyLoad [ day ] + + ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
updatedTemplates . Add ( new TemplateNextRunResultDto
2026-01-20 15:04:36 +10:00
(
Id : template . Id ,
NextRun : nextRun . Value ,
NextRunOld : template . NextRun
) ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
logger . LogDebug ( "Шаблону {TemplateId} назначен NextRun: {NextRun}, NextRunOld: {NextRunOld}" , template . Id , nextRun , template . NextRun ) ;
}
else
{
logger . LogWarning ( "Н е удалось назначить дату для шаблона {TemplateId}" , template . Id ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
// Возвращаем с ошибкой
updatedTemplates . Add ( new TemplateNextRunResultDto
2026-01-20 15:04:36 +10:00
(
Id : template . Id ,
NextRun : CreateDateTimeOffset ( DateTimeOffset . UtcNow . AddYears ( offsetYearsIfErrorCalcNextRun ) , referenceDate ) ,
NextRunOld : template . NextRun
) ) ;
2025-12-25 12:07:53 +10:00
}
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
#endregion
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
updatedTemplates = updatedTemplates . OrderBy ( t = > t . NextRun ) . ToList ( ) ;
2025-12-22 17:03:41 +10:00
2026-01-20 15:04:36 +10:00
logger . LogDebug ( "--- Результат распределения ---" ) ;
2025-12-25 12:07:53 +10:00
foreach ( var item in updatedTemplates )
logger . LogDebug ( "Шаблон {templateId}, NextRun: {NextRun}, NextRunOld: {NextRunOld}" , item . Id , item . NextRun , item . NextRunOld ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
logger . LogInformation ( "Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт." , updatedTemplates . Count , updatedTemplates . Count ( t = > t . NextRun = = t . NextRunOld ) , updatedTemplates . Count ( t = > t . NextRun ! = t . NextRunOld ) ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
return updatedTemplates ;
2025-12-22 17:03:41 +10:00
}
2026-01-20 15:04:36 +10:00
/// <summary>
/// Заполнить TemplateNextRunResultDto, если нет рабочих дней с о смещением
/// </summary>
/// <param name="template"></param>
/// <returns></returns>
private TemplateNextRunResultDto FillTemplateResultWithOffsetError ( TemplateNextRunDto template , DateOnly startDate , DateTimeOffset referenceDate )
{
var result = new TemplateNextRunResultDto
(
Id : template . Id ,
//NextRun = template.NextRun.HasValue
// // если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun
// ? CreateNextRun(template.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate)
// // если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun
// : CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
NextRun : CreateDateTimeOffset ( startDate . AddYears ( offsetYearsIfErrorCalcNextRun ) , referenceDate ) ,
NextRunOld : template . NextRun
) ;
logger . LogDebug ( "Нет рабочих дней, сдвинул nextRun у шаблона {Id} на {offsetYearsIfErrorCalcNextRun} лет вперед, nextRun: {NextRun}, nextRunOld: {NextRunOld}" , result . Id , offsetYearsIfErrorCalcNextRun , result . NextRun , result . NextRunOld ) ;
return result ;
}
/// <summary>
/// Получает NextRun не учитывая нагрузку на день
/// </summary>
/// <param name="workDays"></param>
/// <param name="referenseDate"></param>
/// <param name="isSearchFirst">Искать с начала = true, false - искать с конца</param>
/// <returns></returns>
private DateTimeOffset FindNextAvailableDateIgnoreLoad ( List < DateOnly > workDays , DateTimeOffset referenseDate , bool isSearchFirst )
{
// возможно это временная реализация метода, используется для GetValidNextRunForTemplateAsync
if ( ! isSearchFirst )
{
// берем первый рабочий день с конца и возвращаем е г о
var lastDate = CreateDateTimeOffset ( workDays . Last ( ) , referenseDate ) ;
logger . LogDebug ( "Берем первый рабочий день с конца. Сгенерированный NextRun: {lastDate}" , lastDate ) ;
return lastDate ;
}
// будем искать сначала
// берем первый день, и смотрим не прошло ли уже время
var firstDate = CreateDateTimeOffset ( workDays . First ( ) , referenseDate ) ;
if ( firstDate > DateTimeOffset . UtcNow )
{
logger . LogDebug ( "Берем первый день с начала. Сгенерированный NextRun: {firstDate}" , firstDate ) ;
return firstDate ;
}
// тогда берем второй день
if ( workDays . Count > 1 )
{
var secondDate = CreateDateTimeOffset ( workDays . ElementAt ( 1 ) , referenseDate ) ;
logger . LogDebug ( "Берем второй день сначала, так как у первого дня уже время прошло. Сгенерированный NextRun: {secondDate}" , secondDate ) ;
return secondDate ;
}
logger . LogWarning ( "Вернули nextRun: {firstDate} из первого дня распределения, хотя время уже просрочено. В с е г о рабочих дней: {workDays}" , firstDate , workDays . Count ) ;
// если вдруг список рабочих дней был не >1, то тогда берем первый и вренем е г о . Так никогда не должно быть
return firstDate ;
}
2025-12-22 17:03:41 +10:00
/// <summary>
2025-12-25 12:07:53 +10:00
/// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad
2025-12-22 17:03:41 +10:00
/// </summary>
2025-12-25 12:07:53 +10:00
/// <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 )
2025-12-22 17:03:41 +10:00
{
2025-12-25 12:07:53 +10:00
foreach ( var day in workDays )
{
var expected = expectedDailyLoad . GetValueOrDefault ( day , 0 ) ;
var current = currentDailyLoad . GetValueOrDefault ( day , 0 ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
if ( current < expected )
{
2026-01-20 15:04:36 +10:00
return CreateDateTimeOffset ( day , referenceDate ) ;
2025-12-25 12:07:53 +10:00
}
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
// Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план
foreach ( var day in workDays )
2025-12-22 17:03:41 +10:00
{
2025-12-25 12:07:53 +10:00
var expected = expectedDailyLoad . GetValueOrDefault ( day , 0 ) ;
var current = currentDailyLoad . GetValueOrDefault ( day , 0 ) ;
if ( current < expected )
2025-12-22 17:03:41 +10:00
{
2026-01-20 15:04:36 +10:00
return CreateDateTimeOffset ( day , referenceDate ) ;
2025-12-22 17:03:41 +10:00
}
}
2025-12-25 12:07:53 +10:00
// Если все дни уже загружены по плану — ищем день с минимальной нагрузкой
var minLoad = currentDailyLoad . Values . Min ( ) ;
var minDay = currentDailyLoad . First ( x = > x . Value = = minLoad ) . Key ;
2025-12-22 17:03:41 +10:00
2026-01-20 15:04:36 +10:00
return CreateDateTimeOffset ( minDay , referenceDate ) ;
2025-12-22 17:03:41 +10:00
}
2025-12-25 12:07:53 +10:00
2025-12-22 17:03:41 +10:00
/// <summary>
2025-12-25 12:07:53 +10:00
/// Получить план: дни, на которые должны быть назначены шаблоны
2025-12-22 17:03:41 +10:00
/// </summary>
2025-12-25 12:07:53 +10:00
/// <param name="totalWorks"></param>
/// <param name="workDays"></param>
/// <returns></returns>
2026-01-20 15:04:36 +10:00
private List < DateOnly > GetPlan ( int totalWorks , List < DateOnly > workDays )
2025-12-22 17:03:41 +10:00
{
2026-01-20 15:04:36 +10:00
var totalDays = workDays . Count ( ) ;
2025-12-25 12:07:53 +10:00
var interval = GetInterval ( totalDays , totalWorks ) ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
var plan = new List < DateOnly > ( ) ;
for ( int i = 0 ; i < totalWorks ; i + + )
2025-12-22 17:03:41 +10:00
{
2025-12-25 12:07:53 +10:00
#region В ы н е с т и в м е т о д
// Вычисляем индекс дня, на который нужно назначить i-ю работу
//var dayIndex = (int)Math.Round(i * interval);
var dayIndex = ( int ) Math . Floor ( i * interval ) ;
// Защита от возможного выхода за границы массива из-за округления
if ( dayIndex > = totalDays )
dayIndex = totalDays - 1 ;
#endregion
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
var date = workDays [ dayIndex ] ;
plan . Add ( date ) ;
2025-12-22 17:03:41 +10:00
}
2026-01-20 15:04:36 +10:00
logger . LogDebug ( "--- План распределения ---" ) ;
foreach ( var item in plan . GroupBy ( t = > t ) )
logger . LogDebug ( "Дата: {date}, кол-во работ: {workCount}" , item . Key , item . Count ( ) ) ;
2025-12-25 12:07:53 +10:00
return plan ;
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
/// <summary>
/// Рассчитать интервал между шаблонами
/// </summary>
/// <param name="totalDays">Кол-во рабочих дней</param>
/// <param name="totalWorks">Кол-во шаблонов</param>
/// <returns></returns>
private double GetInterval ( int totalDays , int totalWorks )
{
var interval = ( double ) totalDays / totalWorks ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
return interval ;
2025-12-22 17:03:41 +10:00
}
/// <summary>
2026-01-20 15:04:36 +10:00
/// Создать DateTimeOffset (из DateTimeOffset + refDate)
2025-12-22 17:03:41 +10:00
/// </summary>
2025-12-25 12:07:53 +10:00
/// <param name="date">Откуда взять дату</param>
/// <param name="refDate">Откуда взять время</param>
/// <returns></returns>
2026-01-20 15:04:36 +10:00
private DateTimeOffset CreateDateTimeOffset ( DateTimeOffset date , DateTimeOffset refDate )
2025-12-22 17:03:41 +10:00
{
2026-01-20 15:04:36 +10:00
return CreateDateTimeOffset ( DateOnly . FromDateTime ( date . Date ) , refDate ) ;
2025-12-25 12:07:53 +10:00
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
/// <summary>
2026-01-20 15:04:36 +10:00
/// Создать DateTimeOffset (из DateOnly + refDate)
2025-12-25 12:07:53 +10:00
/// </summary>
/// <param name="date"></param>
/// <param name="refDate"></param>
/// <returns></returns>
2026-01-20 15:04:36 +10:00
private DateTimeOffset CreateDateTimeOffset ( DateOnly date , DateTimeOffset refDate )
2025-12-25 12:07:53 +10:00
{
return new DateTimeOffset ( date . Year , date . Month , date . Day , refDate . Hour , refDate . Minute , refDate . Second , TimeSpan . Zero ) ;
}
/// <summary>
/// Получить дату начала распределения относительно StartDate, ReferenceDate и текущей даты
/// </summary>
/// <param name="startDate"></param>
/// <param name="referenceDate"></param>
/// <returns></returns>
private DateOnly GetStartPeriodDate ( DateOnly startDate , DateTimeOffset referenceDate )
{
// дата начала не может быть раньше referenceDate
// и не может быть раньше чем сегодня
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
var dateStart = startDate ;
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
// если dateStart < чем refDate, то берем refDate
if ( dateStart < DateOnly . FromDateTime ( referenceDate . Date ) )
dateStart = DateOnly . FromDateTime ( referenceDate . Date ) ;
2026-01-20 15:04:36 +10:00
// если dateStart < чем текущая дата, то берем текущую дату
2025-12-25 12:07:53 +10:00
if ( dateStart < DateOnly . FromDateTime ( DateTime . UtcNow . Date ) )
dateStart = DateOnly . FromDateTime ( DateTime . UtcNow . Date ) ;
2026-01-20 15:04:36 +10:00
// если текущее время > referenceDate, то прибавляем 1 день
var dateTimeStart = CreateDateTimeOffset ( dateStart , referenceDate ) ;
if ( dateTimeStart < DateTimeOffset . UtcNow )
dateStart = dateStart . AddDays ( 1 ) ;
2025-12-25 12:07:53 +10:00
return dateStart ;
2025-12-22 17:03:41 +10:00
}
/// <summary>
2025-12-25 12:07:53 +10:00
/// Получить дату конца периода распределения
2025-12-22 17:03:41 +10:00
/// </summary>
2025-12-25 12:07:53 +10:00
/// <param name="startDate"></param>
/// <param name="duration"></param>
/// <returns></returns>
private DateOnly GetEndPeriodDate ( DateOnly startDate , int duration )
2025-12-22 17:03:41 +10:00
{
2025-12-25 12:07:53 +10:00
// duration — количество дней, включая startDate
// поэтому вычитаем 1, чтобы получить сдвиг
return startDate . AddDays ( duration - 1 ) ;
}
2025-12-22 17:03:41 +10:00
2025-12-25 12:07:53 +10:00
/// <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 ( ) ;
2025-12-22 17:03:41 +10:00
2026-01-20 15:04:36 +10:00
logger . LogDebug ( "Найдено не рабочих дней: {Count}" , weekends . Count ) ;
foreach ( var weekend in weekends )
logger . LogDebug ( "Н е рабочий день: {weekend}" , weekend ) ;
2025-12-25 12:07:53 +10:00
return weekends . ToHashSet ( ) ;
2025-12-22 17:03:41 +10:00
}
/// <summary>
/// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники.
/// </summary>
2025-12-25 12:07:53 +10:00
/// <param name="start"></param>
/// <param name="end"></param>
2026-01-20 15:04:36 +10:00
/// <param name="excludeWeekends">Исключить выходные и праздники</param>
2025-12-25 12:07:53 +10:00
/// <returns></returns>
2026-01-20 15:04:36 +10:00
private async Task < List < DateOnly > > GetWorkDaysAsync ( DateOnly start , DateOnly end , bool excludeWeekends )
2025-12-22 17:03:41 +10:00
{
2026-01-20 15:04:36 +10:00
// Получаем список выходных/праздничных дней
var weekends = new HashSet < DateOnly > ( ) ;
if ( excludeWeekends )
weekends = await GetWeekendsAsync ( start , end ) ;
2025-12-22 17:03:41 +10:00
var result = new List < DateOnly > ( ) ;
var date = start ;
while ( date < = end )
{
2025-12-25 12:07:53 +10:00
var dayOfWeek = date . DayOfWeek ;
2025-12-22 17:03:41 +10:00
if ( ! weekends . Contains ( date ) )
{
result . Add ( date ) ;
}
date = date . AddDays ( 1 ) ;
}
2026-01-20 15:04:36 +10:00
logger . LogDebug ( "Найдено рабочих дней: {Count}" , result . Count ) ;
foreach ( var workDay in result )
logger . LogDebug ( "Рабочий день: {workDay}" , workDay ) ;
2025-12-22 17:03:41 +10:00
return result ;
}
2025-12-25 12:07:53 +10:00
2025-12-22 17:03:41 +10:00
}
}