using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Common; using PARR.Constants; using PARR.DAL.Contracts; using PARR.DAL.DomainModels; using PARR.DAL.Extensions; using PARR.DAL.Services.Interfaces; namespace PARR.DAL.TransformServices { internal class EsppScheduleTransformService : IEsppScheduleTransformService { private readonly IEsppSchTypeConfigService esppSchTypeConfigService; private readonly IApplicationsInWorkService applicationsInWorkService; private readonly IWeekendDayService weekendDayService; private readonly ILogger logger; private readonly Dictionary monthDict = new Dictionary() { { "Январь", 1 }, { "Февраль", 2 }, { "Март", 3 }, { "Апрель", 4 }, { "Май", 5 }, { "Июнь", 6 }, { "Июль", 7 }, { "Август", 8 }, { "Сентябрь", 9 }, { "Октябрь", 10 }, { "Ноябрь", 11 }, { "Декабрь", 12 } }; private readonly Dictionary orderDict = new Dictionary() { { "Первый", 1 }, { "Второй", 2 }, { "Третий", 3 }, { "Четвертый", 4 }, { "Последний", 5 } }; private readonly Dictionary dwDict = new Dictionary() { {"Понедельник", 1 }, {"Вторник", 2 }, {"Среда", 3 }, {"Четверг", 4 }, {"Пятница", 5 }, {"Суббота", 6 }, {"Воскресенье", 0 } }; public EsppScheduleTransformService( ILogger logger, IEsppSchTypeConfigService esppSchTypeConfigService, IApplicationsInWorkService applicationsInWorkService, IWeekendDayService weekendDayService ) { this.esppSchTypeConfigService = esppSchTypeConfigService; this.applicationsInWorkService = applicationsInWorkService; this.weekendDayService = weekendDayService; this.logger = logger; } public DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset lastRun) { var nextRun = lastRun; //если больше текущего вернуть lastRun. Это и есть следующая дата if (lastRun > DateTimeOffset.UtcNow) nextRun = lastRun; //return lastRun; switch (esppSchedule.TypeSchedule.Id) { case (int)EsppSchTypeScheduleEnum.Regularly: //return GetNextDateRegularly(esppSchedule.Values, lastRun); nextRun = GetNextDateRegularly(esppSchedule.Values, lastRun); break; case (int)EsppSchTypeScheduleEnum.Weekly: //return GetNextDateWeekly(esppSchedule.Values, lastRun); nextRun = GetNextDateWeekly(esppSchedule.Values, lastRun); break; case (int)EsppSchTypeScheduleEnum.Monthly: //return GetNextDateMonthly(esppSchedule.Values, lastRun); nextRun = GetNextDateMonthly(esppSchedule.Values, lastRun); break; case (int)EsppSchTypeScheduleEnum.Monthly2: //return GetNextDateMonthly2(esppSchedule.Values, lastRun); nextRun = GetNextDateMonthly2(esppSchedule.Values, lastRun); break; case (int)EsppSchTypeScheduleEnum.Annually: //return GetNextDateAnnually(esppSchedule.Values, lastRun); nextRun = GetNextDateAnnually(esppSchedule.Values, lastRun); break; case (int)EsppSchTypeScheduleEnum.Annually2: //return GetNextDateAnnually2(esppSchedule.Values, lastRun); nextRun = GetNextDateAnnually2(esppSchedule.Values, lastRun); break; } nextRun = GetWorkDayAsync(nextRun).GetAwaiter().GetResult(); return nextRun; } private List GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset lastRun) { // расписание на оставшееся на сегодня время. его так мало... lastRun = lastRun.UtcDateTime; var schedules = new List(); var curTime = lastRun; //Если ещё не произошло то добавляем расписание, возможно это новое AiW и он ещё ниразу не запускался. //lastRun это будущее if (lastRun >= DateTimeOffset.UtcNow && lastRun <= DateTimeOffset.UtcNow.EndOfDay()) schedules.Add(lastRun); while (curTime < DateTimeOffset.UtcNow.EndOfDay()) { switch (esppSchedule.TypeSchedule.Id) { case (int)EsppSchTypeScheduleEnum.Regularly: curTime = GetNextDateRegularly(esppSchedule.Values, curTime); break; case (int)EsppSchTypeScheduleEnum.Weekly: curTime = GetNextDateWeekly(esppSchedule.Values, curTime); break; case (int)EsppSchTypeScheduleEnum.Monthly: curTime = GetNextDateMonthly(esppSchedule.Values, curTime); break; case (int)EsppSchTypeScheduleEnum.Monthly2: curTime = GetNextDateMonthly2(esppSchedule.Values, curTime); break; case (int)EsppSchTypeScheduleEnum.Annually: curTime = GetNextDateAnnually(esppSchedule.Values, curTime); break; case (int)EsppSchTypeScheduleEnum.Annually2: curTime = GetNextDateAnnually2(esppSchedule.Values, curTime); break; } schedules.Add(curTime); } schedules.RemoveAll(s => s > DateTimeOffset.UtcNow.EndOfDay()); return schedules.OrderBy(t => t).ToList(); } public async Task GetNextDateAsync(Guid applicationInWorkId, DateTimeOffset lastRun) { //если больше текущего вернуть lastRun. Это и есть следующая дата if (lastRun > DateTimeOffset.UtcNow) return await GetWorkDayAsync(lastRun); var appInWork = await applicationsInWorkService.Get() .Include(aiw => aiw.EsppSchValues) .ThenInclude(esv => esv.EsppSchTypeValue) .ThenInclude(etv => etv!.DistributionPeriod) .FirstOrDefaultAsync(t => t.Id == applicationInWorkId); var esppSchedule = await GetEsppScheduleAsync(applicationInWorkId); if (appInWork!.IsAutoDistributionEnabled) { var period = appInWork.EsppSchValues.FirstOrDefault()?.EsppSchTypeValue?.DistributionPeriod; var periodType = ParseDistributionPeriodType(period!.Type); return GetNextDateForDistributionRun(lastRun, periodType, period.Duration); } return GetNextDate(esppSchedule, lastRun); } public async Task> GetNextScheduleAsync(Guid applicationInWorkId, DateTimeOffset lastRun) { //TODO: тут не проверен переход через выходные дни!!! Переход не используется, так как тут не рассчитываем NextRun. //В общем проверить, когда будем тестировать агента, что с датами все ок var esppSchedule = await GetEsppScheduleAsync(applicationInWorkId); var nextSchedule = GetNextSchedule(esppSchedule, lastRun); return nextSchedule; } private async Task GetEsppScheduleAsync(Guid applicationInWorkId) { var esppSchedule = await esppSchTypeConfigService.GetEsppScheduleDtoAsync(applicationInWorkId); if (esppSchedule == null) { logger.LogError($"Не найдено расписание в БД, applicationInWorkId: {applicationInWorkId}"); throw new Exception($"Не найдено расписание в БД, applicationInWorkId: {applicationInWorkId}"); } return esppSchedule; } private DateTimeOffset GetNextDateRegularly(List values, DateTimeOffset lastRun) { var regDict = new Dictionary { {"Каждый час",1}, {"Каждые 2 часа",2}, {"Каждые 3 часа", 3}, {"Каждые 4 часа", 4}, {"Каждые 6 часов", 6}, {"Каждые 12 часов", 12}, {"Ежедневно", 24}, {"Каждые 72 часа", 72}, {"Каждые 96 часов", 96}, {"Каждые 2 недели", 336}, {"Каждые 60 дней", 1440}, {"Каждые 80 дней", 1920}, {"Каждые 90 дней", 2160}, {"Каждые полгода", 4380}, {"Каждые 1,5 года", 13140}, {"Каждые 3 года", 26280}, }; var regNum = regDict[values[0].Value.Value]; var calcDay = lastRun.AddHours(regNum); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию if (calcDay < DateTimeOffset.UtcNow) calcDay = GetNextDateRegularly(values, calcDay); return calcDay; } private DateTimeOffset GetNextDateWeekly(List values, DateTimeOffset lastRun) { var dwNum = dwDict[values[0].Value.Value]; var calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, DateTimeOffset.UtcNow.Day, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); if (((int)calcDay.DayOfWeek) != dwNum) OffsetToDayOfWeek(dwNum, ref calcDay); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию if (calcDay < DateTimeOffset.UtcNow) calcDay = calcDay.AddDays(7);//а вот тут что-то новенькое return calcDay; } private DateTimeOffset GetNextDateMonthly(List values, DateTimeOffset lastRun) { int.TryParse(values[0].Value.Value, out int dayNum); var calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию if (calcDay < DateTimeOffset.UtcNow) calcDay = calcDay.AddMonths(1);//и вот тут что-то новенькое return calcDay; } private DateTimeOffset GetNextDateMonthly2(List values, DateTimeOffset lastRun) { var orderNum = orderDict[values[0].Value.Value]; var dwNum = dwDict[values[1].Value.Value]; var calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 1, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); List daysList = FindAllDaysInMonth(dwNum, ref calcDay); if (orderNum == 5)//5-последний calcDay = daysList.Last(); else calcDay = daysList[orderNum - 1]; //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию if (calcDay < DateTimeOffset.UtcNow) calcDay = GetNextDateAnnually2(values, calcDay); return calcDay; } private DateTimeOffset GetNextDateAnnually(List values, DateTimeOffset lastRun) { var monthNum = monthDict[values[0].Value.Value]; int.TryParse(values[1].Value.Value, out int dayNum); var calcDay = new DateTimeOffset(lastRun.Year + 1, monthNum, dayNum, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию if (calcDay < DateTimeOffset.UtcNow) calcDay = GetNextDateAnnually(values, calcDay); return calcDay; } private DateTimeOffset GetNextDateAnnually2(List values, DateTimeOffset lastRun) { var monthADict = new Dictionary { { "Января", 1 }, { "Февраля", 2 }, { "Марта", 3 }, { "Апреля", 4 }, { "Мая", 5 }, { "Июня", 6 }, { "Июля", 7 }, { "Августа", 8 }, { "Сентября", 9 }, { "Октября", 10 }, { "Ноября", 11 }, { "Декабря", 12 } }; var monthNum = monthADict[values[2].Value.Value]; var orderNum = orderDict[values[0].Value.Value]; var dwNum = dwDict[values[1].Value.Value]; var calcDay = new DateTimeOffset(lastRun.Year + 1, monthNum, 1, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); List daysList = FindAllDaysInMonth(dwNum, ref calcDay); if (orderNum == 5)//5-последний calcDay = daysList.Last(); else calcDay = daysList[orderNum]; //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию if (calcDay < DateTimeOffset.UtcNow) calcDay = GetNextDateAnnually2(values, calcDay); return calcDay; } private List FindAllDaysInMonth(int dwNum, ref DateTimeOffset calcDay) { if (((int)calcDay.DayOfWeek) != dwNum) OffsetToDayOfWeek(dwNum, ref calcDay); var daysList = new List(); var monthNum = calcDay.Month; while (calcDay.Month == monthNum) { daysList.Add(calcDay); calcDay = calcDay.AddDays(7); } return daysList; } private void OffsetToDayOfWeek(int dwNum, ref DateTimeOffset calcDay) { var daysOffset = dwNum - ((int)calcDay.DayOfWeek); if (daysOffset < 0) daysOffset = 7 + daysOffset; calcDay = calcDay.AddDays(daysOffset); } public DateTimeOffset GetNextDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod) { var nextRun = lastRun; switch (periodType) { case (DistributionPeriodTypeEnum.Day): //return lastRun.AddDays(ParseInt(distributionPeriod)); nextRun = lastRun.AddDays(ParseInt(distributionPeriod)); break; case (DistributionPeriodTypeEnum.Month): //return lastRun.AddMonths(ParseInt(distributionPeriod)); nextRun = lastRun.AddMonths(ParseInt(distributionPeriod)); break; case (DistributionPeriodTypeEnum.Year): //return lastRun.AddYears(ParseInt(distributionPeriod)); nextRun = lastRun.AddYears(ParseInt(distributionPeriod)); break; } return GetWorkDayAsync(nextRun).GetAwaiter().GetResult(); } private DateTimeOffset GetPrevDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod) { switch (periodType) { case (DistributionPeriodTypeEnum.Day): return lastRun.AddDays(-ParseInt(distributionPeriod)); case (DistributionPeriodTypeEnum.Month): return lastRun.AddMonths(-ParseInt(distributionPeriod)); case (DistributionPeriodTypeEnum.Year): return lastRun.AddYears(-ParseInt(distributionPeriod)); } return lastRun; } public async Task GetStartPeriodForDateAsync(Guid applicationInWorkId, DateTimeOffset date, DateTimeOffset referenceDate, DistributionPeriodTypeEnum periodType, string distributionPeriod) { var esppSchedule = await GetEsppScheduleAsync(applicationInWorkId); var currentStartPeriod = referenceDate; var currentEndPeriod = GetNextDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod); while (!(date >= currentStartPeriod && date < currentEndPeriod)) { if (date > currentEndPeriod) { currentStartPeriod = currentEndPeriod; currentEndPeriod = GetNextDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod); } else { currentEndPeriod = currentStartPeriod; currentStartPeriod = GetPrevDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod); } } return DateOnly.FromDateTime(currentStartPeriod.DateTime); } private int ParseInt(string value) { try { return int.Parse(value); } catch (Exception ex) { logger.LogError(ex, $"{nameof(this.GetType)}, получение конца периода. Не смог распарсить string в int для значения {value}."); return 0; } } private DistributionPeriodTypeEnum ParseDistributionPeriodType(string value) { var result = (DistributionPeriodTypeEnum)Enum.Parse(typeof(DistributionPeriodTypeEnum), value); return result; } /// /// Получить рабочий день следующего срабатывания /// /// /// private async Task GetWorkDayAsync(DateTimeOffset nextRun) { // получаем день относительно часового пояса МСК var date = DateOnly.FromDateTime(DateResolver.IsCurrentDayRelativeMskTime(nextRun) ? nextRun.Date : nextRun.Date.AddDays(1)); if (await weekendDayService.IsWorkDayAsync(date, true)) { //текущий nextRun рабочий день, возвращаем его return nextRun; } // ищем следующий рабочий день, максимум 20 итераций, если за их кол-во не нашли рабочий, берём последний не рабочий int count = 20; while (count > 0) { date = date.AddDays(1); var isWorkDay = await weekendDayService.IsWorkDayAsync(date, true); if (isWorkDay) break; // это рабочий день, выбираем его count--; if (!isWorkDay && count == 0) logger.LogWarning($"Не нашли рабочий день за 20 итераций, взяли последний нерабочий {date}"); } // смотрим, если смещали из-за пояса МСК на день вперед, вычитаем этот день назад if (!DateResolver.IsCurrentDayRelativeMskTime(nextRun)) date = date.AddDays(-1); // считаем новый NextRun var dayCount = date.DayNumber - DateOnly.FromDateTime(nextRun.Date).DayNumber; return nextRun.AddDays(dayCount); } } }