diff --git a/PARR.API/Contracts/V1/Requests/BaseRequests/TimeZoneOffsetClient.cs b/PARR.API/Contracts/V1/Requests/BaseRequests/TimeZoneOffsetClient.cs index 386ed565..cead2206 100644 --- a/PARR.API/Contracts/V1/Requests/BaseRequests/TimeZoneOffsetClient.cs +++ b/PARR.API/Contracts/V1/Requests/BaseRequests/TimeZoneOffsetClient.cs @@ -1,10 +1,23 @@ -namespace PARR.API.Contracts.V1.Requests.BaseRequests +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace PARR.API.Contracts.V1.Requests.BaseRequests { /// /// Смещение таймзоны относительно клиента /// public class TimeZoneOffsetClient { + /// + /// Удалить TimeZoneOffsetHours и не использовать!!!!!! Использовать только в минутах TimeZoneOffsetMinutes + /// public int TimeZoneOffsetHours { get; set; } = 0; + + /// + /// Часовая зона клиента, в минутах + /// + public int TimeZoneOffsetMinutes { get; set; } = 0; + + [BindNever] + public TimeSpan TimeZoneOffset => TimeSpan.FromMinutes(TimeZoneOffsetMinutes); } } diff --git a/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs b/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs index 9380f968..8f6b06ba 100644 --- a/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs +++ b/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs @@ -23,6 +23,8 @@ public DateTimeOffset ReferenceDate { get; set; } + public int? UserTimeZoneOffsetMinutes { get; set; } + /// /// Использовать таймзону рабочей группы ответственного за ЭК шаблона /// diff --git a/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs b/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs index 0935433e..50a3dd4c 100644 --- a/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs +++ b/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs @@ -18,6 +18,8 @@ public DateTimeOffset ReferenceDate { get; set; } + public int? UserTimeZoneOffsetMinutes { get; set; } + public bool IsWorkGroupTimezone { get; set; } public required JobGroupTypeResponse GroupType { get; set; } diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatTemplateDistributorResponse.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatTemplateDistributorResponse.cs index 7a090e07..61298176 100644 --- a/PARR.API/Contracts/V1/Responses/Statistics/StatTemplateDistributorResponse.cs +++ b/PARR.API/Contracts/V1/Responses/Statistics/StatTemplateDistributorResponse.cs @@ -29,7 +29,10 @@ public class StatDistributorItemResponse { - public string? WorkGroupName { get; set; } + /// + /// Наименование поля по которому группируется + /// + public string? GroupFieldName { get; set; } public int AllCount => Statistics?.Sum(t => t.AllCount) ?? 0; public int IsActivatedCount => Statistics?.Sum(t => t.IsActivatedCount) ?? 0; @@ -47,15 +50,19 @@ public int IsActivatedCount { get; set; } public int IsDeactivatedCount { get; set; } public int TemplatesInWeekendCount => !IsWorkDay && Templates != null ? Templates.Count : 0; - public List? Templates { get; set; } + public List? Templates { get; set; } } - public class StatTempleteDistribItem + public class StatTemplateDistribItem { + /// + /// TemplateId + /// + public Guid Id { get; set; } public required string Name { get; set; } public DateTimeOffset NextRun { get; set; } public bool IsActiveTemplate { get; set; } public bool IsActiveSchedular { get; set; } - public required ScheduleResponseAreaTimeOffsetResponse ResponseAreaOffset { get; set; } + //public required ScheduleResponseAreaTimeOffsetResponse ResponseAreaOffset { get; set; } } } diff --git a/PARR.API/Contracts/V1/Responses/TemplateResponse.cs b/PARR.API/Contracts/V1/Responses/TemplateResponse.cs index f7810309..3fd702f8 100644 --- a/PARR.API/Contracts/V1/Responses/TemplateResponse.cs +++ b/PARR.API/Contracts/V1/Responses/TemplateResponse.cs @@ -45,7 +45,7 @@ public bool IsAutoDistributionEnabled { get; set; } - public ScheduleResponseAreaTimeOffsetResponse? ResponseAreaOffset { get; set; } + //public ScheduleResponseAreaTimeOffsetResponse? ResponseAreaOffset { get; set; } } diff --git a/PARR.API/Controllers/V1/JobGroupController.cs b/PARR.API/Controllers/V1/JobGroupController.cs index a9608703..532fd288 100644 --- a/PARR.API/Controllers/V1/JobGroupController.cs +++ b/PARR.API/Controllers/V1/JobGroupController.cs @@ -39,7 +39,6 @@ namespace PARR.API.Controllers.V1 private readonly IEsppSchTypeConfigService esppConfigService; private readonly IValidator validator; private readonly IJobGroupTypeService jobGroupTypeService; - private readonly SettingsFromDb settingsFromDb; private readonly IMatchingStatusService matchingStatusService; private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; @@ -52,7 +51,6 @@ namespace PARR.API.Controllers.V1 IEsppSchTypeConfigService esppConfigService, IValidator validator, IJobGroupTypeService jobGroupTypeService, - SettingsFromDb settingsFromDb, IMatchingStatusService matchingStatusService, IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService ) @@ -65,7 +63,6 @@ namespace PARR.API.Controllers.V1 this.esppConfigService = esppConfigService; this.validator = validator; this.jobGroupTypeService = jobGroupTypeService; - this.settingsFromDb = settingsFromDb; this.matchingStatusService = matchingStatusService; this.scheduleResponseAreaTimeOffsetService = scheduleResponseAreaTimeOffsetService; } @@ -168,6 +165,7 @@ namespace PARR.API.Controllers.V1 Solution = request.Solution.Trim(), TemplateDuration = request.TemplateDuration.Trim(), ReferenceDate = request.ReferenceDate, + UserTimeZoneOffsetMinutes = request.UserTimeZoneOffsetMinutes, ScheduleExcludeTypeId = request.ScheduleExcludeTypeId, ScheduleExcludeTypeCalendarId = request.ScheduleExcludeTypeCalendarId, IsAutoDistributionEnabled = request.IsAutoDistributionEnabled, @@ -262,6 +260,7 @@ namespace PARR.API.Controllers.V1 orig.Solution = request.Solution.Trim(); orig.TemplateDuration = request.TemplateDuration.Trim(); orig.ReferenceDate = request.ReferenceDate; + orig.UserTimeZoneOffsetMinutes = request.UserTimeZoneOffsetMinutes; orig.ScheduleExcludeTypeId = request.ScheduleExcludeTypeId; orig.ScheduleExcludeTypeCalendarId = request.ScheduleExcludeTypeCalendarId; orig.IsAutoDistributionEnabled = request.IsAutoDistributionEnabled; diff --git a/PARR.API/Controllers/V1/RobotTaskController.cs b/PARR.API/Controllers/V1/RobotTaskController.cs index b86a4e33..a2c54b19 100644 --- a/PARR.API/Controllers/V1/RobotTaskController.cs +++ b/PARR.API/Controllers/V1/RobotTaskController.cs @@ -31,7 +31,7 @@ namespace PARR.API.Controllers.V1 private readonly IClientService clientService; private readonly IRobotHistoryService robotHistoryService; private readonly IShortcodesService shortcodesService; - private readonly INextRunService nextRunService; + private readonly INextRunServiceV2 nextRunService; private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; public RobotTaskController( @@ -43,7 +43,7 @@ namespace PARR.API.Controllers.V1 IClientService clientService, IRobotHistoryService robotHistoryService, IShortcodesService shortcodesService, - INextRunService nextRunService, + INextRunServiceV2 nextRunService, IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService ) { @@ -156,11 +156,6 @@ namespace PARR.API.Controllers.V1 // сортируем по NextRun, чтобы те у которых дата след срабатывания ближе к текущей, выполнились скорее query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template.IsActiveTemplate); - #region Так нельзя делать!!! Должно всегда сортироваться по NextRun и не важно какой тип у шаблона!!! - // Сортируем сначала по StatusTypeId, т е те что used, будут первыми, потом сортируем по NextRun, чтобы ближайшие даты выполнились скорее - //query = query.OrderBy(t => t.Template!.StatusTypeId).ThenBy(t => t.Template!.NextRun); - #endregion - RobotConfiguration? task = null; //ищем задание в ожидании, если нашли, выбираем его @@ -213,20 +208,35 @@ namespace PARR.API.Controllers.V1 } case RobotsEnum.ScheduleOrder: { // если был запрос на расписание, проверяем у него nextRun, lastRun, обновляем их - await UpdateLastNextRunDate(task); + var resultUpdateNextRun = await UpdateNextRunAsync(task); + if (!resultUpdateNextRun) + { + logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}", task.TemplateId); + return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при расчете NextRun" } })); + } //RobotTaskScheduleResponse var robotTaskScheduleResponse = mapper.Map(task); - robotTaskScheduleResponse.Timezone = GetTimezoneForTemplate(task.Template!); + robotTaskScheduleResponse.Timezone = settingsFromDb.EsppScheduleTimezone; robotTaskScheduleResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.WorkGroup, task.Template!); robotTaskScheduleResponse.ResponseArea = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.ResponseArea, task.Template!); - var nextRunWithTimezone = nextRunService.GetNextRunWithTimezoneEsppAndResponseArea(task.Template!.NextRun, task.Template!.Job?.Group?.IsResponseAreaTimezone, robotTaskScheduleResponse.ResponseArea); + //var nextRunWithRobotTz = nextRunService.GetNextRunWithTimezoneEsppAndResponseArea(task.Template!.NextRun, task.Template!.Job?.Group?.IsResponseAreaTimezone, robotTaskScheduleResponse.ResponseArea); + //nextRun в часовой зоне УЗ Робота ЕСПП + var nextRunWithRobotTz = task.Template!.NextRun.Add(nextRunService.GetEsppAccountOffset()); - robotTaskScheduleResponse.NextStart = EsppScheduleHelpers.GetNextRun(nextRunWithTimezone); - robotTaskScheduleResponse.GenerationTime = EsppScheduleHelpers.GetGenerationTime(nextRunWithTimezone); + //на всякий случай еще раз проверяем, что дата не устарела и отправляем задание + if (nextRunWithRobotTz < DateTimeOffset.UtcNow) + { + logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}", + task.TemplateId, nextRunWithRobotTz, DateTimeOffset.UtcNow); + return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при расчете NextRun" } })); + } + + robotTaskScheduleResponse.NextStart = EsppScheduleHelpers.GetNextRun(nextRunWithRobotTz); + robotTaskScheduleResponse.GenerationTime = EsppScheduleHelpers.GetGenerationTime(nextRunWithRobotTz); return Ok(new Response(robotTaskScheduleResponse, true)); } @@ -243,54 +253,36 @@ namespace PARR.API.Controllers.V1 /// /// /// - private async Task UpdateLastNextRunDate(RobotConfiguration task) + private async Task UpdateNextRunAsync(RobotConfiguration task) { var template = task.Template!; - #region old - // передаем LastRun, если его нет, то NextRun - //var nextRun = await esppScheduleTransformService.GetNextDateAsync(task.Template!.ApplicationInWorkId, task.Template.ApplicationsInWork!.LastRun ?? task.Template.ApplicationsInWork.NextRun); - // всегда считаем по nextRun - //var nextRun = await esppScheduleTransformService.GetNextDateAsync(template.Job!.GroupId, template.NextRun); - #endregion - //var nextRun = await esppScheduleTransformService.GetNextDateAsync(template.Job!.GroupId, template!.Job!.Group!.ReferenceDate); var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false); + if (!nextRun.HasValue) + { + logger.LogError("При обновлении nextRun для шаблона {templateId}, расчитанный nextRun=null, ошибка в расчетах.", template.Id); + return false; + } + + if (nextRun.Value < DateTimeOffset.UtcNow) + { + logger.LogError("При обновлении nextRun для шаблона {templateId}, расчитанный nextRun - /// Получить таймзону для шаблона - /// - /// - /// - /// - private string GetTimezoneForTemplate(Template template) - { - if (template.Job?.Group?.IsResponseAreaTimezone != true) - return scheduleResponseAreaTimeOffsetService.GetDefault.EsppValue; - - var responseArea = template.Unit?.BaseFields?.ResponseArea; - - if (string.IsNullOrEmpty(responseArea)) - { - throw new InvalidOperationException( - $"У шаблона Id={template.Id}, Name='{template.Name}' не задана ResponseArea в Unit.BaseFields, " + - "но включена настройка 'использовать часовой пояс рабочей группы'."); - } - - return scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea).EsppValue; - } - } } diff --git a/PARR.API/Controllers/V1/Statistics/StatTemplateDistributionController.cs b/PARR.API/Controllers/V1/Statistics/StatTemplateDistributionController.cs index 862a196f..378843f8 100644 --- a/PARR.API/Controllers/V1/Statistics/StatTemplateDistributionController.cs +++ b/PARR.API/Controllers/V1/Statistics/StatTemplateDistributionController.cs @@ -29,7 +29,7 @@ namespace PARR.API.Controllers.V1.Statistics private readonly IJobGroupService jobGroupService; private readonly IMapper mapper; private readonly IShortcodesService shortcodesService; - private readonly INextRunService nextRunService; + private readonly INextRunServiceV2 nextRunService; private readonly ILogger logger; private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; @@ -38,7 +38,7 @@ namespace PARR.API.Controllers.V1.Statistics IJobGroupService jobGroupService, IMapper mapper, IShortcodesService shortcodesService, - INextRunService nextRunService, + INextRunServiceV2 nextRunService, ILogger logger, IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService ) @@ -63,8 +63,6 @@ namespace PARR.API.Controllers.V1.Statistics { // eadc5498-dba6-4f10-9b4b-a1653e3c3e61 - //TODO: похоже что timeZoneQuery лишняя - var jobGroup = await jobGroupService.Get() .Include(t => t.DistributionConfig) .ThenInclude(t => t.DistributionPeriod) @@ -95,65 +93,86 @@ namespace PARR.API.Controllers.V1.Statistics ItemsList = null }, true)); - var (dateStart, dateEnd) = GetStartEndPeriod(jobGroup, items.Max(t => t.Template.NextRun), timeZoneQuery.TimeZoneOffsetHours); + + var (dateStart, dateEnd) = GetStartEndPeriod(jobGroup, items.Min(t => t.Template.NextRun), items.Max(t => t.Template.NextRun), timeZoneQuery.TimeZoneOffset); // получаем список рабочих дней // получет все дни, даже с выходными, чтоб видеть реальную картину - var workDays = await nextRunService.GetWorkDaysAsync(dateStart, dateEnd, false, jobGroup.ReferenceDate); + var allDays = await nextRunService.GetWorkDaysAsync(dateStart, dateEnd, false); // получить список выходных дней (показываем только если IsExcludeWeekends = true) var weekends = jobGroup.DistributionConfig.IsExcludeWeekends - ? await nextRunService.GetWeekendsAsync(dateStart, dateEnd, jobGroup.ReferenceDate) + ? await nextRunService.GetWeekendsAsync(dateStart, dateEnd) : new HashSet(); + var responseItemsList = new List(); if (jobGroup.DistributionConfig.IsGroupingByWorkGroup) { // тут группируем по рабочим группам + logger.LogDebug("Группируем по рабочим группам"); // Получаем список пар (WorkGroupName, Template) - var templatesWithWorkGroupNames = new List<(string WorkGroupName, (Template Template, string ResponseArea))>(); + var templatesWithWorkGroupNames = new List<(string WorkGroupName, Template Template)>(); - // получаем названия рабочих групп и ЗО + // получаем названия рабочих групп foreach (var item in items) { var workGroupName = await shortcodesService.ApplyShortcodesAsync(item.WorkGroupMask, item.Template); - var responseArea = await shortcodesService.ApplyShortcodesAsync(item.ResponseAreaMask, item.Template); - templatesWithWorkGroupNames.Add((workGroupName, (item.Template, responseArea))); + templatesWithWorkGroupNames.Add((workGroupName, item.Template)); } // Группируем по WorkGroupName var grouped = templatesWithWorkGroupNames .GroupBy(x => x.WorkGroupName) - .ToDictionary(g => g.Key, g => g.Select(x => x.Item2).ToList()); + .ToDictionary(g => g.Key, g => g.Select(x => x.Template).ToList()); foreach (var groupedItem in grouped) { - var statResult = GetResponseByWorkGroup(groupedItem.Key, groupedItem.Value.ToList(), workDays, weekends, timeZoneQuery.TimeZoneOffsetHours, jobGroup.ReferenceDate); + var statResult = GetResponseByWorkGroup(groupedItem.Key, groupedItem.Value.ToList(), allDays, weekends, timeZoneQuery.TimeZoneOffset); + responseItemsList.Add(statResult); + } + } + else if (jobGroup.IsResponseAreaTimezone && !jobGroup.DistributionConfig.IsGroupingByWorkGroup) + { + // грппируем только по ЗО + logger.LogDebug("Группируем по ЗО"); + + // Будет группироваться по EsppValue, по MSK, MSK+1... + // получаем названия ЗО, из ЗО часовой пояс, формируем список шаблонов с часовым поясом ЗО + var templatesWithTimeZone = new List<(Template Template, string MskTimeZone)>(); + foreach (var item in items) + { + var responseArea = await shortcodesService.ApplyShortcodesAsync(item.ResponseAreaMask, item.Template); + var mskTimeZone = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea); + templatesWithTimeZone.Add((item.Template, mskTimeZone.EsppValue)); + } + + // Группируем по часовому поясу + var grouped = templatesWithTimeZone + .GroupBy(x => x.MskTimeZone) + .ToDictionary(g => g.Key, g => g.Select(x => x.Template).ToList()); + + foreach (var groupedItem in grouped) + { + var statResult = GetResponseByWorkGroup(groupedItem.Key, groupedItem.Value.ToList(), allDays, weekends, timeZoneQuery.TimeZoneOffset); responseItemsList.Add(statResult); } } else { - // не нужно группировать по рабочим группам - - // получаем названия ЗО - var templatesWithResponseArea = new List<(Template Template, string ResponseArea)>(); - foreach (var item in items) - { - var responseArea = await shortcodesService.ApplyShortcodesAsync(item.ResponseAreaMask, item.Template); - templatesWithResponseArea.Add((item.Template, responseArea)); - } + // не нужно группировать + logger.LogDebug("Без группировки"); // сразу формируем response - responseItemsList.Add(GetResponseByWorkGroup(null, templatesWithResponseArea, workDays, weekends, timeZoneQuery.TimeZoneOffsetHours, jobGroup.ReferenceDate)); + responseItemsList.Add(GetResponseByWorkGroup(null, items.Select(t => t.Template).ToList(), allDays, weekends, timeZoneQuery.TimeZoneOffset)); } var response = new StatTemplateDistributorResponse { JobGroup = mapper.Map(jobGroup), - ItemsList = responseItemsList.OrderBy(t => t.WorkGroupName).ToList() + ItemsList = responseItemsList.OrderBy(t => t.GroupFieldName).ToList() }; return Ok(new Response(response, true)); @@ -163,29 +182,33 @@ namespace PARR.API.Controllers.V1.Statistics /// Получить дату начала и конца периода /// /// + /// /// - /// + /// /// - private (DateOnly dateStart, DateOnly dateEnd) GetStartEndPeriod(JobGroup jobGroup, DateTimeOffset maxNextRunTemplate, int timeZoneOffsetHours) + private (DateOnly dateStart, DateOnly dateEnd) GetStartEndPeriod(JobGroup jobGroup, DateTimeOffset minNextRunTemplate, DateTimeOffset maxNextRunTemplate, TimeSpan clientOffset) { var durationDays = nextRunService.GetDurationDays(jobGroup.DistributionConfig!); - var dateStart = DateOnly.FromDateTime(DateTime.UtcNow.AddHours(timeZoneOffsetHours)); + var dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Add(clientOffset)); // последний день, не может быть меньше чем durationDays var dateEnd = dateStart.AddDays(durationDays); // последний день, не может быть меньше чем refDate+durationDays - var sumRefDuration = DateOnly.FromDateTime(jobGroup.ReferenceDate.AddHours(timeZoneOffsetHours).AddDays(durationDays).Date); + var sumRefDuration = DateOnly.FromDateTime(jobGroup.ReferenceDate.Add(clientOffset).AddDays(durationDays).Date); if (dateEnd < sumRefDuration) dateEnd = sumRefDuration; - // с DateEnd вообще какая-то шурпатня :( - // как наглядно понять ок не ок распределяется, если на каком-то графике dateEnd может ухеать за период распределения // последний день, не может быть меньше чем дата последнего шаблона maxNextRunTemplate - var lastTempalteDate = DateOnly.FromDateTime(maxNextRunTemplate.AddHours(timeZoneOffsetHours).Date); - if (dateEnd < lastTempalteDate) - dateEnd = lastTempalteDate; + var lastTemplateDate = DateOnly.FromDateTime(maxNextRunTemplate.Add(clientOffset).Date); + if (dateEnd < lastTemplateDate) + dateEnd = lastTemplateDate; + + // Дата начала, не может быть меньше чем minNextRunTemplate + var minNextRunDate = DateOnly.FromDateTime(minNextRunTemplate.Add(clientOffset).Date); + if (dateStart > minNextRunDate) + dateStart = minNextRunDate; return (dateStart, dateEnd); } @@ -193,49 +216,49 @@ namespace PARR.API.Controllers.V1.Statistics /// /// Заполнить респонс /// - /// + /// /// - /// - /// + /// + /// + /// /// - private StatDistributorItemResponse GetResponseByWorkGroup(string? workGroupName, List<(Template Template, string ResponseArea)> templateList, List workDays, HashSet weekends, int timeZoneOffsetHours, DateTimeOffset referenceDate) + private StatDistributorItemResponse GetResponseByWorkGroup(string? groupFieldName, List