432 lines
24 KiB
C#
432 lines
24 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using PARR.BLL.Services.Interfaces;
|
||
using PARR.Constants;
|
||
using PARR.DAL.Models;
|
||
using PARR.DAL.Services.Interfaces;
|
||
using PARR.DAL.TransformServices;
|
||
|
||
namespace PARR.TemplateDistributor
|
||
{
|
||
internal class TemplateDistributor : ITemplateDistributor
|
||
{
|
||
private readonly ILogger<TemplateDistributor> logger;
|
||
private readonly ITemplateService templateService;
|
||
private readonly IApplicationsInWorkService applicationsInWorkService;
|
||
private readonly ICalendarService calendarService;
|
||
private readonly IEsppScheduleTransformService esppScheduleTransformService;
|
||
private readonly IWeekendDayService weekendDayService;
|
||
private readonly INextRunModifierService nextRunModifierService;
|
||
|
||
public TemplateDistributor(
|
||
ILogger<TemplateDistributor> logger,
|
||
ITemplateService templateService,
|
||
IApplicationsInWorkService applicationsInWorkService,
|
||
ICalendarService calendarService,
|
||
IEsppScheduleTransformService esppScheduleTransformService,
|
||
IWeekendDayService weekendDayService,
|
||
INextRunModifierService nextRunModifierService
|
||
)
|
||
{
|
||
this.logger = logger;
|
||
this.templateService = templateService;
|
||
this.applicationsInWorkService = applicationsInWorkService;
|
||
this.calendarService = calendarService;
|
||
this.esppScheduleTransformService = esppScheduleTransformService;
|
||
this.weekendDayService = weekendDayService;
|
||
this.nextRunModifierService = nextRunModifierService;
|
||
}
|
||
|
||
|
||
public async Task UpdateScheduleAsync(Guid applicationInWorkId)
|
||
{
|
||
var appInWork = await applicationsInWorkService.GetAsync(applicationInWorkId);
|
||
|
||
var templates = await templateService.Get()
|
||
.Include(t => t.Host)
|
||
.ThenInclude(h => h!.WorkGroup)
|
||
.Where(t => t.ApplicationInWorkId == applicationInWorkId && t.Host!.WorkGroupId != null)
|
||
.ToListAsync();
|
||
|
||
//Группируем шаблоны по рабочим группам
|
||
var workGroupsWithTemplates = templates.GroupBy(t => t.Host!.WorkGroupId);
|
||
|
||
foreach (var workGroupWithTemplates in workGroupsWithTemplates)
|
||
{
|
||
var wgId = (Guid)workGroupWithTemplates.Key!;
|
||
var values = workGroupWithTemplates.ToList();
|
||
|
||
//Обновляем сразу время при необходимости, так как дальше будем работать только с датой
|
||
var timesList = values.Select(t => t.NextRun.TimeOfDay).Distinct().ToList();
|
||
if (timesList.Count() > 1 || (timesList.Count() == 1 && timesList[0] != appInWork!.ReferenceDate.TimeOfDay))
|
||
{
|
||
values.ForEach(t =>
|
||
{
|
||
if (t.NextRun.TimeOfDay != appInWork!.ReferenceDate.TimeOfDay)
|
||
t.NextRun = new DateTimeOffset(t.NextRun.Year, t.NextRun.Month, t.NextRun.Day,
|
||
appInWork!.ReferenceDate.Hour, appInWork!.ReferenceDate.Minute, appInWork!.ReferenceDate.Second,
|
||
new TimeSpan(0, 0, 0));
|
||
});
|
||
if (!await templateService.CommitAsync())
|
||
{
|
||
logger.LogError($"Ошибка записи изменений в БД при актуализации времени NextRun шаблонов РР({applicationInWorkId})");
|
||
return;
|
||
}
|
||
}
|
||
|
||
var distrTemplates = await DistributeTemplateAsync(new List<Template>(), applicationInWorkId, wgId);
|
||
|
||
if (distrTemplates.Any())
|
||
if (!await templateService.CommitAsync())
|
||
logger.LogError($"Ошибка записи изменений в БД при перераспределении NextRun шаблонов РР({applicationInWorkId})");
|
||
}
|
||
}
|
||
|
||
|
||
public async Task<List<Template>> DistributeTemplateAsync(List<Template> templates, Guid applicationInWorkId, Guid workGroupId)
|
||
{
|
||
var appInWork = await applicationsInWorkService.Get()
|
||
.Include(aiw => aiw.EsppSchValues)
|
||
.ThenInclude(esv => esv.EsppSchTypeValue)
|
||
.ThenInclude(etv => etv!.DistributionPeriod)
|
||
.FirstAsync(t => t.Id == applicationInWorkId);
|
||
|
||
var refDate = appInWork.ReferenceDate;
|
||
|
||
//Проверяем наличие распределения РР на период
|
||
if (appInWork.IsAutoDistributionEnabled)
|
||
{
|
||
var existingTemplates = await templateService.Get()
|
||
.Include(t => t.Host)
|
||
.Where(t => t.ApplicationInWorkId == applicationInWorkId && t.Host!.WorkGroupId == workGroupId)
|
||
.ToListAsync();
|
||
|
||
//Удалим из входных шаблонов уже существующие в БД (по идее, такого никогда не должно случиться)
|
||
var duplicates = templates.Select(t => t.Id).Where(t => existingTemplates.Any(et => et.Id == t)).ToList();
|
||
if (duplicates.Count > 0)
|
||
{
|
||
duplicates.ForEach(t =>
|
||
{
|
||
logger.LogWarning($"Для распределения AiW({applicationInWorkId}),workGroupId({workGroupId}) передан существующий в базе данных шаблон {t}");
|
||
});
|
||
templates.RemoveAll(t => duplicates.Any(dt => dt == t.Id));
|
||
}
|
||
|
||
|
||
existingTemplates.AddRange(templates);
|
||
|
||
//Если распределенная РР получаем начало
|
||
var period = appInWork.EsppSchValues.First()!.EsppSchTypeValue!.DistributionPeriod;
|
||
var periodType = ParseDistributionPeriodType(period!.Type);
|
||
|
||
var workDays = await GetWorkDaysAsync(appInWork);
|
||
|
||
var result = new List<Template>();
|
||
|
||
//распределяем РР отдельно активированные
|
||
var activatedTemplates = existingTemplates.Where(t => t.IsActiveTemplate && t.IsActiveSchedule).ToList();
|
||
if (activatedTemplates.Count > 0)
|
||
{
|
||
Distribute(ref activatedTemplates, refDate, period, periodType, workDays);
|
||
// result.AddRange(activatedTemplates);
|
||
}
|
||
|
||
//распределяем РР отдельно деактивированные
|
||
var deactivatedTemplates = existingTemplates.Where(t =>
|
||
!t.IsActiveTemplate || !t.IsActiveSchedule || (
|
||
!t.IsActiveTemplate && !t.IsActiveSchedule
|
||
)).ToList();
|
||
if (deactivatedTemplates.Count > 0)
|
||
{
|
||
Distribute(ref deactivatedTemplates, refDate, period, periodType, workDays);
|
||
// result.AddRange(deactivatedTemplates);
|
||
}
|
||
|
||
|
||
if (templates.Any())
|
||
{
|
||
// это была генерация шаблонов, не нужны все шаблоны, нужны только те которые передал для получения NextRun
|
||
result.AddRange(templates);
|
||
}
|
||
else
|
||
{
|
||
// это было распределение шаблонов, добавляем активированные и деактивированные
|
||
result.AddRange(activatedTemplates);
|
||
result.AddRange(deactivatedTemplates);
|
||
}
|
||
|
||
return result;
|
||
#region comments
|
||
|
||
//var d = new Dictionary<DateOnly, List<Template>>();
|
||
//foreach (var wd in workDays)
|
||
//{
|
||
// var wrokDateTimeOffset = new DateTimeOffset(
|
||
// wd.Year, wd.Month, wd.Day,
|
||
// refDate.Hour, refDate.Minute, refDate.Second,
|
||
// new TimeSpan(0, 0, 0));
|
||
// if (wrokDateTimeOffset < DateTimeOffset.UtcNow)
|
||
// wrokDateTimeOffset = esppScheduleTransformService.GetNextDateForDistributionRun(wrokDateTimeOffset, periodType, period.Duration);
|
||
|
||
// var assignedTemplates = existingTemplates.Where(t => t.NextRun == wrokDateTimeOffset).ToList();
|
||
// d.Add(wd, assignedTemplates);
|
||
//}
|
||
|
||
//var templatesPerStep = (double)templates.Count() / workDays.Count();//
|
||
//var currentTemplateStep = (int)Math.Ceiling(templatesPerStep);
|
||
//var delta = templatesPerStep - currentTemplateStep;
|
||
////templates.First().NextRun = currentDay;
|
||
//var templateDistributed = 0;
|
||
|
||
////Отталкиваясь от количества
|
||
//while (templateDistributed < templates.Count())
|
||
//{
|
||
// var wdCounts = d.Select(wd => wd.Value).OrderBy(count => count).ToArray();
|
||
// var templateCountDelta = d.Max(t => t.Value.Count()) - d.Min(t => t.Value.Count());
|
||
// var addingTemplatesCount = wdCounts.Length > 1 ?
|
||
// (templateCountDelta == 0) ? (int)Math.Ceiling(templatesPerStep) : templateCountDelta :
|
||
// (int)Math.Ceiling(templatesPerStep);
|
||
// var workDaysWithMinTemplates = d.Where(wd => wd.Value == wdCounts[0]).ToArray();
|
||
|
||
// if (workDaysWithMinTemplates.Length * addingTemplatesCount < templates.Count())
|
||
// {
|
||
// foreach (var wd in workDaysWithMinTemplates)
|
||
// {
|
||
// //Считаем DateTimeOffset, потому что у нас только дата пока
|
||
// var currentDay = new DateTimeOffset(
|
||
// wd.Key.Year, wd.Key.Month, wd.Key.Day,
|
||
// refDate.Hour, refDate.Minute, refDate.Second,
|
||
// new TimeSpan(0, 0, 0));
|
||
|
||
// templates.Skip(templateDistributed).Take(addingTemplatesCount).ToList().ForEach(t =>
|
||
// {
|
||
// t.NextRun = currentDay;
|
||
// d[wd.Key].Add(t);
|
||
// });
|
||
// templateDistributed += addingTemplatesCount;
|
||
|
||
// }
|
||
// }
|
||
// else
|
||
// {
|
||
// while (templateDistributed < templates.Count())
|
||
// {
|
||
// var wd = d.Where(wd => wd.Value == d.Min(t => t.Value)).First();
|
||
// //Считаем DateTimeOffset, потому что у нас только дата пока
|
||
// var currentDay = new DateTimeOffset(
|
||
// wd.Key.Year, wd.Key.Month, wd.Key.Day,
|
||
// refDate.Hour, refDate.Minute, refDate.Second,
|
||
// new TimeSpan(0, 0, 0));
|
||
|
||
// templates.Skip(templateDistributed).Take(addingTemplatesCount).ToList().ForEach(t =>
|
||
// {
|
||
// t.NextRun = currentDay;
|
||
// d[wd.Key].Add(t);
|
||
// });
|
||
// templateDistributed += addingTemplatesCount;
|
||
// }
|
||
// }
|
||
//}
|
||
|
||
#endregion
|
||
}
|
||
else
|
||
// templates.ForEach(t => t.NextRun = appInWork!.ReferenceDate);
|
||
// это не автораспределение, по ReferenceDate получаем ближайший рабочий день
|
||
templates.ForEach(async t => t.NextRun = await nextRunModifierService.GetWorkDayAsync(appInWork!.ReferenceDate));
|
||
|
||
return templates;
|
||
}
|
||
|
||
private void Distribute(ref List<Template> templates, DateTimeOffset refDate, DistributionPeriod period, DistributionPeriodTypeEnum periodType, List<DateOnly> workDays)
|
||
{
|
||
//Готовим план распределения
|
||
var distrPlan = GetDistributionPlan(workDays, templates.Count);
|
||
|
||
//Проверяем шаблоны уже распределённые, чтобы не перемещать лишний раз
|
||
var templateToDistrib = GetTemplatesToDistribute(ref distrPlan, templates);
|
||
|
||
//logger.LogInformation($"{GetType().Name}(AppInWId:{templates.Select(t => t.ApplicationInWorkId).First()}, WorkGroup:{templates.Select(t => t.Host!.WorkGroup!.Name).First()}) запланировал изменение даты следующего срабатывания у {templateToDistrib.Count()} существующих шаблона(ов), {templates.Count() - templateToDistrib.Count()} остались без изменений");
|
||
string? loggerWorkGroup = templates.FirstOrDefault()?.Host?.WorkGroup != null ? templates.FirstOrDefault()?.Host?.WorkGroup?.Name : templates.FirstOrDefault()?.Host?.WorkGroupId?.ToString();
|
||
logger.LogInformation($"{GetType().Name}(AppInWId:{templates.Select(t => t.ApplicationInWorkId).First()}, " +
|
||
$"WorkGroup:{loggerWorkGroup}) " +
|
||
$"запланировал изменение даты следующего срабатывания у {templateToDistrib.Count()} существующих шаблона(ов), {templates.Count() - templateToDistrib.Count()} остались без изменений");
|
||
|
||
if (!templateToDistrib.Any())
|
||
return;
|
||
|
||
var distributedTemplates = 0;
|
||
//Распределяем шаблоны в соответствии с планом непосредственно
|
||
foreach (var workDay in distrPlan)
|
||
{
|
||
var templatesCountForCurDay = workDay.Value;
|
||
if (templatesCountForCurDay < 1)
|
||
continue;
|
||
|
||
templateToDistrib.Skip(distributedTemplates).Take(templatesCountForCurDay).ToList().ForEach(t =>
|
||
{
|
||
var nextRun = new DateTimeOffset(
|
||
workDay.Key.Year, workDay.Key.Month, workDay.Key.Day,
|
||
refDate.Hour, refDate.Minute, refDate.Second,
|
||
new TimeSpan(0, 0, 0));
|
||
if (nextRun < DateTimeOffset.UtcNow)
|
||
nextRun = esppScheduleTransformService.GetNextDateForDistributionRun(nextRun, periodType, period.Duration);
|
||
|
||
t.NextRun = nextRun;
|
||
});
|
||
|
||
distributedTemplates += templatesCountForCurDay;
|
||
|
||
distrPlan[workDay.Key] -= templatesCountForCurDay;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить список рабочих дней из календаря и перенести прошедшие даты на следующий период по РР
|
||
/// </summary>
|
||
/// <param name="appInWork"></param>
|
||
/// <returns></returns>
|
||
private async Task<List<DateOnly>> GetWorkDaysAsync(ApplicationsInWork appInWork)
|
||
{
|
||
var period = appInWork.EsppSchValues.FirstOrDefault()?.EsppSchTypeValue?.DistributionPeriod;
|
||
var periodType = ParseDistributionPeriodType(period!.Type);
|
||
|
||
//старт период, берем сейчас, будем получать рабочие дни с вычетом выходных на весь период сразу.
|
||
var startPeriod = DateOnly.FromDateTime(DateTimeOffset.UtcNow.Date);
|
||
// старт период считаем относительно refDate
|
||
//var startPeriod = await esppScheduleTransformService.GetStartPeriodForDateAsync(appInWork.Id, appInWork.ReferenceDate, appInWork.ReferenceDate, periodType, period.Duration);
|
||
|
||
var workDays = calendarService.GetWorkDatesForPeriod(startPeriod, TimeOnly.FromDateTime(appInWork.ReferenceDate.DateTime), periodType, period.Duration, weekendDayService.GetWeekends);
|
||
|
||
#region старая логика
|
||
//var resultWorkDays = new List<DateOnly>();
|
||
|
||
//workDays.ForEach(wd =>
|
||
//{
|
||
// var wrokDateTimeOffset = new DateTimeOffset(
|
||
// wd.Year, wd.Month, wd.Day,
|
||
// appInWork.ReferenceDate.Hour, appInWork.ReferenceDate.Minute, appInWork.ReferenceDate.Second,
|
||
// new TimeSpan(0, 0, 0));
|
||
|
||
// if (wrokDateTimeOffset < DateTimeOffset.UtcNow)
|
||
// {
|
||
// wrokDateTimeOffset = esppScheduleTransformService.GetNextDateForDistributionRun(wrokDateTimeOffset, periodType, period.Duration);
|
||
// //wd = DateOnly.FromDateTime(wrokDateTimeOffset.DateTime);
|
||
// resultWorkDays.Add(DateOnly.FromDateTime(wrokDateTimeOffset.DateTime));
|
||
// }
|
||
// else
|
||
// {
|
||
// resultWorkDays.Add(wd);
|
||
// }
|
||
//});
|
||
|
||
////----- убираем даты, которые могли выйти за конец периода (из-за сдвига NextRun по рабочим дням)
|
||
//var endPeriod = calendarService.GetEndPeriodDate(DateOnly.FromDateTime(DateTimeOffset.UtcNow.Date), periodType, period.Duration);
|
||
//resultWorkDays = resultWorkDays.Where(t => t <= endPeriod).Distinct().OrderBy(t => t).ToList();
|
||
////-----
|
||
|
||
//return resultWorkDays;
|
||
#endregion
|
||
|
||
return workDays;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверить соответствие распределения существующих шаблонов нововму плану.
|
||
/// На выходе получаем шаблоны, которым требуется изменить дату следующего запуска.
|
||
/// </summary>
|
||
/// <param name="distributionPlan"></param>
|
||
/// <param name="templates"></param>
|
||
/// <returns></returns>
|
||
private List<Template> GetTemplatesToDistribute(ref Dictionary<DateOnly, int> distributionPlan, List<Template> templates)
|
||
{
|
||
var result = new List<Template>();
|
||
|
||
foreach (var wd in distributionPlan)
|
||
{
|
||
var templatesInWd = templates.Where(t => DateOnly.FromDateTime(t.NextRun.DateTime) == wd.Key).ToList();
|
||
|
||
if (templatesInWd.Any() && templatesInWd.Count > wd.Value)
|
||
{
|
||
result.AddRange(templatesInWd.Skip(wd.Value).Take(templatesInWd.Count - wd.Value).ToList());
|
||
distributionPlan[wd.Key] -= wd.Value;
|
||
}
|
||
else
|
||
distributionPlan[wd.Key] -= templatesInWd.Count;
|
||
}
|
||
|
||
//Определим шаблоны за границами рабочих дней. Их тоже необходимо перераспределить
|
||
var workDays = distributionPlan.Keys.ToList();
|
||
var templateWithNextRunOut = templates.Where(t => workDays.All(w => w != DateOnly.FromDateTime(t.NextRun.DateTime))).ToList();
|
||
|
||
if (templateWithNextRunOut.Any())
|
||
result.AddRange(templateWithNextRunOut);
|
||
|
||
if (result.Any())
|
||
//Обнулим nextRun
|
||
result.ForEach(t => t.NextRun = DateTimeOffset.MinValue);
|
||
|
||
return result;
|
||
}
|
||
|
||
|
||
private DistributionPeriodTypeEnum ParseDistributionPeriodType(string value)
|
||
{
|
||
var result = (DistributionPeriodTypeEnum)Enum.Parse(typeof(DistributionPeriodTypeEnum), value);
|
||
|
||
return result;
|
||
}
|
||
|
||
|
||
private Dictionary<DateOnly, int> GetDistributionPlan(List<DateOnly> workDays, int templateCount)
|
||
{
|
||
var result = new Dictionary<DateOnly, int>();
|
||
//Заполняем будующий план датами из полученных на входе данных
|
||
foreach (var wd in workDays)
|
||
{
|
||
var workDate = new DateOnly(wd.Year, wd.Month, wd.Day);
|
||
result.Add(workDate, 0);
|
||
}
|
||
|
||
var templateDistributedCount = 0;//количество распределённых на текущий момент шаблонов
|
||
|
||
//Если количество шаблонов больше количества рабочих дней, то сразу распределяем их равным количеством по всем рабочим дням
|
||
if (templateCount >= workDays.Count)
|
||
{
|
||
//количеством шаблонов на один рабочий день
|
||
var mainPartTemplateCountToDistribute = templateCount / workDays.Count;
|
||
|
||
//перебираем рабочие дня и записываем количество шаблонов
|
||
foreach (var item in result)
|
||
{
|
||
result[item.Key] = mainPartTemplateCountToDistribute;
|
||
|
||
templateDistributedCount += mainPartTemplateCountToDistribute;
|
||
}
|
||
}
|
||
|
||
var daysBetweenTemplates = (double)workDays.Count / (templateCount % workDays.Count);//количество дней между шаблонами, если распределить их равномерно
|
||
var currentStepBetweenTemplates = (double)Math.Floor(daysBetweenTemplates);//инициализация шага перехода между датами запуска двух шаблонов
|
||
var balance = (double)daysBetweenTemplates - Math.Floor(currentStepBetweenTemplates);//остаток между датами, потому что мы берём целые дни, его нужно учесть в следующей итирации
|
||
var currentDayIndex = 0;//инициализация индекса даты словаря рабочих дней
|
||
|
||
while (templateDistributedCount < templateCount)
|
||
{
|
||
//лишняя проверка так как шагом ранее мы распределям шаблоны равным количеством и currentStepBetweenTemplates не может быть меньше одного дня
|
||
//if (currentStepBetweenTemplates > 0)
|
||
//{
|
||
result[result.ElementAt(currentDayIndex).Key] += 1;
|
||
templateDistributedCount++;
|
||
currentDayIndex += (int)Math.Floor(currentStepBetweenTemplates);
|
||
//}
|
||
|
||
currentStepBetweenTemplates = daysBetweenTemplates + balance;
|
||
balance = daysBetweenTemplates - currentStepBetweenTemplates;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
}
|