feat(dal,core): Перенесены оставшиеся объекты из dal в core.

This commit is contained in:
Mikhail Trubnikov
2026-05-04 11:55:07 +10:00
parent 238244790b
commit a89605d02a
87 changed files with 276 additions and 174 deletions

View File

@@ -0,0 +1,449 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Schedule;
using PARR.Domain.Enums;
namespace PARR.Core.Services.NextRunServices.Subservices
{
internal class EsppScheduleTransformService : IEsppScheduleTransformService
{
private readonly IEsppSchTypeConfigRepository esppSchTypeConfigService;
private readonly ILogger<EsppScheduleTransformService> logger;
private static readonly Dictionary<string, int> monthDict = new Dictionary<string, int>()
{
{ "Январь", 1 },
{ "Февраль", 2 },
{ "Март", 3 },
{ "Апрель", 4 },
{ "Май", 5 },
{ "Июнь", 6 },
{ "Июль", 7 },
{ "Август", 8 },
{ "Сентябрь", 9 },
{ "Октябрь", 10 },
{ "Ноябрь", 11 },
{ "Декабрь", 12 }
};
private static readonly Dictionary<string, int> orderDict = new Dictionary<string, int>()
{
{ "Первый", 1 },
{ "Второй", 2 },
{ "Третий", 3 },
{ "Четвертый", 4 },
{ "Последний", 5 }
};
private static readonly Dictionary<string, int> dwDict = new Dictionary<string, int>()
{
{"Понедельник", 1 },
{"Вторник", 2 },
{"Среда", 3 },
{"Четверг", 4 },
{"Пятница", 5 },
{"Суббота", 6 },
{"Воскресенье", 0 }
};
public EsppScheduleTransformService(
ILogger<EsppScheduleTransformService> logger,
IEsppSchTypeConfigRepository esppSchTypeConfigService
)
{
this.esppSchTypeConfigService = esppSchTypeConfigService;
this.logger = logger;
}
public async Task<DateTimeOffset> GetNextDateAsync(Guid jobGroupId, DateTimeOffset referenceDate, TimeSpan? offset = null)
{
var esppSchedule = await GetEsppScheduleAsync(jobGroupId);
return GetNextDate(esppSchedule, referenceDate, offset);
}
private DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate, TimeSpan? offset)
{
//offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК
logger.LogDebug("Начинаю рассчет nextRun по расписанию ЕСПП. Входные данные: date: {referenceDate}, offset: {offset}", referenceDate, offset);
var nextRun = referenceDate;
//var finalOffset = new TimeSpan(3, 0, 0) + (offset ?? TimeSpan.Zero);
var finalOffset = offset ?? TimeSpan.Zero;
var offsetReferenceDate = referenceDate.ToOffset(finalOffset);
logger.LogDebug("Дата после добавления offset - {offsetReferenceDate}", offsetReferenceDate);
////---------
//// Для рассчета по МСК времени, потому что в ЮТС может быть еще ВСК, а по МСК это уже ПНД
//var offsetReferenceDate = referenceDate.ToOffset(new TimeSpan(3, 0, 0));
////---------
//// может быть offset ,это работает когда смещение для УЗ ЕСПП и применение ЗО РГ
//if (offset.HasValue)
// offsetReferenceDate = offsetReferenceDate.Off(offsetReferenceDate.Offset + offset);
// //offsetReferenceDate = offsetReferenceDate.Add(offset.Value);
switch (esppSchedule.TypeSchedule.Id)
{
case (int)EsppSchTypeScheduleEnum.Regularly:
nextRun = GetNextDateRegularly(esppSchedule.Values, offsetReferenceDate);
break;
case (int)EsppSchTypeScheduleEnum.Weekly:
nextRun = GetNextDateWeekly(esppSchedule.Values, offsetReferenceDate);
break;
case (int)EsppSchTypeScheduleEnum.Monthly:
nextRun = GetNextDateMonthly(esppSchedule.Values, offsetReferenceDate);
break;
case (int)EsppSchTypeScheduleEnum.Monthly2:
nextRun = GetNextDateMonthly2(esppSchedule.Values, offsetReferenceDate);
break;
case (int)EsppSchTypeScheduleEnum.Annually:
nextRun = GetNextDateAnnually(esppSchedule.Values, offsetReferenceDate);
break;
case (int)EsppSchTypeScheduleEnum.Annually2:
nextRun = GetNextDateAnnually2(esppSchedule.Values, offsetReferenceDate);
break;
}
logger.LogDebug("Рассчитанная дата, с учетом offset: {nextRun}", nextRun);
// Возвращаем обратно в ЮТС
nextRun = nextRun.ToOffset(TimeSpan.Zero);
logger.LogDebug("Рассчитанная дата с нулевым offset: {nextRun}", nextRun);
return nextRun;
}
/// <summary>
/// Получить расписание из БД
/// </summary>
/// <param name="jobGroupId"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private async Task<EsppScheduleDto> GetEsppScheduleAsync(Guid jobGroupId)
{
var esppSchedule = await esppSchTypeConfigService.GetEsppScheduleDtoAsync(jobGroupId);
if (esppSchedule == null)
{
logger.LogError($"Не найдено расписание в БД, jobId: {jobGroupId}");
throw new Exception($"Не найдено расписание в БД, jobId: {jobGroupId}");
}
return esppSchedule;
}
private DateTimeOffset GetNextDateRegularly(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
{
var regDict = new Dictionary<string, int>
{
{"Каждый час",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},
};
if (!regDict.TryGetValue(values[0].Value.Value, out int regNum))
{
logger.LogError("Неизвестное значение: {Value}", values[0].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
}
var calcDay = referenceDate;
//TODO: вот это повторяется от метода к методу
//Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию
if (calcDay < DateTimeOffset.UtcNow)
calcDay = GetNextDateRegularly(values, calcDay.AddHours(regNum));
return calcDay;
}
private DateTimeOffset GetNextDateWeekly(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
{
if (!dwDict.TryGetValue(values[0].Value.Value, out int dwNum))
{
logger.LogError("Неизвестное значение дня недели: {Value}", values[0].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
}
var calcDay = referenceDate;
if (calcDay < DateTimeOffset.UtcNow)
{
var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
calcDay = new DateTimeOffset(
nowInTargetTz.Year,
//DateTimeOffset.UtcNow.Year,
nowInTargetTz.Month,
//DateTimeOffset.UtcNow.Month,
nowInTargetTz.Day,
//DateTimeOffset.UtcNow.Day,
referenceDate.Hour,
referenceDate.Minute,
referenceDate.Second,
referenceDate.Offset
);
}
if (((int)calcDay.DayOfWeek) != dwNum)
calcDay = OffsetToDayOfWeek(dwNum, calcDay);
//TODO: вот это повторяется от метода к методу
//Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
if (calcDay < DateTimeOffset.UtcNow)
calcDay = GetNextDateWeekly(values, calcDay.AddDays(7));//а вот тут что-то новенькое
return calcDay;
}
private DateTimeOffset GetNextDateMonthly(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
{
if (!int.TryParse(values[0].Value.Value, out int dayNum))
{
logger.LogError("Неизвестное значение дня месяца: {Value}", values[0].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
}
var calcDay = new DateTimeOffset(referenceDate.Year, referenceDate.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
if (referenceDate < DateTimeOffset.UtcNow)
{
var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
calcDay = new DateTimeOffset(
nowInTargetTz.Year,
nowInTargetTz.Month,
dayNum,
referenceDate.Hour,
referenceDate.Minute,
referenceDate.Second,
referenceDate.Offset
);
//calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
}
//TODO: вот это повторяется от метода к методу
//Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
if (calcDay < DateTimeOffset.UtcNow)
calcDay = GetNextDateMonthly(values, calcDay.AddMonths(1));
return calcDay;
}
private DateTimeOffset GetNextDateMonthly2(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
{
if (!orderDict.TryGetValue(values[0].Value.Value, out int orderNum))
{
logger.LogError("Неизвестное значение порядка дня: {Value}", values[0].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
}
if (!dwDict.TryGetValue(values[1].Value.Value, out int dwNum))
{
logger.LogError("Неизвестное значение дня недели: {Value}", values[1].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}");
}
//var startDay = referenceDate < DateTimeOffset.UtcNow
// ? new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset)
// : new DateTimeOffset(referenceDate.Year, referenceDate.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
var startDay = referenceDate < DateTimeOffset.UtcNow
? new DateTimeOffset(
DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Year,
DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Month,
1,
referenceDate.Hour,
referenceDate.Minute,
referenceDate.Second,
referenceDate.Offset
)
: new DateTimeOffset(
referenceDate.Year,
referenceDate.Month,
1,
referenceDate.Hour,
referenceDate.Minute,
referenceDate.Second,
referenceDate.Offset
);
var daysList = FindAllDaysInMonth(dwNum, startDay);
DateTimeOffset calcDay;
if (orderNum == 5) // Последний
{
calcDay = daysList.Last();
}
else
{
if (orderNum > daysList.Count)
{
logger.LogError("Запрошенный порядковый день недели ({OrderNum}) не существует в месяце {Month}/{Year}. Найдено только {Count} дней.", orderNum, startDay.Month, startDay.Year, daysList.Count);
throw new InvalidOperationException($"Запрошенный порядковый день недели ({orderNum}) не существует в месяце {startDay.Month}/{startDay.Year}. Найдено только {daysList.Count} дней.");
}
else
calcDay = daysList[orderNum - 1];
}
// Если дата <= текущей, переходим на следующий месяц
if (calcDay <= DateTimeOffset.UtcNow)
return GetNextDateMonthly2(values, startDay.AddMonths(1));
return calcDay;
}
private DateTimeOffset GetNextDateAnnually(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
{
if (!monthDict.TryGetValue(values[0].Value.Value, out int monthNum))
{
logger.LogError("Неизвестное значение номера месяца: {Value}", values[0].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
}
if (!int.TryParse(values[1].Value.Value, out int dayNum))
{
logger.LogError("Неизвестное значение дня месяца: {Value}", values[0].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
}
//var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year;
//сравниваем года в одном часовом поясе
var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
var year = (referenceDate.Year < nowInTargetTz.Year)
? nowInTargetTz.Year
: referenceDate.Year;
var calcDay = new DateTimeOffset(
year, monthNum, dayNum,
referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
//TODO: вот это повторяется от метода к методу
//Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
if (calcDay < DateTimeOffset.UtcNow)
calcDay = GetNextDateAnnually(values, calcDay.AddYears(1));
return calcDay;
}
private DateTimeOffset GetNextDateAnnually2(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
{
var monthADict = new Dictionary<string, int>
{
{ "Января", 1 },
{ "Февраля", 2 },
{ "Марта", 3 },
{ "Апреля", 4 },
{ "Мая", 5 },
{ "Июня", 6 },
{ "Июля", 7 },
{ "Августа", 8 },
{ "Сентября", 9 },
{ "Октября", 10 },
{ "Ноября", 11 },
{ "Декабря", 12 }
};
if (!monthADict.TryGetValue(values[2].Value.Value, out int monthNum))
{
logger.LogError("Неизвестное значение номера месяца: {Value}", values[2].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[2].Value.Value}");
}
if (!orderDict.TryGetValue(values[0].Value.Value, out int orderNum))
{
logger.LogError("Неизвестное значение порядка дня: {Value}", values[0].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
}
if (!dwDict.TryGetValue(values[1].Value.Value, out int dwNum))
{
logger.LogError("Неизвестное значение дня недели: {Value}", values[1].Value.Value);
throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}");
}
//var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year;
// сравниваем года в одном часовом поясе
var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
var year = (referenceDate.Year < nowInTargetTz.Year)
? nowInTargetTz.Year
: referenceDate.Year;
//if (year == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.Month)
if (year == nowInTargetTz.Year && referenceDate.Month < nowInTargetTz.Month)
year += 1;
var calcDay = new DateTimeOffset(
year, monthNum, 1,
referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
List<DateTimeOffset> daysList = FindAllDaysInMonth(dwNum, calcDay);
if (orderNum == 5)//5-последний
calcDay = daysList.Last();
else
calcDay = daysList[orderNum - 1];
//TODO: вот это повторяется от метода к методу
//Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
if (calcDay < DateTimeOffset.UtcNow)
calcDay = GetNextDateAnnually2(values, calcDay.AddYears(1));
return calcDay;
}
private List<DateTimeOffset> FindAllDaysInMonth(int dwNum, DateTimeOffset startDay)
{
var calcDay = startDay;
if (((int)calcDay.DayOfWeek) != dwNum)
calcDay = OffsetToDayOfWeek(dwNum, calcDay);
var daysList = new List<DateTimeOffset>();
var monthNum = calcDay.Month;
while (calcDay.Month == monthNum)
{
daysList.Add(calcDay);
calcDay = calcDay.AddDays(7);
}
return daysList;
}
private DateTimeOffset OffsetToDayOfWeek(int dwNum, DateTimeOffset calcDay)
{
var daysOffset = dwNum - ((int)calcDay.DayOfWeek);
if (daysOffset < 0)
daysOffset = 7 + daysOffset;
return calcDay.AddDays(daysOffset);
}
}
}

View File

@@ -0,0 +1,17 @@
namespace PARR.Core.Services.NextRunServices.Subservices
{
/// <summary>
/// Сервис трансформации расписания ЕСПП в дату/расписание
/// </summary>
internal interface IEsppScheduleTransformService
{
/// <summary>
/// Получить следующую дату по jobGroupId
/// </summary>
/// <param name="jobGroupId"></param>
/// <param name="referenceDate">referenceDate или nextRun</param>
/// <param name="offset">offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК</param>
/// <returns></returns>
Task<DateTimeOffset> GetNextDateAsync(Guid jobGroupId, DateTimeOffset referenceDate, TimeSpan? offset = null);
}
}

View File

@@ -0,0 +1,51 @@
using PARR.Core.Services.NextRunServices.Models;
namespace PARR.Core.Services.NextRunServices.Subservices
{
internal interface ITemplateDistributor
{
/// <summary>
/// Распределить шаблоны
/// </summary>
/// <param name="startDate">Дата начала, в любом часовом поясе</param>
/// <param name="duration"></param>
/// <param name="referenceDate"></param>
/// <param name="offset"></param>
/// <param name="allTemplates"></param>
/// <param name="excludeWeekends"></param>
/// <returns></returns>
Task<List<TemplateNextRunResultDto>> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List<TemplateNextRunDto> allTemplates, bool excludeWeekends);
/// <summary>
/// Метод проверяет, актуален ли текущий NextRun шаблона, если не актуален, возвращает актуальное значение
/// При проверке загружаются шаблоны из БД! Если хотим рассчитывать nextRun для НЕСКОЛЬКИХ шаблонов, то следует рассчитать nextRun для одного шаблона, сохранить в БД, затем рассчитать для следующего
/// </summary>
/// <param name="startDate">Дата начала, в любом часовом поясе</param>
/// <param name="duration"></param>
/// <param name="referenceDate"></param>
/// <param name="offset"></param>
/// <param name="targetTemplate"></param>
/// <param name="allTemplates"></param>
/// <param name="excludeWeekends"></param>
/// <param name="isNew"></param>
/// <returns></returns>
Task<TemplateNextRunResultDto> GetNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, TemplateNextRunDto targetTemplate, List<TemplateNextRunDto> allTemplates, bool excludeWeekends, bool isNew);
/// <summary>
/// Получить список выходных дней
/// </summary>
/// <param name="startDate"></param>
/// <param name="endDate"></param>
/// <returns></returns>
Task<HashSet<DateOnly>> GetWeekendsAsync(DateOnly startDate, DateOnly endDate);
/// <summary>
/// Получить список рабочих дней в диапазоне, исключая выходные и праздники.
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="excludeWeekends">Исключить выходные и праздники</param>
/// <returns></returns>
Task<List<DateOnly>> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends);
}
}

File diff suppressed because it is too large Load Diff