Merge branch 'next-run-service' into dev

# Conflicts:
#	PARR.DAL/Contracts/SettingsFromDb.cs
This commit is contained in:
Mikhail Trubnikov
2026-02-26 08:42:07 +10:00
37 changed files with 7328 additions and 897 deletions

View File

@@ -189,7 +189,7 @@ namespace PARR.DAL.Context
new { Name = nameof(SettingsFromDb.TemplatePrefixName), Description = "Префикс имени шаблона в ЕСПП", Value = "%PREFIX%-ЭИТИ-ПТК-ПАРР" },
new { Name = nameof(SettingsFromDb.RobotAttemptsNumber), Description = "Количество попыток выполнения задания роботом", Value = "3" },
new { Name = nameof(SettingsFromDb.RobotWaitTime), Description = "Время ожидания выполнения роботом задания", Value = "00:15:00" },
//new { Name = nameof(SettingsFromDb.ScheduleTimezone), Description = "Расписание регламентной работы - В каком часовом поясе", Value = "MSK" },
new { Name = nameof(SettingsFromDb.EsppScheduleTimezone), Description = "Расписание регламентной работы - В каком часовом поясе", Value = "MSK" },
//new { Name = nameof(SettingsFromDb.ScheduleExcludeType), Description = "Расписание регламентной работы - Тип исключения", Value = "Выполнить ТОЛЬКО В указанном календаре" },
//new { Name = nameof(SettingsFromDb.ScheduleExcludeCalendar), Description = "Расписание регламентной работы - Календарь", Value = "8x5 (8.00-17.00)" },
new { Name = nameof(SettingsFromDb.ScheduleRepeatRange), Description = "Расписание регламентной работы - Диапазн повторов", Value = "Отсутствует дата завершения" },

View File

@@ -76,6 +76,21 @@ namespace PARR.DAL.Contracts
/// </summary>
public TimeSpan RobotWaitTime { get; set; }
/// <summary>
/// Расписание регламентной работы, В каком часовом поясе
/// </summary>
public string EsppScheduleTimezone { get; set; } = string.Empty;
///// <summary>
///// Расписание регламентной работы - Тип исключения
///// </summary>
//public string ScheduleExcludeType { get; set; } = string.Empty;
///// <summary>
///// Расписание регламентной работы - Календарь
///// </summary>
//public string ScheduleExcludeCalendar { get; set; } = string.Empty;
/// <summary>
/// Расписание регламентной работы - Диапазон повторов
/// </summary>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PARR.DAL.Migrations
{
/// <inheritdoc />
public partial class tblJobGroupAddUserTimeZoneOffsetMinutes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "UserTimeZoneOffsetMinutes",
schema: "job",
table: "Groups",
type: "integer",
nullable: true,
comment: "Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "UserTimeZoneOffsetMinutes",
schema: "job",
table: "Groups");
}
}
}

View File

@@ -1702,6 +1702,10 @@ namespace PARR.DAL.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<int?>("UserTimeZoneOffsetMinutes")
.HasColumnType("integer")
.HasComment("Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate");
b.HasKey("Id");
b.HasIndex("GroupTypeId");

View File

@@ -92,6 +92,21 @@ namespace PARR.DAL.Models.Job
public DateTimeOffset ReferenceDate { get; set; }
/// <summary>
/// Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate
/// </summary>
[Comment("Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate")]
public int? UserTimeZoneOffsetMinutes { get; set; }
/// <summary>
/// Смещение часового пояса пользователя относительно UTC на момент ReferenceDate, рассчитывается из поля UserTimeZoneOffsetMinutes
/// </summary>
[NotMapped]
public TimeSpan? UserTimeZoneOffset =>
UserTimeZoneOffsetMinutes.HasValue
? TimeSpan.FromMinutes(UserTimeZoneOffsetMinutes.Value)
: null;
/// <summary>
/// Использовать таймзону рабочей группы ответственного за ЭК шаблона
/// </summary>

View File

@@ -72,12 +72,12 @@ namespace PARR.DAL.NextRunServices
/// <returns></returns>
DateTimeOffset GetNextRunWithTimezoneEsppAndResponseArea(DateTimeOffset nextRun, bool? isResponseAreaTimezone = null, string? responseArea = null);
/// <summary>
/// Получить NextRun согласно таймзоны ЗО
/// </summary>
/// <param name="nextRun"></param>
/// <param name="responseArea"></param>
/// <returns></returns>
DateTimeOffset GetNextRunWithResponseAreaOffset(DateTimeOffset nextRun, string responseArea);
///// <summary>
///// Получить NextRun согласно таймзоны ЗО
///// </summary>
///// <param name="nextRun"></param>
///// <param name="responseArea"></param>
///// <returns></returns>
//DateTimeOffset GetNextRunWithResponseAreaOffset(DateTimeOffset nextRun, string responseArea);
}
}

View File

@@ -0,0 +1,66 @@
using PARR.Constants;
using PARR.DAL.Models.Job;
using PARR.DAL.NextRunServices.Models;
namespace PARR.DAL.NextRunServices
{
public interface INextRunServiceV2
{
/// <summary>
/// Распределить шаблоны в группе работ (получить список распределенных шаблонов с актуальными nextRun)
/// </summary>
/// <param name="jobGroupId">ИД группы работ</param>
/// <param name="templateStatusType">Статус шаблона, если не передать, берутся все шаблоны группы</param>
/// <returns>Список TemplateId с NextRun и NextRunOld</returns>
Task<List<TemplateNextRunResultDto>?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId, TemplateStatusTypeEnum? templateStatusType);
/// <summary>
/// Получить nextRun для вновь создаваемого шаблона, которого еще нет в БД
/// </summary>
/// <param name="jobGroupId">ИД группы работ</param>
/// <param name="workGroupName">Рабочая группа, не маска</param>
/// <param name="responseAreaName">Зона ответственности, не маска</param>
/// <returns>NextRun или null, если null - то ошибка</returns>
Task<DateTimeOffset?> GetNextRunForNewTemplateAsync(Guid jobGroupId, string workGroupName, string responseAreaName);
/// <summary>
/// Получить nextRun по id существующего в БД шаблона
/// </summary>
/// <param name="templateId">ИД шаблона</param>
/// <param name="isNew">При перемещении шаблона в новую для него работу, ставить isNew=true</param>
/// <returns>NextRun или null, если null - то ошибка</returns>
Task<DateTimeOffset?> GetNextRunForTemplateAsync(Guid templateId, bool isNew);
/// <summary>
/// Получить часовой пояс УЗ ЕСПП
/// </summary>
/// <returns></returns>
TimeSpan GetEsppAccountOffset();
/// <summary>
/// Получить продолжительность в днях
/// </summary>
/// <param name="config"></param>
/// <returns></returns>
int GetDurationDays(JobGroupDistributionConfig config);
/// <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);
/// <summary>
/// Получить список выходных дней
/// </summary>
/// <param name="startDate"></param>
/// <param name="endDate"></param>
/// <returns></returns>
Task<HashSet<DateOnly>> GetWeekendsAsync(DateOnly startDate, DateOnly endDate);
}
}

View File

@@ -153,7 +153,7 @@ namespace PARR.DAL.NextRunServices
public async Task<DateTimeOffset> GetNextRunForNewTemplateAsync(Guid jobGroupId, string responseArea)
{
// определяю это автораспределение или нет, вызываю соответствующий рассчет
// определяю это автораспределение или нет, вызываю соответствующий расчет
var jobGroup = await jobGroupService.Get()
.Include(t => t.DistributionConfig)
@@ -295,6 +295,7 @@ namespace PARR.DAL.NextRunServices
}
#region Ok
/// <summary>
/// Получить продолжительность в днях
@@ -331,6 +332,8 @@ namespace PARR.DAL.NextRunServices
return periodDays;
}
#endregion
/// <summary>
/// Получить дату начала распределения
@@ -438,14 +441,15 @@ namespace PARR.DAL.NextRunServices
//return resultNextRun;
}
public DateTimeOffset GetNextRunWithResponseAreaOffset(DateTimeOffset nextRun, string responseArea)
{
var responseAreaOffset = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea).UtcTimeOffset;
var resulstNextRun = nextRun.Add(responseAreaOffset);
logger.LogDebug("Смещение nextRun для ЗО {responseArea}, входящий: {nextRun}, смещение: {offset}, результат: {result}", responseArea, nextRun, responseAreaOffset, resulstNextRun);
//// был паблик
//private DateTimeOffset GetNextRunWithResponseAreaOffset(DateTimeOffset nextRun, string responseArea)
//{
// var responseAreaOffset = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea).UtcTimeOffset;
// var resulstNextRun = nextRun.Add(responseAreaOffset);
// logger.LogDebug("Смещение nextRun для ЗО {responseArea}, входящий: {nextRun}, смещение: {offset}, результат: {result}", responseArea, nextRun, responseAreaOffset, resulstNextRun);
return resulstNextRun;
}
// return resulstNextRun;
//}
/// <summary>

