2026-02-12 16:15:36 +10:00
using Microsoft.EntityFrameworkCore ;
using Microsoft.Extensions.Logging ;
2026-04-30 10:35:31 +10:00
using PARR.Core.Repositories.Interfaces ;
2026-07-03 11:25:39 +10:00
using PARR.Core.Repositories.Interfaces.JobGroupRepositories ;
2026-04-30 10:35:31 +10:00
using PARR.Core.Repositories.Interfaces.Schedule ;
2026-05-04 11:55:07 +10:00
using PARR.Core.Services.NextRunServices.Models ;
using PARR.Core.Services.NextRunServices.Subservices ;
using PARR.Core.Services.Shortcodes ;
2026-04-30 10:35:31 +10:00
using PARR.Domain.Entities ;
2026-06-15 16:56:23 +10:00
using PARR.Domain.Entities.JobGroupEntities ;
2026-04-14 12:01:49 +10:00
using PARR.Domain.Enums ;
2026-05-04 11:55:07 +10:00
using PARR.Domain.Settings ;
2026-02-12 16:15:36 +10:00
2026-05-04 11:55:07 +10:00
namespace PARR.Core.Services.NextRunServices
2026-02-12 16:15:36 +10:00
{
2026-03-05 10:19:54 +10:00
internal class NextRunService : INextRunService
2026-02-12 16:15:36 +10:00
{
2026-03-05 10:19:54 +10:00
private readonly ILogger < NextRunService > logger ;
2026-04-30 10:35:31 +10:00
private readonly ITemplateRepository templateService ;
private readonly IJobGroupRepository jobGroupService ;
2026-02-12 16:15:36 +10:00
private readonly IEsppScheduleTransformService esppScheduleTransformService ;
2026-03-05 10:19:54 +10:00
private readonly ITemplateDistributor templateDistributor ;
2026-02-12 16:15:36 +10:00
private readonly IShortcodesService shortcodesService ;
2026-04-30 10:35:31 +10:00
private readonly IScheduleResponseAreaTimeOffsetRepository scheduleResponseAreaTimeOffsetService ;
2026-02-12 16:15:36 +10:00
private readonly SettingsFromDb settingsFromDb ;
2026-03-05 10:19:54 +10:00
public NextRunService (
ILogger < NextRunService > logger ,
2026-04-30 10:35:31 +10:00
ITemplateRepository templateService ,
IJobGroupRepository jobGroupService ,
2026-02-12 16:15:36 +10:00
IEsppScheduleTransformService esppScheduleTransformService ,
2026-03-05 10:19:54 +10:00
ITemplateDistributor templateDistributor ,
2026-02-12 16:15:36 +10:00
IShortcodesService shortcodesService ,
2026-04-30 10:35:31 +10:00
IScheduleResponseAreaTimeOffsetRepository scheduleResponseAreaTimeOffsetService ,
2026-02-12 16:15:36 +10:00
SettingsFromDb settingsFromDb
)
{
this . logger = logger ;
this . templateService = templateService ;
this . jobGroupService = jobGroupService ;
this . esppScheduleTransformService = esppScheduleTransformService ;
this . templateDistributor = templateDistributor ;
this . shortcodesService = shortcodesService ;
this . scheduleResponseAreaTimeOffsetService = scheduleResponseAreaTimeOffsetService ;
this . settingsFromDb = settingsFromDb ;
}
2026-02-13 15:39:21 +10:00
public async Task < List < TemplateNextRunResultDto > ? > GetNextRunForJobGroupWithAutoDistributionAsync ( Guid jobGroupId , TemplateStatusTypeEnum ? templateStatusType )
2026-02-12 16:15:36 +10:00
{
2026-02-13 15:39:21 +10:00
logger . LogInformation ( "Начинаю распределять шаблоны для группы работ {gobGroupId}, templateStatusType: {templateStatusType}" , jobGroupId , templateStatusType ) ;
2026-02-12 16:15:36 +10:00
var jobGroup = await jobGroupService . Get ( )
2026-02-13 15:39:21 +10:00
. AsNoTracking ( )
2026-02-12 16:15:36 +10:00
. Include ( t = > t . DistributionConfig )
. ThenInclude ( t = > t . DistributionPeriod )
. FirstOrDefaultAsync ( t = > t . Id = = jobGroupId ) ;
if ( jobGroup = = null )
{
logger . LogError ( "Н е найдена группа работа с id: {id}" , jobGroupId ) ;
return null ;
}
if ( ! jobGroup . IsAutoDistributionEnabled | | jobGroup . DistributionConfig = = null )
{
logger . LogError ( "Группа работ id: {id} не подходит для автораспределения, у нее или отсутствуют настройки или не включено автораспределение. IsAutoDistributionEnabled: {IsAutoDistributionEnabled}. Есть конфиг: {DistributionConfig}" , jobGroupId , jobGroup . IsAutoDistributionEnabled , jobGroup . DistributionConfig ! = null ) ;
return null ;
}
logger . LogInformation ( "Параметры распределения. groupId: {groupId}, name: {groupName}, referenceDate: {referenceDate}, " +
"distributionPeriodName: {distributionPeriodName}, distributionPeriodDuration: {distributionPeriodDuration}, " +
"distributionPeriodType: {distributionPeriodType}, IsExcludeWeekends: {IsExcludeWeekends}, IsGroupingByWorkGroup: {IsGroupingByWorkGroup}, IsResponseAreaTimezone: {IsResponseAreaTimezone}" ,
jobGroupId , jobGroup . GroupName , jobGroup . ReferenceDate , jobGroup . DistributionConfig . DistributionPeriod . Name , jobGroup . DistributionConfig . DistributionPeriod . Duration , jobGroup . DistributionConfig . DistributionPeriod . Type ,
jobGroup . DistributionConfig . IsExcludeWeekends , jobGroup . DistributionConfig . IsGroupingByWorkGroup , jobGroup . IsResponseAreaTimezone ) ;
//получаем шаблоны только в статусе Used
2026-02-13 15:39:21 +10:00
var templateQuery = templateService . Get ( )
. AsNoTracking ( )
. Include ( t = > t . Job )
. Where ( t = >
t . Job ! . GroupId = = jobGroupId
//&& t.StatusTypeId == TemplateStatusTypeEnum.Used
) ;
if ( templateStatusType . HasValue )
templateQuery = templateQuery . Where ( t = > t . StatusTypeId = = templateStatusType ) ;
var allTemplates = await templateQuery . ToListAsync ( ) ;
2026-02-12 16:15:36 +10:00
if ( ! allTemplates . Any ( ) )
{
logger . LogInformation ( "В группе c ИД {jobGroupId} отсутствуют шаблоны в статусе Used" , jobGroupId ) ;
return null ;
}
logger . LogDebug ( "В с е г о шаблонов для распределения в статусе Used: {count} шт." , allTemplates . Count ) ;
// продолжительность в днях
var durationDays = GetDurationDays ( jobGroup . DistributionConfig ) ;
// группировать по РГ
var isGroupingByWorkGroup = jobGroup . DistributionConfig . IsGroupingByWorkGroup ;
// использовать часовой пояс РГ
var isResponseAreaTimeZone = jobGroup . IsResponseAreaTimezone ;
var distributedTemplates = new List < TemplateNextRunResultDto > ( ) ;
if ( isGroupingByWorkGroup )
{
// группировать по РГ
2026-02-13 15:39:21 +10:00
logger . LogDebug ( "Нужно группировать по РГ" ) ;
2026-02-12 16:15:36 +10:00
2026-02-13 15:39:21 +10:00
// получаем для каждого шаблона РГ и сразу группируем по РГ
2026-02-16 12:23:24 +10:00
var templatesByWorkGroup = new Dictionary < string? , List < Template > > ( StringComparer . OrdinalIgnoreCase ) ;
2026-02-13 15:39:21 +10:00
foreach ( var template in allTemplates )
2026-02-12 16:15:36 +10:00
{
2026-02-13 15:39:21 +10:00
var workGroupName = await shortcodesService . ApplyShortcodesAsync ( template . Job ! . WorkGroupMask , template ) ;
// Если даже workGroupName == null, ну и ладно, сгруппируем по null, и распределим шаблоны в рамках этого null
if ( workGroupName = = null )
logger . LogWarning ( "Для шаблона {id}, с маской РГ '{workGroupMask}', не смог с помощью шорткода определить рабочую группу, шорткод сервис вернул РГ: '{workGroupName}'" ,
template . Id , template . Job ! . WorkGroupMask , workGroupName ) ;
// Добавляем шаблон в группу
if ( ! templatesByWorkGroup . TryGetValue ( workGroupName , out var templatesList ) )
{
templatesList = new List < Template > ( ) ;
templatesByWorkGroup [ workGroupName ] = templatesList ;
}
templatesList . Add ( template ) ;
2026-02-12 16:15:36 +10:00
}
2026-02-13 15:39:21 +10:00
logger . LogDebug ( "Групп для распределения: {count}" , templatesByWorkGroup . Count ) ;
var dateStart = GetDateStart ( ) ;
// Будем распределять каждую РГ отдельно
foreach ( var ( workGroupName , templates ) in templatesByWorkGroup )
2026-02-12 16:15:36 +10:00
{
2026-02-13 15:39:21 +10:00
logger . LogDebug ( "Готовлюсь распределать шаблоны {templateCount} шт. в РГ '{workGroup}'" , templates . Count , workGroupName ) ;
if ( isResponseAreaTimeZone )
{
// использовать часовой пояс РГ
logger . LogDebug ( "Использовать часовой пояс РГ" ) ;
// считаем, что у всех шаблонов сгруппированных по РГ, одна З О
// получаем З О для первого шаблона
var firstTemplate = templates . First ( ) ;
var responseArea = await shortcodesService . ApplyShortcodesAsync ( firstTemplate . Job ! . ResponseAreaMask , firstTemplate ) ;
if ( responseArea = = null )
logger . LogWarning ( "Для шаблона {id}, с маской З О '{responseAreaMask}', не смог с помощью шорткода определить З О , шорткод сервис вернул З О : '{responseArea}'" ,
firstTemplate . Id , firstTemplate . Job ! . ResponseAreaMask , responseArea ) ;
var offset = GetOffsetForDistributionByResponseArea ( responseArea ) ;
var templatesForDistribute = templates . Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) ) . ToList ( ) ;
var referenceDate = GetReferenceDate ( jobGroup , responseArea ) ;
logger . LogInformation ( "Буду распределать шаблоны {count} шт, сгруппированные по РГ '{workGroup}', использую часовой пояс З О . dateStart: {dateStart}, offset: {offset}, referenceDate: {referenceDate}" ,
templatesForDistribute . Count , workGroupName , dateStart , offset , referenceDate ) ;
var distributionResult = await templateDistributor . DistributeTemplatesAsync ( dateStart , durationDays , referenceDate , offset , templatesForDistribute , jobGroup . DistributionConfig . IsExcludeWeekends ) ;
logger . LogDebug ( "Закончил распределение для РГ '{workGroupName}', referenceDate: {referenceDate}, часового пояса З О {responseArea}, utcOffset: {offset}, распределил шаблонов: {count} шт." ,
workGroupName , referenceDate , responseArea , offset , distributionResult . Count ) ;
distributedTemplates . AddRange ( distributionResult ) ;
}
else
{
// не нужно использовать часовой пояс РГ (используем часовой пояс У З ЕСПП)
logger . LogDebug ( "Н е нужно использовать часовой пояс РГ (используем часовой пояс У З ЕСПП)" ) ;
var offset = GetOffsetForDistributionByResponseArea ( ) ;
var templatesForDistribute = templates . Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) ) . ToList ( ) ;
var referenceDate = GetReferenceDate ( jobGroup , null ) ;
logger . LogInformation ( "Буду распределать шаблоны {count} шт, сгруппированные по РГ '{workGroup}', Н Е НУЖНО использовать часовой пояс З О . dateStart: {dateStart}, offset: {offset}, referenceDate: {referenceDate}" ,
templatesForDistribute . Count , workGroupName , dateStart , offset , referenceDate ) ;
var distributionResult = await templateDistributor . DistributeTemplatesAsync ( dateStart , durationDays , referenceDate , offset , templatesForDistribute , jobGroup . DistributionConfig . IsExcludeWeekends ) ;
logger . LogDebug ( "Закончил распределение для РГ '{workGroup}', referenceDate: {referenceDate}, utcOffset: {offset}, распределил шаблонов: {count} шт." , workGroupName , referenceDate , offset , distributionResult . Count ) ;
distributedTemplates . AddRange ( distributionResult ) ;
}
2026-02-12 16:15:36 +10:00
}
}
else
{
//не нужно группировать по РГ
2026-02-13 15:39:21 +10:00
logger . LogDebug ( "Н е нужно группировать по РГ" ) ;
2026-02-12 16:15:36 +10:00
if ( isResponseAreaTimeZone )
{
2026-02-13 15:39:21 +10:00
// использовать ЧАСОВОЙ ПОЯС З О (нужно сгруппировать шаблоны по часовым поясам, затем отдельно распределить каждую группу)
logger . LogDebug ( "Использовать ЧАСОВОЙ ПОЯС З О (нужно сгруппировать шаблоны по часовым поясам, затем отдельно распределить каждую группу)" ) ;
// получаем для каждого шаблона З О и сразу группируем по З О
2026-02-16 12:23:24 +10:00
var templatesByResponseArea = new Dictionary < string? , List < Template > > ( StringComparer . OrdinalIgnoreCase ) ;
2026-02-13 15:39:21 +10:00
foreach ( var template in allTemplates )
{
var responseArea = await shortcodesService . ApplyShortcodesAsync ( template . Job ! . ResponseAreaMask , template ) ;
if ( responseArea = = null )
logger . LogWarning ( "Для шаблона {id}, с маской З О '{responseAreaMask}', не смог с помощью шорткода определить З О , шорткод сервис вернул З О : '{responseArea}'" ,
template . Id , template . Job ! . ResponseAreaMask , responseArea ) ;
// Добавляем шаблон в группу
if ( ! templatesByResponseArea . TryGetValue ( responseArea , out var templateList ) )
{
templateList = new List < Template > ( ) ;
templatesByResponseArea [ responseArea ] = templateList ;
}
templateList . Add ( template ) ;
}
logger . LogDebug ( "Групп для распределения: {count}" , templatesByResponseArea . Count ) ;
var dateStart = GetDateStart ( ) ;
// для каждой З О получить offset и распределить шаблоны
foreach ( var ( responseArea , templates ) in templatesByResponseArea )
{
var offset = GetOffsetForDistributionByResponseArea ( responseArea ) ;
var templatesForDistribute = templates . Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) ) . ToList ( ) ;
var referenceDate = GetReferenceDate ( jobGroup , responseArea ) ;
logger . LogInformation ( "Буду распределять шаблоны {count} шт, их Н Е НУЖНО группировать по РГ, НУЖНО использовать часовой пояс З О : {responseArea}, utcOffset: {offset}. dateStart: {dateStart}, referenceDate: {referenceDate}" ,
templatesForDistribute . Count , responseArea , offset , dateStart , referenceDate ) ;
var distributionResult = await templateDistributor . DistributeTemplatesAsync ( dateStart , durationDays , referenceDate , offset , templatesForDistribute , jobGroup . DistributionConfig . IsExcludeWeekends ) ;
logger . LogDebug ( "Закончил распределение для часового пояса З О {responseArea}, utcOffset: {offset}, referenceDate: {referenceDate}, распределил шаблонов: {count} шт." , responseArea , offset , referenceDate , distributionResult . Count ) ;
distributedTemplates . AddRange ( distributionResult ) ;
}
2026-02-12 16:15:36 +10:00
}
else
{
2026-02-13 15:39:21 +10:00
// не нужно использовать часовой пояс З О
logger . LogDebug ( "Н е нужно использовать часовой пояс З О . Распределю все шаблоны без группировки." ) ;
2026-02-12 16:15:36 +10:00
2026-02-13 15:39:21 +10:00
var offset = GetOffsetForDistributionByResponseArea ( ) ;
2026-02-12 16:15:36 +10:00
var templatesForDistribute = allTemplates . Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) ) . ToList ( ) ;
2026-02-13 15:39:21 +10:00
var referenceDate = GetReferenceDate ( jobGroup , null ) ;
var dateStart = GetDateStart ( ) ;
2026-02-12 16:15:36 +10:00
2026-02-13 15:39:21 +10:00
logger . LogInformation ( "Буду распределать шаблоны {count} шт, их Н Е НУЖНО группировать по РГ, Н Е НУЖНО использовать часовой пояс З О . dateStart: {dateStart}, offset: {offset}, referenceDate: {referenceDate}" ,
templatesForDistribute . Count , dateStart , offset , referenceDate ) ;
2026-02-12 16:15:36 +10:00
2026-02-13 15:39:21 +10:00
distributedTemplates = await templateDistributor . DistributeTemplatesAsync ( dateStart , durationDays , referenceDate , offset , templatesForDistribute , jobGroup . DistributionConfig . IsExcludeWeekends ) ;
2026-02-12 16:15:36 +10:00
}
}
2026-02-13 15:39:21 +10:00
logger . LogInformation ( "Закончил распределение шаблонов {count} шт. для jobGroupId: {jobGroupId}. Изменено: {changedCount}, без изменений: {oldCount}" ,
distributedTemplates . Count , jobGroupId , distributedTemplates . Count ( t = > t . NextRun ! = t . NextRunOld ) , distributedTemplates . Count ( t = > t . NextRun = = t . NextRunOld ) ) ;
2026-02-12 16:15:36 +10:00
return distributedTemplates ;
}
2026-02-16 14:48:43 +10:00
public async Task < DateTimeOffset ? > GetNextRunForNewTemplateAsync ( Guid jobGroupId , string workGroupName , string responseAreaName )
2026-02-16 12:23:24 +10:00
{
logger . LogDebug ( "Получить nextRun для вновь создаваемого шаблона. jobGroupId: {jobGroupId}, workGroupName: {workGroupName}, responseAreaName: {responseAreaName}" ,
jobGroupId , workGroupName , responseAreaName ) ;
var jobGroup = await jobGroupService . Get ( )
. AsNoTracking ( )
. Include ( t = > t . DistributionConfig )
. ThenInclude ( t = > t . DistributionPeriod )
. FirstOrDefaultAsync ( t = > t . Id = = jobGroupId ) ;
if ( jobGroup = = null )
{
logger . LogError ( "Н е найдена группа работ с Id: {jobGroupId}." , jobGroupId ) ;
2026-02-16 14:48:43 +10:00
//throw new ArgumentNullException(nameof(jobGroupId), $"Н е найдена группа работ с Id: {jobGroupId}");
return null ;
2026-02-16 12:23:24 +10:00
}
if ( jobGroup . IsAutoDistributionEnabled )
{
// автораспределение
logger . LogDebug ( "Расчет используя метод распределения работ" ) ;
if ( jobGroup . DistributionConfig = = null )
{
logger . LogError ( "Для группы работ {jobGroupId}, указано автораспределение, но отсутствует конфиг в таблице {GroupDistributionConfigs}" , jobGroupId , nameof ( JobGroupDistributionConfig ) ) ;
2026-02-16 14:48:43 +10:00
//throw new ArgumentNullException(nameof(JobGroupDistributionConfig), $"Для jobGroupId: {jobGroupId} отсутствует конфигурация автораспределения в таблице {nameof(JobGroupDistributionConfig)}");
return null ;
2026-02-16 12:23:24 +10:00
}
var startDate = GetDateStart ( ) ;
var duration = GetDurationDays ( jobGroup . DistributionConfig ) ;
var excludeWeekends = jobGroup . DistributionConfig . IsExcludeWeekends ;
var targetTemplate = new TemplateNextRunDto ( Guid . Empty , null ) ;
var templateStatusType = TemplateStatusTypeEnum . Used ;
var allTemplatesQuery = templateService . Get ( )
. AsNoTracking ( )
. Include ( t = > t . Job )
. Where ( t = > t . StatusTypeId = = templateStatusType & & t . Job ! . GroupId = = jobGroupId ) ;
// группировать по РГ
var isGroupingByWorkGroup = jobGroup . DistributionConfig . IsGroupingByWorkGroup ;
// использовать часовой пояс РГ
var isResponseAreaTimeZone = jobGroup . IsResponseAreaTimezone ;
if ( isGroupingByWorkGroup )
{
// группировать по РГ
logger . LogDebug ( "Нужно группировать по РГ" ) ;
// получаем для всех шаблонов РГ и группируем их по РГ
var templatesByWorkGroup = new Dictionary < string? , List < Template > > ( StringComparer . OrdinalIgnoreCase ) ;
var allTemplates = await allTemplatesQuery . ToListAsync ( ) ;
foreach ( var template in allTemplates )
{
var templateWorkGroupName = await shortcodesService . ApplyShortcodesAsync ( template . Job ! . WorkGroupMask , template ) ;
// Если даже workGroupName == null, ну и ладно, сгруппируем по null
if ( templateWorkGroupName = = null )
logger . LogWarning ( "Для шаблона {id}, с маской РГ '{workGroupMask}', не смог с помощью шорткода определить рабочую группу, шорткод сервис вернул РГ: '{workGroupName}'" ,
template . Id , template . Job ! . WorkGroupMask , templateWorkGroupName ) ;
// Добавляем шаблон в группу
if ( ! templatesByWorkGroup . TryGetValue ( templateWorkGroupName , out var templatesList ) )
{
templatesList = new List < Template > ( ) ;
templatesByWorkGroup [ templateWorkGroupName ] = templatesList ;
}
templatesList . Add ( template ) ;
}
//Берем шаблоны только нашей РГ workGroupName
templatesByWorkGroup . TryGetValue ( workGroupName , out var templatesWithWorkGroup ) ;
var templatesByWorkGroupDto = ( templatesWithWorkGroup ? ? new List < Template > ( ) ) . Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) ) . ToList ( ) ;
logger . LogDebug ( "Шаблонов в РГ {workGroupName} - {count} шт." , workGroupName , templatesByWorkGroupDto . Count ) ;
if ( isResponseAreaTimeZone )
{
// использовать часовой пояс РГ
logger . LogDebug ( "Использовать часовой пояс РГ '{responseAreaName}'" , responseAreaName ) ;
var offset = GetOffsetForDistributionByResponseArea ( responseAreaName ) ;
var referenceDate = GetReferenceDate ( jobGroup , responseAreaName ) ;
var nextRun = await templateDistributor . GetNextRunForTemplateAsync ( startDate , duration , referenceDate , offset , targetTemplate , templatesByWorkGroupDto , excludeWeekends , isNew : true ) ;
return nextRun . NextRun ;
}
else
{
// не нужно использовать часовой пояс РГ (используем часовой пояс У З ЕСПП)
logger . LogDebug ( "Н е нужно использовать часовой пояс РГ (используем часовой пояс У З ЕСПП)" ) ;
var offset = GetOffsetForDistributionByResponseArea ( ) ;
var referenceDate = GetReferenceDate ( jobGroup , null ) ;
var nextRun = await templateDistributor . GetNextRunForTemplateAsync ( startDate , duration , referenceDate , offset , targetTemplate , templatesByWorkGroupDto , excludeWeekends , isNew : true ) ;
return nextRun . NextRun ;
}
}
else
{
// не нужно группировать по РГ
logger . LogDebug ( "Н е нужно группировать по РГ" ) ;
if ( isResponseAreaTimeZone )
{
// использовать ЧАСОВОЙ ПОЯС З О
logger . LogDebug ( "Нужно использовать часовой пояс З О '{responseAreaName}'" , responseAreaName ) ;
// получить все шаблоны, сгруппировать их по З О , получить nextRun в рамках переданной З О
var referenceDate = GetReferenceDate ( jobGroup , responseAreaName ) ;
var offset = GetOffsetForDistributionByResponseArea ( responseAreaName ) ;
var allTemplates = await allTemplatesQuery . ToListAsync ( ) ;
// получаем для каждого шаблона З О и сразу группируем по З О
var templatesByResponseArea = new Dictionary < string? , List < Template > > ( StringComparer . OrdinalIgnoreCase ) ;
foreach ( var template in allTemplates )
{
var responseArea = await shortcodesService . ApplyShortcodesAsync ( template . Job ! . ResponseAreaMask , template ) ;
if ( responseArea = = null )
logger . LogWarning ( "Для шаблона {id}, с маской З О '{responseAreaMask}', не смог с помощью шорткода определить З О , шорткод сервис вернул З О : '{responseArea}'" ,
template . Id , template . Job ! . ResponseAreaMask , responseArea ) ;
// Добавляем шаблон в группу
if ( ! templatesByResponseArea . TryGetValue ( responseArea , out var templateList ) )
{
templateList = new List < Template > ( ) ;
templatesByResponseArea [ responseArea ] = templateList ;
}
templateList . Add ( template ) ;
}
// из сгруппированных шаблонов, получаем существующие с нашей переданной responseAreaName
templatesByResponseArea . TryGetValue ( responseAreaName , out var templatesWithResponseArea ) ;
var templatesWithResponseAreaDto = ( templatesWithResponseArea ? ? new List < Template > ( ) ) . Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) ) . ToList ( ) ;
logger . LogDebug ( "Существующих шаблонов в БД с responseAreaName: {responseAreaName} - {count} шт." , responseAreaName , templatesWithResponseAreaDto . Count ) ;
var nextRun = await templateDistributor . GetNextRunForTemplateAsync ( startDate , duration , referenceDate , offset , targetTemplate , templatesWithResponseAreaDto , excludeWeekends , isNew : true ) ;
return nextRun . NextRun ;
}
else
{
// не нужно использовать часовой пояс З О
logger . LogDebug ( "Н е нужно использовать часовой пояс З О ." ) ;
var referenceDate = GetReferenceDate ( jobGroup , null ) ;
var offset = GetOffsetForDistributionByResponseArea ( ) ;
var allTemplates = await allTemplatesQuery
. Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) )
. ToListAsync ( ) ;
var nextRun = await templateDistributor . GetNextRunForTemplateAsync ( startDate , duration , referenceDate , offset , targetTemplate , allTemplates , excludeWeekends , isNew : true ) ;
return nextRun . NextRun ;
}
}
}
else
{
// расписание ЕСПП
logger . LogDebug ( "Расчет используя метод ЕСПП" ) ;
if ( jobGroup . IsResponseAreaTimezone )
{
2026-02-16 14:48:43 +10:00
// использовать часовой пояс З О
2026-02-16 12:23:24 +10:00
var referenceDate = GetReferenceDate ( jobGroup , responseAreaName ) ;
var offset = GetOffsetForDistributionByResponseArea ( responseAreaName ) ;
logger . LogDebug ( "Расчет nextRun используя метод ЕСПП, использую часовой пояс З О '{responseAreaName}, referenceDate: {referenceDate}, offset: {offset}'" , responseAreaName , referenceDate , offset ) ;
var nextRun = await esppScheduleTransformService . GetNextDateAsync ( jobGroupId , referenceDate , offset ) ;
return nextRun ;
}
else
{
2026-02-16 14:48:43 +10:00
// не использовать часовой пояс З О
2026-02-16 12:23:24 +10:00
// так как это классическое расписание еспп, и не используется часовой пояс РГ, то нам нужно считать в часовом поясе У З ЕСПП, т.е . М С К
var referenceDate = GetReferenceDate ( jobGroup , null ) ;
var offset = GetOffsetForDistributionByResponseArea ( ) ;
logger . LogDebug ( "Расчет nextRun используя метод ЕСПП, не использую часовой пояс З О , использую часовой пояс У З ЕСПП, referenceDate: {referenceDate}, offset: {offset}" , referenceDate , offset ) ;
var nextRun = await esppScheduleTransformService . GetNextDateAsync ( jobGroupId , referenceDate , offset ) ;
return nextRun ;
}
}
}
2026-02-16 14:48:43 +10:00
public async Task < DateTimeOffset ? > GetNextRunForTemplateAsync ( Guid templateId , bool isNew )
2026-02-16 12:23:24 +10:00
{
logger . LogDebug ( "Получить nextRun для существующего шаблона, templateId: {templateId}, isNew: {isNew}" , templateId , isNew ) ;
2026-02-16 14:48:43 +10:00
var template = await templateService . Get ( )
. AsNoTracking ( )
. Include ( t = > t . Job )
. ThenInclude ( t = > t . Group ) . ThenInclude ( t = > t . DistributionConfig ) . ThenInclude ( t = > t . DistributionPeriod )
. FirstOrDefaultAsync ( t = > t . Id = = templateId ) ;
if ( template = = null )
{
logger . LogError ( "Н е найден шаблон с Id: {templateId}." , templateId ) ;
//throw new ArgumentNullException(nameof(templateId), $"Н е найден шаблон с Id: {templateId}");
return null ;
}
if ( template . Job ! . Group ! . IsAutoDistributionEnabled = = true )
{
// включено автораспределение
logger . LogDebug ( "Включено автораспределение" ) ;
if ( template . Job ! . Group . DistributionConfig = = null )
{
logger . LogError ( "Для шаблона {templateId}, jobGroupId {jobGroupId}, указано автораспределение, но отсутствует конфиг в таблице {GroupDistributionConfigs}" , template . Id , template . Job . GroupId , nameof ( JobGroupDistributionConfig ) ) ;
// throw new ArgumentNullException(nameof(JobGroupDistributionConfig), $"Для jobGroupId: {template.Job.GroupId} отсутствует конфигурация автораспределения в таблице {nameof(JobGroupDistributionConfig)}");
return null ;
}
var startDate = GetDateStart ( ) ;
var duration = GetDurationDays ( template . Job . Group . DistributionConfig ) ;
var excludeWeekends = template . Job . Group . DistributionConfig . IsExcludeWeekends ;
var targetTemplate = new TemplateNextRunDto ( template . Id , template . NextRun ) ;
var templateStatusType = TemplateStatusTypeEnum . Used ;
var jobGroup = template . Job . Group ;
// получаем все шаблоны из этой же группы что и template
var allTemplatesQuery = templateService . Get ( )
. AsNoTracking ( )
. Include ( t = > t . Job )
. Where ( t = > t . StatusTypeId = = templateStatusType & & t . Job ! . GroupId = = jobGroup . Id ) ;
// группировать по РГ
var isGroupingByWorkGroup = jobGroup . DistributionConfig . IsGroupingByWorkGroup ;
// использовать часовой пояс РГ
var isResponseAreaTimeZone = jobGroup . IsResponseAreaTimezone ;
if ( isGroupingByWorkGroup )
{
var targetTemplateWorkGroup = await shortcodesService . ApplyShortcodesAsync ( template . Job . WorkGroupMask , template ) ;
// группировать по РГ
logger . LogDebug ( "Нужно группировать по РГ" ) ;
// получаем для всех шаблонов РГ и группируем их по РГ
var templatesByWorkGroup = new Dictionary < string? , List < Template > > ( StringComparer . OrdinalIgnoreCase ) ;
var allTemplates = await allTemplatesQuery . ToListAsync ( ) ;
foreach ( var templateItem in allTemplates )
{
var templateWorkGroupName = await shortcodesService . ApplyShortcodesAsync ( templateItem . Job ! . WorkGroupMask , templateItem ) ;
// Если даже workGroupName == null, ну и ладно, сгруппируем по null
if ( templateWorkGroupName = = null )
logger . LogWarning ( "Для шаблона {id}, с маской РГ '{workGroupMask}', не смог с помощью шорткода определить рабочую группу, шорткод сервис вернул РГ: '{workGroupName}'" ,
templateItem . Id , templateItem . Job ! . WorkGroupMask , templateWorkGroupName ) ;
// Добавляем шаблон в группу
if ( ! templatesByWorkGroup . TryGetValue ( templateWorkGroupName , out var templatesList ) )
{
templatesList = new List < Template > ( ) ;
templatesByWorkGroup [ templateWorkGroupName ] = templatesList ;
}
templatesList . Add ( templateItem ) ;
}
//Берем шаблоны только нашей РГ workGroupName
templatesByWorkGroup . TryGetValue ( targetTemplateWorkGroup , out var templatesWithWorkGroup ) ;
var templatesByWorkGroupDto = ( templatesWithWorkGroup ? ? new List < Template > ( ) ) . Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) ) . ToList ( ) ;
logger . LogDebug ( "Шаблонов в РГ {targetTemplateWorkGroup} - {count} шт." , targetTemplateWorkGroup , templatesByWorkGroupDto . Count ) ;
if ( isResponseAreaTimeZone )
{
// использовать часовой пояс РГ
var targetTemplateResponseArea = await shortcodesService . ApplyShortcodesAsync ( template . Job . ResponseAreaMask , template ) ;
logger . LogDebug ( "Использовать часовой пояс РГ '{responseAreaName}'" , targetTemplateResponseArea ) ;
var offset = GetOffsetForDistributionByResponseArea ( targetTemplateResponseArea ) ;
var referenceDate = GetReferenceDate ( jobGroup , targetTemplateResponseArea ) ;
var nextRun = await templateDistributor . GetNextRunForTemplateAsync ( startDate , duration , referenceDate , offset , targetTemplate , templatesByWorkGroupDto , excludeWeekends , isNew ) ;
return nextRun . NextRun ;
}
else
{
// не нужно использовать часовой пояс РГ (используем часовой пояс У З ЕСПП)
logger . LogDebug ( "Н е нужно использовать часовой пояс РГ (используем часовой пояс У З ЕСПП)" ) ;
2026-02-20 10:39:53 +10:00
2026-02-16 14:48:43 +10:00
var offset = GetOffsetForDistributionByResponseArea ( ) ;
var referenceDate = GetReferenceDate ( jobGroup , null ) ;
var nextRun = await templateDistributor . GetNextRunForTemplateAsync ( startDate , duration , referenceDate , offset , targetTemplate , templatesByWorkGroupDto , excludeWeekends , isNew ) ;
return nextRun . NextRun ;
}
}
else
{
// не нужно группировать по РГ
logger . LogDebug ( "Н е нужно группировать по РГ" ) ;
2026-02-16 12:23:24 +10:00
2026-02-16 14:48:43 +10:00
if ( isResponseAreaTimeZone )
{
// использовать ЧАСОВОЙ ПОЯС З О
var responseAreaName = await shortcodesService . ApplyShortcodesAsync ( template . Job . ResponseAreaMask , template ) ;
logger . LogDebug ( "Нужно использовать часовой пояс З О '{responseAreaName}'" , responseAreaName ) ;
// получить все шаблоны, сгруппировать их по З О , получить nextRun в рамках переданной З О
var referenceDate = GetReferenceDate ( jobGroup , responseAreaName ) ;
var offset = GetOffsetForDistributionByResponseArea ( responseAreaName ) ;
var allTemplates = await allTemplatesQuery . ToListAsync ( ) ;
// получаем для каждого шаблона З О и сразу группируем по З О
var templatesByResponseArea = new Dictionary < string? , List < Template > > ( StringComparer . OrdinalIgnoreCase ) ;
foreach ( var templateItem in allTemplates )
{
var responseArea = await shortcodesService . ApplyShortcodesAsync ( templateItem . Job ! . ResponseAreaMask , templateItem ) ;
if ( responseArea = = null )
logger . LogWarning ( "Для шаблона {id}, с маской З О '{responseAreaMask}', не смог с помощью шорткода определить З О , шорткод сервис вернул З О : '{responseArea}'" ,
templateItem . Id , templateItem . Job ! . ResponseAreaMask , responseArea ) ;
// Добавляем шаблон в группу
if ( ! templatesByResponseArea . TryGetValue ( responseArea , out var templateList ) )
{
templateList = new List < Template > ( ) ;
templatesByResponseArea [ responseArea ] = templateList ;
}
templateList . Add ( templateItem ) ;
}
// из сгруппированных шаблонов, получаем существующие с З О нашего шаблона responseAreaName
templatesByResponseArea . TryGetValue ( responseAreaName , out var templatesWithResponseArea ) ;
var templatesWithResponseAreaDto = ( templatesWithResponseArea ? ? new List < Template > ( ) ) . Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) ) . ToList ( ) ;
logger . LogDebug ( "Существующих шаблонов в БД с responseAreaName: {responseAreaName} - {count} шт." , responseAreaName , templatesWithResponseAreaDto . Count ) ;
var nextRun = await templateDistributor . GetNextRunForTemplateAsync ( startDate , duration , referenceDate , offset , targetTemplate , templatesWithResponseAreaDto , excludeWeekends , isNew ) ;
return nextRun . NextRun ;
}
else
{
// не нужно использовать часовой пояс З О
logger . LogDebug ( "Н е нужно использовать часовой пояс З О ." ) ;
var referenceDate = GetReferenceDate ( jobGroup , null ) ;
var offset = GetOffsetForDistributionByResponseArea ( ) ;
var allTemplates = await allTemplatesQuery
. Select ( t = > new TemplateNextRunDto ( t . Id , t . NextRun ) )
. ToListAsync ( ) ;
var nextRun = await templateDistributor . GetNextRunForTemplateAsync ( startDate , duration , referenceDate , offset , targetTemplate , allTemplates , excludeWeekends , isNew ) ;
return nextRun . NextRun ;
}
}
}
else
{
// метод расчета ЕСПП
logger . LogDebug ( "Метод расчета ЕСПП" ) ;
if ( template . Job . Group . IsResponseAreaTimezone )
{
// использовать часовой пояс З О
var responseAreaName = await shortcodesService . ApplyShortcodesAsync ( template . Job . ResponseAreaMask , template ) ;
var jobGroup = template . Job . Group ;
var referenceDate = GetReferenceDate ( jobGroup , responseAreaName ) ;
var offset = GetOffsetForDistributionByResponseArea ( responseAreaName ) ;
logger . LogDebug ( "Расчет nextRun используя метод ЕСПП, использую часовой пояс З О '{responseAreaName}, referenceDate: {referenceDate}, offset: {offset}'" , responseAreaName , referenceDate , offset ) ;
var nextRun = await esppScheduleTransformService . GetNextDateAsync ( jobGroup . Id , referenceDate , offset ) ;
return nextRun ;
}
else
{
// не использовать часовой пояс З О
// так как это классическое расписание еспп, и не используется часовой пояс РГ, то нам нужно считать в часовом поясе У З ЕСПП, т.е . М С К
var referenceDate = GetReferenceDate ( template . Job . Group , null ) ;
var offset = GetOffsetForDistributionByResponseArea ( ) ;
logger . LogDebug ( "Расчет nextRun используя метод ЕСПП, не использую часовой пояс З О , использую часовой пояс У З ЕСПП, referenceDate: {referenceDate}, offset: {offset}" , referenceDate , offset ) ;
var nextRun = await esppScheduleTransformService . GetNextDateAsync ( template . Job . GroupId , referenceDate , offset ) ;
return nextRun ;
}
}
2026-02-16 12:23:24 +10:00
}
2026-02-20 10:39:53 +10:00
public int GetDurationDays ( JobGroupDistributionConfig config )
2026-02-12 16:15:36 +10:00
{
var period = config . DistributionPeriod ;
var periodType = period ! . Type ;
int . TryParse ( period . Duration , out var duration ) ;
var periodDays = duration ;
if ( periodType = = DistributionPeriodTypeEnum . Day . ToString ( ) )
periodDays = duration ;
if ( periodType = = DistributionPeriodTypeEnum . Month . ToString ( ) )
// в месяце 30 дней, duration*30
periodDays = duration * 30 ;
if ( periodType = = DistributionPeriodTypeEnum . Year . ToString ( ) )
// в году 365 дней, duration*365
periodDays = duration * 365 ;
logger . LogDebug ( "Период распределения: {name}, duration: {duration}, type: {type}. Итого в днях: {days}" , period . Name , period . Duration , period . Type , periodDays ) ;
if ( periodDays = = 0 )
{
periodDays = 1 ;
logger . LogWarning ( "Полученный период распределения 0 дней. Неверный конфиг распределения в таблице {table}. {name}, duration: {duration}, type: {type}. Устанавливаем минимальный период распределения {periodDays} дней." , nameof ( DistributionPeriod ) , period . Name , period . Duration , period . Type , periodDays ) ;
}
return periodDays ;
}
2026-02-20 10:39:53 +10:00
public async Task < List < DateOnly > > GetWorkDaysAsync ( DateOnly start , DateOnly end , bool excludeWeekends )
{
return await templateDistributor . GetWorkDaysAsync ( start , end , excludeWeekends ) ;
}
public async Task < HashSet < DateOnly > > GetWeekendsAsync ( DateOnly startDate , DateOnly endDate )
{
return await templateDistributor . GetWeekendsAsync ( startDate , endDate ) ;
}
2026-02-12 16:15:36 +10:00
/// <summary>
/// Получить часовую зону для распределения
/// </summary>
/// <returns></returns>
2026-02-13 15:39:21 +10:00
private TimeSpan GetOffsetForDistributionByResponseArea ( string? responseArea = null )
2026-02-12 16:15:36 +10:00
{
2026-02-13 15:39:21 +10:00
// или часовая зона робота, или часовая зона РГ
2026-02-12 16:15:36 +10:00
if ( responseArea ! = null )
{
// возвращаем часовой пояс З О
var responseAreaOffset = scheduleResponseAreaTimeOffsetService . GetByResponseAreaOrDefault ( responseArea ) . UtcTimeOffset ;
logger . LogDebug ( "Получил часовой пояс для З О : {responseArea}, UtcTimeOffset: {responseAreaOffset}" , responseArea , responseAreaOffset ) ;
return responseAreaOffset ;
}
else
{
// возвращаем часовой пояс робота
var esppOffset = GetEsppAccountOffset ( ) ;
logger . LogDebug ( "Н е передана З О , использую часовой пояс У З ЕСПП, UtcTimeOffset: {UtcTimeOffset}" , esppOffset ) ;
return esppOffset ;
}
}
2026-02-16 16:54:06 +10:00
public TimeSpan GetEsppAccountOffset ( )
2026-02-12 16:15:36 +10:00
{
var esppOffset = TimeSpan . FromHours ( settingsFromDb . EsppRobotAccountTimeZoneHour ) ;
logger . LogDebug ( "Оффсет У З ЕСПП: {esppOffset}" , esppOffset ) ;
return esppOffset ;
}
/// <summary>
2026-02-13 15:39:21 +10:00
/// Получить дату начала распределения
2026-02-12 16:15:36 +10:00
/// </summary>
/// <param name="offset"></param>
/// <returns></returns>
2026-02-13 15:39:21 +10:00
private DateOnly GetDateStart ( )
2026-02-12 16:15:36 +10:00
{
2026-02-13 15:39:21 +10:00
//var dateStart = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).Date);
//logger.LogDebug("Дата начала распределения (сегодня) в целевом часовом поясе {dateStart}, offset: {offset}", dateStart, offset);
var dateStart = DateOnly . FromDateTime ( DateTime . UtcNow . Date ) ;
logger . LogDebug ( "Дата начала распределения (сегодня): {dateStart}" , dateStart ) ;
2026-02-12 16:15:36 +10:00
return dateStart ;
}
2026-02-13 15:39:21 +10:00
/// <summary>
/// Получить referenceDate для расчета с учетом часового пояса З О или без учета
/// </summary>
/// <param name="jobGroup"></param>
/// <param name="responseArea">Н е обзяательный параметр, нужен обязательно при расчете с учетом часового пояса З О </param>
/// <returns></returns>
private DateTimeOffset GetReferenceDate ( JobGroup jobGroup , string? responseArea )
{
// для того чтобы посчитать nextRun в часовом поясе З О , нужно пересчитать refDate чтобы понять в какое время хотел выполнять заказчик
logger . LogDebug ( "Исходный referenceDate UTC: {referenceDate}" , jobGroup . ReferenceDate ) ;
if ( jobGroup . IsResponseAreaTimezone )
{
// нужно вернуть с учетом часового пояса клиента сохранившего refDate (используется для расчета с учетом часового пояса З О )
var clientOffset = jobGroup . UserTimeZoneOffset ;
if ( ! clientOffset . HasValue )
{
logger . LogError ( "При расчете referenceDate для З О '{responseArea}', UserTimeZoneOffset=null. Вернул referenceDate {referenceDate} без смещения З О " ,
responseArea , jobGroup . ReferenceDate ) ;
return jobGroup . ReferenceDate ;
}
if ( responseArea = = null )
{
logger . LogError ( "При расчете referenceDate для З О '{responseArea}', responseArea=null. Вернул referenceDate {referenceDate} без смещения З О " ,
responseArea , jobGroup . ReferenceDate ) ;
return jobGroup . ReferenceDate ;
}
var responseAreaOffset = scheduleResponseAreaTimeOffsetService . GetByResponseAreaOrDefault ( responseArea ) . UtcTimeOffset ;
// = реф + офКлиента(который нажал в ГУИ сохранить)-офЗО
var resultOffset = clientOffset . Value - responseAreaOffset ;
var refDateWithResponseAreaOffset = jobGroup . ReferenceDate . Add ( resultOffset ) ;
logger . LogDebug ( "Клиент ожидает, что будет выполняться задача в каждой З О в {time}. referenceDate+clientOffset, {referenceDate}, {clientOffset}" ,
TimeOnly . FromDateTime ( jobGroup . ReferenceDate . Add ( clientOffset . Value ) . DateTime ) , jobGroup . ReferenceDate , clientOffset ) ;
logger . LogDebug ( "Итоговый referenceDate UTC: {resultRefDate}, для использования часовой зоны З О : '{responseArea}', добавлено времени {resultOffset} к исходному {refDate}" ,
refDateWithResponseAreaOffset , responseArea , resultOffset , jobGroup . ReferenceDate ) ;
logger . LogDebug ( "Если итоговый referenceDate UTC {refDateWithResponseAreaOffset} перевести в часовой пояс З О {responseAreaOffset}, то дата в часовом поясе З О будет {refDateTargetTz}" ,
refDateWithResponseAreaOffset , responseAreaOffset , refDateWithResponseAreaOffset . Add ( responseAreaOffset ) ) ;
return refDateWithResponseAreaOffset ;
}
else
{
// если Н Е НУЖНО в часовом поясе З О , то просто возвращаем refDate
logger . LogDebug ( "Итоговый referenceDate UTC: {refDate}, не нужно использовать часовой пояс З О " , jobGroup . ReferenceDate ) ;
return jobGroup . ReferenceDate ;
}
}
2026-02-12 16:15:36 +10:00
}
}