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