View File

@@ -0,0 +1,833 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Constants;
using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.NextRunServices.Models;
using PARR.DAL.NextRunServices.Subservices;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Schedule;
namespace PARR.DAL.NextRunServices
{
internal class NextRunServiceV2 : INextRunServiceV2
{
private readonly ILogger<NextRunServiceV2> logger;
private readonly ITemplateService templateService;
private readonly IJobGroupService jobGroupService;
private readonly IEsppScheduleTransformService esppScheduleTransformService;
private readonly ITemplateDistributorV2 templateDistributor;
private readonly IShortcodesService shortcodesService;
private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService;
private readonly SettingsFromDb settingsFromDb;
public NextRunServiceV2(
ILogger<NextRunServiceV2> logger,
ITemplateService templateService,
IJobGroupService jobGroupService,
IEsppScheduleTransformService esppScheduleTransformService,
ITemplateDistributorV2 templateDistributor,
IShortcodesService shortcodesService,
IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService,
SettingsFromDb settingsFromDb
)
{
this.logger = logger;
this.templateService = templateService;
this.jobGroupService = jobGroupService;
this.esppScheduleTransformService = esppScheduleTransformService;
this.templateDistributor = templateDistributor;
this.shortcodesService = shortcodesService;
this.scheduleResponseAreaTimeOffsetService = scheduleResponseAreaTimeOffsetService;
this.settingsFromDb = settingsFromDb;
}
public async Task<List<TemplateNextRunResultDto>?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId, TemplateStatusTypeEnum? templateStatusType)
{
logger.LogInformation("Начинаю распределять шаблоны для группы работ {gobGroupId}, templateStatusType: {templateStatusType}", jobGroupId, templateStatusType);
var jobGroup = await jobGroupService.Get()
.AsNoTracking()
.Include(t => t.DistributionConfig)
.ThenInclude(t => t.DistributionPeriod)
.FirstOrDefaultAsync(t => t.Id == jobGroupId);
if (jobGroup == null)
{
logger.LogError("Не найдена группа работа с id: {id}", jobGroupId);
return null;
}
if (!jobGroup.IsAutoDistributionEnabled || jobGroup.DistributionConfig == null)
{
logger.LogError("Группа работ id: {id} не подходит для автораспределения, у нее или отсутствуют настройки или не включено автораспределение. IsAutoDistributionEnabled: {IsAutoDistributionEnabled}. Есть конфиг: {DistributionConfig}", jobGroupId, jobGroup.IsAutoDistributionEnabled, jobGroup.DistributionConfig != null);
return null;
}
logger.LogInformation("Параметры распределения. groupId: {groupId}, name: {groupName}, referenceDate: {referenceDate}, " +
"distributionPeriodName: {distributionPeriodName}, distributionPeriodDuration: {distributionPeriodDuration}, " +
"distributionPeriodType: {distributionPeriodType}, IsExcludeWeekends: {IsExcludeWeekends}, IsGroupingByWorkGroup: {IsGroupingByWorkGroup}, IsResponseAreaTimezone: {IsResponseAreaTimezone}",
jobGroupId, jobGroup.GroupName, jobGroup.ReferenceDate, jobGroup.DistributionConfig.DistributionPeriod.Name, jobGroup.DistributionConfig.DistributionPeriod.Duration, jobGroup.DistributionConfig.DistributionPeriod.Type,
jobGroup.DistributionConfig.IsExcludeWeekends, jobGroup.DistributionConfig.IsGroupingByWorkGroup, jobGroup.IsResponseAreaTimezone);
//получаем шаблоны только в статусе Used
var templateQuery = templateService.Get()
.AsNoTracking()
.Include(t => t.Job)
.Where(t =>
t.Job!.GroupId == jobGroupId
//&& t.StatusTypeId == TemplateStatusTypeEnum.Used
);
if (templateStatusType.HasValue)
templateQuery = templateQuery.Where(t => t.StatusTypeId == templateStatusType);
var allTemplates = await templateQuery.ToListAsync();
if (!allTemplates.Any())
{
logger.LogInformation("В группе c ИД {jobGroupId} отсутствуют шаблоны в статусе Used", jobGroupId);
return null;
}
logger.LogDebug("Всего шаблонов для распределения в статусе Used: {count} шт.", allTemplates.Count);
// продолжительность в днях
var durationDays = GetDurationDays(jobGroup.DistributionConfig);
// группировать по РГ
var isGroupingByWorkGroup = jobGroup.DistributionConfig.IsGroupingByWorkGroup;
// использовать часовой пояс РГ
var isResponseAreaTimeZone = jobGroup.IsResponseAreaTimezone;
var distributedTemplates = new List<TemplateNextRunResultDto>();
if (isGroupingByWorkGroup)
{
// группировать по РГ
logger.LogDebug("Нужно группировать по РГ");
// получаем для каждого шаблона РГ и сразу группируем по РГ
var templatesByWorkGroup = new Dictionary<string?, List<Template>>(StringComparer.OrdinalIgnoreCase);
foreach (var template in allTemplates)
{
var workGroupName = await shortcodesService.ApplyShortcodesAsync(template.Job!.WorkGroupMask, template);
// Если даже workGroupName == null, ну и ладно, сгруппируем по null, и распределим шаблоны в рамках этого null
if (workGroupName == null)
logger.LogWarning("Для шаблона {id}, с маской РГ '{workGroupMask}', не смог с помощью шорткода определить рабочую группу, шорткод сервис вернул РГ: '{workGroupName}'",
template.Id, template.Job!.WorkGroupMask, workGroupName);
// Добавляем шаблон в группу
if (!templatesByWorkGroup.TryGetValue(workGroupName, out var templatesList))
{
templatesList = new List<Template>();
templatesByWorkGroup[workGroupName] = templatesList;
}
templatesList.Add(template);
}
logger.LogDebug("Групп для распределения: {count}", templatesByWorkGroup.Count);
var dateStart = GetDateStart();
// Будем распределять каждую РГ отдельно
foreach (var (workGroupName, templates) in templatesByWorkGroup)
{
logger.LogDebug("Готовлюсь распределать шаблоны {templateCount} шт. в РГ '{workGroup}'", templates.Count, workGroupName);
if (isResponseAreaTimeZone)
{
// использовать часовой пояс РГ
logger.LogDebug("Использовать часовой пояс РГ");
// считаем, что у всех шаблонов сгруппированных по РГ, одна ЗО
// получаем ЗО для первого шаблона
var firstTemplate = templates.First();
var responseArea = await shortcodesService.ApplyShortcodesAsync(firstTemplate.Job!.ResponseAreaMask, firstTemplate);
if (responseArea == null)
logger.LogWarning("Для шаблона {id}, с маской ЗО '{responseAreaMask}', не смог с помощью шорткода определить ЗО, шорткод сервис вернул ЗО: '{responseArea}'",
firstTemplate.Id, firstTemplate.Job!.ResponseAreaMask, responseArea);
var offset = GetOffsetForDistributionByResponseArea(responseArea);
var templatesForDistribute = templates.Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
var referenceDate = GetReferenceDate(jobGroup, responseArea);
logger.LogInformation("Буду распределать шаблоны {count} шт, сгруппированные по РГ '{workGroup}', использую часовой пояс ЗО. dateStart: {dateStart}, offset: {offset}, referenceDate: {referenceDate}",
templatesForDistribute.Count, workGroupName, dateStart, offset, referenceDate);
var distributionResult = await templateDistributor.DistributeTemplatesAsync(dateStart, durationDays, referenceDate, offset, templatesForDistribute, jobGroup.DistributionConfig.IsExcludeWeekends);
logger.LogDebug("Закончил распределение для РГ '{workGroupName}', referenceDate: {referenceDate}, часового пояса ЗО {responseArea}, utcOffset: {offset}, распределил шаблонов: {count} шт.",
workGroupName, referenceDate, responseArea, offset, distributionResult.Count);
distributedTemplates.AddRange(distributionResult);
}
else
{
// не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП)
logger.LogDebug("Не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП)");
var offset = GetOffsetForDistributionByResponseArea();
var templatesForDistribute = templates.Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
var referenceDate = GetReferenceDate(jobGroup, null);
logger.LogInformation("Буду распределать шаблоны {count} шт, сгруппированные по РГ '{workGroup}', НЕ НУЖНО использовать часовой пояс ЗО. dateStart: {dateStart}, offset: {offset}, referenceDate: {referenceDate}",
templatesForDistribute.Count, workGroupName, dateStart, offset, referenceDate);
var distributionResult = await templateDistributor.DistributeTemplatesAsync(dateStart, durationDays, referenceDate, offset, templatesForDistribute, jobGroup.DistributionConfig.IsExcludeWeekends);
logger.LogDebug("Закончил распределение для РГ '{workGroup}', referenceDate: {referenceDate}, utcOffset: {offset}, распределил шаблонов: {count} шт.", workGroupName, referenceDate, offset, distributionResult.Count);
distributedTemplates.AddRange(distributionResult);
}
}
}
else
{
//не нужно группировать по РГ
logger.LogDebug("Не нужно группировать по РГ");
if (isResponseAreaTimeZone)
{
// использовать ЧАСОВОЙ ПОЯС ЗО (нужно сгруппировать шаблоны по часовым поясам, затем отдельно распределить каждую группу)
logger.LogDebug("Использовать ЧАСОВОЙ ПОЯС ЗО (нужно сгруппировать шаблоны по часовым поясам, затем отдельно распределить каждую группу)");
// получаем для каждого шаблона ЗО и сразу группируем по ЗО
var templatesByResponseArea = new Dictionary<string?, List<Template>>(StringComparer.OrdinalIgnoreCase);
foreach (var template in allTemplates)
{
var responseArea = await shortcodesService.ApplyShortcodesAsync(template.Job!.ResponseAreaMask, template);
if (responseArea == null)
logger.LogWarning("Для шаблона {id}, с маской ЗО '{responseAreaMask}', не смог с помощью шорткода определить ЗО, шорткод сервис вернул ЗО: '{responseArea}'",
template.Id, template.Job!.ResponseAreaMask, responseArea);
// Добавляем шаблон в группу
if (!templatesByResponseArea.TryGetValue(responseArea, out var templateList))
{
templateList = new List<Template>();
templatesByResponseArea[responseArea] = templateList;
}
templateList.Add(template);
}
logger.LogDebug("Групп для распределения: {count}", templatesByResponseArea.Count);
var dateStart = GetDateStart();
// для каждой ЗО получить offset и распределить шаблоны
foreach (var (responseArea, templates) in templatesByResponseArea)
{
var offset = GetOffsetForDistributionByResponseArea(responseArea);
var templatesForDistribute = templates.Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
var referenceDate = GetReferenceDate(jobGroup, responseArea);
logger.LogInformation("Буду распределять шаблоны {count} шт, их НЕ НУЖНО группировать по РГ, НУЖНО использовать часовой пояс ЗО: {responseArea}, utcOffset: {offset}. dateStart: {dateStart}, referenceDate: {referenceDate}",
templatesForDistribute.Count, responseArea, offset, dateStart, referenceDate);
var distributionResult = await templateDistributor.DistributeTemplatesAsync(dateStart, durationDays, referenceDate, offset, templatesForDistribute, jobGroup.DistributionConfig.IsExcludeWeekends);
logger.LogDebug("Закончил распределение для часового пояса ЗО {responseArea}, utcOffset: {offset}, referenceDate: {referenceDate}, распределил шаблонов: {count} шт.", responseArea, offset, referenceDate, distributionResult.Count);
distributedTemplates.AddRange(distributionResult);
}
}
else
{
// не нужно использовать часовой пояс ЗО
logger.LogDebug("Не нужно использовать часовой пояс ЗО. Распределю все шаблоны без группировки.");
var offset = GetOffsetForDistributionByResponseArea();
var templatesForDistribute = allTemplates.Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
var referenceDate = GetReferenceDate(jobGroup, null);
var dateStart = GetDateStart();
logger.LogInformation("Буду распределать шаблоны {count} шт, их НЕ НУЖНО группировать по РГ, НЕ НУЖНО использовать часовой пояс ЗО. dateStart: {dateStart}, offset: {offset}, referenceDate: {referenceDate}",
templatesForDistribute.Count, dateStart, offset, referenceDate);
distributedTemplates = await templateDistributor.DistributeTemplatesAsync(dateStart, durationDays, referenceDate, offset, templatesForDistribute, jobGroup.DistributionConfig.IsExcludeWeekends);
}
}
logger.LogInformation("Закончил распределение шаблонов {count} шт. для jobGroupId: {jobGroupId}. Изменено: {changedCount}, без изменений: {oldCount}",
distributedTemplates.Count, jobGroupId, distributedTemplates.Count(t => t.NextRun != t.NextRunOld), distributedTemplates.Count(t => t.NextRun == t.NextRunOld));
return distributedTemplates;
}
public async Task<DateTimeOffset?> GetNextRunForNewTemplateAsync(Guid jobGroupId, string workGroupName, string responseAreaName)
{
logger.LogDebug("Получить nextRun для вновь создаваемого шаблона. jobGroupId: {jobGroupId}, workGroupName: {workGroupName}, responseAreaName: {responseAreaName}",
jobGroupId, workGroupName, responseAreaName);
var jobGroup = await jobGroupService.Get()
.AsNoTracking()
.Include(t => t.DistributionConfig)
.ThenInclude(t => t.DistributionPeriod)
.FirstOrDefaultAsync(t => t.Id == jobGroupId);
if (jobGroup == null)
{
logger.LogError("Не найдена группа работ с Id: {jobGroupId}.", jobGroupId);
//throw new ArgumentNullException(nameof(jobGroupId), $"Не найдена группа работ с Id: {jobGroupId}");
return null;
}
if (jobGroup.IsAutoDistributionEnabled)
{
// автораспределение
logger.LogDebug("Расчет используя метод распределения работ");
if (jobGroup.DistributionConfig == null)
{
logger.LogError("Для группы работ {jobGroupId}, указано автораспределение, но отсутствует конфиг в таблице {GroupDistributionConfigs}", jobGroupId, nameof(JobGroupDistributionConfig));
//throw new ArgumentNullException(nameof(JobGroupDistributionConfig), $"Для jobGroupId: {jobGroupId} отсутствует конфигурация автораспределения в таблице {nameof(JobGroupDistributionConfig)}");
return null;
}
var startDate = GetDateStart();
var duration = GetDurationDays(jobGroup.DistributionConfig);
var excludeWeekends = jobGroup.DistributionConfig.IsExcludeWeekends;
var targetTemplate = new TemplateNextRunDto(Guid.Empty, null);
var templateStatusType = TemplateStatusTypeEnum.Used;
var allTemplatesQuery = templateService.Get()
.AsNoTracking()
.Include(t => t.Job)
.Where(t => t.StatusTypeId == templateStatusType && t.Job!.GroupId == jobGroupId);
// группировать по РГ
var isGroupingByWorkGroup = jobGroup.DistributionConfig.IsGroupingByWorkGroup;
// использовать часовой пояс РГ
var isResponseAreaTimeZone = jobGroup.IsResponseAreaTimezone;
if (isGroupingByWorkGroup)
{
// группировать по РГ
logger.LogDebug("Нужно группировать по РГ");
// получаем для всех шаблонов РГ и группируем их по РГ
var templatesByWorkGroup = new Dictionary<string?, List<Template>>(StringComparer.OrdinalIgnoreCase);
var allTemplates = await allTemplatesQuery.ToListAsync();
foreach (var template in allTemplates)
{
var templateWorkGroupName = await shortcodesService.ApplyShortcodesAsync(template.Job!.WorkGroupMask, template);
// Если даже workGroupName == null, ну и ладно, сгруппируем по null
if (templateWorkGroupName == null)
logger.LogWarning("Для шаблона {id}, с маской РГ '{workGroupMask}', не смог с помощью шорткода определить рабочую группу, шорткод сервис вернул РГ: '{workGroupName}'",
template.Id, template.Job!.WorkGroupMask, templateWorkGroupName);
// Добавляем шаблон в группу
if (!templatesByWorkGroup.TryGetValue(templateWorkGroupName, out var templatesList))
{
templatesList = new List<Template>();
templatesByWorkGroup[templateWorkGroupName] = templatesList;
}
templatesList.Add(template);
}
//Берем шаблоны только нашей РГ workGroupName
templatesByWorkGroup.TryGetValue(workGroupName, out var templatesWithWorkGroup);
var templatesByWorkGroupDto = (templatesWithWorkGroup ?? new List<Template>()).Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
logger.LogDebug("Шаблонов в РГ {workGroupName} - {count} шт.", workGroupName, templatesByWorkGroupDto.Count);
if (isResponseAreaTimeZone)
{
// использовать часовой пояс РГ
logger.LogDebug("Использовать часовой пояс РГ '{responseAreaName}'", responseAreaName);
var offset = GetOffsetForDistributionByResponseArea(responseAreaName);
var referenceDate = GetReferenceDate(jobGroup, responseAreaName);
var nextRun = await templateDistributor.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesByWorkGroupDto, excludeWeekends, isNew: true);
return nextRun.NextRun;
}
else
{
// не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП)
logger.LogDebug("Не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП)");
var offset = GetOffsetForDistributionByResponseArea();
var referenceDate = GetReferenceDate(jobGroup, null);
var nextRun = await templateDistributor.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesByWorkGroupDto, excludeWeekends, isNew: true);
return nextRun.NextRun;
}
}
else
{
// не нужно группировать по РГ
logger.LogDebug("Не нужно группировать по РГ");
if (isResponseAreaTimeZone)
{
// использовать ЧАСОВОЙ ПОЯС ЗО
logger.LogDebug("Нужно использовать часовой пояс ЗО '{responseAreaName}'", responseAreaName);
// получить все шаблоны, сгруппировать их по ЗО, получить nextRun в рамках переданной ЗО
var referenceDate = GetReferenceDate(jobGroup, responseAreaName);
var offset = GetOffsetForDistributionByResponseArea(responseAreaName);
var allTemplates = await allTemplatesQuery.ToListAsync();
// получаем для каждого шаблона ЗО и сразу группируем по ЗО
var templatesByResponseArea = new Dictionary<string?, List<Template>>(StringComparer.OrdinalIgnoreCase);
foreach (var template in allTemplates)
{
var responseArea = await shortcodesService.ApplyShortcodesAsync(template.Job!.ResponseAreaMask, template);
if (responseArea == null)
logger.LogWarning("Для шаблона {id}, с маской ЗО '{responseAreaMask}', не смог с помощью шорткода определить ЗО, шорткод сервис вернул ЗО: '{responseArea}'",
template.Id, template.Job!.ResponseAreaMask, responseArea);
// Добавляем шаблон в группу
if (!templatesByResponseArea.TryGetValue(responseArea, out var templateList))
{
templateList = new List<Template>();
templatesByResponseArea[responseArea] = templateList;
}
templateList.Add(template);
}
// из сгруппированных шаблонов, получаем существующие с нашей переданной responseAreaName
templatesByResponseArea.TryGetValue(responseAreaName, out var templatesWithResponseArea);
var templatesWithResponseAreaDto = (templatesWithResponseArea ?? new List<Template>()).Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
logger.LogDebug("Существующих шаблонов в БД с responseAreaName: {responseAreaName} - {count} шт.", responseAreaName, templatesWithResponseAreaDto.Count);
var nextRun = await templateDistributor.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesWithResponseAreaDto, excludeWeekends, isNew: true);
return nextRun.NextRun;
}
else
{
// не нужно использовать часовой пояс ЗО
logger.LogDebug("Не нужно использовать часовой пояс ЗО.");
var referenceDate = GetReferenceDate(jobGroup, null);
var offset = GetOffsetForDistributionByResponseArea();
var allTemplates = await allTemplatesQuery
.Select(t => new TemplateNextRunDto(t.Id, t.NextRun))
.ToListAsync();
var nextRun = await templateDistributor.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, allTemplates, excludeWeekends, isNew: true);
return nextRun.NextRun;
}
}
}
else
{
// расписание ЕСПП
logger.LogDebug("Расчет используя метод ЕСПП");
if (jobGroup.IsResponseAreaTimezone)
{
// использовать часовой пояс ЗО
var referenceDate = GetReferenceDate(jobGroup, responseAreaName);
var offset = GetOffsetForDistributionByResponseArea(responseAreaName);
logger.LogDebug("Расчет nextRun используя метод ЕСПП, использую часовой пояс ЗО '{responseAreaName}, referenceDate: {referenceDate}, offset: {offset}'", responseAreaName, referenceDate, offset);
var nextRun = await esppScheduleTransformService.GetNextDateAsync(jobGroupId, referenceDate, offset);
return nextRun;
}
else
{
// не использовать часовой пояс ЗО
// так как это классическое расписание еспп, и не используется часовой пояс РГ, то нам нужно считать в часовом поясе УЗ ЕСПП, т.е. МСК
var referenceDate = GetReferenceDate(jobGroup, null);
var offset = GetOffsetForDistributionByResponseArea();
logger.LogDebug("Расчет nextRun используя метод ЕСПП, не использую часовой пояс ЗО, использую часовой пояс УЗ ЕСПП, referenceDate: {referenceDate}, offset: {offset}", referenceDate, offset);
var nextRun = await esppScheduleTransformService.GetNextDateAsync(jobGroupId, referenceDate, offset);
return nextRun;
}
}
}
public async Task<DateTimeOffset?> GetNextRunForTemplateAsync(Guid templateId, bool isNew)
{
logger.LogDebug("Получить nextRun для существующего шаблона, templateId: {templateId}, isNew: {isNew}", templateId, isNew);
var template = await templateService.Get()
.AsNoTracking()
.Include(t => t.Job)
.ThenInclude(t => t.Group).ThenInclude(t => t.DistributionConfig).ThenInclude(t => t.DistributionPeriod)
.FirstOrDefaultAsync(t => t.Id == templateId);
if (template == null)
{
logger.LogError("Не найден шаблон с Id: {templateId}.", templateId);
//throw new ArgumentNullException(nameof(templateId), $"Не найден шаблон с Id: {templateId}");
return null;
}
if (template.Job!.Group!.IsAutoDistributionEnabled == true)
{
// включено автораспределение
logger.LogDebug("Включено автораспределение");
if (template.Job!.Group.DistributionConfig == null)
{
logger.LogError("Для шаблона {templateId}, jobGroupId {jobGroupId}, указано автораспределение, но отсутствует конфиг в таблице {GroupDistributionConfigs}", template.Id, template.Job.GroupId, nameof(JobGroupDistributionConfig));
// throw new ArgumentNullException(nameof(JobGroupDistributionConfig), $"Для jobGroupId: {template.Job.GroupId} отсутствует конфигурация автораспределения в таблице {nameof(JobGroupDistributionConfig)}");
return null;
}
var startDate = GetDateStart();
var duration = GetDurationDays(template.Job.Group.DistributionConfig);
var excludeWeekends = template.Job.Group.DistributionConfig.IsExcludeWeekends;
var targetTemplate = new TemplateNextRunDto(template.Id, template.NextRun);
var templateStatusType = TemplateStatusTypeEnum.Used;
var jobGroup = template.Job.Group;
// получаем все шаблоны из этой же группы что и template
var allTemplatesQuery = templateService.Get()
.AsNoTracking()
.Include(t => t.Job)
.Where(t => t.StatusTypeId == templateStatusType && t.Job!.GroupId == jobGroup.Id);
// группировать по РГ
var isGroupingByWorkGroup = jobGroup.DistributionConfig.IsGroupingByWorkGroup;
// использовать часовой пояс РГ
var isResponseAreaTimeZone = jobGroup.IsResponseAreaTimezone;
if (isGroupingByWorkGroup)
{
var targetTemplateWorkGroup = await shortcodesService.ApplyShortcodesAsync(template.Job.WorkGroupMask, template);
// группировать по РГ
logger.LogDebug("Нужно группировать по РГ");
// получаем для всех шаблонов РГ и группируем их по РГ
var templatesByWorkGroup = new Dictionary<string?, List<Template>>(StringComparer.OrdinalIgnoreCase);
var allTemplates = await allTemplatesQuery.ToListAsync();
foreach (var templateItem in allTemplates)
{
var templateWorkGroupName = await shortcodesService.ApplyShortcodesAsync(templateItem.Job!.WorkGroupMask, templateItem);
// Если даже workGroupName == null, ну и ладно, сгруппируем по null
if (templateWorkGroupName == null)
logger.LogWarning("Для шаблона {id}, с маской РГ '{workGroupMask}', не смог с помощью шорткода определить рабочую группу, шорткод сервис вернул РГ: '{workGroupName}'",
templateItem.Id, templateItem.Job!.WorkGroupMask, templateWorkGroupName);
// Добавляем шаблон в группу
if (!templatesByWorkGroup.TryGetValue(templateWorkGroupName, out var templatesList))
{
templatesList = new List<Template>();
templatesByWorkGroup[templateWorkGroupName] = templatesList;
}
templatesList.Add(templateItem);
}
//Берем шаблоны только нашей РГ workGroupName
templatesByWorkGroup.TryGetValue(targetTemplateWorkGroup, out var templatesWithWorkGroup);
var templatesByWorkGroupDto = (templatesWithWorkGroup ?? new List<Template>()).Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
logger.LogDebug("Шаблонов в РГ {targetTemplateWorkGroup} - {count} шт.", targetTemplateWorkGroup, templatesByWorkGroupDto.Count);
if (isResponseAreaTimeZone)
{
// использовать часовой пояс РГ
var targetTemplateResponseArea = await shortcodesService.ApplyShortcodesAsync(template.Job.ResponseAreaMask, template);
logger.LogDebug("Использовать часовой пояс РГ '{responseAreaName}'", targetTemplateResponseArea);
var offset = GetOffsetForDistributionByResponseArea(targetTemplateResponseArea);
var referenceDate = GetReferenceDate(jobGroup, targetTemplateResponseArea);
var nextRun = await templateDistributor.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesByWorkGroupDto, excludeWeekends, isNew);
return nextRun.NextRun;
}
else
{
// не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП)
logger.LogDebug("Не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП)");
var offset = GetOffsetForDistributionByResponseArea();
var referenceDate = GetReferenceDate(jobGroup, null);
var nextRun = await templateDistributor.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesByWorkGroupDto, excludeWeekends, isNew);
return nextRun.NextRun;
}
}
else
{
// не нужно группировать по РГ
logger.LogDebug("Не нужно группировать по РГ");
if (isResponseAreaTimeZone)
{
// использовать ЧАСОВОЙ ПОЯС ЗО
var responseAreaName = await shortcodesService.ApplyShortcodesAsync(template.Job.ResponseAreaMask, template);
logger.LogDebug("Нужно использовать часовой пояс ЗО '{responseAreaName}'", responseAreaName);
// получить все шаблоны, сгруппировать их по ЗО, получить nextRun в рамках переданной ЗО
var referenceDate = GetReferenceDate(jobGroup, responseAreaName);
var offset = GetOffsetForDistributionByResponseArea(responseAreaName);
var allTemplates = await allTemplatesQuery.ToListAsync();
// получаем для каждого шаблона ЗО и сразу группируем по ЗО
var templatesByResponseArea = new Dictionary<string?, List<Template>>(StringComparer.OrdinalIgnoreCase);
foreach (var templateItem in allTemplates)
{
var responseArea = await shortcodesService.ApplyShortcodesAsync(templateItem.Job!.ResponseAreaMask, templateItem);
if (responseArea == null)
logger.LogWarning("Для шаблона {id}, с маской ЗО '{responseAreaMask}', не смог с помощью шорткода определить ЗО, шорткод сервис вернул ЗО: '{responseArea}'",
templateItem.Id, templateItem.Job!.ResponseAreaMask, responseArea);
// Добавляем шаблон в группу
if (!templatesByResponseArea.TryGetValue(responseArea, out var templateList))
{
templateList = new List<Template>();
templatesByResponseArea[responseArea] = templateList;
}
templateList.Add(templateItem);
}
// из сгруппированных шаблонов, получаем существующие с ЗО нашего шаблона responseAreaName
templatesByResponseArea.TryGetValue(responseAreaName, out var templatesWithResponseArea);
var templatesWithResponseAreaDto = (templatesWithResponseArea ?? new List<Template>()).Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList();
logger.LogDebug("Существующих шаблонов в БД с responseAreaName: {responseAreaName} - {count} шт.", responseAreaName, templatesWithResponseAreaDto.Count);
var nextRun = await templateDistributor.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesWithResponseAreaDto, excludeWeekends, isNew);
return nextRun.NextRun;
}
else
{
// не нужно использовать часовой пояс ЗО
logger.LogDebug("Не нужно использовать часовой пояс ЗО.");
var referenceDate = GetReferenceDate(jobGroup, null);
var offset = GetOffsetForDistributionByResponseArea();
var allTemplates = await allTemplatesQuery
.Select(t => new TemplateNextRunDto(t.Id, t.NextRun))
.ToListAsync();
var nextRun = await templateDistributor.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, allTemplates, excludeWeekends, isNew);
return nextRun.NextRun;
}
}
}
else
{
// метод расчета ЕСПП
logger.LogDebug("Метод расчета ЕСПП");
if (template.Job.Group.IsResponseAreaTimezone)
{
// использовать часовой пояс ЗО
var responseAreaName = await shortcodesService.ApplyShortcodesAsync(template.Job.ResponseAreaMask, template);
var jobGroup = template.Job.Group;
var referenceDate = GetReferenceDate(jobGroup, responseAreaName);
var offset = GetOffsetForDistributionByResponseArea(responseAreaName);
logger.LogDebug("Расчет nextRun используя метод ЕСПП, использую часовой пояс ЗО '{responseAreaName}, referenceDate: {referenceDate}, offset: {offset}'", responseAreaName, referenceDate, offset);
var nextRun = await esppScheduleTransformService.GetNextDateAsync(jobGroup.Id, referenceDate, offset);
return nextRun;
}
else
{
// не использовать часовой пояс ЗО
// так как это классическое расписание еспп, и не используется часовой пояс РГ, то нам нужно считать в часовом поясе УЗ ЕСПП, т.е. МСК
var referenceDate = GetReferenceDate(template.Job.Group, null);
var offset = GetOffsetForDistributionByResponseArea();
logger.LogDebug("Расчет nextRun используя метод ЕСПП, не использую часовой пояс ЗО, использую часовой пояс УЗ ЕСПП, referenceDate: {referenceDate}, offset: {offset}", referenceDate, offset);
var nextRun = await esppScheduleTransformService.GetNextDateAsync(template.Job.GroupId, referenceDate, offset);
return nextRun;
}
}
}
public int GetDurationDays(JobGroupDistributionConfig config)
{
var period = config.DistributionPeriod;
var periodType = period!.Type;
int.TryParse(period.Duration, out var duration);
var periodDays = duration;
if (periodType == DistributionPeriodTypeEnum.Day.ToString())
periodDays = duration;
if (periodType == DistributionPeriodTypeEnum.Month.ToString())
// в месяце 30 дней, duration*30
periodDays = duration * 30;
if (periodType == DistributionPeriodTypeEnum.Year.ToString())
// в году 365 дней, duration*365
periodDays = duration * 365;
logger.LogDebug("Период распределения: {name}, duration: {duration}, type: {type}. Итого в днях: {days}", period.Name, period.Duration, period.Type, periodDays);
if (periodDays == 0)
{
periodDays = 1;
logger.LogWarning("Полученный период распределения 0 дней. Неверный конфиг распределения в таблице {table}. {name}, duration: {duration}, type: {type}. Устанавливаем минимальный период распределения {periodDays} дней.", nameof(DistributionPeriod), period.Name, period.Duration, period.Type, periodDays);
}
return periodDays;
}
public async Task<List<DateOnly>> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends)
{
return await templateDistributor.GetWorkDaysAsync(start, end, excludeWeekends);
}
public async Task<HashSet<DateOnly>> GetWeekendsAsync(DateOnly startDate, DateOnly endDate)
{
return await templateDistributor.GetWeekendsAsync(startDate, endDate);
}
/// <summary>
/// Получить часовую зону для распределения
/// </summary>
/// <returns></returns>
private TimeSpan GetOffsetForDistributionByResponseArea(string? responseArea = null)
{
// или часовая зона робота, или часовая зона РГ
if (responseArea != null)
{
// возвращаем часовой пояс ЗО
var responseAreaOffset = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea).UtcTimeOffset;
logger.LogDebug("Получил часовой пояс для ЗО: {responseArea}, UtcTimeOffset: {responseAreaOffset}", responseArea, responseAreaOffset);
return responseAreaOffset;
}
else
{
// возвращаем часовой пояс робота
var esppOffset = GetEsppAccountOffset();
logger.LogDebug("Не передана ЗО, использую часовой пояс УЗ ЕСПП, UtcTimeOffset: {UtcTimeOffset}", esppOffset);
return esppOffset;
}
}
public TimeSpan GetEsppAccountOffset()
{
var esppOffset = TimeSpan.FromHours(settingsFromDb.EsppRobotAccountTimeZoneHour);
logger.LogDebug("Оффсет УЗ ЕСПП: {esppOffset}", esppOffset);
return esppOffset;
}
/// <summary>
/// Получить дату начала распределения
/// </summary>
/// <param name="offset"></param>
/// <returns></returns>
private DateOnly GetDateStart()
{
//var dateStart = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).Date);
//logger.LogDebug("Дата начала распределения (сегодня) в целевом часовом поясе {dateStart}, offset: {offset}", dateStart, offset);
var dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date);
logger.LogDebug("Дата начала распределения (сегодня): {dateStart}", dateStart);
return dateStart;
}
/// <summary>
/// Получить referenceDate для расчета с учетом часового пояса ЗО или без учета
/// </summary>
/// <param name="jobGroup"></param>
/// <param name="responseArea">Не обзяательный параметр, нужен обязательно при расчете с учетом часового пояса ЗО</param>
/// <returns></returns>
private DateTimeOffset GetReferenceDate(JobGroup jobGroup, string? responseArea)
{
// для того чтобы посчитать nextRun в часовом поясе ЗО, нужно пересчитать refDate чтобы понять в какое время хотел выполнять заказчик
logger.LogDebug("Исходный referenceDate UTC: {referenceDate}", jobGroup.ReferenceDate);
if (jobGroup.IsResponseAreaTimezone)
{
// нужно вернуть с учетом часового пояса клиента сохранившего refDate (используется для расчета с учетом часового пояса ЗО)
var clientOffset = jobGroup.UserTimeZoneOffset;
if (!clientOffset.HasValue)
{
logger.LogError("При расчете referenceDate для ЗО '{responseArea}', UserTimeZoneOffset=null. Вернул referenceDate {referenceDate} без смещения ЗО",
responseArea, jobGroup.ReferenceDate);
return jobGroup.ReferenceDate;
}
if (responseArea == null)
{
logger.LogError("При расчете referenceDate для ЗО '{responseArea}', responseArea=null. Вернул referenceDate {referenceDate} без смещения ЗО",
responseArea, jobGroup.ReferenceDate);
return jobGroup.ReferenceDate;
}
var responseAreaOffset = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea).UtcTimeOffset;
// = реф + офКлиента(который нажал в ГУИ сохранить)-офЗО
var resultOffset = clientOffset.Value - responseAreaOffset;
var refDateWithResponseAreaOffset = jobGroup.ReferenceDate.Add(resultOffset);
logger.LogDebug("Клиент ожидает, что будет выполняться задача в каждой ЗО в {time}. referenceDate+clientOffset, {referenceDate}, {clientOffset}",
TimeOnly.FromDateTime(jobGroup.ReferenceDate.Add(clientOffset.Value).DateTime), jobGroup.ReferenceDate, clientOffset);
logger.LogDebug("Итоговый referenceDate UTC: {resultRefDate}, для использования часовой зоны ЗО: '{responseArea}', добавлено времени {resultOffset} к исходному {refDate}",
refDateWithResponseAreaOffset, responseArea, resultOffset, jobGroup.ReferenceDate);
logger.LogDebug("Если итоговый referenceDate UTC {refDateWithResponseAreaOffset} перевести в часовой пояс ЗО {responseAreaOffset}, то дата в часовом поясе ЗО будет {refDateTargetTz}",
refDateWithResponseAreaOffset, responseAreaOffset, refDateWithResponseAreaOffset.Add(responseAreaOffset));
return refDateWithResponseAreaOffset;
}
else
{
// если НЕ НУЖНО в часовом поясе ЗО, то просто возвращаем refDate
logger.LogDebug("Итоговый referenceDate UTC: {refDate}, не нужно использовать часовой пояс ЗО", jobGroup.ReferenceDate);
return jobGroup.ReferenceDate;
}
}
}
}

