458 lines
20 KiB
C#
458 lines
20 KiB
C#
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<TemplateDistributor> logger;
|
||
private readonly IWeekendDayService weekendDayService;
|
||
|
||
/// <summary>
|
||
/// Смещение nextRun на несколько лет вперед, если не смог его рассчитать
|
||
/// </summary>
|
||
private const int offsetYearsIfErrorCalcNextRun = 5;
|
||
|
||
public TemplateDistributor(
|
||
ILogger<TemplateDistributor> logger,
|
||
IWeekendDayService weekendDayService
|
||
)
|
||
{
|
||
this.logger = logger;
|
||
this.weekendDayService = weekendDayService;
|
||
}
|
||
|
||
|
||
public Task<TemplateNextRunResultDto> GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List<TemplateNextRunDto> allTemplates)
|
||
{
|
||
//TODO:
|
||
throw new NotImplementedException();
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
public async Task<List<TemplateNextRunResultDto>> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List<TemplateNextRunDto> allTemplates)
|
||
{
|
||
var _startDate = GetStartPeriodDate(startDate, referenceDate);
|
||
var _endDate = GetEndPeriodDate(_startDate, duration);
|
||
|
||
logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}", allTemplates.Count, duration, _startDate, _endDate, referenceDate);
|
||
|
||
// Получаем список выходных/праздничных дней
|
||
var weekends = await GetWeekendsAsync(_startDate, _endDate);
|
||
logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count);
|
||
|
||
foreach (var weekend in weekends)
|
||
logger.LogDebug("Не рабочий день: {weekend}", weekend);
|
||
|
||
// Получаем список рабочих дней (исключая выходные и праздники)
|
||
var workDays = GetWorkDays(_startDate, _endDate, weekends);
|
||
|
||
logger.LogDebug("Найдено рабочих дней: {Count}", workDays.Count);
|
||
|
||
foreach (var workDay in workDays)
|
||
logger.LogDebug("Рабочий день: {workDay}", workDay);
|
||
|
||
if (workDays.Count == 0)
|
||
{
|
||
logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun);
|
||
|
||
// смещаем все nextRun вперед
|
||
var result = allTemplates.Select(t => new TemplateNextRunResultDto
|
||
{
|
||
Id = t.Id,
|
||
NextRun = t.NextRun.HasValue
|
||
// если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun
|
||
? CreateNextRun(t.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate)
|
||
// если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun
|
||
: CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
|
||
NextRunOld = t.NextRun
|
||
}).OrderBy(t => t.NextRun).ToList();
|
||
|
||
foreach (var item in result)
|
||
logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id}, nextRun: {NextRun}, nextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld);
|
||
|
||
logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", result.Count, result.Count(t => t.NextRun == t.NextRunOld), result.Count(t => t.NextRun != t.NextRunOld));
|
||
|
||
return result;
|
||
}
|
||
|
||
var totalWorks = allTemplates.Count;
|
||
var totalDays = workDays.Count;
|
||
|
||
// интервал между шаблонами
|
||
var interval = GetInterval(totalDays, totalWorks);
|
||
|
||
// строим план, дни на которые должны быть назанчены шаблоны
|
||
var plan = GetPlan(totalWorks, totalDays, workDays);
|
||
|
||
|
||
// Подсчитываем, сколько работ должно быть на каждый день
|
||
var expectedDailyLoad = new Dictionary<DateOnly, int>();
|
||
|
||
foreach (var day in plan)
|
||
expectedDailyLoad[day] = expectedDailyLoad.GetValueOrDefault(day, 0) + 1;
|
||
|
||
#region Группируем шаблоны по дням, на которые они уже назначены
|
||
|
||
var templatesByDay = new Dictionary<DateOnly, List<TemplateNextRunDto>>();
|
||
|
||
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<TemplateNextRunDto>();
|
||
}
|
||
templatesByDay[day].Add(template);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Подсчитываем текущую нагрузку на каждый день
|
||
|
||
var currentDailyLoad = new Dictionary<DateOnly, int>();
|
||
|
||
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<TemplateNextRunResultDto>();
|
||
var templatesToReassign = new List<TemplateNextRunDto>();
|
||
|
||
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<TemplateNextRunResultDto>(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 = CreateNextRun(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;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad
|
||
/// </summary>
|
||
/// <param name="currentDailyLoad"></param>
|
||
/// <param name="workDays"></param>
|
||
/// <param name="referenceDate"></param>
|
||
/// <param name="expectedDailyLoad"></param>
|
||
/// <returns></returns>
|
||
private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary<DateOnly, int> currentDailyLoad, List<DateOnly> workDays, DateTimeOffset referenceDate, Dictionary<DateOnly, int> expectedDailyLoad)
|
||
{
|
||
foreach (var day in workDays)
|
||
{
|
||
var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
|
||
var current = currentDailyLoad.GetValueOrDefault(day, 0);
|
||
|
||
if (current < expected)
|
||
{
|
||
return CreateNextRun(day, referenceDate);
|
||
}
|
||
}
|
||
|
||
// Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план
|
||
foreach (var day in workDays)
|
||
{
|
||
var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
|
||
var current = currentDailyLoad.GetValueOrDefault(day, 0);
|
||
|
||
if (current < expected)
|
||
{
|
||
return CreateNextRun(day, referenceDate);
|
||
}
|
||
}
|
||
|
||
// Если все дни уже загружены по плану — ищем день с минимальной нагрузкой
|
||
var minLoad = currentDailyLoad.Values.Min();
|
||
var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key;
|
||
|
||
return CreateNextRun(minDay, referenceDate);
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// Получить план: дни, на которые должны быть назначены шаблоны
|
||
/// </summary>
|
||
/// <param name="totalWorks"></param>
|
||
/// <param name="totalDays"></param>
|
||
/// <param name="workDays"></param>
|
||
/// <returns></returns>
|
||
private List<DateOnly> GetPlan(int totalWorks, int totalDays, List<DateOnly> workDays)
|
||
{
|
||
var interval = GetInterval(totalDays, totalWorks);
|
||
|
||
var plan = new List<DateOnly>();
|
||
|
||
for (int i = 0; i < totalWorks; i++)
|
||
{
|
||
#region Вынести в метод
|
||
// Вычисляем индекс дня, на который нужно назначить i-ю работу
|
||
//var dayIndex = (int)Math.Round(i * interval);
|
||
var dayIndex = (int)Math.Floor(i * interval);
|
||
|
||
// Защита от возможного выхода за границы массива из-за округления
|
||
if (dayIndex >= totalDays)
|
||
dayIndex = totalDays - 1;
|
||
#endregion
|
||
|
||
var date = workDays[dayIndex];
|
||
plan.Add(date);
|
||
}
|
||
|
||
return plan;
|
||
// return new HashSet<DateOnly>(plan);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Рассчитать интервал между шаблонами
|
||
/// </summary>
|
||
/// <param name="totalDays">Кол-во рабочих дней</param>
|
||
/// <param name="totalWorks">Кол-во шаблонов</param>
|
||
/// <returns></returns>
|
||
private double GetInterval(int totalDays, int totalWorks)
|
||
{
|
||
var interval = (double)totalDays / totalWorks;
|
||
|
||
return interval;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Создать NextRun (из DateTimeOffset + refDate)
|
||
/// </summary>
|
||
/// <param name="date">Откуда взять дату</param>
|
||
/// <param name="refDate">Откуда взять время</param>
|
||
/// <returns></returns>
|
||
private DateTimeOffset CreateNextRun(DateTimeOffset date, DateTimeOffset refDate)
|
||
{
|
||
return CreateNextRun(DateOnly.FromDateTime(date.Date), refDate);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Создать NextRun (из DateOnly + refDate)
|
||
/// </summary>
|
||
/// <param name="date"></param>
|
||
/// <param name="refDate"></param>
|
||
/// <returns></returns>
|
||
private DateTimeOffset CreateNextRun(DateOnly date, DateTimeOffset refDate)
|
||
{
|
||
return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Получить дату начала распределения относительно StartDate, ReferenceDate и текущей даты
|
||
/// </summary>
|
||
/// <param name="startDate"></param>
|
||
/// <param name="referenceDate"></param>
|
||
/// <returns></returns>
|
||
private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate)
|
||
{
|
||
// дата начала не может быть раньше referenceDate
|
||
// и не может быть раньше чем сегодня
|
||
|
||
var dateStart = startDate;
|
||
|
||
// если dateStart < чем refDate, то берем refDate
|
||
if (dateStart < DateOnly.FromDateTime(referenceDate.Date))
|
||
dateStart = DateOnly.FromDateTime(referenceDate.Date);
|
||
|
||
// если dateStart < чем текущая дата, то берем ьекущую дату
|
||
if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date))
|
||
dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date);
|
||
|
||
return dateStart;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Получить дату конца периода распределения
|
||
/// </summary>
|
||
/// <param name="startDate"></param>
|
||
/// <param name="duration"></param>
|
||
/// <returns></returns>
|
||
private DateOnly GetEndPeriodDate(DateOnly startDate, int duration)
|
||
{
|
||
// duration — количество дней, включая startDate
|
||
// поэтому вычитаем 1, чтобы получить сдвиг
|
||
|
||
return startDate.AddDays(duration - 1);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Получить список выходных дней
|
||
/// </summary>
|
||
/// <param name="startDate"></param>
|
||
/// <param name="endDate"></param>
|
||
/// <returns></returns>
|
||
private async Task<HashSet<DateOnly>> GetWeekendsAsync(DateOnly startDate, DateOnly endDate)
|
||
{
|
||
var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();
|
||
|
||
return weekends.ToHashSet();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники.
|
||
/// </summary>
|
||
/// <param name="start"></param>
|
||
/// <param name="end"></param>
|
||
/// <param name="weekends"></param>
|
||
/// <returns></returns>
|
||
private List<DateOnly> GetWorkDays(DateOnly start, DateOnly end, HashSet<DateOnly> weekends)
|
||
{
|
||
var result = new List<DateOnly>();
|
||
var date = start;
|
||
|
||
while (date <= end)
|
||
{
|
||
var dayOfWeek = date.DayOfWeek;
|
||
if (!weekends.Contains(date))
|
||
{
|
||
result.Add(date);
|
||
}
|
||
date = date.AddDays(1);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
}
|
||
}
|