diff --git a/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs b/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs
index b4a949ea..f94f9291 100644
--- a/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs
+++ b/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs
@@ -2,6 +2,7 @@
using PARR.DAL.Contracts;
using PARR.DAL.DomainModels;
using PARR.DAL.Services.Interfaces;
+using System.Text.Json;
namespace PARR.DAL.NextRunServices.Subservices
{
@@ -67,6 +68,8 @@ namespace PARR.DAL.NextRunServices.Subservices
{
//offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК
+ logger.LogDebug("Начинаю рассчет nextRun по расписанию ЕСПП. Входные данные: date: {referenceDate}, offset: {offset}, esppSchedule: {esppSchedule}", referenceDate, offset, JsonSerializer.Serialize(esppSchedule));
+
var nextRun = referenceDate;
//var finalOffset = new TimeSpan(3, 0, 0) + (offset ?? TimeSpan.Zero);
@@ -74,6 +77,8 @@ namespace PARR.DAL.NextRunServices.Subservices
var offsetReferenceDate = referenceDate.ToOffset(finalOffset);
+ logger.LogDebug("Дата после добавления offset - {offsetReferenceDate}", offsetReferenceDate);
+
////---------
//// Для рассчета по МСК времени, потому что в ЮТС может быть еще ВСК, а по МСК это уже ПНД
//var offsetReferenceDate = referenceDate.ToOffset(new TimeSpan(3, 0, 0));
@@ -107,9 +112,13 @@ namespace PARR.DAL.NextRunServices.Subservices
break;
}
+ logger.LogDebug("Рассчитанная дата, с учетом offset: {nextRun}", nextRun);
+
// Возвращаем обратно в ЮТС
nextRun = nextRun.ToOffset(TimeSpan.Zero);
+ logger.LogDebug("Рассчитанная дата с нулевым offset: {nextRun}", nextRun);
+
return nextRun;
}
diff --git a/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs b/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs
index 82b4a70e..fc4e7d75 100644
--- a/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs
+++ b/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs
@@ -5,56 +5,13 @@
///
internal interface IEsppScheduleTransformService
{
- /////
- ///// Получить следующую дату согласно расписания
- /////
- /////
- /////
- /////
- //DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset lastRun);
-
- /////
- ///// Получить расписание
- /////
- /////
- /////
- /////
- //List GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset lastRun);
-
///
/// Получить следующую дату по jobGroupId
///
///
- ///
+ /// referenceDate или nextRun
/// offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК
///
Task GetNextDateAsync(Guid jobGroupId, DateTimeOffset referenceDate, TimeSpan? offset = null);
-
- /////
- ///// Получить расписание по jobGroupId
- /////
- /////
- /////
- /////
- //Task> GetNextScheduleAsync(Guid jobGroupId, DateTimeOffset lastRun);
-
-
- /////
- ///// Получить следующую дату срабатывания в указанном периоде
- /////
- /////
- /////
- /////
- /////
- //DateTimeOffset GetNextDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod);
-
-
- /////
- ///// Получить дату начала периода распределения относительно опорной даты (Reference Date)
- /////
- /////
- /////
- /////
- //Task GetStartPeriodForDateAsync(Guid jobGroupId, DateTimeOffset date, DateTimeOffset refrenceDate, DistributionPeriodTypeEnum periodType, string distributionPeriod);
}
}
diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs
new file mode 100644
index 00000000..98137dd8
--- /dev/null
+++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs
@@ -0,0 +1,37 @@
+using PARR.DAL.NextRunServices.Models;
+
+namespace PARR.DAL.NextRunServices.Subservices
+{
+ //todo: -> internal
+ public interface ITemplateDistributorV2
+ {
+ ///
+ /// Распределить шаблоны
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends);
+
+ ///
+ /// Получить список выходных дней
+ ///
+ ///
+ ///
+ ///
+ Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate);
+
+ ///
+ /// Получить список рабочих дней
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends);
+ }
+}
diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs
index e93c5dd9..1430c248 100644
--- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs
+++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs
@@ -27,6 +27,9 @@ namespace PARR.DAL.NextRunServices.Subservices
public async Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew)
{
+ //if (offset.HasValue)
+ // referenceDate = referenceDate.ToOffset(offset.Value);
+
//todo: allTemplates - пока не используется. Будет использоваться когда каждый раз будем строить план
// Пока работает так:
@@ -104,6 +107,10 @@ namespace PARR.DAL.NextRunServices.Subservices
public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends)
{
+ //if (offset.HasValue)
+ //referenceDate = referenceDate.ToOffset(offset.Value);
+
+
var _startDate = GetStartPeriodDate(startDate, referenceDate);
var _endDate = GetEndPeriodDate(_startDate, duration);
@@ -417,7 +424,7 @@ namespace PARR.DAL.NextRunServices.Subservices
return CreateDateTimeOffset(minDay, referenceDate);
}
-
+ //---------------------------------------------------------------------------------------------------------------------------------------------------------------------
///
/// Получить план: дни, на которые должны быть назначены шаблоны
@@ -492,7 +499,7 @@ namespace PARR.DAL.NextRunServices.Subservices
///
private DateTimeOffset CreateDateTimeOffset(DateOnly date, DateTimeOffset refDate)
{
- return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero);
+ return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, refDate.Offset);
}
@@ -507,20 +514,61 @@ namespace PARR.DAL.NextRunServices.Subservices
// дата начала не может быть раньше referenceDate
// и не может быть раньше чем сегодня
- var dateStart = startDate;
+ #region old
+ //var dateStart = startDate;
- // если dateStart < чем refDate, то берем refDate
- if (dateStart < DateOnly.FromDateTime(referenceDate.Date))
- dateStart = DateOnly.FromDateTime(referenceDate.Date);
+ //// если 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);
+ //// если dateStart < чем текущая дата, то берем текущую дату
+ //if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date))
+ // dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date);
- // если текущее время > referenceDate, то прибавляем 1 день
+ //// если текущее время > referenceDate, то прибавляем 1 день
+ //var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate);
+ //if (dateTimeStart < DateTimeOffset.UtcNow)
+ // dateStart = dateStart.AddDays(1);
+
+ //return dateStart;
+ #endregion
+
+ // 1. Конвертируем startDate (UTC) в целевой часовой пояс
+ var startDateUtc = new DateTimeOffset(
+ startDate.Year, startDate.Month, startDate.Day,
+ 0, 0, 0,
+ TimeSpan.Zero);
+
+ var startDateInTargetTz = startDateUtc.ToOffset(referenceDate.Offset);
+ var dateStart = DateOnly.FromDateTime(startDateInTargetTz.DateTime);
+
+ logger.LogDebug("startDate UTC ({startDate} 00:00 UTC) конвертирована в целевой часовой пояс {offset}: {dateStart}", startDate, referenceDate.Offset, dateStart);
+
+ // 2. Дата начала не может быть раньше даты из referenceDate (в его часовом поясе)
+ var refDateInTargetTz = DateOnly.FromDateTime(referenceDate.Date);
+ if (dateStart < refDateInTargetTz)
+ {
+ logger.LogDebug("Дата начала {dateStart} раньше даты из referenceDate {refDate}. Используем дату из referenceDate.", dateStart, refDateInTargetTz);
+ dateStart = refDateInTargetTz;
+ }
+
+ // 3. Дата начала не может быть раньше "сегодня" в целевом часовом поясе
+ var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Date);
+ if (dateStart < todayInTargetTz)
+ {
+ logger.LogDebug("Дата начала {dateStart} раньше 'сегодня' в целевом поясе {today}. Используем 'сегодня'.", dateStart, todayInTargetTz);
+ dateStart = todayInTargetTz;
+ }
+
+ // 4. Если момент времени "начало дня в целевом поясе" уже прошёл — добавляем 1 день
var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate);
if (dateTimeStart < DateTimeOffset.UtcNow)
+ {
+ logger.LogDebug("Момент времени {dateTimeStart} уже прошёл (сейчас {now}). Добавляем 1 день.", dateTimeStart, DateTimeOffset.UtcNow);
dateStart = dateStart.AddDays(1);
+ }
+
+ logger.LogDebug("Итоговая дата начала распределения: {dateStart} (в часовом поясе {offset})", dateStart, referenceDate.Offset);
return dateStart;
}
@@ -565,41 +613,44 @@ namespace PARR.DAL.NextRunServices.Subservices
///
///
///
- public async Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate)
+ public async Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TimeSpan? offset = null)
{
+ if (offset.HasValue)
+ referenceDate = referenceDate.ToOffset(offset.Value);
+
// проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd
- bool isPreviousDays = IsPreviousDays(referenceDate);
+ //bool isPreviousDays = IsPreviousDays(referenceDate);
- if (isPreviousDays)
- {
- // Это предыдущие сутки, корректируем
- startDate = startDate.AddDays(-1);
- // так как мы потом будем сдвигать выходные дни влево, то надо взять на один день дальше, чтоб знать какой он
- // вроде бы это не костыль
- endDate = endDate.AddDays(1);
- //endDate = endDate.AddDays(-1);
+ //if (isPreviousDays)
+ //{
+ // // Это предыдущие сутки, корректируем
+ // startDate = startDate.AddDays(-1);
+ // // так как мы потом будем сдвигать выходные дни влево, то надо взять на один день дальше, чтоб знать какой он
+ // // вроде бы это не костыль
+ // endDate = endDate.AddDays(1);
+ // //endDate = endDate.AddDays(-1);
- logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты для получения выходных дней, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, startDate, endDate);
- }
+ // logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты для получения выходных дней, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, startDate, endDate);
+ //}
var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();
var weekendsResult = new List();
- if (isPreviousDays)
- {
- // так как должны быть предыдущие дни, отнимаем 1 день
- foreach (var weekend in weekends)
- {
- var previous = weekend.AddDays(-1);
- logger.LogDebug("Выходной день, -1, так как день должен быть предыдущий, старое значение {weekend}, новое {previous}", weekend, previous);
- weekendsResult.Add(previous);
- }
- }
- else
- {
- weekendsResult = weekends;
- }
+ //if (isPreviousDays)
+ //{
+ // // так как должны быть предыдущие дни, отнимаем 1 день
+ // foreach (var weekend in weekends)
+ // {
+ // var previous = weekend.AddDays(-1);
+ // logger.LogDebug("Выходной день, -1, так как день должен быть предыдущий, старое значение {weekend}, новое {previous}", weekend, previous);
+ // weekendsResult.Add(previous);
+ // }
+ //}
+ //else
+ //{
+ // weekendsResult = weekends;
+ //}
logger.LogDebug("Найдено не рабочих дней: {Count}", weekendsResult.Count);
foreach (var weekend in weekendsResult)
@@ -644,19 +695,22 @@ namespace PARR.DAL.NextRunServices.Subservices
//}
- public async Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate)
+ public async Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate, TimeSpan? offset = null)
{
+ if (offset.HasValue)
+ referenceDate = referenceDate.ToOffset(offset.Value);
+
logger.LogDebug("Формирование списка рабочих дней, start: {start}, end: {end}, excludeWeekends: {excludeWeekends}, referenceDate: {referenceDate}", start, end, excludeWeekends, referenceDate);
- // проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd
- if (IsPreviousDays(referenceDate))
- {
- // Это предыдущие сутки, корректируем
- start = start.AddDays(-1);
- end = end.AddDays(-1);
+ //// проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd
+ //if (IsPreviousDays(referenceDate))
+ //{
+ // // Это предыдущие сутки, корректируем
+ // start = start.AddDays(-1);
+ // end = end.AddDays(-1);
- logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, start, end);
- }
+ // logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, start, end);
+ //}
// Получаем список выходных/праздничных дней
var weekends = new HashSet();
@@ -683,33 +737,45 @@ namespace PARR.DAL.NextRunServices.Subservices
return result;
}
- ///
- /// Это предыдущие сутки? Относительно referenceDate и МСК времени
- ///
- ///
- ///
- private bool IsPreviousDays(DateTimeOffset referenceDate)
+
+
+ public Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate)
{
- // смещение часов относительно UTC - MSK
- int offsetMskHour = 3;
-
- // если время больше 21 часа, то это текущие сутки, если меньше, то прошлые
- int maxHour = 24 - offsetMskHour;
-
- if (referenceDate.Hour >= maxHour && referenceDate.Hour <= 23)
- {
- // это прошлые сутки
- logger.LogDebug("ReferenceDate {refDate} - прошлые стуки", referenceDate);
- return true;
- }
- else
- {
- // это текущие сутки
- logger.LogDebug("ReferenceDate {refDate} - текущие стуки", referenceDate);
- return false;
- }
-
+ throw new NotImplementedException();
}
+ public Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate)
+ {
+ throw new NotImplementedException();
+ }
+
+ /////
+ ///// Это предыдущие сутки? Относительно referenceDate и МСК времени
+ /////
+ /////
+ /////
+ //private bool IsPreviousDays(DateTimeOffset referenceDate)
+ //{
+ // // смещение часов относительно UTC - MSK
+ // int offsetMskHour = 3;
+
+ // // если время больше 21 часа, то это текущие сутки, если меньше, то прошлые
+ // int maxHour = 24 - offsetMskHour;
+
+ // if (referenceDate.Hour >= maxHour && referenceDate.Hour <= 23)
+ // {
+ // // это прошлые сутки
+ // logger.LogDebug("ReferenceDate {refDate} - прошлые стуки", referenceDate);
+ // return true;
+ // }
+ // else
+ // {
+ // // это текущие сутки
+ // logger.LogDebug("ReferenceDate {refDate} - текущие стуки", referenceDate);
+ // return false;
+ // }
+
+ //}
+
}
}
diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs
new file mode 100644
index 00000000..d9f76269
--- /dev/null
+++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs
@@ -0,0 +1,495 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using PARR.DAL.NextRunServices.Models;
+using PARR.DAL.Services.Interfaces;
+
+namespace PARR.DAL.NextRunServices.Subservices
+{
+ ///
+ /// Распределитель шаблонов с поддержкой часовых поясов.
+ /// Все входные даты интерпретируются в целевом часовом поясе (параметр offset).
+ /// Все результаты возвращаются строго в UTC (TimeSpan.Zero).
+ ///
+ internal class TemplateDistributorV2 : ITemplateDistributorV2
+ {
+ private readonly ILogger logger;
+ private readonly IWeekendDayService weekendDayService;
+
+ ///
+ /// Смещение nextRun на несколько лет вперед, если не смог его рассчитать
+ ///
+ private const int offsetYearsIfErrorCalcNextRun = 5;
+
+ public TemplateDistributorV2(
+ ILogger logger,
+ IWeekendDayService weekendDayService
+ )
+ {
+ this.logger = logger;
+ this.weekendDayService = weekendDayService;
+ }
+
+ public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends)
+ {
+ // 1. Определяем целевой часовой пояс
+ var targetOffset = offset ?? TimeSpan.Zero;
+
+ // 2. Применяем оффсет к referenceDate для расчётов в целевом поясе
+ var referenceDateInTargetTz = referenceDate.ToOffset(targetOffset);
+
+ logger.LogDebug("Распределение шаблонов в часовом поясе {offset}. ReferenceDate в целевом поясе: {refDate}", targetOffset, referenceDateInTargetTz);
+
+ // 3. Рассчитываем период распределения С УЧЁТОМ ЧАСОВОГО ПОЯСА
+ var _startDate = GetStartPeriodDate(startDate, referenceDateInTargetTz, targetOffset);
+ var _endDate = GetEndPeriodDate(_startDate, duration);
+
+ logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDateInTargetTz} в целевом поясе: {targetOffset}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDateInTargetTz, targetOffset, excludeWeekends);
+
+ // 4. Получаем рабочие дни (календарные даты, не зависят от пояса)
+ var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends);
+
+ if (workDays.Count == 0)
+ {
+ logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate} (пояс {offset}). Возвращаем шаблоны со смещением {years} лет.", _startDate, _endDate, targetOffset, offsetYearsIfErrorCalcNextRun);
+
+ // смещаем все nextRun вперед
+ var errorResult = allTemplates.Select(t =>
+ FillTemplateResultWithOffsetError(t, _startDate, referenceDateInTargetTz, targetOffset)
+ ).OrderBy(t => t.NextRun).ToList();
+
+ logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", errorResult.Count, errorResult.Count(t => t.NextRun == t.NextRunOld), errorResult.Count(t => t.NextRun != t.NextRunOld));
+
+ return errorResult;
+ }
+
+
+ // 5. Строим план распределения
+ // строим план, дни на которые должны быть назанчены шаблоны
+ var plan = GetPlan(allTemplates.Count, workDays);
+ // Подсчитываем, сколько работ должно быть на каждый день
+ var expectedDailyLoad = plan.GroupBy(d => d)
+ .ToDictionary(g => g.Key, g => g.Count());
+
+ #region Группируем шаблоны по дням В ЦЕЛЕВОМ ПОЯСЕ, на которые они уже назначены
+
+ var templatesByDay = new Dictionary>();
+
+ foreach (var template in allTemplates)
+ {
+ if (template.NextRun.HasValue)
+ {
+ // КОНВЕРТИРУЕМ В ЦЕЛЕВОЙ ПОЯС ДЛЯ ОПРЕДЕЛЕНИЯ ДАТЫ!
+ var dayInTargetTz = DateOnly.FromDateTime(template.NextRun.Value.ToOffset(targetOffset).Date);
+ //var day = DateOnly.FromDateTime(template.NextRun.Value.Date);
+
+ if (!templatesByDay.ContainsKey(dayInTargetTz))
+ {
+ templatesByDay[dayInTargetTz] = new List();
+ }
+ templatesByDay[dayInTargetTz].Add(template);
+
+ logger.LogDebug("Шаблон {Id} с NextRun={nextRun} отнесён к дате {day} в поясе {offset}", template.Id, template.NextRun.Value, dayInTargetTz, targetOffset);
+ }
+ }
+
+ #endregion
+
+ #region 7. Подсчитываем текущую нагрузку на каждый день
+
+ var currentDailyLoad = workDays.ToDictionary(day => day, _ => 0);
+
+ foreach (var kvp in templatesByDay)
+ {
+ if (currentDailyLoad.ContainsKey(kvp.Key))
+ currentDailyLoad[kvp.Key] = kvp.Value.Count;
+ }
+
+ #endregion
+
+ #region 8. Определяем, какие шаблоны нужно оставить, а какие — переназначить
+
+ 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)
+ {
+ // КОНВЕРТИРУЕМ В UTC ПЕРЕД СОХРАНЕНИЕМ!
+ templatesToKeep.Add(new TemplateNextRunResultDto
+ (
+ Id: template.Id,
+ NextRun: template.NextRun.Value.ToOffset(TimeSpan.Zero), // ← В UTC!
+ NextRunOld: template.NextRun
+ ));
+ logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun} (UTC), т.к. нагрузка в день не превышена.", template.Id, template.NextRun.Value.ToOffset(TimeSpan.Zero));
+ }
+ }
+ else
+ {
+ // Оставляем только expectedCount штук, остальные — переназначить
+ for (int i = 0; i < expectedCount; i++)
+ {
+ var template = templates[i];
+ // КОНВЕРТИРУЕМ В UTC ПЕРЕД СОХРАНЕНИЕМ!
+ templatesToKeep.Add(new TemplateNextRunResultDto
+ (
+ Id: template.Id,
+ NextRun: template.NextRun.Value.ToOffset(TimeSpan.Zero), // ← В UTC!
+ NextRunOld: template.NextRun
+ ));
+ logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun} (UTC), т.к. нагрузка в день не превышена.", template.Id, template.NextRun.Value.ToOffset(TimeSpan.Zero));
+ }
+
+ for (int i = expectedCount; i < currentCount; i++)
+ {
+ var template = templates[i];
+ templatesToReassign.Add(template);
+ logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. нагрузка в день превышена.", template.Id);
+ }
+ }
+ }
+
+ #endregion
+
+ #region 9. Добавляем шаблоны, у которых не было даты — тоже нужно распределить
+
+ foreach (var template in allTemplates)
+ {
+ if (!template.NextRun.HasValue)
+ {
+ templatesToReassign.Add(template);
+ logger.LogDebug("Шаблон {TemplateId} не имел даты — будет распределён.", template.Id);
+ }
+ }
+
+ #endregion
+
+ #region 10. Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку
+
+ var updatedTemplates = new List(templatesToKeep);
+
+ foreach (var template in templatesToReassign)
+ {
+ var nextRunInTargetTz = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDateInTargetTz, expectedDailyLoad, targetOffset);
+ if (nextRunInTargetTz.HasValue)
+ {
+ var day = DateOnly.FromDateTime(nextRunInTargetTz.Value.Date);
+ currentDailyLoad[day]++;
+
+ // КОНВЕРТИРУЕМ В UTC ПЕРЕД ВОЗВРАТОМ!
+ updatedTemplates.Add(new TemplateNextRunResultDto
+ (
+ Id: template.Id,
+ NextRun: nextRunInTargetTz.Value.ToOffset(TimeSpan.Zero), // ← В UTC!
+ NextRunOld: template.NextRun
+ ));
+
+ logger.LogDebug("Шаблону {Id} назначен NextRun: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {NextRunOld}", template.Id, nextRunInTargetTz.Value, targetOffset, nextRunInTargetTz.Value.ToOffset(TimeSpan.Zero), template.NextRun);
+ }
+ else
+ {
+ logger.LogWarning("Не удалось назначить дату для шаблона {TemplateId}", template.Id);
+
+ // Возвращаем со смещением на несколько лет, так как ошибка
+ // конвертируем в UTC
+ updatedTemplates.Add(FillTemplateResultWithOffsetError(template, _startDate, referenceDateInTargetTz, targetOffset));
+ }
+ }
+
+ #endregion
+
+ // 11. Сортируем и возвращаем результат в UTC
+
+ var result = updatedTemplates.OrderBy(t => t.NextRun).ToList();
+
+ logger.LogDebug("--- Результат распределения ---");
+ foreach (var item in result)
+ logger.LogDebug("Шаблон {templateId}, NextRun UTC: {NextRun}, NextRunOld UTC: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld);
+
+ logger.LogInformation("Распределение завершено. Возвращаем {Count} записей в UTC. Изменено: {changedCount}. Остались прежние значения: {oldCount}", result.Count, result.Count(t => t.NextRun != t.NextRunOld), result.Count(t => t.NextRun == t.NextRunOld));
+
+ return result;
+ }
+
+
+
+ ///
+ /// Заполнить TemplateNextRunResultDto, если нет рабочих дней со смещением несколько лет
+ ///
+ ///
+ ///
+ private TemplateNextRunResultDto FillTemplateResultWithOffsetError(TemplateNextRunDto template, DateOnly startDate, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset)
+ {
+ // Создаём дату в целевом поясе
+ var errorDateInTargetTz = new DateTimeOffset(
+ startDate.AddYears(offsetYearsIfErrorCalcNextRun).Year,
+ startDate.AddYears(offsetYearsIfErrorCalcNextRun).Month,
+ startDate.AddYears(offsetYearsIfErrorCalcNextRun).Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+
+ // КОНВЕРТИРУЕМ В UTC ДЛЯ ВОЗВРАТА
+ var errorDateUtc = errorDateInTargetTz.ToOffset(TimeSpan.Zero);
+
+ logger.LogWarning("Шаблон {Id}: нет рабочих дней в периоде, сдвиг на {years} лет → {nextRun} (UTC)", template.Id, offsetYearsIfErrorCalcNextRun, errorDateUtc);
+
+ return new TemplateNextRunResultDto(
+ Id: template.Id,
+ NextRun: errorDateUtc,
+ NextRunOld: template.NextRun
+ );
+ }
+
+
+ ///
+ /// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary currentDailyLoad, List workDays, DateTimeOffset referenceDateInTargetTz, Dictionary expectedDailyLoad, TimeSpan targetOffset)
+ {
+ foreach (var day in workDays)
+ {
+ var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
+ var current = currentDailyLoad.GetValueOrDefault(day, 0);
+
+ if (current < expected)
+ {
+ // Создаём момент времени В ЦЕЛЕВОМ ПОЯСЕ
+ return new DateTimeOffset(
+ day.Year, day.Month, day.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+ }
+ }
+
+ // Если все дни загружены — берём день с минимальной нагрузкой
+ var minDay = currentDailyLoad.MinBy(x => x.Value).Key;
+ return new DateTimeOffset(
+ minDay.Year, minDay.Month, minDay.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+ }
+
+
+
+
+
+
+ ///
+ /// Получить план: дни, на которые должны быть назначены шаблоны
+ ///
+ ///
+ ///
+ ///
+ 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;
+
+ logger.LogDebug("Интевал между шаблонами: {interval}", interval);
+
+ return interval;
+ }
+
+
+ ///
+ /// Получить дату начала распределения с учётом целевого часового пояса.
+ /// Метод возвращает календарную дату (DateOnly), рассчитанную в контексте указанного оффсета.
+ ///
+ /// Стартовая дата (интерпретируется в целевом часовом поясе)
+ /// Момент времени в целевом часовом поясе (после применения оффсета)
+ /// Целевой часовой пояс (оффсет)
+ /// Дата начала распределения
+ private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate, TimeSpan offset)
+ {
+ var dateStart = startDate;
+
+ // 1. Дата начала не может быть раньше даты из referenceDate (в целевом поясе)
+ var refDateInTargetTz = DateOnly.FromDateTime(referenceDate.Date);
+ if (dateStart < refDateInTargetTz)
+ {
+ logger.LogDebug("Дата начала {startDate} раньше даты из referenceDate в поясе {offset} ({refDate}). Используем {refDate}.",
+ dateStart, offset, refDateInTargetTz, refDateInTargetTz);
+ dateStart = refDateInTargetTz;
+ }
+
+ // 2. Дата начала не может быть раньше "сегодня" в целевом часовом поясе
+ var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).Date);
+ if (dateStart < todayInTargetTz)
+ {
+ logger.LogDebug("Дата начала {startDate} раньше 'сегодня' в поясе {offset} ({today}). Используем {today}.",
+ dateStart, offset, todayInTargetTz, todayInTargetTz);
+ dateStart = todayInTargetTz;
+ }
+
+ // 3. Если момент времени "начало дня в целевом поясе + время из referenceDate" уже прошёл — добавляем 1 день
+ // ВАЖНО: время берём из самого referenceDate (который уже в целевом поясе!)
+ var dateTimeStart = new DateTimeOffset(
+ dateStart.Year, dateStart.Month, dateStart.Day,
+ referenceDate.Hour, // ← Время из целевого пояса!
+ referenceDate.Minute,
+ referenceDate.Second,
+ offset); // ← Оффсет целевого пояса
+
+ // Сравниваем абсолютные моменты времени (работает корректно даже с разными оффсетами)
+ if (dateTimeStart < DateTimeOffset.UtcNow)
+ {
+ logger.LogDebug("Момент времени {dateTimeStart} (в поясе {offset}) уже прошёл (сейчас {now}). Добавляем 1 день.",
+ dateTimeStart, offset, DateTimeOffset.UtcNow);
+ dateStart = dateStart.AddDays(1);
+ }
+
+ logger.LogDebug("Итоговая дата начала распределения в поясе {offset}: {dateStart}", offset, dateStart);
+ return dateStart;
+ }
+
+
+ ///
+ /// Получить дату окончания периода распределения.
+ /// Продолжительность включает стартовый день (например, duration=7 означает 7 дней: с 1 по 7 июня).
+ ///
+ ///
+ ///
+ ///
+ private DateOnly GetEndPeriodDate(DateOnly startDate, int duration)
+ {
+ if (duration <= 0)
+ {
+ logger.LogWarning("Некорректная продолжительность периода: {duration}. Используем минимальное значение 1.", duration);
+ duration = 1;
+ }
+
+ // duration — количество дней, включая startDate
+ // поэтому вычитаем 1, чтобы получить сдвиг
+
+ var endDate = startDate.AddDays(duration - 1);
+ logger.LogDebug("Расчёт даты окончания периода: startDate={startDate}, duration={duration}, endDate={endDate}", startDate, duration, endDate);
+
+ return endDate;
+ }
+
+
+ ///
+ /// Получить список выходных дней
+ ///
+ ///
+ ///
+ ///
+ public 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();
+ }
+
+
+ ///
+ /// Получить список рабочих дней в диапазоне, исключая выходные и праздники.
+ ///
+ ///
+ ///
+ /// Исключить выходные и праздники
+ ///
+ public 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 21a686e1..0d12389e 100644
--- a/PARR.DAL/ParrDalInstaller.cs
+++ b/PARR.DAL/ParrDalInstaller.cs
@@ -141,6 +141,7 @@ namespace PARR.DAL
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
#endregion
diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs
index afaa3c1a..377468d1 100644
--- a/PARR.Test/NextRun/NextRunTest.cs
+++ b/PARR.Test/NextRun/NextRunTest.cs
@@ -14,20 +14,38 @@ namespace PARR.Test.NextRun
private readonly INextRunService nextRunService;
private readonly ITemplateService templateService;
private readonly IShortcodesService shortcodesService;
+ private readonly ITemplateDistributorV2 templateDistributorV2;
- public NextRunTest(/*ITemplateDistributor templateDistributor, */INextRunService nextRunService, ITemplateService templateService, IShortcodesService shortcodesService)
+ public NextRunTest(
+ /*ITemplateDistributor templateDistributor, */
+ INextRunService nextRunService,
+ ITemplateService templateService,
+ IShortcodesService shortcodesService,
+ ITemplateDistributorV2 templateDistributorV2
+ )
{
//this.templateDistributor = templateDistributor;
this.nextRunService = nextRunService;
this.templateService = templateService;
this.shortcodesService = shortcodesService;
+ this.templateDistributorV2 = templateDistributorV2;
}
- int periodDays = 10;
- DateTimeOffset referenceDate = new DateTimeOffset(2025, 1, 20, 3, 0, 0, TimeSpan.Zero);
+
public async Task Test()
{
+ var duration = 10;
+ var offset = TimeSpan.FromHours(10);
+
+ var startDate = new DateOnly(2026, 2, 10);
+ var referenceDate = new DateTimeOffset(2026, 2, 10, 4, 0, 0, TimeSpan.Zero);
+
+ var templates = GetTemplates();
+
+ var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true);
+
+
//var template = await templateService.Get().Include(t => t.Job).ThenInclude(t => t.Group).FirstAsync(t => t.Name == "ЭИТИ-ПАРР_ЦКИТ-ГВЦ_СХД ТО-1_СХД-AERODISK-432-2-4-U26-EN4SAG022-ГВЦ");
//var responseAreae = await shortcodesService.ApplyShortcodesAsync(template.Job.ResponseAreaMask, template);
//var nextRunWithTimezone = nextRunService.GetNextRunWithTimezoneEsppAndResponseArea(template.NextRun, template.Job?.Group?.IsResponseAreaTimezone, responseAreae);
@@ -117,14 +135,14 @@ namespace PARR.Test.NextRun
}
- private DateOnly GetDateStart()
- {
- var today = DateOnly.FromDateTime(DateTime.UtcNow.Date);
+ //private DateOnly GetDateStart()
+ //{
+ // var today = DateOnly.FromDateTime(DateTime.UtcNow.Date);
- var refDate = DateOnly.FromDateTime(referenceDate.Date);
+ // var refDate = DateOnly.FromDateTime(referenceDate.Date);
- return today < refDate ? refDate : today;
- }
+ // return today < refDate ? refDate : today;
+ //}
//private DateOnly GetDateEnd()
//{
@@ -143,7 +161,7 @@ namespace PARR.Test.NextRun
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("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun: new DateTimeOffset(2026, 2, 13, 4, 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)));