View File

@@ -2,14 +2,13 @@
using PARR.DAL.Contracts;
using PARR.DAL.DomainModels;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.TransformServices;
using System.Text.Json;
namespace PARR.DAL.NextRunServices.Subservices
{
internal class EsppScheduleTransformService : IEsppScheduleTransformService
{
private readonly IEsppSchTypeConfigService esppSchTypeConfigService;
//private readonly INextRunModifierService nextRunModifierService;
private readonly ILogger<EsppScheduleTransformService> logger;
private static readonly Dictionary<string, int> monthDict = new Dictionary<string, int>()
@@ -52,11 +51,9 @@ namespace PARR.DAL.NextRunServices.Subservices
public EsppScheduleTransformService(
ILogger<EsppScheduleTransformService> logger,
IEsppSchTypeConfigService esppSchTypeConfigService
//INextRunModifierService nextRunModifierService
)
{
this.esppSchTypeConfigService = esppSchTypeConfigService;
//this.nextRunModifierService = nextRunModifierService;
this.logger = logger;
}
@@ -69,12 +66,19 @@ namespace PARR.DAL.NextRunServices.Subservices
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 = 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));
@@ -108,71 +112,23 @@ namespace PARR.DAL.NextRunServices.Subservices
break;
}
logger.LogDebug("Рассчитанная дата, с учетом offset: {nextRun}", nextRun);
// Возвращаем обратно в ЮТС
nextRun = nextRun.ToOffset(TimeSpan.Zero);
logger.LogDebug("Рассчитанная дата с нулевым offset: {nextRun}", nextRun);
return nextRun;
}
//private List<DateTimeOffset> GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate)
//{
// // расписание на оставшееся на сегодня время. его так мало...
// referenceDate = referenceDate.UtcDateTime;
// var schedules = new List<DateTimeOffset>();
// var curTime = referenceDate;
// //Если ещё не произошло то добавляем расписание, возможно это новое AiW и он ещё ниразу не запускался.
// //lastRun это будущее
// if (referenceDate >= DateTimeOffset.UtcNow && referenceDate <= DateTimeOffset.UtcNow.EndOfDay())
// schedules.Add(referenceDate);
// 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<List<DateTimeOffset>> GetNextScheduleAsync(Guid jobGroupId, DateTimeOffset referenceDate)
//{
// //TODO: тут не проверен переход через выходные дни!!! Переход не используется, так как тут не рассчитываем NextRun.
// //В общем проверить, когда будем тестировать агента, что с датами все ок
// var esppSchedule = await GetEsppScheduleAsync(jobGroupId);
// var nextSchedule = GetNextSchedule(esppSchedule, referenceDate);
// return nextSchedule;
//}
/// <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);
@@ -236,7 +192,22 @@ namespace PARR.DAL.NextRunServices.Subservices
var calcDay = referenceDate;
if (calcDay < DateTimeOffset.UtcNow)
calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, DateTimeOffset.UtcNow.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
{
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);
@@ -260,7 +231,20 @@ namespace PARR.DAL.NextRunServices.Subservices
var calcDay = new DateTimeOffset(referenceDate.Year, referenceDate.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
if (referenceDate < DateTimeOffset.UtcNow)
calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
{
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: вот это повторяется от метода к методу
//Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
@@ -285,9 +269,29 @@ namespace PARR.DAL.NextRunServices.Subservices
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.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);
? 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);
@@ -330,7 +334,12 @@ namespace PARR.DAL.NextRunServices.Subservices
throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
}
var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year;
//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,
@@ -379,9 +388,15 @@ namespace PARR.DAL.NextRunServices.Subservices
throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}");
}
var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year;
//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 == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.Month)
if (year == nowInTargetTz.Year && referenceDate.Month < nowInTargetTz.Month)
year += 1;
var calcDay = new DateTimeOffset(
@@ -432,91 +447,5 @@ namespace PARR.DAL.NextRunServices.Subservices
return calcDay.AddDays(daysOffset);
}
//public DateTimeOffset GetNextDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod)
//{
// var nextRun = lastRun;
// switch (periodType)
// {
// case (DistributionPeriodTypeEnum.Day):
// nextRun = lastRun.AddDays(ParseInt(distributionPeriod));
// break;
// case (DistributionPeriodTypeEnum.Month):
// nextRun = lastRun.AddMonths(ParseInt(distributionPeriod));
// break;
// case (DistributionPeriodTypeEnum.Year):
// nextRun = lastRun.AddYears(ParseInt(distributionPeriod));
// break;
// }
// return nextRunModifierService.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<DateOnly> GetStartPeriodForDateAsync(Guid jobGroupId, DateTimeOffset date, DateTimeOffset referenceDate, DistributionPeriodTypeEnum periodType, string distributionPeriod)
//{
// var esppSchedule = await GetEsppScheduleAsync(jobGroupId);
// 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;
//}
}
}

