From 858dd8c31cdd926a31b5cfc954d129e58a462245 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Thu, 12 Feb 2026 16:15:36 +1000 Subject: [PATCH] =?UTF-8?q?feat(dal):=20NextRunServiceV2=20-=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D1=80=D0=B0=D0=BC=D0=BA=D0=B0=D1=85=20JobGroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.DAL/NextRunServices/INextRunServiceV2.cs | 14 + PARR.DAL/NextRunServices/NextRunServiceV2.cs | 240 ++++++++++++++++++ PARR.DAL/ParrDalInstaller.cs | 1 + .../ScheduleResponseAreaTimeOffsetService.cs | 20 +- PARR.Test/NextRun/NextRunTest.cs | 39 ++- 5 files changed, 299 insertions(+), 15 deletions(-) create mode 100644 PARR.DAL/NextRunServices/INextRunServiceV2.cs create mode 100644 PARR.DAL/NextRunServices/NextRunServiceV2.cs diff --git a/PARR.DAL/NextRunServices/INextRunServiceV2.cs b/PARR.DAL/NextRunServices/INextRunServiceV2.cs new file mode 100644 index 00000000..b8bdbb86 --- /dev/null +++ b/PARR.DAL/NextRunServices/INextRunServiceV2.cs @@ -0,0 +1,14 @@ +using PARR.DAL.NextRunServices.Models; + +namespace PARR.DAL.NextRunServices +{ + public interface INextRunServiceV2 + { + /// + /// Распределить шаблоны в группе работ (получить список распределенных шаблонов с актуальными nextRun) + /// + /// + /// Список TemplateId с NextRun и NextRunOld + Task?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId); + } +} diff --git a/PARR.DAL/NextRunServices/NextRunServiceV2.cs b/PARR.DAL/NextRunServices/NextRunServiceV2.cs new file mode 100644 index 00000000..515102e2 --- /dev/null +++ b/PARR.DAL/NextRunServices/NextRunServiceV2.cs @@ -0,0 +1,240 @@ +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; + } + + + + } +} diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index 0d12389e..e329d200 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -142,6 +142,7 @@ namespace PARR.DAL services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); #endregion diff --git a/PARR.DAL/Services/Implementations/Schedule/ScheduleResponseAreaTimeOffsetService.cs b/PARR.DAL/Services/Implementations/Schedule/ScheduleResponseAreaTimeOffsetService.cs index 5a6a9ac2..8a05b428 100644 --- a/PARR.DAL/Services/Implementations/Schedule/ScheduleResponseAreaTimeOffsetService.cs +++ b/PARR.DAL/Services/Implementations/Schedule/ScheduleResponseAreaTimeOffsetService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using PARR.DAL.Context; using PARR.DAL.Contracts; using PARR.DAL.Models.Schedule; @@ -17,8 +18,9 @@ namespace PARR.DAL.Services.Implementations.Schedule /// Настройки, если не нашли в offsetList /// private readonly ScheduleResponseAreaTimeOffset defaultOffset; + private readonly ILogger logger; - public ScheduleResponseAreaTimeOffsetService(DataContext dataContext, SettingsFromDb settingsFromDb) + public ScheduleResponseAreaTimeOffsetService(DataContext dataContext, SettingsFromDb settingsFromDb, ILogger logger) { offsetList = dataContext.ScheduleResponseAreaTimeOffsets .AsNoTracking() @@ -36,6 +38,7 @@ namespace PARR.DAL.Services.Implementations.Schedule EsppValue = "", UtcTimeOffset = new TimeSpan(3, 0, 0) }; + this.logger = logger; } @@ -45,7 +48,20 @@ namespace PARR.DAL.Services.Implementations.Schedule public ScheduleResponseAreaTimeOffset GetByResponseAreaOrDefault(string responseArea) { - return offsetList.TryGetValue(responseArea, out var value) ? value : defaultOffset; + offsetList.TryGetValue(responseArea, out var value); + + if (value != null) + { + logger.LogDebug("Получил часовой пояс по ЗО '{responseArea}', esppValue: {EsppValue}, utcTimeOffset: {utcTimeOffset}", responseArea, value.EsppValue, value.UtcTimeOffset); + return value; + } + else + { + logger.LogWarning("Не смог получить часовой пояс по ЗО '{responseArea}', вернул часово пояс по умолчанию: name: {name}, esppValue: {EsppValue}, utcTimeOffset: {utcTimeOffset}", + responseArea, defaultOffset.ResponseArea, defaultOffset.EsppValue, defaultOffset.UtcTimeOffset); + return defaultOffset; + } + //return offsetList.TryGetValue(responseArea, out var value) ? value : defaultOffset; } diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index 2444d03a..a960e90c 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -13,13 +13,15 @@ namespace PARR.Test.NextRun private readonly ITemplateService templateService; private readonly IShortcodesService shortcodesService; private readonly ITemplateDistributorV2 templateDistributorV2; + private readonly INextRunServiceV2 nextRunServiceV2; public NextRunTest( /*ITemplateDistributor templateDistributor, */ INextRunService nextRunService, ITemplateService templateService, IShortcodesService shortcodesService, - ITemplateDistributorV2 templateDistributorV2 + ITemplateDistributorV2 templateDistributorV2, + INextRunServiceV2 nextRunServiceV2 ) { //this.templateDistributor = templateDistributor; @@ -27,29 +29,40 @@ namespace PARR.Test.NextRun this.templateService = templateService; this.shortcodesService = shortcodesService; this.templateDistributorV2 = templateDistributorV2; + this.nextRunServiceV2 = nextRunServiceV2; } public async Task Test() { - var duration = 2; - var offset = TimeSpan.FromHours(3); + #region тестирование INextRunServiceV2 - var startDate = new DateOnly(2026, 2, 10); - var referenceDate = new DateTimeOffset(2026, 2, 13, 23, 0, 0, TimeSpan.Zero); + var distributeResult = await nextRunServiceV2.GetNextRunForJobGroupWithAutoDistributionAsync(Guid.Parse("d25d8a9e-898f-417a-9bec-ef3a356e7c94")); - var templates = GetTemplates(); - var templatesEmpty = new List(); - var templatesToHandeler = templatesEmpty; // templatesEmpty // templates + #endregion - // распределить шаблоны - var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); - // получить актуальный nextRun - var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 15, 23, 30, 0, TimeSpan.Zero)); - //var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesToHandeler, true, isNew: false); + #region Тестирование templateDistributorV2 + //var duration = 2; + //var offset = TimeSpan.FromHours(3); + + //var startDate = new DateOnly(2026, 2, 10); + //var referenceDate = new DateTimeOffset(2026, 2, 13, 23, 0, 0, TimeSpan.Zero); + + //var templates = GetTemplates(); + //var templatesEmpty = new List(); + //var templatesToHandeler = templatesEmpty; // templatesEmpty // templates + + //// распределить шаблоны + //var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); + + //// получить актуальный nextRun + //var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 15, 23, 30, 0, TimeSpan.Zero)); + ////var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesToHandeler, true, isNew: false); + + #endregion //var template = await templateService.Get().Include(t => t.Job).ThenInclude(t => t.Group).FirstAsync(t => t.Name == "ЭИТИ-ПАРР_ЦКИТ-ГВЦ_СХД ТО-1_СХД-AERODISK-432-2-4-U26-EN4SAG022-ГВЦ"); //var responseAreae = await shortcodesService.ApplyShortcodesAsync(template.Job.ResponseAreaMask, template);