Files
parr_api/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs

458 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}