View File

@@ -5,55 +5,13 @@
/// </summary>
internal interface IEsppScheduleTransformService
{
///// <summary>
///// Получить следующую дату согласно расписания
///// </summary>
///// <param name="esppSchedule"></param>
///// <param name="lastRun"></param>
///// <returns></returns>
//DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset lastRun);
///// <summary>
///// Получить расписание
///// </summary>
///// <param name="esppSchedule"></param>
///// <param name="lastRun"></param>
///// <returns></returns>
//List<DateTimeOffset> GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset lastRun);
/// <summary>
/// Получить следующую дату по jobGroupId
/// </summary>
/// <param name="jobGroupId"></param>
/// <param name="lastRun"></param>
/// <param name="referenceDate">referenceDate или nextRun</param>
/// <param name="offset">offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК</param>
/// <returns></returns>
Task<DateTimeOffset> GetNextDateAsync(Guid jobGroupId, DateTimeOffset lastRun, TimeSpan? offset = null);
///// <summary>
///// Получить расписание по jobGroupId
///// </summary>
///// <param name="jobGroupId"></param>
///// <param name="lastRun"></param>
///// <returns></returns>
//Task<List<DateTimeOffset>> GetNextScheduleAsync(Guid jobGroupId, DateTimeOffset lastRun);
///// <summary>
///// Получить следующую дату срабатывания в указанном периоде
///// </summary>
///// <param name="lastRun"></param>
///// <param name="periodType"></param>
///// <param name="distributionPeriod"></param>
///// <returns></returns>
//DateTimeOffset GetNextDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod);
///// <summary>
///// Получить дату начала периода распределения относительно опорной даты (Reference Date)
///// </summary>
///// <param name="jobGroupId"></param>
///// <param name="date"></param>
///// <returns></returns>
//Task<DateOnly> GetStartPeriodForDateAsync(Guid jobGroupId, DateTimeOffset date, DateTimeOffset refrenceDate, DistributionPeriodTypeEnum periodType, string distributionPeriod);
Task<DateTimeOffset> GetNextDateAsync(Guid jobGroupId, DateTimeOffset referenceDate, TimeSpan? offset = null);
}
}

