diff --git a/PARR.API.sln b/PARR.API.sln index 1c482b16..88f49b8a 100644 --- a/PARR.API.sln +++ b/PARR.API.sln @@ -88,10 +88,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}" @@ -260,14 +256,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..3800f9f9 --- /dev/null +++ b/PARR.DAL/NextRunServices/INextRunService.cs @@ -0,0 +1,30 @@ +namespace PARR.DAL.NextRunServices +{ + public interface INextRunService + { + /// + /// Получить список TemplateId, NextRun по jobGroupId с автораспределением + /// + /// + /// + Task> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId); + + + /// + /// Получить NextRun по jobGroupId с расписанием ЕСПП + /// + /// + /// + Task GetNextRunForJobGroupWithEsppSchedulleAsync(Guid jobGroupId); + + + /// + /// Получить NextRun по id шаблона + /// + /// + /// Новый шаблон или шаблон переведенный из unused + /// + Task GetNextRunForTemplate(Guid templateId, bool isNew); + + } +} diff --git a/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs b/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs new file mode 100644 index 00000000..e2a498f6 --- /dev/null +++ b/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs @@ -0,0 +1,14 @@ +namespace PARR.DAL.NextRunServices.Models +{ + /// + /// Модель шаблона, для расчета NextRun + /// + public record TemplateNextRunDto(Guid Id, DateTimeOffset? NextRun); + + //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 new file mode 100644 index 00000000..4852c7b4 --- /dev/null +++ b/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs @@ -0,0 +1,16 @@ +namespace PARR.DAL.NextRunServices.Models +{ + /// + /// Модель, результат распределения шаблона + /// + public record TemplateNextRunResultDto(Guid Id, DateTimeOffset NextRun, DateTimeOffset? NextRunOld); + + //public class TemplateNextRunResultDto + //{ + // public Guid Id { 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 new file mode 100644 index 00000000..ff5348b7 --- /dev/null +++ b/PARR.DAL/NextRunServices/NextRunService.cs @@ -0,0 +1,85 @@ + +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; + +namespace PARR.DAL.NextRunServices +{ + internal class NextRunService : INextRunService + { + private readonly ILogger logger; + 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, + ITemplateDistributor templateDistributor + ) + { + this.logger = logger; + this.templateService = templateService; + this.jobGroupService = jobGroupService; + this.esppScheduleTransformService = esppScheduleTransformService; + this.templateDistributor = templateDistributor; + } + + + public async Task> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) + { + //TODO: + throw new NotImplementedException(); + //var ditributedTemplateList = await templateDistributor.DistributeTemplatesAsync(); + } + + + 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, bool isNew) + { + 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(); + //return await templateDistributor.GetValidNextRunForTemplateAsync(); + } + 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..8ef6474a --- /dev/null +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs @@ -0,0 +1,37 @@ +using PARR.DAL.NextRunServices.Models; + +namespace PARR.DAL.NextRunServices.Subservices +{ + //todo: public -> internal!!! + public interface ITemplateDistributor + { + /// + /// Распределяет все шаблоны на указанный период, учитывая план нагрузки. + /// Возвращает список всех шаблонов с обновлёнными датами выполнения. + /// + /// Дата начала распределения + /// Длительность распределения, дней + /// ReferenceDate + /// Спиоск шаблонов для распределения + /// Исключать выходные из распределения + /// + 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, bool isNew); + } +} + diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs new file mode 100644 index 00000000..be239755 --- /dev/null +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs @@ -0,0 +1,593 @@ +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; + + /// + /// Смещение nextRun на несколько лет вперед, если не смог его рассчитать + /// + private const int offsetYearsIfErrorCalcNextRun = 5; + + public TemplateDistributor( + ILogger logger, + IWeekendDayService weekendDayService + ) + { + this.logger = logger; + this.weekendDayService = weekendDayService; + } + + + public async Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew) + { + //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}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDate, excludeWeekends); + + // Получаем список рабочих дней (исключая выходные и праздники) + 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 => + 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)); + + return result; + } + + var totalWorks = allTemplates.Count; + + // интервал между шаблонами + var interval = GetInterval(workDays.Count, totalWorks); + + // строим план, дни на которые должны быть назанчены шаблоны + var plan = GetPlan(totalWorks, 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)) + { + templatesByDay[day] = new List(); + } + 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) + { + 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); + } + } + } + + #endregion + + #region Добавляем шаблоны, у которых не было даты — тоже нужно распределить + + foreach (var template in allTemplates) + { + if (!template.NextRun.HasValue) + { + templatesToReassign.Add(template); + logger.LogDebug("Шаблон {TemplateId} не имел даты — будет распределён.", template.Id); + } + } + + #endregion + + #region Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку + + var updatedTemplates = new List(templatesToKeep); + + foreach (var template in templatesToReassign) + { + var nextRun = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDate, expectedDailyLoad); + if (nextRun.HasValue) + { + var day = DateOnly.FromDateTime(nextRun.Value.Date); + currentDailyLoad[day]++; + + updatedTemplates.Add(new TemplateNextRunResultDto + ( + 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: CreateDateTimeOffset(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRunOld: template.NextRun + )); + } + } + + #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; + } + + + /// + /// Заполнить 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 + /// + /// + /// + /// + /// + /// + private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary currentDailyLoad, List workDays, DateTimeOffset referenceDate, Dictionary expectedDailyLoad) + { + foreach (var day in workDays) + { + var expected = expectedDailyLoad.GetValueOrDefault(day, 0); + var current = currentDailyLoad.GetValueOrDefault(day, 0); + + if (current < expected) + { + return CreateDateTimeOffset(day, referenceDate); + } + } + + // Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план + foreach (var day in workDays) + { + var expected = expectedDailyLoad.GetValueOrDefault(day, 0); + var current = currentDailyLoad.GetValueOrDefault(day, 0); + + if (current < expected) + { + return CreateDateTimeOffset(day, referenceDate); + } + } + + // Если все дни уже загружены по плану — ищем день с минимальной нагрузкой + var minLoad = currentDailyLoad.Values.Min(); + var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key; + + return CreateDateTimeOffset(minDay, referenceDate); + } + + + + /// + /// Получить план: дни, на которые должны быть назначены шаблоны + /// + /// + /// + /// + private List GetPlan(int totalWorks, List workDays) + { + var totalDays = workDays.Count(); + + 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); + } + + logger.LogDebug("--- План распределения ---"); + foreach (var item in plan.GroupBy(t => t)) + logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count()); + + return plan; + } + + + /// + /// Рассчитать интервал между шаблонами + /// + /// Кол-во рабочих дней + /// Кол-во шаблонов + /// + private double GetInterval(int totalDays, int totalWorks) + { + var interval = (double)totalDays / totalWorks; + + return interval; + } + + + /// + /// Создать DateTimeOffset (из DateTimeOffset + refDate) + /// + /// Откуда взять дату + /// Откуда взять время + /// + private DateTimeOffset CreateDateTimeOffset(DateTimeOffset date, DateTimeOffset refDate) + { + return CreateDateTimeOffset(DateOnly.FromDateTime(date.Date), refDate); + } + + /// + /// Создать DateTimeOffset (из DateOnly + 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); + } + + + /// + /// Получить дату начала распределения относительно 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); + + // если текущее время > referenceDate, то прибавляем 1 день + var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate); + if (dateTimeStart < DateTimeOffset.UtcNow) + dateStart = dateStart.AddDays(1); + + 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(); + + logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); + foreach (var weekend in weekends) + logger.LogDebug("Не рабочий день: {weekend}", weekend); + + return weekends.ToHashSet(); + } + + + /// + /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. + /// + /// + /// + /// Исключить выходные и праздники + /// + 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; + + while (date <= end) + { + var dayOfWeek = date.DayOfWeek; + if (!weekends.Contains(date)) + { + result.Add(date); + } + 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/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index 8db373a5..c90624fd 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -9,6 +9,8 @@ using PARR.DAL.DomainServices.Implementations; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes; 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; @@ -20,6 +22,7 @@ using PARR.DAL.Services.Interfaces.Schedule; using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.Settings; using PARR.DAL.TransformServices; +using System.ComponentModel; namespace PARR.DAL { @@ -135,6 +138,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 549e8bdc..8daaaec5 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 7b7f6e34..2775212c 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/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs new file mode 100644 index 00000000..c815937f --- /dev/null +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -0,0 +1,124 @@ +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 = 10; + DateTimeOffset referenceDate = new DateTimeOffset(2025, 1, 20, 5, 0, 0, TimeSpan.Zero); + + public async Task Test() + { + 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); + } + + + /// + /// распределить шаблоны + /// + private async Task DistributeTemplatesAsync() + { + // исключать выходные и праздники + var excludeWeekends = true; + + var templates = await templateDistributor.DistributeTemplatesAsync(GetDateStart(), periodDays, referenceDate, GetTemplates(), excludeWeekends); + + //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()) //{ 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