Files
parr_api/PARR.Core/Services/NextRunServices/NextRunService.cs

834 lines
53 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
using PARR.Core.Repositories.Interfaces.Schedule;
using PARR.Core.Services.NextRunServices.Models;
using PARR.Core.Services.NextRunServices.Subservices;
using PARR.Core.Services.Shortcodes;
using PARR.Domain.Entities;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.Domain.Enums;
using PARR.Domain.Settings;
namespace PARR.Core.Services.NextRunServices
{
internal class NextRunService : INextRunService
{
private readonly ILogger<NextRunService> logger;
private readonly ITemplateRepository templateService;
private readonly IJobGroupRepository jobGroupService;
private readonly IEsppScheduleTransformService esppScheduleTransformService;
private readonly ITemplateDistributor templateDistributor;
private readonly IShortcodesService shortcodesService;
private readonly IScheduleResponseAreaTimeOffsetRepository scheduleResponseAreaTimeOffsetService;
private readonly SettingsFromDb settingsFromDb;
public NextRunService(
ILogger<NextRunService> logger,
ITemplateRepository templateService,
IJobGroupRepository jobGroupService,
IEsppScheduleTransformService esppScheduleTransformService,
ITemplateDistributor templateDistributor,
IShortcodesService shortcodesService,
IScheduleResponseAreaTimeOffsetRepository scheduleResponseAreaTimeOffsetService,
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;
}
public async Task<List<TemplateNextRunResultDto>?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId, TemplateStatusTypeEnum? templateStatusType)
{
logger.LogInformation("Начинаю распределять шаблоны для группы работ {gobGroupId}, templateStatusType: {templateStatusType}", jobGroupId, templateStatusType);
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: {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
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();
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)
{
// группировать по РГ
logger.LogDebug("Нужно группировать по РГ");
// получаем для каждого шаблона РГ и сразу группируем по РГ
var templatesByWorkGroup = new Dictionary<string?, List<Template>>(StringComparer.OrdinalIgnoreCase);
foreach (var template in allTemplates)
{
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);
}
logger.LogDebug("Групп для распределения: {count}", templatesByWorkGroup.Count);
var dateStart = GetDateStart();
// Будем распределять каждую РГ отдельно
foreach (var (workGroupName, templates) in templatesByWorkGroup)
{
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);
}
}
}
else
{
//не нужно группировать по РГ
logger.LogDebug("Не нужно группировать по РГ");
if (isResponseAreaTimeZone)
{
// использовать ЧАСОВОЙ ПОЯС ЗО (нужно сгруппировать шаблоны по часовым поясам, затем отдельно распределить каждую группу)
logger.LogDebug("Использовать ЧАСОВОЙ ПОЯС ЗО (нужно сгруппировать шаблоны по часовым поясам, затем отдельно распределить каждую группу)");
// получаем для каждого шаблона ЗО и сразу группируем по ЗО
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);
}
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);
}
}
else
{
// не нужно использовать часовой пояс ЗО
logger.LogDebug("Не нужно использовать часовой пояс ЗО. Распределю все шаблоны без группировки.");
var offset = GetOffsetForDistributionByResponseArea();
var templatesForDistribute = allTemplates.Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
var referenceDate = GetReferenceDate(jobGroup, null);
var dateStart = GetDateStart();
logger.LogInformation("Буду распределать шаблоны {count} шт, их НЕ НУЖНО группировать по РГ, НЕ НУЖНО использовать часовой пояс ЗО. dateStart: {dateStart}, offset: {offset}, referenceDate: {referenceDate}",
templatesForDistribute.Count, dateStart, offset, referenceDate);
distributedTemplates = await templateDistributor.DistributeTemplatesAsync(dateStart, durationDays, referenceDate, offset, templatesForDistribute, jobGroup.DistributionConfig.IsExcludeWeekends);
}
}
logger.LogInformation("Закончил распределение шаблонов {count} шт. для jobGroupId: {jobGroupId}. Изменено: {changedCount}, без изменений: {oldCount}",
distributedTemplates.Count, jobGroupId, distributedTemplates.Count(t => t.NextRun != t.NextRunOld), distributedTemplates.Count(t => t.NextRun == t.NextRunOld));
return distributedTemplates;
}
public async Task<DateTimeOffset?> GetNextRunForNewTemplateAsync(Guid jobGroupId, string workGroupName, string responseAreaName)
{
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);
//throw new ArgumentNullException(nameof(jobGroupId), $"Не найдена группа работ с Id: {jobGroupId}");
return null;
}
if (jobGroup.IsAutoDistributionEnabled)
{
// автораспределение
logger.LogDebug("Расчет используя метод распределения работ");
if (jobGroup.DistributionConfig == null)
{
logger.LogError("Для группы работ {jobGroupId}, указано автораспределение, но отсутствует конфиг в таблице {GroupDistributionConfigs}", jobGroupId, nameof(JobGroupDistributionConfig));
//throw new ArgumentNullException(nameof(JobGroupDistributionConfig), $"Для jobGroupId: {jobGroupId} отсутствует конфигурация автораспределения в таблице {nameof(JobGroupDistributionConfig)}");
return null;
}
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)
{
// использовать часовой пояс ЗО
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
{
// не использовать часовой пояс ЗО
// так как это классическое расписание еспп, и не используется часовой пояс РГ, то нам нужно считать в часовом поясе УЗ ЕСПП, т.е. МСК
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;
}
}
}
public async Task<DateTimeOffset?> GetNextRunForTemplateAsync(Guid templateId, bool isNew)
{
logger.LogDebug("Получить nextRun для существующего шаблона, templateId: {templateId}, isNew: {isNew}", templateId, isNew);
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("Не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП)");
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("Не нужно группировать по РГ");
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;
}
}
}
public int GetDurationDays(JobGroupDistributionConfig config)
{
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;
}
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);
}
/// <summary>
/// Получить часовую зону для распределения
/// </summary>
/// <returns></returns>
private TimeSpan GetOffsetForDistributionByResponseArea(string? responseArea = null)
{
// или часовая зона робота, или часовая зона РГ
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;
}
}
public TimeSpan GetEsppAccountOffset()
{
var esppOffset = TimeSpan.FromHours(settingsFromDb.EsppRobotAccountTimeZoneHour);
logger.LogDebug("Оффсет УЗ ЕСПП: {esppOffset}", esppOffset);
return esppOffset;
}
/// <summary>
/// Получить дату начала распределения
/// </summary>
/// <param name="offset"></param>
/// <returns></returns>
private DateOnly GetDateStart()
{
//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);
return dateStart;
}
/// <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;
}
}
}
}