From a7a82b902a218baf14f78824e4cbd8aa1c277d52 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Mon, 22 Dec 2025 17:03:41 +1000 Subject: [PATCH 1/4] =?UTF-8?q?feat(distributor):=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API.sln | 12 - PARR.DAL/NextRunServices/INextRunService.cs | 29 ++ .../Models/TemplateNextRunDto.cs | 12 + PARR.DAL/NextRunServices/NextRunService.cs | 79 +++++ .../Subservices/ITemplateDistributor.cs | 18 + .../Subservices/TemplateDistributor.cs | 322 ++++++++++++++++++ PARR.DAL/ParrDalInstaller.cs | 11 + .../Implementations/WeekendDayService.cs | 2 +- .../TemplateTaskGenerator.cs | 1 + docker-compose.dcproj | 1 - docker-compose.template-task-generator.yml | 27 -- 11 files changed, 473 insertions(+), 41 deletions(-) create mode 100644 PARR.DAL/NextRunServices/INextRunService.cs create mode 100644 PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs create mode 100644 PARR.DAL/NextRunServices/NextRunService.cs create mode 100644 PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs create mode 100644 PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs delete mode 100644 docker-compose.template-task-generator.yml diff --git a/PARR.API.sln b/PARR.API.sln index 393cb427..3f066499 100644 --- a/PARR.API.sln +++ b/PARR.API.sln @@ -92,10 +92,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.AIHITRelationshipsSync EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateGeneratorWorker", "PARR.TemplateGeneratorWorker\PARR.TemplateGeneratorWorker.csproj", "{17245D0B-3290-4C96-B5B1-4B0711BABAF2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateTaskGenerator", "PARR.TemplateTaskGenerator\PARR.TemplateTaskGenerator.csproj", "{D4C51090-CE06-4158-9A0E-961744872411}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateTaskGeneratorWorker", "PARR.TemplateTaskGeneratorWorker\PARR.TemplateTaskGeneratorWorker.csproj", "{93249642-DF7D-41A1-87C0-6E3BAECB43EC}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateMatcher", "PARR.TemplateMatcher\PARR.TemplateMatcher.csproj", "{EADC9A11-426A-41E9-98B4-010295412FF5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateMatcherWorker", "PARR.TemplateMatcherWorker\PARR.TemplateMatcherWorker.csproj", "{DA6B2A17-6E64-4E2C-8D4C-E278BEE94E03}" @@ -272,14 +268,6 @@ Global {17245D0B-3290-4C96-B5B1-4B0711BABAF2}.Debug|Any CPU.Build.0 = Debug|Any CPU {17245D0B-3290-4C96-B5B1-4B0711BABAF2}.Release|Any CPU.ActiveCfg = Release|Any CPU {17245D0B-3290-4C96-B5B1-4B0711BABAF2}.Release|Any CPU.Build.0 = Release|Any CPU - {D4C51090-CE06-4158-9A0E-961744872411}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4C51090-CE06-4158-9A0E-961744872411}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4C51090-CE06-4158-9A0E-961744872411}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4C51090-CE06-4158-9A0E-961744872411}.Release|Any CPU.Build.0 = Release|Any CPU - {93249642-DF7D-41A1-87C0-6E3BAECB43EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {93249642-DF7D-41A1-87C0-6E3BAECB43EC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {93249642-DF7D-41A1-87C0-6E3BAECB43EC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {93249642-DF7D-41A1-87C0-6E3BAECB43EC}.Release|Any CPU.Build.0 = Release|Any CPU {EADC9A11-426A-41E9-98B4-010295412FF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EADC9A11-426A-41E9-98B4-010295412FF5}.Debug|Any CPU.Build.0 = Debug|Any CPU {EADC9A11-426A-41E9-98B4-010295412FF5}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/PARR.DAL/NextRunServices/INextRunService.cs b/PARR.DAL/NextRunServices/INextRunService.cs new file mode 100644 index 00000000..d62fd757 --- /dev/null +++ b/PARR.DAL/NextRunServices/INextRunService.cs @@ -0,0 +1,29 @@ +namespace PARR.DAL.NextRunServices +{ + public interface INextRunService + { + /// + /// Получить список TemplateId, NextRun по jobGroupId с автораспределением + /// + /// + /// + Task> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId); + + + /// + /// Получить NextRun по jobGroupId с расписанием ЕСПП + /// + /// + /// + Task GetNextRunForJobGroupWithEsppSchedulleAsync(Guid jobGroupId); + + + /// + /// Получить NextRun по id шаблона + /// + /// + /// + Task GetNextRunForTemplate(Guid templateId); + + } +} diff --git a/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs b/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs new file mode 100644 index 00000000..839fe5c6 --- /dev/null +++ b/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs @@ -0,0 +1,12 @@ +namespace PARR.DAL.NextRunServices.Models +{ + /// + /// Модель шаблона, для расчета NextRun + /// + public class TemplateNextRunDto + { + public Guid Id { get; set; } + + public DateTimeOffset? NextRun { get; set; } + } +} diff --git a/PARR.DAL/NextRunServices/NextRunService.cs b/PARR.DAL/NextRunServices/NextRunService.cs new file mode 100644 index 00000000..796e6599 --- /dev/null +++ b/PARR.DAL/NextRunServices/NextRunService.cs @@ -0,0 +1,79 @@ + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.DAL.Services.Interfaces; +using PARR.DAL.Services.Interfaces.Job; +using PARR.DAL.TransformServices; + +namespace PARR.DAL.NextRunServices +{ + internal class NextRunService : INextRunService + { + private readonly ILogger logger; + private readonly ITemplateService templateService; + private readonly IJobGroupService jobGroupService; + private readonly IEsppScheduleTransformService esppScheduleTransformService; + + public NextRunService( + ILogger logger, + ITemplateService templateService, + IJobGroupService jobGroupService, + IEsppScheduleTransformService esppScheduleTransformService + ) + { + this.logger = logger; + this.templateService = templateService; + this.jobGroupService = jobGroupService; + this.esppScheduleTransformService = esppScheduleTransformService; + } + + + public Task> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) + { + //TODO: + throw new NotImplementedException(); + } + + + public async Task GetNextRunForJobGroupWithEsppSchedulleAsync(Guid jobGroupId) + { + var jobGroup = await jobGroupService.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Id == jobGroupId); + + if (jobGroup == null) + { + logger.LogError("Не найдена группа работ с Id: {jobGroupId}.", jobGroupId); + throw new ArgumentNullException(nameof(jobGroupId), $"Не найдена группа работ с Id: {jobGroupId}"); + } + + // Берем из обычного расписания ЕСПП + return await esppScheduleTransformService.GetNextDateAsync(jobGroupId, jobGroup.ReferenceDate); + } + + + public async Task GetNextRunForTemplate(Guid templateId) + { + var template = await templateService.Get() + .Include(t => t.Job).ThenInclude(t => t.Group) + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Id == templateId); + + if (template == null) + { + logger.LogError("Не найдена шаблон с Id: {templateId}.", templateId); + throw new ArgumentNullException(nameof(templateId), $"Не найдена шаблон с Id: {templateId}"); + } + + if (template.Job!.Group!.IsAutoDistributionEnabled == true) + { + // включено автораспределение + //TODO: !!!!!!!!!!!!!!!! добавить метод рассчета с учетом распределения + throw new NotImplementedException("добавить метод рассчета с учетом распределения"); + } + else + { + // считаем как ЕСПП + return await esppScheduleTransformService.GetNextDateAsync(template.Job.Group.Id, template.Job.Group.ReferenceDate); + } + } + } +} diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs new file mode 100644 index 00000000..dd8cccd4 --- /dev/null +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs @@ -0,0 +1,18 @@ +using PARR.DAL.Models; +using PARR.DAL.NextRunServices.Models; + +namespace PARR.DAL.NextRunServices.Subservices +{ + internal interface ITemplateDistributor + { + Task> GetValidNextRunAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); + + Task> GetValidNextRunsAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); + + Task> ReassignOutdatedRunsAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); + + Task> RebalanceTemplatesAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); + + Task> ScheduleTemplatesAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); + } +} diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs new file mode 100644 index 00000000..25bf5191 --- /dev/null +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs @@ -0,0 +1,322 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.DAL.NextRunServices.Models; +using PARR.DAL.Services.Interfaces; + +namespace PARR.DAL.NextRunServices.Subservices +{ + internal class TemplateDistributor : ITemplateDistributor + { + private readonly ILogger logger; + private readonly IWeekendDayService weekendDayService; + + public TemplateDistributor( + ILogger logger, + IWeekendDayService weekendDayService + ) + { + this.logger = logger; + this.weekendDayService = weekendDayService; + } + + /// + /// Распределяет шаблоны по рабочим дням в заданном диапазоне с равномерной загрузкой. + /// Возвращает список с Id и новой датой выполнения для каждого шаблона. + /// + public async Task> ScheduleTemplatesAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) + { + logger.LogInformation("Начинаем распределение {Count} шаблонов с {StartDate} по {EndDate}", templates.Count, startDate, endDate); + + // Получаем список выходных/праздничных дней в заданном диапазоне + var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); + + // Получаем список рабочих дней (исключая выходные и праздники) + var workDays = GetWorkDays(startDate, endDate, weekends); + + logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count); + + // Проверяем, есть ли хотя бы один рабочий день + if (workDays.Count == 0) + { + logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate); + return new List(); + } + + var totalWorks = templates.Count; // Общее количество шаблонов для распределения + var totalDays = workDays.Count; // Общее количество доступных рабочих дней + + // Сортируем шаблоны: сначала без даты (NextRun == null), затем с датой + var sortedTemplates = templates.OrderBy(t => t.NextRun.HasValue ? 1 : 0).ToList(); + + // Вычисляем интервал между работами: сколько "дней" приходится на одну работу + // Это позволяет равномерно распределить работы по всему периоду + // т е например 2 работы распределятся на 30 дней равномерно, не будут идти подряд + var interval = (double)totalDays / totalWorks; + + // Проходим по всем шаблонам и распределяем их равномерно по дням + for (int i = 0; i < totalWorks; i++) + { + // Вычисляем индекс дня, на который нужно назначить i-ю работу + // Используем округление, чтобы получить ближайший день + var dayIndex = (int)Math.Round(i * interval); + + // Защита от возможного выхода за границы массива из-за округления + if (dayIndex >= totalDays) dayIndex = totalDays - 1; + + // Получаем дату из списка рабочих дней по вычисленному индексу + var date = workDays[dayIndex]; + + // Формируем дату выполнения: дата + заданное время (executionTime) + var nextRun = new DateTimeOffset(date.Year, date.Month, date.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); + + // Назначаем вычисленную дату выполнения шаблону + sortedTemplates[i].NextRun = nextRun; + + logger.LogDebug("Шаблон {TemplateId} назначен на {NextRun}", sortedTemplates[i].Id, nextRun); + } + + logger.LogInformation("Распределение завершено. Возвращаем {Count} записей.", templates.Count); + + // Возвращаем список шаблонов с обновлёнными датами выполнения + return templates.Select(t => new TemplateNextRunDto { Id = t.Id, NextRun = t.NextRun!.Value }).ToList(); + } + + + /// + /// Обновляет устаревшие даты выполнения шаблонов. + /// Возвращает список шаблонов, чьи даты были изменены. + /// + public async Task> ReassignOutdatedRunsAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) + { + logger.LogInformation("Начинаем переназначение устаревших дат выполнения"); + + // Получаем список выходных/праздничных дней + var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); + var workDays = GetWorkDays(startDate, endDate, weekends).OrderBy(d => d).ToList(); + + var now = DateTimeOffset.UtcNow; + var result = new List(); + + foreach (var template in templates) + { + if (template.NextRun.HasValue && template.NextRun.Value < now) + { + logger.LogDebug("Шаблон {TemplateId} с датой {OldDate} устарел", template.Id, template.NextRun); + + var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(now.DateTime), DateOnly.FromDateTime(template.NextRun.Value.Date)); + if (newDate.HasValue) + { + var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); + template.NextRun = nextRun; + + logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", template.Id, nextRun); + + result.Add(new TemplateNextRunDto { Id = template.Id, NextRun = nextRun }); + } + else + { + logger.LogWarning("Не удалось найти новую дату для шаблона {TemplateId}", template.Id); + } + } + } + + logger.LogInformation("Переназначение завершено. Изменено {Count} записей.", result.Count); + + return result; + } + + + /// + /// Перераспределяет только те шаблоны, у которых дата недействительна (выходной, вне периода). + /// Возвращает список всех шаблонов с новыми или старыми датами. + /// + public async Task> RebalanceTemplatesAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) + { + logger.LogInformation("Начинаем перераспределение шаблонов"); + + // Получаем список выходных/праздничных дней + var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); + + var workDays = GetWorkDays(startDate, endDate, weekends); + var validDates = new HashSet(workDays); + + // Отфильтруем шаблоны с недействительной датой + var unassigned = templates.Where(t => !t.NextRun.HasValue || !validDates.Contains(DateOnly.FromDateTime(t.NextRun.Value.Date))).ToList(); + + logger.LogDebug("Найдено {Count} шаблонов для переназначения", unassigned.Count); + + // Перераспределяем только невалидные + var reassigned = await ScheduleTemplatesAsync(unassigned, startDate, endDate, executionTime); + + // Возвращаем все шаблоны с их датами + var result = templates.Select(t => new TemplateNextRunDto { Id = t.Id, NextRun = t.NextRun!.Value }).ToList(); + + logger.LogInformation("Перераспределение завершено. Возвращаем {Count} записей.", result.Count); + + return result; + } + + + /// + /// Возвращает актуальную дату выполнения для одного шаблона. + /// Результат — список из одного элемента (если успешно) или пустой. + /// + public async Task> GetValidNextRunAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) + { + logger.LogDebug("Запрос актуальной даты для шаблона {TemplateId}", template.Id); + + // Получаем список выходных/праздничных дней + var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); + + var nextRun = await GetValidNextRunInternalAsync(template, startDate, endDate, weekends, executionTime); + if (nextRun.HasValue) + { + logger.LogDebug("Шаблон {TemplateId} имеет актуальную дату {NextRun}", template.Id, nextRun); + return new List { new TemplateNextRunDto { Id = template.Id, NextRun = nextRun.Value } }; + } + + logger.LogWarning("Не удалось получить актуальную дату для шаблона {TemplateId}", template.Id); + return new List(); + } + + + /// + /// Возвращает список актуальных дат выполнения для всех переданных шаблонов. + /// + public async Task> GetValidNextRunsAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) + { + logger.LogDebug("Запрос актуальных дат для {Count} шаблонов", templates.Count); + + // Получаем список выходных/праздничных дней + var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); + + var result = new List(); + + foreach (var template in templates) + { + var nextRun = await GetValidNextRunInternalAsync(template, startDate, endDate, weekends, executionTime); + if (nextRun.HasValue) + { + result.Add(new TemplateNextRunDto { Id = template.Id, NextRun = nextRun.Value }); + } + } + + logger.LogInformation("Получено {Count} актуальных дат выполнения", result.Count); + + return result; + } + + + /// + /// Внутренний метод для получения актуальной даты выполнения для одного шаблона. + /// + private async Task GetValidNextRunInternalAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, HashSet weekends, TimeSpan executionTime) + { + var workDays = GetWorkDays(startDate, endDate, weekends); + var today = DateOnly.FromDateTime(DateTime.UtcNow); + + // Если у шаблона нет даты, назначаем первую доступную + if (!template.NextRun.HasValue) + { + var firstDay = workDays.FirstOrDefault(); + if (firstDay != default) + { + var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); + logger.LogDebug("Шаблон {TemplateId} получил первую доступную дату: {NextRun}", template.Id, nextRun); + return nextRun; + } + + logger.LogWarning("Нет доступных рабочих дней для шаблона {TemplateId}", template.Id); + return null; + } + + var scheduledDate = DateOnly.FromDateTime(template.NextRun.Value.Date); + + // Если дата валидна и не устарела — возвращаем как есть + if (workDays.Contains(scheduledDate)) + { + if (template.NextRun.Value < DateTimeOffset.UtcNow) + { + logger.LogDebug("Дата шаблона {TemplateId} ({Date}) устарела", template.Id, template.NextRun); + var newDate = FindNextAvailableDate(workDays, today, scheduledDate); + if (newDate.HasValue) + { + var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); + logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", template.Id, nextRun); + return nextRun; + } + logger.LogWarning("Не удалось найти новую дату для устаревшего шаблона {TemplateId}", template.Id); + return null; + } + // Дата валидна и не устарела + logger.LogDebug("Шаблон {TemplateId} имеет действительную дату: {NextRun}", template.Id, template.NextRun); + return template.NextRun; + } + + // Дата недействительна (выходной или вне периода) — ищем ближайшую + var closest = FindClosestValidDate(workDays, scheduledDate); + if (closest.HasValue) + { + var nextRun = new DateTimeOffset(closest.Value.Year, closest.Value.Month, closest.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); + logger.LogDebug("Шаблон {TemplateId} перемещен на ближайшую дату: {NextRun}", template.Id, nextRun); + return nextRun; + } + + logger.LogWarning("Не удалось найти подходящую дату для шаблона {TemplateId}", template.Id); + return null; + } + + + /// + /// Ищет следующую доступную дату после указанной, если не находит — ищет назад. + /// + private DateOnly? FindNextAvailableDate(List workDays, DateOnly afterDate, DateOnly originalDate) + { + var future = workDays.FirstOrDefault(d => d > afterDate); + + if (future != default) + return future; + + var past = workDays.Where(t => t >= originalDate).OrderByDescending(t => t).FirstOrDefault(); + + return past != default ? (DateOnly?)past : null; + } + + + /// + /// Ищет ближайшую дату (вперед или назад) к заданной, среди доступных рабочих дней. + /// + private DateOnly? FindClosestValidDate(List workDays, DateOnly targetDate) + { + var future = workDays.FirstOrDefault(t => t >= targetDate); + + if (future != default) + return future; + + var past = workDays.Where(t => t <= targetDate).OrderByDescending(t => t).FirstOrDefault(); + + return past != default ? (DateOnly?)past : null; + } + + + /// + /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. + /// + private List GetWorkDays(DateOnly start, DateOnly end, HashSet weekends) + { + var result = new List(); + var date = start; + + while (date <= end) + { + if (!weekends.Contains(date)) + { + result.Add(date); + } + date = date.AddDays(1); + } + + return result; + } + } +} diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index abf7f915..9ba7a052 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -8,6 +8,8 @@ using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Implementations; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.InfluxDbServices; +using PARR.DAL.NextRunServices; +using PARR.DAL.NextRunServices.Subservices; using PARR.DAL.Services.Implementation; using PARR.DAL.Services.Implementations; using PARR.DAL.Services.Implementations.Job; @@ -17,6 +19,7 @@ using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.Settings; using PARR.DAL.TransformServices; +using System.ComponentModel; namespace PARR.DAL { @@ -121,6 +124,14 @@ namespace PARR.DAL services.AddTransient(); services.AddTransient(); + #region NextRun Services + + services.AddTransient(); + services.AddTransient(); + + #endregion + + #region DomainServces services.AddTransient(); services.AddTransient(); diff --git a/PARR.DAL/Services/Implementations/WeekendDayService.cs b/PARR.DAL/Services/Implementations/WeekendDayService.cs index 8bb97949..8749532f 100644 --- a/PARR.DAL/Services/Implementations/WeekendDayService.cs +++ b/PARR.DAL/Services/Implementations/WeekendDayService.cs @@ -36,7 +36,7 @@ namespace PARR.DAL.Services.Implementations public IQueryable GetWeekends(DateOnly start, DateOnly end) { - return EntitySet.Where(t => t.Date >= start && t.Date <= end).Select(t => t.Date); + return EntitySet.Where(t => t.Date >= start && t.Date <= end).AsNoTracking().Select(t => t.Date); } diff --git a/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs b/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs index 7d3453d0..956827d6 100644 --- a/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs +++ b/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs @@ -43,6 +43,7 @@ namespace PARR.TemplateTaskGenerator { var mqRequest = new TemplateGeneratorMq { + UnitsInTemplate=new List(), JobId = jobId, UnitId = unitId, HistoryInitiator = new HistoryInitiator { InitiatorComment = "Запрос на создание шаблона", InitiatorParrComponentId = ParrComponentsEnum.TemplateTaskGenerator } diff --git a/docker-compose.dcproj b/docker-compose.dcproj index 079df966..825b0fcb 100644 --- a/docker-compose.dcproj +++ b/docker-compose.dcproj @@ -10,7 +10,6 @@ - diff --git a/docker-compose.template-task-generator.yml b/docker-compose.template-task-generator.yml deleted file mode 100644 index ac94de89..00000000 --- a/docker-compose.template-task-generator.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: '3.4' - -#PARR TEMPLATE TASK GENERATOR -services: - parr-template-task-generator: - image: harbor.dvgd.rzd/parr/parr-template-task-generator:${tag-latest} - environment: - - ASPNETCORE_ENVIRONMENT=Production - - TZ=Europe/Moscow - logging: - driver: fluentd - options: - fluentd-address: dvgd-efk-01.dvgd.oao.rzd:24224 - fluentd-retry-wait: '10s' - fluentd-max-retries: '30' - fluentd-async: 'true' - fluentd-buffer-limit: '52428800' - tag: parr.template-task-generator.serilog - deploy: - replicas: 1 - networks: - parr-network: - -networks: - parr-network: - driver: overlay - external: true \ No newline at end of file From 6a8f90c860aa3fd3ee97626a6c5c915da097645e Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Thu, 25 Dec 2025 12:07:53 +1000 Subject: [PATCH 2/4] =?UTF-8?q?feat(distributor):=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BA=D0=B0=20=D1=80=D0=B0=D1=81=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=D1=81=D0=B5?= =?UTF-8?q?=D1=85=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=B5=20DistributeTempl?= =?UTF-8?q?atesAsync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/TemplateNextRunResultDto.cs | 14 + .../Subservices/ITemplateDistributor.cs | 29 +- .../Subservices/TemplateDistributor.cs | 587 +++++++++++------- .../Subservices/TemplateDistributor_old.cs | 345 ++++++++++ PARR.Test/NextRun/NextRunTest.cs | 99 +++ PARR.Test/Program.cs | 15 +- PARR.Test/Worker.cs | 14 + 7 files changed, 860 insertions(+), 243 deletions(-) create mode 100644 PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs create mode 100644 PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs create mode 100644 PARR.Test/NextRun/NextRunTest.cs diff --git a/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs b/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs new file mode 100644 index 00000000..3477327c --- /dev/null +++ b/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs @@ -0,0 +1,14 @@ +namespace PARR.DAL.NextRunServices.Models +{ + /// + /// Модель, результат распределения шаблона + /// + public class TemplateNextRunResultDto + { + public Guid Id { get; set; } + + public DateTimeOffset NextRun { get; set; } + + public DateTimeOffset? NextRunOld { get; set; } + } +} diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs index dd8cccd4..7c04ff58 100644 --- a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs @@ -1,18 +1,25 @@ -using PARR.DAL.Models; -using PARR.DAL.NextRunServices.Models; +using PARR.DAL.NextRunServices.Models; namespace PARR.DAL.NextRunServices.Subservices { - internal interface ITemplateDistributor + //todo: public -> internal!!! + public interface ITemplateDistributor { - Task> GetValidNextRunAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); + /// + /// Распределяет все шаблоны на указанный период, учитывая план нагрузки. + /// Возвращает список всех шаблонов с обновлёнными датами выполнения. + /// + /// Дата начала распределения + /// Длительность распределения, дней + /// ReferenceDate + /// Спиоск шаблонов для распределения + /// + Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates); - Task> GetValidNextRunsAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); - - Task> ReassignOutdatedRunsAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); - - Task> RebalanceTemplatesAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); - - Task> ScheduleTemplatesAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime); + /// + /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки. + /// Возвращает обновлённый шаблон с новой датой выполнения. + /// + Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates); } } diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs index 25bf5191..21c431d3 100644 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs @@ -10,8 +10,13 @@ namespace PARR.DAL.NextRunServices.Subservices private readonly ILogger logger; private readonly IWeekendDayService weekendDayService; + /// + /// Смещение nextRun на несколько лет вперед, если не смог его рассчитать + /// + private const int offsetYearsIfErrorCalcNextRun = 5; + public TemplateDistributor( - ILogger logger, + ILogger logger, IWeekendDayService weekendDayService ) { @@ -19,289 +24,417 @@ namespace PARR.DAL.NextRunServices.Subservices this.weekendDayService = weekendDayService; } - /// - /// Распределяет шаблоны по рабочим дням в заданном диапазоне с равномерной загрузкой. - /// Возвращает список с Id и новой датой выполнения для каждого шаблона. - /// - public async Task> ScheduleTemplatesAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) - { - logger.LogInformation("Начинаем распределение {Count} шаблонов с {StartDate} по {EndDate}", templates.Count, startDate, endDate); - // Получаем список выходных/праздничных дней в заданном диапазоне - var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); + public Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates) + { + //TODO: + throw new NotImplementedException(); + } + + + + + + public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates) + { + var _startDate = GetStartPeriodDate(startDate, referenceDate); + var _endDate = GetEndPeriodDate(_startDate, duration); + + logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}", allTemplates.Count, duration, _startDate, _endDate, referenceDate); + + // Получаем список выходных/праздничных дней + var weekends = await GetWeekendsAsync(_startDate, _endDate); + logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); + + foreach (var weekend in weekends) + logger.LogDebug("Не рабочий день: {weekend}", weekend); // Получаем список рабочих дней (исключая выходные и праздники) - var workDays = GetWorkDays(startDate, endDate, weekends); + var workDays = GetWorkDays(_startDate, _endDate, weekends); - logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count); + logger.LogDebug("Найдено рабочих дней: {Count}", workDays.Count); + + foreach (var workDay in workDays) + logger.LogDebug("Рабочий день: {workDay}", workDay); - // Проверяем, есть ли хотя бы один рабочий день if (workDays.Count == 0) { - logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate); - return new List(); - } + logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun); - var totalWorks = templates.Count; // Общее количество шаблонов для распределения - var totalDays = workDays.Count; // Общее количество доступных рабочих дней - - // Сортируем шаблоны: сначала без даты (NextRun == null), затем с датой - var sortedTemplates = templates.OrderBy(t => t.NextRun.HasValue ? 1 : 0).ToList(); - - // Вычисляем интервал между работами: сколько "дней" приходится на одну работу - // Это позволяет равномерно распределить работы по всему периоду - // т е например 2 работы распределятся на 30 дней равномерно, не будут идти подряд - var interval = (double)totalDays / totalWorks; - - // Проходим по всем шаблонам и распределяем их равномерно по дням - for (int i = 0; i < totalWorks; i++) - { - // Вычисляем индекс дня, на который нужно назначить i-ю работу - // Используем округление, чтобы получить ближайший день - var dayIndex = (int)Math.Round(i * interval); - - // Защита от возможного выхода за границы массива из-за округления - if (dayIndex >= totalDays) dayIndex = totalDays - 1; - - // Получаем дату из списка рабочих дней по вычисленному индексу - var date = workDays[dayIndex]; - - // Формируем дату выполнения: дата + заданное время (executionTime) - var nextRun = new DateTimeOffset(date.Year, date.Month, date.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); - - // Назначаем вычисленную дату выполнения шаблону - sortedTemplates[i].NextRun = nextRun; - - logger.LogDebug("Шаблон {TemplateId} назначен на {NextRun}", sortedTemplates[i].Id, nextRun); - } - - logger.LogInformation("Распределение завершено. Возвращаем {Count} записей.", templates.Count); - - // Возвращаем список шаблонов с обновлёнными датами выполнения - return templates.Select(t => new TemplateNextRunDto { Id = t.Id, NextRun = t.NextRun!.Value }).ToList(); - } - - - /// - /// Обновляет устаревшие даты выполнения шаблонов. - /// Возвращает список шаблонов, чьи даты были изменены. - /// - public async Task> ReassignOutdatedRunsAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) - { - logger.LogInformation("Начинаем переназначение устаревших дат выполнения"); - - // Получаем список выходных/праздничных дней - var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); - var workDays = GetWorkDays(startDate, endDate, weekends).OrderBy(d => d).ToList(); - - var now = DateTimeOffset.UtcNow; - var result = new List(); - - foreach (var template in templates) - { - if (template.NextRun.HasValue && template.NextRun.Value < now) + // смещаем все nextRun вперед + var result = allTemplates.Select(t => new TemplateNextRunResultDto { - logger.LogDebug("Шаблон {TemplateId} с датой {OldDate} устарел", template.Id, template.NextRun); + Id = t.Id, + NextRun = t.NextRun.HasValue + // если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun + ? CreateNextRun(t.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate) + // если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun + : CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRunOld = t.NextRun + }).OrderBy(t => t.NextRun).ToList(); - var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(now.DateTime), DateOnly.FromDateTime(template.NextRun.Value.Date)); - if (newDate.HasValue) + foreach (var item in result) + logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id}, nextRun: {NextRun}, nextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld); + + logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", result.Count, result.Count(t => t.NextRun == t.NextRunOld), result.Count(t => t.NextRun != t.NextRunOld)); + + return result; + } + + var totalWorks = allTemplates.Count; + var totalDays = workDays.Count; + + // интервал между шаблонами + var interval = GetInterval(totalDays, totalWorks); + + // строим план, дни на которые должны быть назанчены шаблоны + var plan = GetPlan(totalWorks, totalDays, workDays); + + + // Подсчитываем, сколько работ должно быть на каждый день + var expectedDailyLoad = new Dictionary(); + + foreach (var day in plan) + expectedDailyLoad[day] = expectedDailyLoad.GetValueOrDefault(day, 0) + 1; + + #region Группируем шаблоны по дням, на которые они уже назначены + + var templatesByDay = new Dictionary>(); + + foreach (var template in allTemplates) + { + if (template.NextRun.HasValue) + { + var day = DateOnly.FromDateTime(template.NextRun.Value.Date); + if (!templatesByDay.ContainsKey(day)) { - var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); - template.NextRun = nextRun; - - logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", template.Id, nextRun); - - result.Add(new TemplateNextRunDto { Id = template.Id, NextRun = nextRun }); + templatesByDay[day] = new List(); } - else + templatesByDay[day].Add(template); + } + } + + #endregion + + #region Подсчитываем текущую нагрузку на каждый день + + var currentDailyLoad = new Dictionary(); + + foreach (var day in workDays) + { + currentDailyLoad[day] = 0; + } + + foreach (var kvp in templatesByDay) + { + var day = kvp.Key; + var count = kvp.Value.Count; + currentDailyLoad[day] = count; + } + + #endregion + + #region Определяем, какие шаблоны нужно оставить, а какие — переназначить + + var templatesToKeep = new List(); + var templatesToReassign = new List(); + + foreach (var kvp in templatesByDay) + { + var day = kvp.Key; + var templates = kvp.Value; + + // Если день — нерабочий, то все шаблоны нужно переназначить + if (!workDays.Contains(day)) + { + foreach (var template in templates) { - logger.LogWarning("Не удалось найти новую дату для шаблона {TemplateId}", template.Id); + templatesToReassign.Add(template); + logger.LogDebug("Шаблон {TemplateId} назначен на нерабочий день {NextRun} — будет переназначен.", template.Id, template.NextRun); + } + continue; + } + + var expectedCount = expectedDailyLoad.GetValueOrDefault(day, 0); + var currentCount = templates.Count; + + if (currentCount <= expectedCount) + { + // Все шаблоны в этом дне можно оставить + foreach (var template in templates) + { + templatesToKeep.Add(new TemplateNextRunResultDto + { + Id = template.Id, + NextRun = template.NextRun.Value, + NextRunOld = template.NextRun + }); + logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun); + } + } + else + { + // Оставляем только expectedCount штук, остальные — переназначить + for (int i = 0; i < expectedCount; i++) + { + var template = templates[i]; + templatesToKeep.Add(new TemplateNextRunResultDto + { + Id = template.Id, + NextRun = template.NextRun.Value, + NextRunOld = template.NextRun + }); + logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun); + } + + for (int i = expectedCount; i < currentCount; i++) + { + var template = templates[i]; + templatesToReassign.Add(template); + logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. нагрузка в день превышена.", template.Id); } } } - logger.LogInformation("Переназначение завершено. Изменено {Count} записей.", result.Count); + #endregion - return result; - } + #region Добавляем шаблоны, у которых не было даты — тоже нужно распределить - - /// - /// Перераспределяет только те шаблоны, у которых дата недействительна (выходной, вне периода). - /// Возвращает список всех шаблонов с новыми или старыми датами. - /// - public async Task> RebalanceTemplatesAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) - { - logger.LogInformation("Начинаем перераспределение шаблонов"); - - // Получаем список выходных/праздничных дней - var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); - - var workDays = GetWorkDays(startDate, endDate, weekends); - var validDates = new HashSet(workDays); - - // Отфильтруем шаблоны с недействительной датой - var unassigned = templates.Where(t => !t.NextRun.HasValue || !validDates.Contains(DateOnly.FromDateTime(t.NextRun.Value.Date))).ToList(); - - logger.LogDebug("Найдено {Count} шаблонов для переназначения", unassigned.Count); - - // Перераспределяем только невалидные - var reassigned = await ScheduleTemplatesAsync(unassigned, startDate, endDate, executionTime); - - // Возвращаем все шаблоны с их датами - var result = templates.Select(t => new TemplateNextRunDto { Id = t.Id, NextRun = t.NextRun!.Value }).ToList(); - - logger.LogInformation("Перераспределение завершено. Возвращаем {Count} записей.", result.Count); - - return result; - } - - - /// - /// Возвращает актуальную дату выполнения для одного шаблона. - /// Результат — список из одного элемента (если успешно) или пустой. - /// - public async Task> GetValidNextRunAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) - { - logger.LogDebug("Запрос актуальной даты для шаблона {TemplateId}", template.Id); - - // Получаем список выходных/праздничных дней - var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); - - var nextRun = await GetValidNextRunInternalAsync(template, startDate, endDate, weekends, executionTime); - if (nextRun.HasValue) + foreach (var template in allTemplates) { - logger.LogDebug("Шаблон {TemplateId} имеет актуальную дату {NextRun}", template.Id, nextRun); - return new List { new TemplateNextRunDto { Id = template.Id, NextRun = nextRun.Value } }; + if (!template.NextRun.HasValue) + { + templatesToReassign.Add(template); + logger.LogDebug("Шаблон {TemplateId} не имел даты — будет распределён.", template.Id); + } } - logger.LogWarning("Не удалось получить актуальную дату для шаблона {TemplateId}", template.Id); - return new List(); - } + #endregion + #region Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку - /// - /// Возвращает список актуальных дат выполнения для всех переданных шаблонов. - /// - public async Task> GetValidNextRunsAsync(List templates, DateOnly startDate, DateOnly endDate, TimeSpan executionTime) - { - logger.LogDebug("Запрос актуальных дат для {Count} шаблонов", templates.Count); + var updatedTemplates = new List(templatesToKeep); - // Получаем список выходных/праздничных дней - var weekends = (await weekendDayService.GetWeekends(startDate, endDate).ToListAsync()).ToHashSet(); - - var result = new List(); - - foreach (var template in templates) + foreach (var template in templatesToReassign) { - var nextRun = await GetValidNextRunInternalAsync(template, startDate, endDate, weekends, executionTime); + var nextRun = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDate, expectedDailyLoad); if (nextRun.HasValue) { - result.Add(new TemplateNextRunDto { Id = template.Id, NextRun = nextRun.Value }); - } - } + var day = DateOnly.FromDateTime(nextRun.Value.Date); + currentDailyLoad[day]++; - logger.LogInformation("Получено {Count} актуальных дат выполнения", result.Count); - - return result; - } - - - /// - /// Внутренний метод для получения актуальной даты выполнения для одного шаблона. - /// - private async Task GetValidNextRunInternalAsync(TemplateNextRunDto template, DateOnly startDate, DateOnly endDate, HashSet weekends, TimeSpan executionTime) - { - var workDays = GetWorkDays(startDate, endDate, weekends); - var today = DateOnly.FromDateTime(DateTime.UtcNow); - - // Если у шаблона нет даты, назначаем первую доступную - if (!template.NextRun.HasValue) - { - var firstDay = workDays.FirstOrDefault(); - if (firstDay != default) - { - var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); - logger.LogDebug("Шаблон {TemplateId} получил первую доступную дату: {NextRun}", template.Id, nextRun); - return nextRun; - } - - logger.LogWarning("Нет доступных рабочих дней для шаблона {TemplateId}", template.Id); - return null; - } - - var scheduledDate = DateOnly.FromDateTime(template.NextRun.Value.Date); - - // Если дата валидна и не устарела — возвращаем как есть - if (workDays.Contains(scheduledDate)) - { - if (template.NextRun.Value < DateTimeOffset.UtcNow) - { - logger.LogDebug("Дата шаблона {TemplateId} ({Date}) устарела", template.Id, template.NextRun); - var newDate = FindNextAvailableDate(workDays, today, scheduledDate); - if (newDate.HasValue) + updatedTemplates.Add(new TemplateNextRunResultDto { - var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); - logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", template.Id, nextRun); - return nextRun; - } - logger.LogWarning("Не удалось найти новую дату для устаревшего шаблона {TemplateId}", template.Id); - return null; + Id = template.Id, + NextRun = nextRun.Value, + NextRunOld = template.NextRun + }); + + logger.LogDebug("Шаблону {TemplateId} назначен NextRun: {NextRun}, NextRunOld: {NextRunOld}", template.Id, nextRun, template.NextRun); + } + else + { + logger.LogWarning("Не удалось назначить дату для шаблона {TemplateId}", template.Id); + + // Возвращаем с ошибкой + updatedTemplates.Add(new TemplateNextRunResultDto + { + Id = template.Id, + NextRun = CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRunOld = template.NextRun + }); } - // Дата валидна и не устарела - logger.LogDebug("Шаблон {TemplateId} имеет действительную дату: {NextRun}", template.Id, template.NextRun); - return template.NextRun; } - // Дата недействительна (выходной или вне периода) — ищем ближайшую - var closest = FindClosestValidDate(workDays, scheduledDate); - if (closest.HasValue) + #endregion + + + updatedTemplates = updatedTemplates.OrderBy(t => t.NextRun).ToList(); + + logger.LogDebug("--- Результат распределения: ---"); + foreach (var item in updatedTemplates) + logger.LogDebug("Шаблон {templateId}, NextRun: {NextRun}, NextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld); + + logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", updatedTemplates.Count, updatedTemplates.Count(t => t.NextRun == t.NextRunOld), updatedTemplates.Count(t => t.NextRun != t.NextRunOld)); + + return updatedTemplates; + } + + + /// + /// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad + /// + /// + /// + /// + /// + /// + private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary currentDailyLoad, List workDays, DateTimeOffset referenceDate, Dictionary expectedDailyLoad) + { + foreach (var day in workDays) { - var nextRun = new DateTimeOffset(closest.Value.Year, closest.Value.Month, closest.Value.Day, executionTime.Hours, executionTime.Minutes, executionTime.Seconds, TimeSpan.Zero); - logger.LogDebug("Шаблон {TemplateId} перемещен на ближайшую дату: {NextRun}", template.Id, nextRun); - return nextRun; + var expected = expectedDailyLoad.GetValueOrDefault(day, 0); + var current = currentDailyLoad.GetValueOrDefault(day, 0); + + if (current < expected) + { + return CreateNextRun(day, referenceDate); + } } - logger.LogWarning("Не удалось найти подходящую дату для шаблона {TemplateId}", template.Id); - return null; + // Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план + foreach (var day in workDays) + { + var expected = expectedDailyLoad.GetValueOrDefault(day, 0); + var current = currentDailyLoad.GetValueOrDefault(day, 0); + + if (current < expected) + { + return CreateNextRun(day, referenceDate); + } + } + + // Если все дни уже загружены по плану — ищем день с минимальной нагрузкой + var minLoad = currentDailyLoad.Values.Min(); + var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key; + + return CreateNextRun(minDay, referenceDate); + } + + + + /// + /// Получить план: дни, на которые должны быть назначены шаблоны + /// + /// + /// + /// + /// + private List GetPlan(int totalWorks, int totalDays, List workDays) + { + var interval = GetInterval(totalDays, totalWorks); + + var plan = new List(); + + for (int i = 0; i < totalWorks; i++) + { + #region Вынести в метод + // Вычисляем индекс дня, на который нужно назначить i-ю работу + //var dayIndex = (int)Math.Round(i * interval); + var dayIndex = (int)Math.Floor(i * interval); + + // Защита от возможного выхода за границы массива из-за округления + if (dayIndex >= totalDays) + dayIndex = totalDays - 1; + #endregion + + var date = workDays[dayIndex]; + plan.Add(date); + } + + return plan; + // return new HashSet(plan); } /// - /// Ищет следующую доступную дату после указанной, если не находит — ищет назад. + /// Рассчитать интервал между шаблонами /// - private DateOnly? FindNextAvailableDate(List workDays, DateOnly afterDate, DateOnly originalDate) + /// Кол-во рабочих дней + /// Кол-во шаблонов + /// + private double GetInterval(int totalDays, int totalWorks) { - var future = workDays.FirstOrDefault(d => d > afterDate); + var interval = (double)totalDays / totalWorks; - if (future != default) - return future; - - var past = workDays.Where(t => t >= originalDate).OrderByDescending(t => t).FirstOrDefault(); - - return past != default ? (DateOnly?)past : null; + return interval; } /// - /// Ищет ближайшую дату (вперед или назад) к заданной, среди доступных рабочих дней. + /// Создать NextRun (из DateTimeOffset + refDate) /// - private DateOnly? FindClosestValidDate(List workDays, DateOnly targetDate) + /// Откуда взять дату + /// Откуда взять время + /// + private DateTimeOffset CreateNextRun(DateTimeOffset date, DateTimeOffset refDate) { - var future = workDays.FirstOrDefault(t => t >= targetDate); + return CreateNextRun(DateOnly.FromDateTime(date.Date), refDate); + } - if (future != default) - return future; + /// + /// Создать NextRun (из DateOnly + refDate) + /// + /// + /// + /// + private DateTimeOffset CreateNextRun(DateOnly date, DateTimeOffset refDate) + { + return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero); + } - var past = workDays.Where(t => t <= targetDate).OrderByDescending(t => t).FirstOrDefault(); - return past != default ? (DateOnly?)past : null; + /// + /// Получить дату начала распределения относительно StartDate, ReferenceDate и текущей даты + /// + /// + /// + /// + private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate) + { + // дата начала не может быть раньше referenceDate + // и не может быть раньше чем сегодня + + var dateStart = startDate; + + // если dateStart < чем refDate, то берем refDate + if (dateStart < DateOnly.FromDateTime(referenceDate.Date)) + dateStart = DateOnly.FromDateTime(referenceDate.Date); + + // если dateStart < чем текущая дата, то берем ьекущую дату + if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date)) + dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date); + + return dateStart; + } + + + /// + /// Получить дату конца периода распределения + /// + /// + /// + /// + private DateOnly GetEndPeriodDate(DateOnly startDate, int duration) + { + // duration — количество дней, включая startDate + // поэтому вычитаем 1, чтобы получить сдвиг + + return startDate.AddDays(duration - 1); + } + + + /// + /// Получить список выходных дней + /// + /// + /// + /// + private async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate) + { + var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); + + return weekends.ToHashSet(); } /// /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. /// + /// + /// + /// + /// private List GetWorkDays(DateOnly start, DateOnly end, HashSet weekends) { var result = new List(); @@ -309,6 +442,7 @@ namespace PARR.DAL.NextRunServices.Subservices while (date <= end) { + var dayOfWeek = date.DayOfWeek; if (!weekends.Contains(date)) { result.Add(date); @@ -318,5 +452,6 @@ namespace PARR.DAL.NextRunServices.Subservices return result; } + } } diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs new file mode 100644 index 00000000..93c7ba2f --- /dev/null +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs @@ -0,0 +1,345 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.DAL.NextRunServices.Models; +using PARR.DAL.Services.Interfaces; + +namespace PARR.DAL.NextRunServices.Subservices +{ + internal class TemplateDistributor_old //: ITemplateDistributor + { + private readonly ILogger logger; + private readonly IWeekendDayService weekendDayService; + + public TemplateDistributor_old( + ILogger logger, + IWeekendDayService weekendDayService + ) + { + this.logger = logger; + this.weekendDayService = weekendDayService; + } + + /// + /// Распределяет все шаблоны на указанный период, учитывая план нагрузки. + /// Возвращает список всех шаблонов с обновлёнными датами выполнения. + /// + public async Task> DistributeTemplatesAsync( + DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List allTemplates) + { + logger.LogInformation("Начинаем распределение {Count} шаблонов с {StartDate} по {EndDate}", allTemplates.Count, startDate, endDate); + + // Получаем список выходных/праздничных дней + var weekends = await GetWeekendsAsync(startDate, endDate); + + // Получаем список рабочих дней (исключая выходные и праздники) + var workDays = GetWorkDays(startDate, endDate, weekends); + + logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count); + + if (workDays.Count == 0) + { + logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate); + return allTemplates; + } + + var totalWorks = allTemplates.Count; + var totalDays = workDays.Count; + + //------------- + + // Вычисляем интервал между работами: сколько "дней" приходится на одну работу + var interval = (double)totalDays / totalWorks; + + // Создаём новый список шаблонов с обновлёнными датами + var updatedTemplates = new List(); + + for (int i = 0; i < totalWorks; i++) + { + // Вычисляем индекс дня, на который нужно назначить i-ю работу + var dayIndex = (int)Math.Round(i * interval); + + // Защита от возможного выхода за границы массива из-за округления + if (dayIndex >= totalDays) dayIndex = totalDays - 1; + + var date = workDays[dayIndex]; + + // Формируем дату выполнения: дата + время из referenceDate + var nextRun = new DateTimeOffset(date.Year, date.Month, date.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + + //------------------ + // Проверяем, не раньше ли дата, чем referenceDate (не включая саму referenceDate) + if (nextRun < referenceDate) + { + logger.LogWarning("Шаблон {TemplateId} имеет дату {NextRun}, которая раньше referenceDate {ReferenceDate}. Переносим на ближайший рабочий день после referenceDate (включая её).", allTemplates[i].Id, nextRun, referenceDate); + + // Ищем ближайший день после referenceDate (включая его) + var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(referenceDate.DateTime), date); + if (newDate.HasValue) + { + nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + } + else + { + // Если не нашли — используем первый рабочий день + logger.LogWarning("Не удалось найти дату после referenceDate для шаблона {TemplateId}. Используем первый рабочий день.", allTemplates[i].Id); + var firstDay = workDays[0]; + nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + } + } + //------------------ + + var updatedTemplate = new TemplateNextRunDto + { + Id = allTemplates[i].Id, + NextRun = nextRun + }; + + updatedTemplates.Add(updatedTemplate); + + logger.LogDebug("Шаблон {TemplateId} назначен на {NextRun}", updatedTemplate.Id, updatedTemplate.NextRun); + } + + logger.LogInformation("Распределение завершено. Возвращаем {Count} записей.", updatedTemplates.Count); + + return updatedTemplates; + } + + /// + /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки. + /// Возвращает обновлённый шаблон с новой датой выполнения. + /// + public async Task GetValidNextRunForTemplateAsync( + DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates) + { + logger.LogDebug("Запрос актуальной даты для шаблона {TemplateId}", targetTemplate.Id); + + // Получаем список выходных/праздничных дней + var weekends = await GetWeekendsAsync(startDate, endDate); + + // Получаем список рабочих дней + var workDays = GetWorkDays(startDate, endDate, weekends); + + logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count); + + if (workDays.Count == 0) + { + logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate); + + // Возвращаем дату, равную referenceDate, если не нашли рабочих дней + logger.LogWarning("Используем referenceDate как дату выполнения для шаблона {TemplateId}", targetTemplate.Id); + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = referenceDate }; + } + + var now = DateTimeOffset.UtcNow; + + // Проверяем, устарела ли дата + if (targetTemplate.NextRun.HasValue && targetTemplate.NextRun.Value < now) + { + logger.LogDebug("Дата шаблона {TemplateId} ({Date}) устарела", targetTemplate.Id, targetTemplate.NextRun); + + // Строим план на основе оставшихся шаблонов (без устаревшего) + var remainingTemplates = allTemplates.Where(t => t.Id != targetTemplate.Id && t.NextRun.HasValue).ToList(); + + // Вычисляем план для оставшихся + var plan = await BuildPlanAsync(startDate, endDate, referenceDate, remainingTemplates, weekends, workDays); + + // Находим следующую дату в плане, не занятую + var newDate = FindNextAvailableDateNotInPlan(workDays, DateOnly.FromDateTime(now.DateTime), plan); + if (newDate.HasValue) + { + var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); + + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; + } + else + { + logger.LogWarning("Не удалось найти новую дату для устаревшего шаблона {TemplateId} по плану. Используем ближайшую дату.", targetTemplate.Id); + + // Используем ближайшую дату после now, не в плане + var fallbackDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(now.DateTime), DateOnly.FromDateTime(now.DateTime)); + if (fallbackDate.HasValue) + { + var nextRun = new DateTimeOffset(fallbackDate.Value.Year, fallbackDate.Value.Month, fallbackDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + logger.LogDebug("Шаблон {TemplateId} перенесен на резервную дату {NewDate}", targetTemplate.Id, nextRun); + + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; + } + else + { + logger.LogWarning("Не удалось найти резервную дату для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); + + var firstDay = workDays[0]; + var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; + } + } + } + + // Если дата не устарела, но не соответствует плану — переносим + var currentPlan = await BuildPlanAsync(startDate, endDate, referenceDate, allTemplates, weekends, workDays); + var expectedDate = currentPlan.FirstOrDefault(d => d == DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); + if (expectedDate == default) + { + logger.LogDebug("Дата шаблона {TemplateId} ({Date}) не соответствует плану. Переносим на ближайшую дату в плане.", targetTemplate.Id, targetTemplate.NextRun); + + var newDate = FindNextAvailableDateNotInPlan(workDays, DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime), currentPlan); + if (newDate.HasValue) + { + var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); + + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; + } + else + { + logger.LogWarning("Не удалось найти новую дату для шаблона {TemplateId} по плану. Используем ближайшую дату.", targetTemplate.Id); + + // Используем ближайшую дату, не в плане + var fallbackDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime), DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); + if (fallbackDate.HasValue) + { + var nextRun = new DateTimeOffset(fallbackDate.Value.Year, fallbackDate.Value.Month, fallbackDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + logger.LogDebug("Шаблон {TemplateId} перенесен на резервную дату {NewDate}", targetTemplate.Id, nextRun); + + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; + } + else + { + logger.LogWarning("Не удалось найти резервную дату для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); + + var firstDay = workDays[0]; + var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; + } + } + } + + logger.LogDebug("Шаблон {TemplateId} имеет актуальную дату: {NextRun}", targetTemplate.Id, targetTemplate.NextRun); + + // Проверяем, не раньше ли дата, чем referenceDate (не включая саму referenceDate) + if (targetTemplate.NextRun < referenceDate) + { + logger.LogWarning("Шаблон {TemplateId} имеет дату {NextRun}, которая раньше referenceDate {ReferenceDate}. Переносим на ближайшую дату.", targetTemplate.Id, targetTemplate.NextRun, referenceDate); + + var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(referenceDate.DateTime), DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); + if (newDate.HasValue) + { + var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); + + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; + } + else + { + logger.LogWarning("Не удалось найти дату после referenceDate для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); + + var firstDay = workDays[0]; + var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + + return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; + } + } + + return targetTemplate; + } + + /// + /// Строит план работ на основе списка шаблонов + /// + private async Task> BuildPlanAsync( + DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List templates, HashSet weekends, List workDays) + { + var totalWorks = templates.Count; + var totalDays = workDays.Count; + + if (totalDays == 0) return new List(); + + var interval = (double)totalDays / totalWorks; + var plan = new List(); + + for (int i = 0; i < totalWorks; i++) + { + var dayIndex = (int)Math.Round(i * interval); + if (dayIndex >= totalDays) dayIndex = totalDays - 1; + + var date = workDays[dayIndex]; + plan.Add(date); + } + + return plan.OrderBy(d => d).ToList(); + } + + /// + /// Ищет следующую дату в плане, которая не занята + /// + private DateOnly? FindNextAvailableDateNotInPlan(List workDays, DateOnly afterDate, List plan) + { + var planSet = new HashSet(plan); + + // Ищем вперед + foreach (var day in workDays) + { + if (day > afterDate && !planSet.Contains(day)) + { + return day; + } + } + + // Ищем назад + foreach (var day in workDays.AsEnumerable().Reverse()) + { + if (day <= afterDate && !planSet.Contains(day)) + { + return day; + } + } + + return null; + } + + /// + /// Ищет следующую доступную дату после указанной, если не находит — ищет назад. + /// + private DateOnly? FindNextAvailableDate(List workDays, DateOnly afterDate, DateOnly originalDate) + { + var future = workDays.FirstOrDefault(d => d >= afterDate); + if (future != default) return future; + + var past = workDays.Where(t => t >= originalDate).OrderByDescending(t => t).FirstOrDefault(); + return past != default ? (DateOnly?)past : null; + } + + /// + /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. + /// + private List GetWorkDays(DateOnly start, DateOnly end, HashSet weekends) + { + var result = new List(); + var date = start; + + while (date <= end) + { + var dayOfWeek = date.DayOfWeek; + if (!weekends.Contains(date)) + { + result.Add(date); + } + date = date.AddDays(1); + } + + return result; + } + + /// + /// Получить список выходных дней + /// + private async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate) + { + var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); + return weekends.ToHashSet(); + } + } +} \ No newline at end of file diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs new file mode 100644 index 00000000..4e431db9 --- /dev/null +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -0,0 +1,99 @@ +using PARR.DAL.NextRunServices.Models; +using PARR.DAL.NextRunServices.Subservices; + +namespace PARR.Test.NextRun +{ + internal class NextRunTest + { + private readonly ITemplateDistributor templateDistributor; + + public NextRunTest(ITemplateDistributor templateDistributor) + { + this.templateDistributor = templateDistributor; + } + + int periodDays = 5; + DateTimeOffset referenceDate = new DateTimeOffset(2025, 12, 1, 7, 0, 0, TimeSpan.Zero); + + public async Task Test() + { + await DistributeTemplatesAsync(); + } + + /// + /// распределить шаблоны + /// + private async Task DistributeTemplatesAsync() + { + var templates = await templateDistributor.DistributeTemplatesAsync(GetDateStart(), periodDays, referenceDate, GetTemplates()); + + //await GetValidNextRunForTemplateAsync(templates); + } + + + /// + /// получить nextRun + /// + private async Task GetValidNextRunForTemplateAsync(List? templates = null) + { + //var templateList = templates ?? GetTemplates(); + + //var targetTemplate = new TemplateNextRunDto { Id= Guid.Parse("4C2F61E9-7B47-4049-A881-9200D71303F7"), NextRun= new DateTimeOffset(2025, 12, 1, 3, 0, 0, TimeSpan.Zero) }; + + //var distribTemplates = await templateDistributor.GetValidNextRunForTemplateAsync(GetDateStart(), GetDateEnd(), referenceDate, targetTemplate, templateList); + + //// еще раз + //templateList.Add(targetTemplate); + //var targetTemplate2 = new TemplateNextRunDto { Id = Guid.Parse("4C2F61E9-7B47-4049-A881-9200D71303F7"), NextRun = new DateTimeOffset(2025, 12, 1, 3, 0, 0, TimeSpan.Zero) }; + //var distribTemplates2 = await templateDistributor.GetValidNextRunForTemplateAsync(GetDateStart(), GetDateEnd(), referenceDate, targetTemplate2, templateList); + + } + + + private DateOnly GetDateStart() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow.Date); + + var refDate = DateOnly.FromDateTime(referenceDate.Date); + + return today < refDate ? refDate : today; + } + + //private DateOnly GetDateEnd() + //{ + // return GetDateStart().AddDays(periodDays); + //} + + + private List GetTemplates() + { + var templates = new List(); + + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("55F047E6-3631-4676-8959-FC75FF6BC963"), NextRun = new DateTimeOffset(2027, 1, 15, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("DD15E9F6-AAD4-4FE6-9441-7E7FF67781A3"), NextRun = new DateTimeOffset(2025, 12, 24, 3, 0, 0, TimeSpan.Zero) }); + + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("78EE4A94-DBEE-41D1-B51A-CE3BD5AEE4D4"), NextRun = new DateTimeOffset(2025, 12, 29, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B397F1FE-DF2B-4B50-B8D6-2DABECD3D5A1"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("619D9621-2B45-4108-AF54-CF96862A8E9B"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("D74D831C-D435-46BF-9B5F-A24884AAA7DC"), NextRun = new DateTimeOffset(2025, 12, 25, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("94A7375E-D1BD-49B7-9186-D26E85F1CF74"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B3681F5F-10A5-425C-BBEA-EA2B85FE0967"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("3259B408-48A3-4995-AF9A-097477ECAB81"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto { Id = Guid.Parse("25F53A28-D657-4B9A-82F3-CE9AF4D53385"), NextRun = new DateTimeOffset(2025, 12, 30, 3, 0, 0, TimeSpan.Zero) }); + //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("F75F6B1A-875F-4CB2-98C8-6721FD06C18C"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("2C06C63D-BE92-4312-98FB-0547E74B8A33"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B22D314E-9F86-49BB-826F-20919EC55A7F"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); + //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("061CB58A-B350-456F-8E02-C937D488D6FC"), NextRun = null }); + + //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("C51E2C3F-655D-4395-BF9D-D6D64DA314D9"), NextRun = null }); + //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("C9A77DEF-0FB8-4903-87B7-16B8C3EC97A4"), NextRun = null }); + //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("EC703575-B38F-4A7F-9010-BBB3F92EDEA9"), NextRun = null }); + //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("188181C3-AACD-4205-BCB6-7414966CB33B"), NextRun = null }); + + + return templates; + } + + } +} diff --git a/PARR.Test/Program.cs b/PARR.Test/Program.cs index 8f2fc0c8..58eb52d2 100644 --- a/PARR.Test/Program.cs +++ b/PARR.Test/Program.cs @@ -1,10 +1,11 @@ -using PARR.BLL; -using PARR.EsppApi; -using PARR.DAL; -using PARR.Test; -using Serilog; -using PARR.TemplateDistributor; using Elastic.CommonSchema.Serilog; +using PARR.BLL; +using PARR.DAL; +using PARR.EsppApi; +using PARR.TemplateDistributor; +using PARR.Test; +using PARR.Test.NextRun; +using Serilog; /*IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => @@ -48,6 +49,8 @@ builder.Services.InstallBllServices(builder.Configuration); builder.Configuration.AddDalConfigurations(builder.Services); builder.Services.AddDallSettings(builder.Configuration); +builder.Services.AddTransient(); + builder.Services.AddHostedService(); var host = builder.Build(); diff --git a/PARR.Test/Worker.cs b/PARR.Test/Worker.cs index e8a38173..1a9aa91c 100644 --- a/PARR.Test/Worker.cs +++ b/PARR.Test/Worker.cs @@ -6,6 +6,7 @@ using PARR.DAL.Services.Interfaces; using PARR.EsppApi; using PARR.EsppApi.Constants; using PARR.EsppApi.Models.Query; +using PARR.Test.NextRun; namespace PARR.Test { @@ -34,6 +35,19 @@ namespace PARR.Test protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + + #region NextRunTest + + using (var scope = serviceProvider.CreateScope()) + { + var service = scope.ServiceProvider.GetService(); + + await service!.Test(); + } + + #endregion + + #region test //using (var scope = serviceProvider.CreateScope()) //{ From 6e8e3c89c209530e7f1c218bed75849fb52d7053 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Mon, 19 Jan 2026 16:33:11 +1000 Subject: [PATCH 3/4] =?UTF-8?q?feat(dal):=20TemplateDistributer=20-=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80,=20=D1=83=D1=87=D0=B8?= =?UTF-8?q?=D1=82=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B8=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B5=D1=82=20=D0=B2=D1=8B=D1=85=D0=BE=D0=B4=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=B8=20=D0=BF=D1=80=D0=B0=D0=B7=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Subservices/ITemplateDistributor.cs | 5 +++-- .../Subservices/TemplateDistributor.cs | 16 ++++++++++------ PARR.Test/NextRun/NextRunTest.cs | 7 +++++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs index 7c04ff58..0936d6a9 100644 --- a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs @@ -13,13 +13,14 @@ namespace PARR.DAL.NextRunServices.Subservices /// Длительность распределения, дней /// ReferenceDate /// Спиоск шаблонов для распределения + /// Исключать выходные из распределения /// - Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates); + Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends); /// /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки. /// Возвращает обновлённый шаблон с новой датой выполнения. /// - Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates); + Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends); } } diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs index 21c431d3..bbcf2c4b 100644 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs @@ -25,7 +25,7 @@ namespace PARR.DAL.NextRunServices.Subservices } - public Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates) + public Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends) { //TODO: throw new NotImplementedException(); @@ -35,7 +35,7 @@ namespace PARR.DAL.NextRunServices.Subservices - public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates) + public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends) { var _startDate = GetStartPeriodDate(startDate, referenceDate); var _endDate = GetEndPeriodDate(_startDate, duration); @@ -43,11 +43,15 @@ namespace PARR.DAL.NextRunServices.Subservices logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}", allTemplates.Count, duration, _startDate, _endDate, referenceDate); // Получаем список выходных/праздничных дней - var weekends = await GetWeekendsAsync(_startDate, _endDate); - logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); + var weekends = new HashSet(); + if (excludeWeekends) + { + weekends = await GetWeekendsAsync(_startDate, _endDate); + logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); - foreach (var weekend in weekends) - logger.LogDebug("Не рабочий день: {weekend}", weekend); + foreach (var weekend in weekends) + logger.LogDebug("Не рабочий день: {weekend}", weekend); + } // Получаем список рабочих дней (исключая выходные и праздники) var workDays = GetWorkDays(_startDate, _endDate, weekends); diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index 4e431db9..5791f4a2 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -12,7 +12,7 @@ namespace PARR.Test.NextRun this.templateDistributor = templateDistributor; } - int periodDays = 5; + int periodDays = 10; DateTimeOffset referenceDate = new DateTimeOffset(2025, 12, 1, 7, 0, 0, TimeSpan.Zero); public async Task Test() @@ -25,7 +25,10 @@ namespace PARR.Test.NextRun /// private async Task DistributeTemplatesAsync() { - var templates = await templateDistributor.DistributeTemplatesAsync(GetDateStart(), periodDays, referenceDate, GetTemplates()); + // исключать выходные и праздники + var excludeWeekends = true; + + var templates = await templateDistributor.DistributeTemplatesAsync(GetDateStart(), periodDays, referenceDate, GetTemplates(), excludeWeekends); //await GetValidNextRunForTemplateAsync(templates); } From 2038f7b57d070c9956699d065707a137767d8c30 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Tue, 20 Jan 2026 15:04:36 +1000 Subject: [PATCH 4/4] =?UTF-8?q?feat(dal):=20template=20distributor=20-=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20NextRun.=20=D0=A0=D0=B5=D1=84?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.DAL/NextRunServices/INextRunService.cs | 3 +- .../Models/TemplateNextRunDto.cs | 12 +- .../Models/TemplateNextRunResultDto.cs | 14 +- PARR.DAL/NextRunServices/NextRunService.cs | 14 +- .../Subservices/ITemplateDistributor.cs | 13 +- .../Subservices/TemplateDistributor.cs | 282 ++++++++++---- .../Subservices/TemplateDistributor_old.cs | 345 ------------------ PARR.Test/NextRun/NextRunTest.cs | 50 ++- 8 files changed, 282 insertions(+), 451 deletions(-) delete mode 100644 PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs diff --git a/PARR.DAL/NextRunServices/INextRunService.cs b/PARR.DAL/NextRunServices/INextRunService.cs index d62fd757..3800f9f9 100644 --- a/PARR.DAL/NextRunServices/INextRunService.cs +++ b/PARR.DAL/NextRunServices/INextRunService.cs @@ -22,8 +22,9 @@ /// Получить NextRun по id шаблона /// /// + /// Новый шаблон или шаблон переведенный из unused /// - Task GetNextRunForTemplate(Guid templateId); + Task GetNextRunForTemplate(Guid templateId, bool isNew); } } diff --git a/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs b/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs index 839fe5c6..e2a498f6 100644 --- a/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs +++ b/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs @@ -3,10 +3,12 @@ /// /// Модель шаблона, для расчета NextRun /// - public class TemplateNextRunDto - { - public Guid Id { get; set; } + public record TemplateNextRunDto(Guid Id, DateTimeOffset? NextRun); - public DateTimeOffset? NextRun { get; set; } - } + //public class TemplateNextRunDto + //{ + // public Guid Id { get; set; } + + // public DateTimeOffset? NextRun { get; set; } + //} } diff --git a/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs b/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs index 3477327c..4852c7b4 100644 --- a/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs +++ b/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs @@ -3,12 +3,14 @@ /// /// Модель, результат распределения шаблона /// - public class TemplateNextRunResultDto - { - public Guid Id { get; set; } + public record TemplateNextRunResultDto(Guid Id, DateTimeOffset NextRun, DateTimeOffset? NextRunOld); - public DateTimeOffset NextRun { get; set; } + //public class TemplateNextRunResultDto + //{ + // public Guid Id { get; set; } - public DateTimeOffset? NextRunOld { get; set; } - } + // public DateTimeOffset NextRun { get; set; } + + // public DateTimeOffset? NextRunOld { get; set; } + //} } diff --git a/PARR.DAL/NextRunServices/NextRunService.cs b/PARR.DAL/NextRunServices/NextRunService.cs index 796e6599..ff5348b7 100644 --- a/PARR.DAL/NextRunServices/NextRunService.cs +++ b/PARR.DAL/NextRunServices/NextRunService.cs @@ -1,6 +1,7 @@  using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using PARR.DAL.NextRunServices.Subservices; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.TransformServices; @@ -13,25 +14,29 @@ namespace PARR.DAL.NextRunServices private readonly ITemplateService templateService; private readonly IJobGroupService jobGroupService; private readonly IEsppScheduleTransformService esppScheduleTransformService; + private readonly ITemplateDistributor templateDistributor; public NextRunService( ILogger logger, ITemplateService templateService, IJobGroupService jobGroupService, - IEsppScheduleTransformService esppScheduleTransformService + IEsppScheduleTransformService esppScheduleTransformService, + ITemplateDistributor templateDistributor ) { this.logger = logger; this.templateService = templateService; this.jobGroupService = jobGroupService; this.esppScheduleTransformService = esppScheduleTransformService; + this.templateDistributor = templateDistributor; } - public Task> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) + public async Task> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) { //TODO: throw new NotImplementedException(); + //var ditributedTemplateList = await templateDistributor.DistributeTemplatesAsync(); } @@ -50,7 +55,7 @@ namespace PARR.DAL.NextRunServices } - public async Task GetNextRunForTemplate(Guid templateId) + public async Task GetNextRunForTemplate(Guid templateId, bool isNew) { var template = await templateService.Get() .Include(t => t.Job).ThenInclude(t => t.Group) @@ -67,7 +72,8 @@ namespace PARR.DAL.NextRunServices { // включено автораспределение //TODO: !!!!!!!!!!!!!!!! добавить метод рассчета с учетом распределения - throw new NotImplementedException("добавить метод рассчета с учетом распределения"); + throw new NotImplementedException(); + //return await templateDistributor.GetValidNextRunForTemplateAsync(); } else { diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs index 0936d6a9..8ef6474a 100644 --- a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs @@ -17,10 +17,21 @@ namespace PARR.DAL.NextRunServices.Subservices /// Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends); + + /// /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки. /// Возвращает обновлённый шаблон с новой датой выполнения. /// - Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends); + /// + /// + /// + /// + /// + /// + /// Новый шаблон + /// + Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew); } } + diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs index bbcf2c4b..be239755 100644 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs @@ -25,60 +25,100 @@ namespace PARR.DAL.NextRunServices.Subservices } - public Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends) + public async Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew) { - //TODO: - throw new NotImplementedException(); + //todo: allTemplates - пока не используется. Будет использоваться когда каждый раз будем строить план + + // Пока работает так: + // Получаем список рабочих дней, если работа попадает в эти дни, то считаем дату валидной + // Если нет, то перемещаем на последний день периода распределения или первый в зависимости от параметра isNew + + var _startDate = GetStartPeriodDate(startDate, referenceDate); + var _endDate = GetEndPeriodDate(_startDate, duration); + + logger.LogDebug("Получаю актуальный nextRun для шаблона {templateId}. Текущий nextRun: {nextRun}. Период {duration} дней, с {_startDate} по {_endDate}. Исключить выходные и праздники: {excludeWeekends}. Для нового шаблона: {isNew}", targetTemplate.Id, targetTemplate.NextRun, duration, _startDate, _endDate, excludeWeekends, isNew); + + // Получаем список рабочих дней (исключая выходные и праздники) + var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); + + if (workDays.Count == 0) + { + logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем шаблон со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun); + + // смещаем nextRun вперед + var result = FillTemplateResultWithOffsetError(targetTemplate, _startDate, referenceDate); + + logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld); + + return result; + } + + // если isNew, то берем первый день периода + if (isNew) + { + var result = new TemplateNextRunResultDto + ( + Id: targetTemplate.Id, + NextRun: FindNextAvailableDateIgnoreLoad(workDays, referenceDate, true), //CreateDateTimeOffset(workDays.First(), referenceDate), + NextRunOld: targetTemplate.NextRun + ); + + logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. Это было создание нового, поместили его в начало периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld); + + return result; + } + + // есть ли NextRun и попадает ли он на рабочий день + // проверяем чтоб было не меньше чем UtcNow + //TODO: тут еще сделать проверку нагрузки на день + if (targetTemplate.NextRun.HasValue && workDays.Contains(DateOnly.FromDateTime(targetTemplate.NextRun.Value.Date)) && targetTemplate.NextRun.Value > DateTimeOffset.UtcNow) + { + var result = new TemplateNextRunResultDto + ( + Id: targetTemplate.Id, + NextRun: CreateDateTimeOffset(targetTemplate.NextRun.Value, referenceDate), + NextRunOld: targetTemplate.NextRun + ); + + logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun актуальный, возвращаем его. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld); + + return result; + } + + + //TODO: Временный костыль, перемещаем на конец периода + + var endPeriodResult = new TemplateNextRunResultDto + ( + Id: targetTemplate.Id, + NextRun: FindNextAvailableDateIgnoreLoad(workDays, referenceDate, false), + NextRunOld: targetTemplate.NextRun + ); + + logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun перемещен на конец периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", endPeriodResult.Id, endPeriodResult.NextRun, endPeriodResult.NextRunOld); + + return endPeriodResult; } - - - public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends) { var _startDate = GetStartPeriodDate(startDate, referenceDate); var _endDate = GetEndPeriodDate(_startDate, duration); - logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}", allTemplates.Count, duration, _startDate, _endDate, referenceDate); - - // Получаем список выходных/праздничных дней - var weekends = new HashSet(); - if (excludeWeekends) - { - weekends = await GetWeekendsAsync(_startDate, _endDate); - logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); - - foreach (var weekend in weekends) - logger.LogDebug("Не рабочий день: {weekend}", weekend); - } + logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDate, excludeWeekends); // Получаем список рабочих дней (исключая выходные и праздники) - var workDays = GetWorkDays(_startDate, _endDate, weekends); - - logger.LogDebug("Найдено рабочих дней: {Count}", workDays.Count); - - foreach (var workDay in workDays) - logger.LogDebug("Рабочий день: {workDay}", workDay); + var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); if (workDays.Count == 0) { logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun); // смещаем все nextRun вперед - var result = allTemplates.Select(t => new TemplateNextRunResultDto - { - Id = t.Id, - NextRun = t.NextRun.HasValue - // если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun - ? CreateNextRun(t.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate) - // если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun - : CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), - NextRunOld = t.NextRun - }).OrderBy(t => t.NextRun).ToList(); - - foreach (var item in result) - logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id}, nextRun: {NextRun}, nextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld); + var result = allTemplates.Select(t => + FillTemplateResultWithOffsetError(t, _startDate, referenceDate) + ).OrderBy(t => t.NextRun).ToList(); logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", result.Count, result.Count(t => t.NextRun == t.NextRunOld), result.Count(t => t.NextRun != t.NextRunOld)); @@ -86,13 +126,12 @@ namespace PARR.DAL.NextRunServices.Subservices } var totalWorks = allTemplates.Count; - var totalDays = workDays.Count; // интервал между шаблонами - var interval = GetInterval(totalDays, totalWorks); + var interval = GetInterval(workDays.Count, totalWorks); // строим план, дни на которые должны быть назанчены шаблоны - var plan = GetPlan(totalWorks, totalDays, workDays); + var plan = GetPlan(totalWorks, workDays); // Подсчитываем, сколько работ должно быть на каждый день @@ -168,11 +207,11 @@ namespace PARR.DAL.NextRunServices.Subservices foreach (var template in templates) { templatesToKeep.Add(new TemplateNextRunResultDto - { - Id = template.Id, - NextRun = template.NextRun.Value, - NextRunOld = template.NextRun - }); + ( + Id: template.Id, + NextRun: template.NextRun.Value, + NextRunOld: template.NextRun + )); logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun); } } @@ -183,11 +222,11 @@ namespace PARR.DAL.NextRunServices.Subservices { var template = templates[i]; templatesToKeep.Add(new TemplateNextRunResultDto - { - Id = template.Id, - NextRun = template.NextRun.Value, - NextRunOld = template.NextRun - }); + ( + Id: template.Id, + NextRun: template.NextRun.Value, + NextRunOld: template.NextRun + )); logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun); } @@ -228,11 +267,11 @@ namespace PARR.DAL.NextRunServices.Subservices currentDailyLoad[day]++; updatedTemplates.Add(new TemplateNextRunResultDto - { - Id = template.Id, - NextRun = nextRun.Value, - NextRunOld = template.NextRun - }); + ( + Id: template.Id, + NextRun: nextRun.Value, + NextRunOld: template.NextRun + )); logger.LogDebug("Шаблону {TemplateId} назначен NextRun: {NextRun}, NextRunOld: {NextRunOld}", template.Id, nextRun, template.NextRun); } @@ -242,11 +281,11 @@ namespace PARR.DAL.NextRunServices.Subservices // Возвращаем с ошибкой updatedTemplates.Add(new TemplateNextRunResultDto - { - Id = template.Id, - NextRun = CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), - NextRunOld = template.NextRun - }); + ( + Id: template.Id, + NextRun: CreateDateTimeOffset(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRunOld: template.NextRun + )); } } @@ -255,7 +294,7 @@ namespace PARR.DAL.NextRunServices.Subservices updatedTemplates = updatedTemplates.OrderBy(t => t.NextRun).ToList(); - logger.LogDebug("--- Результат распределения: ---"); + logger.LogDebug("--- Результат распределения ---"); foreach (var item in updatedTemplates) logger.LogDebug("Шаблон {templateId}, NextRun: {NextRun}, NextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld); @@ -265,6 +304,77 @@ namespace PARR.DAL.NextRunServices.Subservices } + /// + /// Заполнить TemplateNextRunResultDto, если нет рабочих дней со смещением + /// + /// + /// + private TemplateNextRunResultDto FillTemplateResultWithOffsetError(TemplateNextRunDto template, DateOnly startDate, DateTimeOffset referenceDate) + { + var result = new TemplateNextRunResultDto + ( + Id: template.Id, + //NextRun = template.NextRun.HasValue + // // если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun + // ? CreateNextRun(template.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate) + // // если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun + // : CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRun: CreateDateTimeOffset(startDate.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRunOld: template.NextRun + ); + + logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id} на {offsetYearsIfErrorCalcNextRun} лет вперед, nextRun: {NextRun}, nextRunOld: {NextRunOld}", result.Id, offsetYearsIfErrorCalcNextRun, result.NextRun, result.NextRunOld); + + return result; + } + + + + /// + /// Получает NextRun не учитывая нагрузку на день + /// + /// + /// + /// Искать с начала = true, false - искать с конца + /// + private DateTimeOffset FindNextAvailableDateIgnoreLoad(List workDays, DateTimeOffset referenseDate, bool isSearchFirst) + { + // возможно это временная реализация метода, используется для GetValidNextRunForTemplateAsync + + if (!isSearchFirst) + { + // берем первый рабочий день с конца и возвращаем его + var lastDate = CreateDateTimeOffset(workDays.Last(), referenseDate); + logger.LogDebug("Берем первый рабочий день с конца. Сгенерированный NextRun: {lastDate}", lastDate); + return lastDate; + } + + // будем искать сначала + // берем первый день, и смотрим не прошло ли уже время + var firstDate = CreateDateTimeOffset(workDays.First(), referenseDate); + if (firstDate > DateTimeOffset.UtcNow) + { + logger.LogDebug("Берем первый день с начала. Сгенерированный NextRun: {firstDate}", firstDate); + return firstDate; + } + + + // тогда берем второй день + if (workDays.Count > 1) + { + var secondDate = CreateDateTimeOffset(workDays.ElementAt(1), referenseDate); + logger.LogDebug("Берем второй день сначала, так как у первого дня уже время прошло. Сгенерированный NextRun: {secondDate}", secondDate); + return secondDate; + } + + logger.LogWarning("Вернули nextRun: {firstDate} из первого дня распределения, хотя время уже просрочено. Всего рабочих дней: {workDays}", firstDate, workDays.Count); + // если вдруг список рабочих дней был не >1, то тогда берем первый и вренем его. Так никогда не должно быть + return firstDate; + } + + + + /// /// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad /// @@ -282,7 +392,7 @@ namespace PARR.DAL.NextRunServices.Subservices if (current < expected) { - return CreateNextRun(day, referenceDate); + return CreateDateTimeOffset(day, referenceDate); } } @@ -294,7 +404,7 @@ namespace PARR.DAL.NextRunServices.Subservices if (current < expected) { - return CreateNextRun(day, referenceDate); + return CreateDateTimeOffset(day, referenceDate); } } @@ -302,7 +412,7 @@ namespace PARR.DAL.NextRunServices.Subservices var minLoad = currentDailyLoad.Values.Min(); var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key; - return CreateNextRun(minDay, referenceDate); + return CreateDateTimeOffset(minDay, referenceDate); } @@ -311,11 +421,12 @@ namespace PARR.DAL.NextRunServices.Subservices /// Получить план: дни, на которые должны быть назначены шаблоны /// /// - /// /// /// - private List GetPlan(int totalWorks, int totalDays, List workDays) + private List GetPlan(int totalWorks, List workDays) { + var totalDays = workDays.Count(); + var interval = GetInterval(totalDays, totalWorks); var plan = new List(); @@ -336,8 +447,11 @@ namespace PARR.DAL.NextRunServices.Subservices plan.Add(date); } + logger.LogDebug("--- План распределения ---"); + foreach (var item in plan.GroupBy(t => t)) + logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count()); + return plan; - // return new HashSet(plan); } @@ -356,23 +470,23 @@ namespace PARR.DAL.NextRunServices.Subservices /// - /// Создать NextRun (из DateTimeOffset + refDate) + /// Создать DateTimeOffset (из DateTimeOffset + refDate) /// /// Откуда взять дату /// Откуда взять время /// - private DateTimeOffset CreateNextRun(DateTimeOffset date, DateTimeOffset refDate) + private DateTimeOffset CreateDateTimeOffset(DateTimeOffset date, DateTimeOffset refDate) { - return CreateNextRun(DateOnly.FromDateTime(date.Date), refDate); + return CreateDateTimeOffset(DateOnly.FromDateTime(date.Date), refDate); } /// - /// Создать NextRun (из DateOnly + refDate) + /// Создать DateTimeOffset (из DateOnly + refDate) /// /// /// /// - private DateTimeOffset CreateNextRun(DateOnly date, DateTimeOffset refDate) + private DateTimeOffset CreateDateTimeOffset(DateOnly date, DateTimeOffset refDate) { return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero); } @@ -395,10 +509,15 @@ namespace PARR.DAL.NextRunServices.Subservices if (dateStart < DateOnly.FromDateTime(referenceDate.Date)) dateStart = DateOnly.FromDateTime(referenceDate.Date); - // если dateStart < чем текущая дата, то берем ьекущую дату + // если dateStart < чем текущая дата, то берем текущую дату if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date)) dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date); + // если текущее время > referenceDate, то прибавляем 1 день + var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate); + if (dateTimeStart < DateTimeOffset.UtcNow) + dateStart = dateStart.AddDays(1); + return dateStart; } @@ -428,6 +547,10 @@ namespace PARR.DAL.NextRunServices.Subservices { var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); + logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); + foreach (var weekend in weekends) + logger.LogDebug("Не рабочий день: {weekend}", weekend); + return weekends.ToHashSet(); } @@ -437,10 +560,15 @@ namespace PARR.DAL.NextRunServices.Subservices /// /// /// - /// + /// Исключить выходные и праздники /// - private List GetWorkDays(DateOnly start, DateOnly end, HashSet weekends) + private async Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends) { + // Получаем список выходных/праздничных дней + var weekends = new HashSet(); + if (excludeWeekends) + weekends = await GetWeekendsAsync(start, end); + var result = new List(); var date = start; @@ -454,6 +582,10 @@ namespace PARR.DAL.NextRunServices.Subservices date = date.AddDays(1); } + logger.LogDebug("Найдено рабочих дней: {Count}", result.Count); + foreach (var workDay in result) + logger.LogDebug("Рабочий день: {workDay}", workDay); + return result; } diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs deleted file mode 100644 index 93c7ba2f..00000000 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs +++ /dev/null @@ -1,345 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using PARR.DAL.NextRunServices.Models; -using PARR.DAL.Services.Interfaces; - -namespace PARR.DAL.NextRunServices.Subservices -{ - internal class TemplateDistributor_old //: ITemplateDistributor - { - private readonly ILogger logger; - private readonly IWeekendDayService weekendDayService; - - public TemplateDistributor_old( - ILogger logger, - IWeekendDayService weekendDayService - ) - { - this.logger = logger; - this.weekendDayService = weekendDayService; - } - - /// - /// Распределяет все шаблоны на указанный период, учитывая план нагрузки. - /// Возвращает список всех шаблонов с обновлёнными датами выполнения. - /// - public async Task> DistributeTemplatesAsync( - DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List allTemplates) - { - logger.LogInformation("Начинаем распределение {Count} шаблонов с {StartDate} по {EndDate}", allTemplates.Count, startDate, endDate); - - // Получаем список выходных/праздничных дней - var weekends = await GetWeekendsAsync(startDate, endDate); - - // Получаем список рабочих дней (исключая выходные и праздники) - var workDays = GetWorkDays(startDate, endDate, weekends); - - logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count); - - if (workDays.Count == 0) - { - logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate); - return allTemplates; - } - - var totalWorks = allTemplates.Count; - var totalDays = workDays.Count; - - //------------- - - // Вычисляем интервал между работами: сколько "дней" приходится на одну работу - var interval = (double)totalDays / totalWorks; - - // Создаём новый список шаблонов с обновлёнными датами - var updatedTemplates = new List(); - - for (int i = 0; i < totalWorks; i++) - { - // Вычисляем индекс дня, на который нужно назначить i-ю работу - var dayIndex = (int)Math.Round(i * interval); - - // Защита от возможного выхода за границы массива из-за округления - if (dayIndex >= totalDays) dayIndex = totalDays - 1; - - var date = workDays[dayIndex]; - - // Формируем дату выполнения: дата + время из referenceDate - var nextRun = new DateTimeOffset(date.Year, date.Month, date.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - - //------------------ - // Проверяем, не раньше ли дата, чем referenceDate (не включая саму referenceDate) - if (nextRun < referenceDate) - { - logger.LogWarning("Шаблон {TemplateId} имеет дату {NextRun}, которая раньше referenceDate {ReferenceDate}. Переносим на ближайший рабочий день после referenceDate (включая её).", allTemplates[i].Id, nextRun, referenceDate); - - // Ищем ближайший день после referenceDate (включая его) - var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(referenceDate.DateTime), date); - if (newDate.HasValue) - { - nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - } - else - { - // Если не нашли — используем первый рабочий день - logger.LogWarning("Не удалось найти дату после referenceDate для шаблона {TemplateId}. Используем первый рабочий день.", allTemplates[i].Id); - var firstDay = workDays[0]; - nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - } - } - //------------------ - - var updatedTemplate = new TemplateNextRunDto - { - Id = allTemplates[i].Id, - NextRun = nextRun - }; - - updatedTemplates.Add(updatedTemplate); - - logger.LogDebug("Шаблон {TemplateId} назначен на {NextRun}", updatedTemplate.Id, updatedTemplate.NextRun); - } - - logger.LogInformation("Распределение завершено. Возвращаем {Count} записей.", updatedTemplates.Count); - - return updatedTemplates; - } - - /// - /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки. - /// Возвращает обновлённый шаблон с новой датой выполнения. - /// - public async Task GetValidNextRunForTemplateAsync( - DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates) - { - logger.LogDebug("Запрос актуальной даты для шаблона {TemplateId}", targetTemplate.Id); - - // Получаем список выходных/праздничных дней - var weekends = await GetWeekendsAsync(startDate, endDate); - - // Получаем список рабочих дней - var workDays = GetWorkDays(startDate, endDate, weekends); - - logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count); - - if (workDays.Count == 0) - { - logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate); - - // Возвращаем дату, равную referenceDate, если не нашли рабочих дней - logger.LogWarning("Используем referenceDate как дату выполнения для шаблона {TemplateId}", targetTemplate.Id); - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = referenceDate }; - } - - var now = DateTimeOffset.UtcNow; - - // Проверяем, устарела ли дата - if (targetTemplate.NextRun.HasValue && targetTemplate.NextRun.Value < now) - { - logger.LogDebug("Дата шаблона {TemplateId} ({Date}) устарела", targetTemplate.Id, targetTemplate.NextRun); - - // Строим план на основе оставшихся шаблонов (без устаревшего) - var remainingTemplates = allTemplates.Where(t => t.Id != targetTemplate.Id && t.NextRun.HasValue).ToList(); - - // Вычисляем план для оставшихся - var plan = await BuildPlanAsync(startDate, endDate, referenceDate, remainingTemplates, weekends, workDays); - - // Находим следующую дату в плане, не занятую - var newDate = FindNextAvailableDateNotInPlan(workDays, DateOnly.FromDateTime(now.DateTime), plan); - if (newDate.HasValue) - { - var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти новую дату для устаревшего шаблона {TemplateId} по плану. Используем ближайшую дату.", targetTemplate.Id); - - // Используем ближайшую дату после now, не в плане - var fallbackDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(now.DateTime), DateOnly.FromDateTime(now.DateTime)); - if (fallbackDate.HasValue) - { - var nextRun = new DateTimeOffset(fallbackDate.Value.Year, fallbackDate.Value.Month, fallbackDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на резервную дату {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти резервную дату для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); - - var firstDay = workDays[0]; - var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - } - } - - // Если дата не устарела, но не соответствует плану — переносим - var currentPlan = await BuildPlanAsync(startDate, endDate, referenceDate, allTemplates, weekends, workDays); - var expectedDate = currentPlan.FirstOrDefault(d => d == DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); - if (expectedDate == default) - { - logger.LogDebug("Дата шаблона {TemplateId} ({Date}) не соответствует плану. Переносим на ближайшую дату в плане.", targetTemplate.Id, targetTemplate.NextRun); - - var newDate = FindNextAvailableDateNotInPlan(workDays, DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime), currentPlan); - if (newDate.HasValue) - { - var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти новую дату для шаблона {TemplateId} по плану. Используем ближайшую дату.", targetTemplate.Id); - - // Используем ближайшую дату, не в плане - var fallbackDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime), DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); - if (fallbackDate.HasValue) - { - var nextRun = new DateTimeOffset(fallbackDate.Value.Year, fallbackDate.Value.Month, fallbackDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на резервную дату {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти резервную дату для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); - - var firstDay = workDays[0]; - var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - } - } - - logger.LogDebug("Шаблон {TemplateId} имеет актуальную дату: {NextRun}", targetTemplate.Id, targetTemplate.NextRun); - - // Проверяем, не раньше ли дата, чем referenceDate (не включая саму referenceDate) - if (targetTemplate.NextRun < referenceDate) - { - logger.LogWarning("Шаблон {TemplateId} имеет дату {NextRun}, которая раньше referenceDate {ReferenceDate}. Переносим на ближайшую дату.", targetTemplate.Id, targetTemplate.NextRun, referenceDate); - - var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(referenceDate.DateTime), DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); - if (newDate.HasValue) - { - var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти дату после referenceDate для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); - - var firstDay = workDays[0]; - var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - } - - return targetTemplate; - } - - /// - /// Строит план работ на основе списка шаблонов - /// - private async Task> BuildPlanAsync( - DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List templates, HashSet weekends, List workDays) - { - var totalWorks = templates.Count; - var totalDays = workDays.Count; - - if (totalDays == 0) return new List(); - - var interval = (double)totalDays / totalWorks; - var plan = new List(); - - for (int i = 0; i < totalWorks; i++) - { - var dayIndex = (int)Math.Round(i * interval); - if (dayIndex >= totalDays) dayIndex = totalDays - 1; - - var date = workDays[dayIndex]; - plan.Add(date); - } - - return plan.OrderBy(d => d).ToList(); - } - - /// - /// Ищет следующую дату в плане, которая не занята - /// - private DateOnly? FindNextAvailableDateNotInPlan(List workDays, DateOnly afterDate, List plan) - { - var planSet = new HashSet(plan); - - // Ищем вперед - foreach (var day in workDays) - { - if (day > afterDate && !planSet.Contains(day)) - { - return day; - } - } - - // Ищем назад - foreach (var day in workDays.AsEnumerable().Reverse()) - { - if (day <= afterDate && !planSet.Contains(day)) - { - return day; - } - } - - return null; - } - - /// - /// Ищет следующую доступную дату после указанной, если не находит — ищет назад. - /// - private DateOnly? FindNextAvailableDate(List workDays, DateOnly afterDate, DateOnly originalDate) - { - var future = workDays.FirstOrDefault(d => d >= afterDate); - if (future != default) return future; - - var past = workDays.Where(t => t >= originalDate).OrderByDescending(t => t).FirstOrDefault(); - return past != default ? (DateOnly?)past : null; - } - - /// - /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. - /// - private List GetWorkDays(DateOnly start, DateOnly end, HashSet weekends) - { - var result = new List(); - var date = start; - - while (date <= end) - { - var dayOfWeek = date.DayOfWeek; - if (!weekends.Contains(date)) - { - result.Add(date); - } - date = date.AddDays(1); - } - - return result; - } - - /// - /// Получить список выходных дней - /// - private async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate) - { - var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); - return weekends.ToHashSet(); - } - } -} \ No newline at end of file diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index 5791f4a2..c815937f 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -11,15 +11,37 @@ namespace PARR.Test.NextRun { this.templateDistributor = templateDistributor; } - + int periodDays = 10; - DateTimeOffset referenceDate = new DateTimeOffset(2025, 12, 1, 7, 0, 0, TimeSpan.Zero); + DateTimeOffset referenceDate = new DateTimeOffset(2025, 1, 20, 5, 0, 0, TimeSpan.Zero); public async Task Test() { - await DistributeTemplatesAsync(); + await DistributeTemplatesAsync(); + //await GetValidNextRunForTemplateAsync(); } + /// + /// Получить актуальный NextRun + /// + /// + private async Task GetValidNextRunForTemplateAsync() + { + // исключать выходные и праздники + var excludeWeekends = true; + bool isNew = false; + + var targetTemplate = new TemplateNextRunDto + ( + Id: Guid.Parse("11F047E6-3631-4676-8959-FC75FF6BC963"), + //NextRun = new DateTimeOffset(2026, 1, 20, 4, 0, 0, TimeSpan.Zero) + NextRun: null + ); + + var template = await templateDistributor.GetValidNextRunForTemplateAsync(GetDateStart(), periodDays, referenceDate, targetTemplate, new List(), excludeWeekends, isNew); + } + + /// /// распределить шаблоны /// @@ -72,18 +94,18 @@ namespace PARR.Test.NextRun { var templates = new List(); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("55F047E6-3631-4676-8959-FC75FF6BC963"), NextRun = new DateTimeOffset(2027, 1, 15, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("DD15E9F6-AAD4-4FE6-9441-7E7FF67781A3"), NextRun = new DateTimeOffset(2025, 12, 24, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("55F047E6-3631-4676-8959-FC75FF6BC963"), NextRun: new DateTimeOffset(2027, 1, 15, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("DD15E9F6-AAD4-4FE6-9441-7E7FF67781A3"), NextRun: new DateTimeOffset(2025, 12, 24, 3, 0, 0, TimeSpan.Zero))); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("78EE4A94-DBEE-41D1-B51A-CE3BD5AEE4D4"), NextRun = new DateTimeOffset(2025, 12, 29, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B397F1FE-DF2B-4B50-B8D6-2DABECD3D5A1"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("619D9621-2B45-4108-AF54-CF96862A8E9B"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("D74D831C-D435-46BF-9B5F-A24884AAA7DC"), NextRun = new DateTimeOffset(2025, 12, 25, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("94A7375E-D1BD-49B7-9186-D26E85F1CF74"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B3681F5F-10A5-425C-BBEA-EA2B85FE0967"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("3259B408-48A3-4995-AF9A-097477ECAB81"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("25F53A28-D657-4B9A-82F3-CE9AF4D53385"), NextRun = new DateTimeOffset(2025, 12, 30, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("78EE4A94-DBEE-41D1-B51A-CE3BD5AEE4D4"), NextRun: new DateTimeOffset(2025, 12, 29, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("B397F1FE-DF2B-4B50-B8D6-2DABECD3D5A1"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("619D9621-2B45-4108-AF54-CF96862A8E9B"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("D74D831C-D435-46BF-9B5F-A24884AAA7DC"), NextRun: new DateTimeOffset(2025, 12, 25, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("94A7375E-D1BD-49B7-9186-D26E85F1CF74"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("B3681F5F-10A5-425C-BBEA-EA2B85FE0967"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("3259B408-48A3-4995-AF9A-097477ECAB81"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("25F53A28-D657-4B9A-82F3-CE9AF4D53385"), NextRun: new DateTimeOffset(2025, 12, 30, 3, 0, 0, TimeSpan.Zero))); //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("F75F6B1A-875F-4CB2-98C8-6721FD06C18C"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("2C06C63D-BE92-4312-98FB-0547E74B8A33"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B22D314E-9F86-49BB-826F-20919EC55A7F"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) });