View File

@@ -0,0 +1,51 @@
using PARR.DAL.NextRunServices.Models;
namespace PARR.DAL.NextRunServices.Subservices
{
internal interface ITemplateDistributorV2
{
/// <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);
}
}

View File

@@ -27,6 +27,9 @@ namespace PARR.DAL.NextRunServices.Subservices
public async Task<TemplateNextRunResultDto> GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List<TemplateNextRunDto> allTemplates, bool excludeWeekends, bool isNew)
{
//if (offset.HasValue)
// referenceDate = referenceDate.ToOffset(offset.Value);
//todo: allTemplates - пока не используется. Будет использоваться когда каждый раз будем строить план
// Пока работает так:
@@ -104,6 +107,10 @@ namespace PARR.DAL.NextRunServices.Subservices
public async Task<List<TemplateNextRunResultDto>> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List<TemplateNextRunDto> allTemplates, bool excludeWeekends)
{
//if (offset.HasValue)
//referenceDate = referenceDate.ToOffset(offset.Value);
var _startDate = GetStartPeriodDate(startDate, referenceDate);
var _endDate = GetEndPeriodDate(_startDate, duration);
@@ -417,7 +424,7 @@ namespace PARR.DAL.NextRunServices.Subservices
return CreateDateTimeOffset(minDay, referenceDate);
}
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
/// <summary>
/// Получить план: дни, на которые должны быть назначены шаблоны
@@ -492,7 +499,7 @@ namespace PARR.DAL.NextRunServices.Subservices
/// <returns></returns>
private DateTimeOffset CreateDateTimeOffset(DateOnly date, DateTimeOffset refDate)
{
return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero);
return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, refDate.Offset);
}
@@ -507,20 +514,61 @@ namespace PARR.DAL.NextRunServices.Subservices
// дата начала не может быть раньше referenceDate
// и не может быть раньше чем сегодня
var dateStart = startDate;
#region old
//var dateStart = startDate;
// если dateStart < чем refDate, то берем refDate
if (dateStart < DateOnly.FromDateTime(referenceDate.Date))
dateStart = DateOnly.FromDateTime(referenceDate.Date);
//// если 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);
//// если dateStart < чем текущая дата, то берем текущую дату
//if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date))
// dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date);
// если текущее время > referenceDate, то прибавляем 1 день
//// если текущее время > referenceDate, то прибавляем 1 день
//var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate);
//if (dateTimeStart < DateTimeOffset.UtcNow)
// dateStart = dateStart.AddDays(1);
//return dateStart;
#endregion
// 1. Конвертируем startDate (UTC) в целевой часовой пояс
var startDateUtc = new DateTimeOffset(
startDate.Year, startDate.Month, startDate.Day,
0, 0, 0,
TimeSpan.Zero);
var startDateInTargetTz = startDateUtc.ToOffset(referenceDate.Offset);
var dateStart = DateOnly.FromDateTime(startDateInTargetTz.DateTime);
logger.LogDebug("startDate UTC ({startDate} 00:00 UTC) конвертирована в целевой часовой пояс {offset}: {dateStart}", startDate, referenceDate.Offset, dateStart);
// 2. Дата начала не может быть раньше даты из referenceDate (в его часовом поясе)
var refDateInTargetTz = DateOnly.FromDateTime(referenceDate.Date);
if (dateStart < refDateInTargetTz)
{
logger.LogDebug("Дата начала {dateStart} раньше даты из referenceDate {refDate}. Используем дату из referenceDate.", dateStart, refDateInTargetTz);
dateStart = refDateInTargetTz;
}
// 3. Дата начала не может быть раньше "сегодня" в целевом часовом поясе
var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Date);
if (dateStart < todayInTargetTz)
{
logger.LogDebug("Дата начала {dateStart} раньше 'сегодня' в целевом поясе {today}. Используем 'сегодня'.", dateStart, todayInTargetTz);
dateStart = todayInTargetTz;
}
// 4. Если момент времени "начало дня в целевом поясе" уже прошёл — добавляем 1 день
var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate);
if (dateTimeStart < DateTimeOffset.UtcNow)
{
logger.LogDebug("Момент времени {dateTimeStart} уже прошёл (сейчас {now}). Добавляем 1 день.", dateTimeStart, DateTimeOffset.UtcNow);
dateStart = dateStart.AddDays(1);
}
logger.LogDebug("Итоговая дата начала распределения: {dateStart} (в часовом поясе {offset})", dateStart, referenceDate.Offset);
return dateStart;
}
@@ -565,41 +613,44 @@ namespace PARR.DAL.NextRunServices.Subservices
/// <param name="startDate"></param>
/// <param name="endDate"></param>
/// <returns></returns>
public async Task<HashSet<DateOnly>> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate)
public async Task<HashSet<DateOnly>> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TimeSpan? offset = null)
{
if (offset.HasValue)
referenceDate = referenceDate.ToOffset(offset.Value);
// проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd
bool isPreviousDays = IsPreviousDays(referenceDate);
//bool isPreviousDays = IsPreviousDays(referenceDate);
if (isPreviousDays)
{
// Это предыдущие сутки, корректируем
startDate = startDate.AddDays(-1);
// так как мы потом будем сдвигать выходные дни влево, то надо взять на один день дальше, чтоб знать какой он
// вроде бы это не костыль
endDate = endDate.AddDays(1);
//endDate = endDate.AddDays(-1);
//if (isPreviousDays)
//{
// // Это предыдущие сутки, корректируем
// startDate = startDate.AddDays(-1);
// // так как мы потом будем сдвигать выходные дни влево, то надо взять на один день дальше, чтоб знать какой он
// // вроде бы это не костыль
// endDate = endDate.AddDays(1);
// //endDate = endDate.AddDays(-1);
logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты для получения выходных дней, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, startDate, endDate);
}
// logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты для получения выходных дней, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, startDate, endDate);
//}
var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();
var weekendsResult = new List<DateOnly>();
if (isPreviousDays)
{
// так как должны быть предыдущие дни, отнимаем 1 день
foreach (var weekend in weekends)
{
var previous = weekend.AddDays(-1);
logger.LogDebug("Выходной день, -1, так как день должен быть предыдущий, старое значение {weekend}, новое {previous}", weekend, previous);
weekendsResult.Add(previous);
}
}
else
{
weekendsResult = weekends;
}
//if (isPreviousDays)
//{
// // так как должны быть предыдущие дни, отнимаем 1 день
// foreach (var weekend in weekends)
// {
// var previous = weekend.AddDays(-1);
// logger.LogDebug("Выходной день, -1, так как день должен быть предыдущий, старое значение {weekend}, новое {previous}", weekend, previous);
// weekendsResult.Add(previous);
// }
//}
//else
//{
// weekendsResult = weekends;
//}
logger.LogDebug("Найдено не рабочих дней: {Count}", weekendsResult.Count);
foreach (var weekend in weekendsResult)
@@ -644,19 +695,22 @@ namespace PARR.DAL.NextRunServices.Subservices
//}
public async Task<List<DateOnly>> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate)
public async Task<List<DateOnly>> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate, TimeSpan? offset = null)
{
if (offset.HasValue)
referenceDate = referenceDate.ToOffset(offset.Value);
logger.LogDebug("Формирование списка рабочих дней, start: {start}, end: {end}, excludeWeekends: {excludeWeekends}, referenceDate: {referenceDate}", start, end, excludeWeekends, referenceDate);
// проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd
if (IsPreviousDays(referenceDate))
{
// Это предыдущие сутки, корректируем
start = start.AddDays(-1);
end = end.AddDays(-1);
//// проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd
//if (IsPreviousDays(referenceDate))
//{
// // Это предыдущие сутки, корректируем
// start = start.AddDays(-1);
// end = end.AddDays(-1);
logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, start, end);
}
// logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, start, end);
//}
// Получаем список выходных/праздничных дней
var weekends = new HashSet<DateOnly>();
@@ -683,33 +737,45 @@ namespace PARR.DAL.NextRunServices.Subservices
return result;
}
/// <summary>
/// Это предыдущие сутки? Относительно referenceDate и МСК времени
/// </summary>
/// <param name="referenceDate"></param>
/// <returns></returns>
private bool IsPreviousDays(DateTimeOffset referenceDate)
public Task<HashSet<DateOnly>> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate)
{
// смещение часов относительно UTC - MSK
int offsetMskHour = 3;
// если время больше 21 часа, то это текущие сутки, если меньше, то прошлые
int maxHour = 24 - offsetMskHour;
if (referenceDate.Hour >= maxHour && referenceDate.Hour <= 23)
{
// это прошлые сутки
logger.LogDebug("ReferenceDate {refDate} - прошлые стуки", referenceDate);
return true;
}
else
{
// это текущие сутки
logger.LogDebug("ReferenceDate {refDate} - текущие стуки", referenceDate);
return false;
}
throw new NotImplementedException();
}
public Task<List<DateOnly>> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate)
{
throw new NotImplementedException();
}
///// <summary>
///// Это предыдущие сутки? Относительно referenceDate и МСК времени
///// </summary>
///// <param name="referenceDate"></param>
///// <returns></returns>
//private bool IsPreviousDays(DateTimeOffset referenceDate)
//{
// // смещение часов относительно UTC - MSK
// int offsetMskHour = 3;
// // если время больше 21 часа, то это текущие сутки, если меньше, то прошлые
// int maxHour = 24 - offsetMskHour;
// if (referenceDate.Hour >= maxHour && referenceDate.Hour <= 23)
// {
// // это прошлые сутки
// logger.LogDebug("ReferenceDate {refDate} - прошлые стуки", referenceDate);
// return true;
// }
// else
// {
// // это текущие сутки
// logger.LogDebug("ReferenceDate {refDate} - текущие стуки", referenceDate);
// return false;
// }
//}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PARR.DAL.Cache.Services.Base;
using PARR.DAL.Configurations.DbSettings;
using PARR.DAL.Context;
@@ -141,6 +142,8 @@ namespace PARR.DAL
services.AddTransient<IEsppScheduleTransformService, EsppScheduleTransformService>();
services.AddTransient<INextRunService, NextRunService>();
services.AddTransient<ITemplateDistributor, TemplateDistributor>();
services.AddTransient<ITemplateDistributorV2, TemplateDistributorV2>();
services.AddTransient<INextRunServiceV2, NextRunServiceV2>();
#endregion
@@ -189,8 +192,9 @@ namespace PARR.DAL
using var scope = provider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
var settingsFromDb = scope.ServiceProvider.GetRequiredService<SettingsFromDb>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<ScheduleResponseAreaTimeOffsetService>>();
return new ScheduleResponseAreaTimeOffsetService(dbContext, settingsFromDb);
return new ScheduleResponseAreaTimeOffsetService(dbContext, settingsFromDb, logger);
});
}

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.DAL.Context;
using PARR.DAL.Contracts;
using PARR.DAL.Models.Schedule;
@@ -17,8 +18,9 @@ namespace PARR.DAL.Services.Implementations.Schedule
/// Настройки, если не нашли в offsetList
/// </summary>
private readonly ScheduleResponseAreaTimeOffset defaultOffset;
private readonly ILogger<ScheduleResponseAreaTimeOffsetService> logger;
public ScheduleResponseAreaTimeOffsetService(DataContext dataContext, SettingsFromDb settingsFromDb)
public ScheduleResponseAreaTimeOffsetService(DataContext dataContext, SettingsFromDb settingsFromDb, ILogger<ScheduleResponseAreaTimeOffsetService> logger)
{
offsetList = dataContext.ScheduleResponseAreaTimeOffsets
.AsNoTracking()
@@ -36,6 +38,7 @@ namespace PARR.DAL.Services.Implementations.Schedule
EsppValue = "",
UtcTimeOffset = new TimeSpan(3, 0, 0)
};
this.logger = logger;
}
@@ -45,7 +48,20 @@ namespace PARR.DAL.Services.Implementations.Schedule
public ScheduleResponseAreaTimeOffset GetByResponseAreaOrDefault(string responseArea)
{
return offsetList.TryGetValue(responseArea, out var value) ? value : defaultOffset;
offsetList.TryGetValue(responseArea, out var value);
if (value != null)
{
logger.LogDebug("Получил часовой пояс по ЗО '{responseArea}', esppValue: {EsppValue}, utcTimeOffset: {utcTimeOffset}", responseArea, value.EsppValue, value.UtcTimeOffset);
return value;
}
else
{
logger.LogWarning("Не смог получить часовой пояс по ЗО '{responseArea}', вернул часово пояс по умолчанию: name: {name}, esppValue: {EsppValue}, utcTimeOffset: {utcTimeOffset}",
responseArea, defaultOffset.ResponseArea, defaultOffset.EsppValue, defaultOffset.UtcTimeOffset);
return defaultOffset;
}
//return offsetList.TryGetValue(responseArea, out var value) ? value : defaultOffset;
}