using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using NodaTime; using PARR.Constants; using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.NextRunServices.Models; using PARR.DAL.NextRunServices.Subservices; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Schedule; namespace PARR.DAL.NextRunServices { internal class NextRunServiceV2 : INextRunServiceV2 { private readonly ILogger logger; private readonly ITemplateService templateService; private readonly IJobGroupService jobGroupService; private readonly IEsppScheduleTransformService esppScheduleTransformService; private readonly ITemplateDistributorV2 templateDistributor; private readonly IShortcodesService shortcodesService; private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; private readonly SettingsFromDb settingsFromDb; public NextRunServiceV2( ILogger logger, ITemplateService templateService, IJobGroupService jobGroupService, IEsppScheduleTransformService esppScheduleTransformService, ITemplateDistributorV2 templateDistributor, IShortcodesService shortcodesService, IScheduleResponseAreaTimeOffsetService 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?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) { logger.LogInformation("Начинаю распределять шаблоны для группы работ {gobGroupId}", jobGroupId); var jobGroup = await jobGroupService.Get() .Include(t => t.DistributionConfig) .ThenInclude(t => t.DistributionPeriod) .AsNoTracking() .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 allTemplates = await templateService.Get() .AsNoTracking() .Include(t => t.Job) .Where(t => t.Job!.GroupId == jobGroupId && t.StatusTypeId == TemplateStatusTypeEnum.Used ).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(); if (isGroupingByWorkGroup) { // группировать по РГ if (isResponseAreaTimeZone) { // использовать часовой пояс РГ } else { // не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП) } } else { //не нужно группировать по РГ if (isResponseAreaTimeZone) { // использовать ЧАСОВОЙ ПОЯС РГ (нужно сгруппировать шаблоны по часовым поясам, затем отдельно распределить каждую группу) } else { // не нужно использовать часовой пояс РГ var offset = GetOffsetForDistribution(); var templatesForDistribute = allTemplates.Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList(); var dateStart = GetDateStart(offset); logger.LogInformation("Буду распределать шаблоны {count} шт, их НЕ НУЖНО группировать по РГ, НЕ НУЖНО использовать часовой пояс РГ. dateStart в целевом часовом поясе: {dateStart}, offset: {offset}", templatesForDistribute.Count, dateStart, offset); distributedTemplates = await templateDistributor.DistributeTemplatesAsync(dateStart, durationDays, jobGroup.ReferenceDate, offset, templatesForDistribute, jobGroup.DistributionConfig.IsExcludeWeekends); } } return distributedTemplates; } /// /// Получить продолжительность в днях /// /// /// private 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; } /// /// Получить часовую зону для распределения /// /// private TimeSpan GetOffsetForDistribution(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; } } /// /// Получить часовой пояс УЗ ЕСПП /// /// private TimeSpan GetEsppAccountOffset() { var esppOffset = TimeSpan.FromHours(settingsFromDb.EsppRobotAccountTimeZoneHour); logger.LogDebug("Оффсет УЗ ЕСПП: {esppOffset}", esppOffset); return esppOffset; } /// /// Получить дату начала распределения в целевом часовом поясе /// /// /// private DateOnly GetDateStart(TimeSpan offset) { var dateStart = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).Date); logger.LogDebug("Дата начала распределения (сегодня) в целевом часовом поясе {dateStart}, offset: {offset}", dateStart, offset); return dateStart; } } }