diff --git a/PARR.API/Contracts/V1/Requests/Queries/StatRobotHistoryQuery.cs b/PARR.API/Contracts/V1/Requests/Queries/StatRobotHistoryQuery.cs
index 06ec616a..88b53c5e 100644
--- a/PARR.API/Contracts/V1/Requests/Queries/StatRobotHistoryQuery.cs
+++ b/PARR.API/Contracts/V1/Requests/Queries/StatRobotHistoryQuery.cs
@@ -12,6 +12,6 @@ namespace PARR.API.Contracts.V1.Requests.Queries
///
/// Фильтр за дату
///
- public DateTime? Date { get; set; }
+ public DateOnly? Date { get; set; }
}
}
diff --git a/PARR.API/Contracts/V1/Responses/Statistics/StatRobotHistoryResponse.cs b/PARR.API/Contracts/V1/Responses/Statistics/StatRobotHistoryResponse.cs
index b7bdc914..cd02bb77 100644
--- a/PARR.API/Contracts/V1/Responses/Statistics/StatRobotHistoryResponse.cs
+++ b/PARR.API/Contracts/V1/Responses/Statistics/StatRobotHistoryResponse.cs
@@ -8,6 +8,6 @@
public int DateCount { get; set; }
- public DateTime Date { get; set; }
+ public DateOnly Date { get; set; }
}
}
diff --git a/PARR.API/Controllers/V1/RobotTaskController.cs b/PARR.API/Controllers/V1/RobotTaskController.cs
index a2c54b19..2fd2f87d 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 INextRunServiceV2 nextRunService;
+ private readonly INextRunService nextRunService;
private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService;
public RobotTaskController(
@@ -43,7 +43,7 @@ namespace PARR.API.Controllers.V1
IClientService clientService,
IRobotHistoryService robotHistoryService,
IShortcodesService shortcodesService,
- INextRunServiceV2 nextRunService,
+ INextRunService nextRunService,
IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService
)
{
diff --git a/PARR.API/Controllers/V1/Statistics/StatOrderController.cs b/PARR.API/Controllers/V1/Statistics/StatOrderController.cs
index fc156d12..f7dc5695 100644
--- a/PARR.API/Controllers/V1/Statistics/StatOrderController.cs
+++ b/PARR.API/Controllers/V1/Statistics/StatOrderController.cs
@@ -8,8 +8,9 @@ using PARR.API.Contracts.V1.Responses;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base;
-using PARR.BLL.Services.Interfaces;
+using PARR.API.Helpers;
using PARR.Constants;
+using PARR.DAL.NextRunServices;
using PARR.DAL.Services.Interfaces;
namespace PARR.API.Controllers.V1.Statistics
@@ -23,34 +24,37 @@ namespace PARR.API.Controllers.V1.Statistics
private readonly IOrderService orderService;
private readonly IOrderStatusService orderStatusService;
private readonly IMapper mapper;
- private readonly ICalendarService calendarService;
+ private readonly INextRunService nextRunService;
public StatOrderController(
IOrderService orderService,
IOrderStatusService orderStatusService,
IMapper mapper,
- ICalendarService calendarService
+ INextRunService nextRunService
)
{
this.orderService = orderService;
this.orderStatusService = orderStatusService;
this.mapper = mapper;
- this.calendarService = calendarService;
+ this.nextRunService = nextRunService;
}
///
- /// Статистика по нарядам - всего в ПАРР / загружено за последние сутки
+ /// Статистика по нарядам - всего в ПАРР / загружено за сегодня
///
///
[HttpGet(ApiRoutes.StatOrder.Get)]
- public async Task Get()
+ public async Task Get([FromQuery] TimeZoneOffsetClient timeZoneQuery)
{
- var queryDate = DateTimeOffset.UtcNow.AddDays(-1);
+ var todayStartUtc = TimeZoneDateHelper.GetUtcSartOfDay(
+ DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime),
+ timeZoneQuery.TimeZoneOffset
+ );
var response = new StatOrderResponse
{
- AllCount = await orderService.Get().CountAsync(),
- DownloadedLastDayCount = await orderService.Get().CountAsync(t => t.DateCreated >= queryDate)
+ AllCount = await orderService.Get().AsNoTracking().CountAsync(),
+ DownloadedLastDayCount = await orderService.Get().AsNoTracking().CountAsync(t => t.DateCreated.UtcDateTime >= todayStartUtc)
};
return Ok(new Response(response, true));
@@ -66,9 +70,9 @@ namespace PARR.API.Controllers.V1.Statistics
{
var response = new StatTaskToEsppResponse
{
- AllCount = await orderService.Get().CountAsync(),
+ AllCount = await orderService.Get().AsNoTracking().CountAsync(),
// такая же логика в PARR.Master
- TaskCount = await orderService.Get().CountAsync(t => t.NextStatusCode.HasValue && t.NextStatusCode.Value != t.StatusCode)
+ TaskCount = await orderService.Get().AsNoTracking().CountAsync(t => t.NextStatusCode.HasValue && t.NextStatusCode.Value != t.StatusCode)
};
return Ok(new Response(response, true));
@@ -82,8 +86,7 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.StatOrder.Statuses)]
public async Task Statuses()
{
- var statuses = await orderStatusService.Get().OrderBy(t => t.Code).ToListAsync();
-
+ var statuses = await orderStatusService.Get().AsNoTracking().OrderBy(t => t.Code).ToListAsync();
var response = new List();
foreach (var stat in statuses)
@@ -91,7 +94,7 @@ namespace PARR.API.Controllers.V1.Statistics
response.Add(new StatOrderStatusesResponse
{
Status = mapper.Map(stat),
- Count = await orderService.Get().CountAsync(t => t.StatusCode == stat.Code)
+ Count = await orderService.Get().AsNoTracking().CountAsync(t => t.StatusCode == stat.Code)
});
}
@@ -100,23 +103,26 @@ namespace PARR.API.Controllers.V1.Statistics
///
- /// Статистика по нарядам - по статусу за последние сутки
+ /// Статистика по нарядам - по статусу за сегодня
///
/// 1,2,3,4,5
///
[HttpGet(ApiRoutes.StatOrder.GetStatusesLastDay)]
- public async Task GetStatusesLastDay([FromRoute] int statusCode)
+ public async Task GetStatusesLastDay([FromRoute] int statusCode, [FromQuery] TimeZoneOffsetClient timeZoneQuery)
{
- var status = await orderStatusService.Get().FirstOrDefaultAsync(t => t.Code == statusCode);
+ var status = await orderStatusService.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Code == statusCode);
if (status == null)
return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Не найден статус с кодом: {statusCode}" } }));
- var queryDate = DateTimeOffset.UtcNow.AddDays(-1);
+ var todayStartUtc = TimeZoneDateHelper.GetUtcSartOfDay(
+ DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime),
+ timeZoneQuery.TimeZoneOffset
+ );
var response = new StatGetStatusesLastDayResponse
{
- Count = await orderService.Get().CountAsync(t => t.DateModified >= queryDate && t.StatusCode == statusCode),
+ Count = await orderService.Get().CountAsync(t => (t.DateModified.HasValue && t.DateModified.Value >= todayStartUtc) && t.StatusCode == statusCode),
Status = mapper.Map(status)
};
@@ -133,36 +139,30 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.StatOrder.GetForPeriod)]
public async Task GetForPeriod([FromQuery] TimeZoneOffsetClient timeZoneQuery, [FromQuery] int startPeriodDays = 3)
{
- var endPeriod = calendarService.GetDateWithTimeZoneOffset(DateTimeOffset.UtcNow, timeZoneQuery.TimeZoneOffsetHours);
- var startPeriod = endPeriod.AddDays(-startPeriodDays);
+ // Формируем период
+ var userEnd = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime);
+ var userStart = userEnd.AddDays(-startPeriodDays);
- var query = orderService.Get()
- .Where(t =>
- t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
- && t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
- ).GroupBy(t => t.DateCreated.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
- .Select(t => new
- {
- Date = t.Key,
- OrderCount = t.Count()
- });
+ var (utcStart, utcEnd) = TimeZoneDateHelper.GetUtcDateRange(
+ userStart,
+ userEnd.AddDays(1),
+ timeZoneQuery.TimeZoneOffset);
- var result = await query.ToListAsync();
+ var allRecords = await orderService.Get()
+ .AsNoTracking()
+ .FilterByDateRangeUtc(t => t.DateCreated, utcStart, utcEnd)
+ .Select(t => new { t.Id, t.DateCreated })
+ .ToListAsync(); ;
- var daysList = calendarService.GetDatesForPeriod(startPeriod, endPeriod);
+ var resultDict = allRecords.GroupByUserDate(t => t.DateCreated, timeZoneQuery.TimeZoneOffset);
- var response = new List();
+ var daysList = await nextRunService.GetWorkDaysAsync(userStart, userEnd, false);
- daysList.ForEach(date =>
+ var response = daysList.Select(date => new StatOrderGetForPeriodResponse
{
- response.Add(new StatOrderGetForPeriodResponse
- {
- Date = date,
- OrderCount = result.FirstOrDefault(t => DateOnly.FromDateTime(t.Date) == date)?.OrderCount ?? 0
- });
- });
-
- response = response.OrderBy(t => t.Date).ToList();
+ Date = date,
+ OrderCount = resultDict.TryGetValue(date, out var cnt) ? cnt : 0
+ }).OrderBy(t => t.Date).ToList();
return Ok(new Response>(response, true));
}
diff --git a/PARR.API/Controllers/V1/Statistics/StatRobotHistoryController.cs b/PARR.API/Controllers/V1/Statistics/StatRobotHistoryController.cs
index e216fc50..0ec3552e 100644
--- a/PARR.API/Controllers/V1/Statistics/StatRobotHistoryController.cs
+++ b/PARR.API/Controllers/V1/Statistics/StatRobotHistoryController.cs
@@ -3,11 +3,13 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PARR.API.Contracts.V1;
+using PARR.API.Contracts.V1.Requests.BaseRequests;
using PARR.API.Contracts.V1.Requests.Queries;
using PARR.API.Contracts.V1.Responses;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base;
+using PARR.API.Helpers;
using PARR.Constants;
using PARR.DAL.Models;
using PARR.DAL.Services.Interfaces;
@@ -40,10 +42,10 @@ namespace PARR.API.Controllers.V1.Statistics
///
///
[HttpGet(ApiRoutes.StatRobotHistory.Get)]
- public async Task Get([FromQuery] StatRobotHistoryQuery request)
+ public async Task Get([FromQuery] StatRobotHistoryQuery request, [FromQuery] TimeZoneOffsetClient timeZoneQuery)
{
if (!request.Date.HasValue)
- request.Date = DateTime.Now.Date;
+ request.Date = DateOnly.FromDateTime(DateTimeOffset.UtcNow.Date);
// всего
var allStat = await robotHistoryService.Get()
@@ -51,9 +53,19 @@ namespace PARR.API.Controllers.V1.Statistics
.Select(t => new { HistoryLevel = t.Key, Count = t.Count() })
.ToListAsync();
+ // Формируем диапазон в пользовательском времени
+ var userDateStart = new DateTimeOffset(request.Date.Value.Year, request.Date.Value.Month, request.Date.Value.Day, 0, 0, 0, timeZoneQuery.TimeZoneOffset);
+ var userDateEnd = userDateStart.AddDays(1);
+
+ // Конвертируем в UTC для сравнения с БД
+ var utcStart = userDateStart.UtcDateTime;
+ var utcEnd = userDateEnd.UtcDateTime;
+
// за дату
var dateStat = await robotHistoryService.Get()
- .Where(t => t.DateCreated.DateTime.Date == request.Date.Value.Date)
+ //.Where(t => t.DateCreated.DateTime.Date == request.Date.Value.Date)
+ //.Where(t => t.DateCreated.UtcDateTime >= utcStart && t.DateCreated.UtcDateTime < utcEnd)
+ .FilterByDateRangeUtc(t => t.DateCreated, utcStart, utcEnd)
.GroupBy(t => t.HistoryLevel)
.Select(t => new { HistoryLevel = t.Key, Count = t.Count() })
.ToListAsync();
@@ -73,7 +85,7 @@ namespace PARR.API.Controllers.V1.Statistics
{
AllCount = allStat.FirstOrDefault(t => t.HistoryLevel == level.Level)?.Count ?? 0,
DateCount = dateStat.FirstOrDefault(t => t.HistoryLevel == level.Level)?.Count ?? 0,
- Date = request.Date.Value.Date,
+ Date = request.Date.Value,
Level = mapper.Map(level)
});
}
diff --git a/PARR.API/Controllers/V1/Statistics/StatRobotStatusController.cs b/PARR.API/Controllers/V1/Statistics/StatRobotStatusController.cs
index 7bd1e90e..59b198e5 100644
--- a/PARR.API/Controllers/V1/Statistics/StatRobotStatusController.cs
+++ b/PARR.API/Controllers/V1/Statistics/StatRobotStatusController.cs
@@ -44,12 +44,13 @@ namespace PARR.API.Controllers.V1.Statistics
public async Task Get()
{
var confStat = await robotConfigurationService.Get()
+ .AsNoTracking()
.GroupBy(t => new { t.RobotCode, t.RobotStatusCode })
.Select(t => new { RobotCode = t.Key.RobotCode, RobotStatusCode = t.Key.RobotStatusCode, Count = t.Count() })
.ToListAsync();
- var robots = await robotService.Get().ToListAsync();
- var robotStatuses = await robotStatusService.Get().ToListAsync();
+ var robots = await robotService.Get().AsNoTracking().ToListAsync();
+ var robotStatuses = await robotStatusService.Get().AsNoTracking().ToListAsync();
var response = new List();
diff --git a/PARR.API/Controllers/V1/Statistics/StatRobotTaskController.cs b/PARR.API/Controllers/V1/Statistics/StatRobotTaskController.cs
index c3939d07..adc5874d 100644
--- a/PARR.API/Controllers/V1/Statistics/StatRobotTaskController.cs
+++ b/PARR.API/Controllers/V1/Statistics/StatRobotTaskController.cs
@@ -49,12 +49,13 @@ namespace PARR.API.Controllers.V1.Statistics
public async Task Get()
{
var confStat = await robotConfigurationService.Get()
+ .AsNoTracking()
.GroupBy(t => new { t.RobotCode, t.TaskStatusCode })
.Select(t => new { RobotCode = t.Key.RobotCode, TaskStatusCode = t.Key.TaskStatusCode, Count = t.Count() })
.ToListAsync();
- var robots = await robotService.Get().ToListAsync();
- var statuses = await taskStatusService.Get().ToListAsync();
+ var robots = await robotService.Get().AsNoTracking().ToListAsync();
+ var statuses = await taskStatusService.Get().AsNoTracking().ToListAsync();
var response = new List();
@@ -95,7 +96,8 @@ namespace PARR.API.Controllers.V1.Statistics
var query = robotHistoryService.Get()
- .Include(t => t.RobotConfiguration)
+ //.Include(t => t.RobotConfiguration)
+ .AsNoTracking()
.Where(t => t.RobotConfiguration!.RobotCode == (int)robot && t.DateCreated >= queryDate)
.GroupBy(t => new { Date = t.DateCreated.Date, Hour = t.DateCreated.Hour })
.Select(t => new
diff --git a/PARR.API/Controllers/V1/Statistics/StatTemplateAutoControlController.cs b/PARR.API/Controllers/V1/Statistics/StatTemplateAutoControlController.cs
index bf7a028d..a760b350 100644
--- a/PARR.API/Controllers/V1/Statistics/StatTemplateAutoControlController.cs
+++ b/PARR.API/Controllers/V1/Statistics/StatTemplateAutoControlController.cs
@@ -5,8 +5,9 @@ using PARR.API.Contracts.V1.Requests.BaseRequests;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base;
-using PARR.BLL.Services.Interfaces;
+using PARR.API.Helpers;
using PARR.Constants;
+using PARR.DAL.NextRunServices;
using PARR.DAL.Services.Interfaces;
namespace PARR.API.Controllers.V1.Statistics
@@ -15,17 +16,17 @@ namespace PARR.API.Controllers.V1.Statistics
{
private readonly ITemplateService templateService;
private readonly ITemplateHistoryService templateHistoryService;
- private readonly ICalendarService calendarService;
+ private readonly INextRunService nextRunService;
public StatTemplateAutoControlController(
ITemplateService templateService,
ITemplateHistoryService templateHistoryService,
- ICalendarService calendarService
+ INextRunService nextRunService
)
{
this.templateService = templateService;
this.templateHistoryService = templateHistoryService;
- this.calendarService = calendarService;
+ this.nextRunService = nextRunService;
}
@@ -38,8 +39,14 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.StatTemplateAutoControl.GetForPeriod)]
public async Task GetForPeriod([FromQuery] TimeZoneOffsetClient timeZoneQuery, [FromQuery] int startPeriodDays = 3)
{
- var endPeriod = calendarService.GetDateWithTimeZoneOffset(DateTimeOffset.Now, timeZoneQuery.TimeZoneOffsetHours);
- var startPeriod = endPeriod.AddDays(-startPeriodDays);
+ //var endPeriod = calendarService.GetDateWithTimeZoneOffset(DateTimeOffset.Now, timeZoneQuery.TimeZoneOffsetHours);
+ //var startPeriod = endPeriod.AddDays(-startPeriodDays);
+
+ var userEndPeriod = DateTimeOffset.UtcNow.Add(timeZoneQuery.TimeZoneOffset);
+ var userStartPeriod = userEndPeriod.AddDays(-startPeriodDays);
+
+ var utcEndPeriod = userEndPeriod.UtcDateTime;
+ var utcStartPeriod = userStartPeriod.UtcDateTime;
var templates = await templateService.Get()
.Where(t =>
@@ -48,12 +55,16 @@ namespace PARR.API.Controllers.V1.Statistics
((
// измененные
t.DateModified.HasValue
- && t.DateModified.Value.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
- && t.DateModified.Value.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ //&& t.DateModified.Value.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
+ //&& t.DateModified.Value.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ && t.DateModified.Value.UtcDateTime >= utcStartPeriod
+ && t.DateModified.Value.UtcDateTime <= utcEndPeriod
) || (
// созданные
- t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
- && t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ //t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
+ //&& t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ t.DateCreated.UtcDateTime >= utcStartPeriod
+ && t.DateCreated.UtcDateTime <= utcEndPeriod
&& t.InitiatorComment != null
&& !t.InitiatorComment.Contains(TemplateActivatorActionsEnum.ActivateAllForAutoControl.ToString())
&& !t.InitiatorComment.Contains(TemplateActivatorActionsEnum.DeactivateAllForAutoControl.ToString())
@@ -72,8 +83,10 @@ namespace PARR.API.Controllers.V1.Statistics
#region ищем созданные, но уже перемещенные в историю
var createdTemplatesAndMoveToHistory = await templateService.Get().Include(t => t.TemplateHistories)
.Where(t =>
- t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
- && t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ //t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
+ //&& t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ t.DateCreated.UtcDateTime >= utcStartPeriod
+ && t.DateCreated.UtcDateTime <= utcEndPeriod
&& t.TemplateHistories.Any(x => x.InitiatorParrComponentId == Constants.ParrComponentsEnum.JobAutoControl)
)
.ToListAsync();
@@ -93,8 +106,10 @@ namespace PARR.API.Controllers.V1.Statistics
.Where(t =>
t.InitiatorParrComponentId == Constants.ParrComponentsEnum.JobAutoControl
&& t.DateModified.HasValue
- && t.DateModified.Value.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
- && t.DateModified.Value.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ //&& t.DateModified.Value.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
+ //&& t.DateModified.Value.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ && t.DateModified.Value.UtcDateTime >= utcStartPeriod
+ && t.DateModified.Value.UtcDateTime <= utcEndPeriod
)
.Select(t => new QueryResult
{
@@ -112,31 +127,36 @@ namespace PARR.API.Controllers.V1.Statistics
objs.AddRange(createdTemplatesAndMoveToHistory.Select(t => new QueryResult { DateCreated = t.DateCreated }));
// создано
- var creations = objs.Where(t => t.DateCreated.HasValue).GroupBy(t => t.DateCreated!.Value.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
- .Select(t => new { Date = t.Key, Created = t.Count() });
+ var creations = objs.Where(t => t.DateCreated.HasValue)
+ //.GroupBy(t => t.DateCreated!.Value.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
+ //.GroupBy(t => DateOnly.FromDateTime(t.DateCreated!.Value.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime))
+ //.ToDictionary(t => t.Key, t => t.Count());
+ .GroupByUserDate(t => t.DateCreated!.Value, timeZoneQuery.TimeZoneOffset);
// активаций (сколько раз, а не сколько объектов)
var activations = objs.Where(t => t.DateModified.HasValue && t.InitiatorComment != null && t.InitiatorComment.Contains(TemplateActivatorActionsEnum.ActivateAllForAutoControl.ToString()))
- .GroupBy(t => t.DateModified!.Value.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
- .Select(t => new { Date = t.Key, Actiovations = t.Count() });
+ //.GroupBy(t => t.DateModified!.Value.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
+ //.GroupBy(t => DateOnly.FromDateTime(t.DateModified!.Value.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime))
+ //.ToDictionary(t => t.Key, t => t.Count());
+ .GroupByUserDate(t => t.DateModified!.Value, timeZoneQuery.TimeZoneOffset);
// деактиваций (сколько раз, а не сколько объектов)
var deactivations = objs.Where(t => t.DateModified.HasValue && t.InitiatorComment != null && t.InitiatorComment.Contains(TemplateActivatorActionsEnum.DeactivateAllForAutoControl.ToString()))
- .GroupBy(t => t.DateModified!.Value.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
- .Select(t => new { Date = t.Key, Deactivations = t.Count() });
+ //.GroupBy(t => t.DateModified!.Value.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
+ //.GroupBy(t => DateOnly.FromDateTime(t.DateModified!.Value.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime))
+ //.ToDictionary(t => t.Key, t => t.Count());
+ .GroupByUserDate(t => t.DateModified!.Value, timeZoneQuery.TimeZoneOffset);
- var daysList = calendarService.GetDatesForPeriod(startPeriod, endPeriod);
- var response = new List();
+ //var daysList = calendarService.GetDatesForPeriod(startPeriod, endPeriod);
+ var daysList = await nextRunService.GetWorkDaysAsync(DateOnly.FromDateTime(userStartPeriod.DateTime), DateOnly.FromDateTime(userEndPeriod.DateTime), false);
- daysList.ForEach(date => response.Add(new StatTemplateAutoControlResponse
+ var response = daysList.Select(date => new StatTemplateAutoControlResponse
{
Date = date,
- Activations = activations.FirstOrDefault(t => DateOnly.FromDateTime(t.Date) == date)?.Actiovations ?? 0,
- Creations = creations.FirstOrDefault(t => DateOnly.FromDateTime(t.Date) == date)?.Created ?? 0,
- Deactivations = deactivations.FirstOrDefault(t => DateOnly.FromDateTime(t.Date) == date)?.Deactivations ?? 0,
- }));
-
- response = response.OrderBy(t => t.Date).ToList();
+ Activations = activations.TryGetValue(date, out var countAct) ? countAct : 0,
+ Creations = creations.TryGetValue(date, out var countCrt) ? countCrt : 0,
+ Deactivations = deactivations.TryGetValue(date, out var countDeact) ? countDeact : 0
+ }).OrderBy(t => t.Date).ToList();
return Ok(new Response>(response, true));
}
diff --git a/PARR.API/Controllers/V1/Statistics/StatTemplateController.cs b/PARR.API/Controllers/V1/Statistics/StatTemplateController.cs
index 7613d6ff..28863f45 100644
--- a/PARR.API/Controllers/V1/Statistics/StatTemplateController.cs
+++ b/PARR.API/Controllers/V1/Statistics/StatTemplateController.cs
@@ -6,9 +6,10 @@ using PARR.API.Contracts.V1.Requests.BaseRequests;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base;
-using PARR.BLL.Services.Interfaces;
+using PARR.API.Helpers;
using PARR.Constants;
using PARR.DAL.Contracts;
+using PARR.DAL.NextRunServices;
using PARR.DAL.Services.Interfaces;
namespace PARR.API.Controllers.V1.Statistics
@@ -20,15 +21,15 @@ namespace PARR.API.Controllers.V1.Statistics
public class StatTemplateController : BaseApiController
{
private readonly ITemplateService templateService;
- private readonly ICalendarService calendarService;
+ private readonly INextRunService nextRunService;
public StatTemplateController(
ITemplateService templateService,
- ICalendarService calendarService
+ INextRunService nextRunService
)
{
this.templateService = templateService;
- this.calendarService = calendarService;
+ this.nextRunService = nextRunService;
}
///
@@ -40,12 +41,12 @@ namespace PARR.API.Controllers.V1.Statistics
{
var response = new StatTemplateResponse
{
- ActivateScheduleCount = await templateService.Get().CountAsync(t => t.IsActiveSchedule),
- ActivateTemplateCount = await templateService.Get().CountAsync(t => t.IsActiveTemplate),
- TemplateAgentCount = await templateService.Get().Include(t => t.Job).ThenInclude(t=>t.Group).CountAsync(t => t.Job!.Group!.IsAgent),
- TemplateCount = await templateService.Get().CountAsync(),
- SyncEsppScheduleCount = await templateService.Get().Include(t => t.RobotConfigurations).CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.ScheduleOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok)),
- SyncEsppTemplatesCount = await templateService.Get().Include(t => t.RobotConfigurations).CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.TemplateOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok))
+ ActivateScheduleCount = await templateService.Get().AsNoTracking().CountAsync(t => t.IsActiveSchedule),
+ ActivateTemplateCount = await templateService.Get().AsNoTracking().CountAsync(t => t.IsActiveTemplate),
+ TemplateAgentCount = await templateService.Get().AsNoTracking().CountAsync(t => t.Job!.Group!.IsAgent),
+ TemplateCount = await templateService.Get().AsNoTracking().CountAsync(),
+ SyncEsppScheduleCount = await templateService.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.ScheduleOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok)),
+ SyncEsppTemplatesCount = await templateService.Get().AsNoTracking().CountAsync(t => t.RobotConfigurations.Any(c => c.RobotCode == (int)RobotsEnum.TemplateOrder && c.TaskStatusCode == (int)TaskStatusEnum.Ok))
};
return Ok(new Response(response, true));
@@ -61,33 +62,31 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.StatTemplate.GetForPeriod)]
public async Task GetForPeriod([FromQuery] TimeZoneOffsetClient timeZoneQuery, [FromQuery] int startPeriodDays = 3)
{
- var endPeriod = calendarService.GetDateWithTimeZoneOffset(DateTimeOffset.UtcNow, timeZoneQuery.TimeZoneOffsetHours);
- var startPeriod = endPeriod.AddDays(-startPeriodDays);
+ // Формируем период
+ var userEnd = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime);
+ var userStart = userEnd.AddDays(-startPeriodDays);
- var query = templateService.Get()
- .Where(t =>
- t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
- && t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
- ).GroupBy(t => t.DateCreated.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
- .Select(t => new
- {
- Date = t.Key,
- CreatedObjs = t.Count()
- });
+ var (utcStart, utcEnd) = TimeZoneDateHelper.GetUtcDateRange(
+ userStart,
+ userEnd.AddDays(1),
+ timeZoneQuery.TimeZoneOffset);
- var result = await query.ToListAsync();
+ var allRecords = await templateService.Get()
+ .AsNoTracking()
+ .FilterByDateRangeUtc(t => t.DateCreated, utcStart, utcEnd)
+ .Select(t => new { t.Id, t.DateCreated })
+ .ToListAsync();
- var daysList = calendarService.GetDatesForPeriod(startPeriod, endPeriod);
+ var resultDict = allRecords.GroupByUserDate(t => t.DateCreated, timeZoneQuery.TimeZoneOffset);
- var response = new List();
- daysList.ForEach(date => response.Add(new StatTemplatePeriodResponse
+ var daysList = await nextRunService.GetWorkDaysAsync(userStart, userEnd, false);
+
+ var response = daysList.Select(date => new StatTemplatePeriodResponse
{
Date = date,
- CreatedTemplatesCount = result.FirstOrDefault(t => DateOnly.FromDateTime(t.Date) == date)?.CreatedObjs ?? 0
- }));
-
- response = response.OrderBy(t => t.Date).ToList();
+ CreatedTemplatesCount = resultDict.TryGetValue(date, out var cnt) ? cnt : 0
+ }).OrderBy(t => t.Date).ToList();
return Ok(new Response>(response, true));
}
diff --git a/PARR.API/Controllers/V1/Statistics/StatTemplateDistributionController.cs b/PARR.API/Controllers/V1/Statistics/StatTemplateDistributionController.cs
index 378843f8..eea541ff 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 INextRunServiceV2 nextRunService;
+ private readonly INextRunService 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,
- INextRunServiceV2 nextRunService,
+ INextRunService nextRunService,
ILogger logger,
IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService
)
diff --git a/PARR.API/Controllers/V1/Statistics/StatUnitController.cs b/PARR.API/Controllers/V1/Statistics/StatUnitController.cs
index c17e99a2..e1427571 100644
--- a/PARR.API/Controllers/V1/Statistics/StatUnitController.cs
+++ b/PARR.API/Controllers/V1/Statistics/StatUnitController.cs
@@ -6,8 +6,9 @@ using PARR.API.Contracts.V1.Requests.BaseRequests;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base;
-using PARR.BLL.Services.Interfaces;
+using PARR.API.Helpers;
using PARR.Constants;
+using PARR.DAL.NextRunServices;
using PARR.DAL.Services.Interfaces.Unit;
namespace PARR.API.Controllers.V1.Statistics
@@ -19,34 +20,36 @@ namespace PARR.API.Controllers.V1.Statistics
public class StatUnitController : BaseApiController
{
private readonly IUnitService unitService;
- private readonly ICalendarService calendarService;
+ private readonly INextRunService nextRunService;
public StatUnitController(
IUnitService unitService,
- ICalendarService calendarService
+ INextRunService nextRunService
)
{
this.unitService = unitService;
- this.calendarService = calendarService;
+ this.nextRunService = nextRunService;
}
///
- /// Статистика синхронизации юнитов (ЭК) с АИХИТ за последние сутки
+ /// Статистика синхронизации юнитов (ЭК) с АИХИТ за сегодня
///
///
[HttpGet(ApiRoutes.StatUnit.Get)]
- public async Task Get()
+ public async Task Get([FromQuery] TimeZoneOffsetClient timeZoneQuery)
{
- var queryDate = DateTimeOffset.UtcNow.AddDays(-1);
+ var todayStartUtc = TimeZoneDateHelper.GetUtcSartOfDay(
+ DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime),
+ timeZoneQuery.TimeZoneOffset
+ );
var response = new StatUnitResponse
{
- AllHosts = await unitService.Get().CountAsync(),
- CreatedLastDay = await unitService.Get().CountAsync(t => t.DateCreated >= queryDate),
- //UpdatedLastDay = await hostService.Get().CountAsync(t => t.DateModified.HasValue && t.DateModified.Value >= queryDate)
+ AllHosts = await unitService.Get().AsNoTracking().CountAsync(),
+ CreatedLastDay = await unitService.Get().AsNoTracking().CountAsync(t => t.DateCreated.UtcDateTime >= todayStartUtc),
UpdatedLastDay = await unitService.Get()
- .Include(t => t.UnitValues)
- .Where(t => t.UnitValues.Any(v => v.DateModified.HasValue && v.DateModified.Value >= queryDate))
+ .Where(t => t.UnitValues.Any(v => v.DateModified.HasValue && v.DateModified.Value.UtcDateTime >= todayStartUtc))
+ .AsNoTracking()
.CountAsync()
};
@@ -63,37 +66,31 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.StatUnit.GetForPeriod)]
public async Task GetForPeriod([FromQuery] TimeZoneOffsetClient timeZoneQuery, [FromQuery] int startPeriodDays = 3)
{
- var endPeriod = calendarService.GetDateWithTimeZoneOffset(DateTimeOffset.UtcNow, timeZoneQuery.TimeZoneOffsetHours);
- var startPeriod = endPeriod.AddDays(-startPeriodDays);
+ // Формируем период
+ var userEnd = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime);
+ var userStart = userEnd.AddDays(-startPeriodDays);
- var query = unitService.Get()
- .Where(t =>
- t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
- && t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
- )
- .GroupBy(t => t.DateCreated.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
- .Select(t => new
- {
- Date = t.Key,
- UnitCount = t.Count()
- });
+ var (utcStart, utcEnd) = TimeZoneDateHelper.GetUtcDateRange(
+ userStart,
+ userEnd.AddDays(1),
+ timeZoneQuery.TimeZoneOffset);
- var result = await query.ToListAsync();
+ var allRecords = await unitService.Get()
+ .AsNoTracking()
+ .FilterByDateRangeUtc(t => t.DateCreated, utcStart, utcEnd)
+ .Select(t => new { t.Id, t.DateCreated })
+ .ToListAsync();
- var daysList = calendarService.GetDatesForPeriod(startPeriod, endPeriod);
- var response = new List();
+ var resultDict = allRecords.GroupByUserDate(t => t.DateCreated, timeZoneQuery.TimeZoneOffset);
- daysList.ForEach(date =>
+ var daysList = await nextRunService.GetWorkDaysAsync(userStart, userEnd, false);
+
+ var response = daysList.Select(date => new StatUnitPeriodResponse
{
- response.Add(new StatUnitPeriodResponse
- {
- Date = date,
- CreatedUnitsCount = result.FirstOrDefault(t => DateOnly.FromDateTime(t.Date) == date)?.UnitCount ?? 0
- });
- });
-
- response = response.OrderBy(t => t.Date).ToList();
+ Date = date,
+ CreatedUnitsCount = resultDict.TryGetValue(date, out var cnt) ? cnt : 0
+ }).OrderBy(t => t.Date).ToList();
return Ok(new Response>(response, true));
}
diff --git a/PARR.API/Controllers/V1/Statistics/StatUnitFieldValuesController.cs b/PARR.API/Controllers/V1/Statistics/StatUnitFieldValuesController.cs
index 1dbf39e5..b04cfa74 100644
--- a/PARR.API/Controllers/V1/Statistics/StatUnitFieldValuesController.cs
+++ b/PARR.API/Controllers/V1/Statistics/StatUnitFieldValuesController.cs
@@ -6,8 +6,8 @@ using PARR.API.Contracts.V1.Requests.BaseRequests;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Contracts.V1.Responses.Statistics;
using PARR.API.Controllers.V1.Base;
-using PARR.BLL.Services.Interfaces;
using PARR.Constants;
+using PARR.DAL.NextRunServices;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Unit;
@@ -21,32 +21,44 @@ namespace PARR.API.Controllers.V1.Statistics
{
private readonly IApplicationService applicationService;
private readonly IUnitFieldValueService unitFieldValueService;
- private readonly ICalendarService calendarService;
+ private readonly INextRunService nextRunService;
+
+ //private readonly ICalendarService calendarService;
public StatUnitFieldValuesController(
IApplicationService applicationService,
IUnitFieldValueService unitFieldValueService,
- ICalendarService calendarService
+ //ICalendarService calendarService
+ INextRunService nextRunService
)
{
this.applicationService = applicationService;
this.unitFieldValueService = unitFieldValueService;
- this.calendarService = calendarService;
+ this.nextRunService = nextRunService;
+ //this.calendarService = calendarService;
}
///
- /// Статистика синхронизации полей юнитов (ЭК) с АИХИТ за последние сутки
+ /// Статистика синхронизации полей юнитов (ЭК) с АИХИТ за сегодня
///
///
[HttpGet(ApiRoutes.StatUnitFieldValues.Get)]
- public async Task Get()
+ public async Task Get([FromQuery] TimeZoneOffsetClient timeZoneQuery)
{
- var queryDate = DateTimeOffset.UtcNow.AddDays(-1);
+ // Формируем начало "сегодня" в часовом поясе пользователя
+ var userNow = DateTimeOffset.UtcNow.ToOffset(timeZoneQuery.TimeZoneOffset);
+ var userTodayStart = new DateTimeOffset(
+ userNow.Year, userNow.Month, userNow.Day,
+ 0, 0, 0,
+ timeZoneQuery.TimeZoneOffset);
+
+ // Конвертируем в UTC для запроса к БД
+ var todayStartUtc = userTodayStart.UtcDateTime;
var response = new StatUnitFieldValuesResponse
{
- AllValues = await unitFieldValueService.Get().CountAsync(),
- CreatedLastDay = await unitFieldValueService.Get().CountAsync(t => t.DateCreated >= queryDate),
+ AllValues = await unitFieldValueService.Get().AsNoTracking().CountAsync(),
+ CreatedLastDay = await unitFieldValueService.Get().AsNoTracking().CountAsync(t => t.DateCreated.UtcDateTime >= todayStartUtc),
//UpdatedLastDay = await applicationService.Get().CountAsync(t => t.DateModified.HasValue && t.DateModified.Value >= queryDate),
};
@@ -63,37 +75,62 @@ namespace PARR.API.Controllers.V1.Statistics
[HttpGet(ApiRoutes.StatUnitFieldValues.GetForPeriod)]
public async Task GetForPeriod([FromQuery] TimeZoneOffsetClient timeZoneQuery, [FromQuery] int startPeriodDays = 3)
{
- var endPeriod = calendarService.GetDateWithTimeZoneOffset(DateTimeOffset.UtcNow, timeZoneQuery.TimeZoneOffsetHours);
- var startPeriod = endPeriod.AddDays(-startPeriodDays);
+ var userEndPeriod = DateTimeOffset.UtcNow.Add(timeZoneQuery.TimeZoneOffset);
+ var userStartPeriod = userEndPeriod.AddDays(-startPeriodDays);
- var query = unitFieldValueService.Get()
+ var utcEndPeriod = userEndPeriod.UtcDateTime;
+ var utcStartPeriod = userStartPeriod.UtcDateTime;
+
+ var allRecords = await unitFieldValueService.Get()
+ .AsNoTracking()
.Where(t =>
- t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
- && t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
- )
- .GroupBy(t => t.DateCreated.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
- .Select(t => new
- {
- Date = t.Key,
- Count = t.Count()
- });
+ //t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
+ //&& t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ utcStartPeriod <= t.DateCreated.UtcDateTime && t.DateCreated.UtcDateTime < utcEndPeriod
+ ).Select(t => new { t.Id, t.DateCreated })
+ .ToListAsync();
- var result = await query.ToListAsync();
+ var resultDict = allRecords.GroupBy(t => DateOnly.FromDateTime(t.DateCreated.ToOffset(timeZoneQuery.TimeZoneOffset).DateTime))
+ .ToDictionary(t => t.Key, t => t.Count());
- var daysList = calendarService.GetDatesForPeriod(startPeriod, endPeriod);
+ //var endPeriod = calendarService.GetDateWithTimeZoneOffset(DateTimeOffset.UtcNow, timeZoneQuery.TimeZoneOffsetHours);
+ //var startPeriod = endPeriod.AddDays(-startPeriodDays);
- var response = new List();
+ //var query = unitFieldValueService.Get()
+ // .Where(t =>
+ // t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date >= startPeriod.Date
+ // && t.DateCreated.DateTime.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date <= endPeriod.Date
+ // )
+ // .GroupBy(t => t.DateCreated.AddHours(timeZoneQuery.TimeZoneOffsetHours).Date)
+ // .Select(t => new
+ // {
+ // Date = t.Key,
+ // Count = t.Count()
+ // });
- daysList.ForEach(date =>
+ //var result = await query.ToListAsync();
+
+ //var daysList = calendarService.GetDatesForPeriod(startPeriod, endPeriod);
+ var daysList = await nextRunService.GetWorkDaysAsync(DateOnly.FromDateTime(userStartPeriod.DateTime), DateOnly.FromDateTime(userEndPeriod.DateTime), false);
+
+ //var response = new List();
+
+ //daysList.ForEach(date =>
+ //{
+ // response.Add(new StatApplicationPeriodResponse
+ // {
+ // Date = date,
+ // CreatedApplicationsCount = result.FirstOrDefault(t => DateOnly.FromDateTime(t.Date) == date)?.Count ?? 0
+ // });
+ //});
+
+ //response = response.OrderBy(t => t.Date).ToList();
+
+ var response = daysList.Select(date => new StatApplicationPeriodResponse
{
- response.Add(new StatApplicationPeriodResponse
- {
- Date = date,
- CreatedApplicationsCount = result.FirstOrDefault(t => DateOnly.FromDateTime(t.Date) == date)?.Count ?? 0
- });
- });
-
- response = response.OrderBy(t => t.Date).ToList();
+ Date = date,
+ CreatedApplicationsCount = resultDict.TryGetValue(date, out var cnt) ? cnt : 0
+ }).OrderBy(t => t.Date).ToList();
return Ok(new Response>(response, true));
}
diff --git a/PARR.API/Helpers/TimeZoneDateHelper.cs b/PARR.API/Helpers/TimeZoneDateHelper.cs
new file mode 100644
index 00000000..b49de37b
--- /dev/null
+++ b/PARR.API/Helpers/TimeZoneDateHelper.cs
@@ -0,0 +1,96 @@
+using System.Linq.Expressions;
+
+namespace PARR.API.Helpers
+{
+ ///
+ /// Хелпер для работы с датами в разных часовых поясах (UTC - пользователь). Используется для формирования СТАТИСТИКИ
+ ///
+ public static class TimeZoneDateHelper
+ {
+ ///
+ /// Создаёт начало дня (00:00:00) в часовом поясе пользователя и конвертирует в UTC
+ ///
+ ///
+ ///
+ ///
+ public static DateTime GetUtcSartOfDay(DateOnly userDate, TimeSpan userOffset)
+ {
+ var userStart = new DateTimeOffset(userDate.Year, userDate.Month, userDate.Day,
+ 0, 0, 0,
+ userOffset);
+
+ return userStart.UtcDateTime;
+ }
+
+
+ ///
+ /// Создаёт полуоткрытый интервал [start; end) в UTC для фильтрации за период (end не включается)
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static (DateTime StartUtc, DateTime EndUtc) GetUtcDateRange(DateOnly userStartDate, DateOnly userEndDateExclusive, TimeSpan userOffset)
+ {
+ var start = new DateTimeOffset(
+ userStartDate.Year, userStartDate.Month, userStartDate.Day,
+ 0, 0, 0, userOffset).UtcDateTime;
+
+ var end = new DateTimeOffset(
+ userEndDateExclusive.Year, userEndDateExclusive.Month, userEndDateExclusive.Day,
+ 0, 0, 0, userOffset).UtcDateTime;
+
+ return (start, end);
+ }
+
+
+ ///
+ /// Применяет фильтр по дате к IQueryable
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IQueryable FilterByDateRangeUtc(this IQueryable query, Expression> dateSelector, DateTime startUtc, DateTime endUtc)
+ {
+ // Строим выражение: t => t.DateCreated.UtcDateTime >= start && < end
+ var param = dateSelector.Parameters[0];
+ var body = Expression.AndAlso(
+ // Строим левую часть: t.DateCreated.UtcDateTime >= startUtc
+ Expression.GreaterThanOrEqual(
+ Expression.Property(dateSelector.Body, nameof(DateTimeOffset.UtcDateTime)),
+ Expression.Constant(startUtc, typeof(DateTime))
+ ),
+ // Строим правую часть: t.DateCreated.UtcDateTime < endUtc
+ Expression.LessThan(
+ Expression.Property(dateSelector.Body, nameof(DateTimeOffset.UtcDateTime)),
+ Expression.Constant(endUtc, typeof(DateTime))
+ )
+ );
+
+ // Собираем финальную лямбду: t => (t.DateCreated.UtcDateTime >= start && < end)
+ var lambda = Expression.Lambda>(body, param);
+
+ // Применяем к запросу
+ return query.Where(lambda);
+ }
+
+
+ ///
+ /// Группирует записи по дате в часовом поясе пользователя (для загруженных записей)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static Dictionary GroupByUserDate(this IEnumerable source, Func dateSelector, TimeSpan userOffset)
+ {
+ return source.GroupBy(t => DateOnly.FromDateTime(dateSelector(t).ToOffset(userOffset).DateTime))
+ .ToDictionary(g => g.Key, g => g.Count());
+
+ }
+ }
+}
diff --git a/PARR.API/appsettings.json b/PARR.API/appsettings.json
index d976bb4a..4b82eb4e 100644
--- a/PARR.API/appsettings.json
+++ b/PARR.API/appsettings.json
@@ -110,7 +110,7 @@
}
],
"RabbitMq": {
- "ThresholdConnections": 18
+ "ThresholdConnections": 28
}
}
}
diff --git a/PARR.BLL/Services/Implementations/CalendarService.cs b/PARR.BLL/Services/Implementations/CalendarService.cs
index 9c4b4613..f6f3f9ea 100644
--- a/PARR.BLL/Services/Implementations/CalendarService.cs
+++ b/PARR.BLL/Services/Implementations/CalendarService.cs
@@ -7,6 +7,13 @@ namespace PARR.BLL.Services.Implementations
{
internal class CalendarService : ICalendarService
{
+
+
+
+ // !!! Вообще не использовать этот класс!!! Удалить его!!! Есть же INextRunServiceV2, вот его и использовать!!!
+
+
+
private readonly ILogger logger;
public CalendarService(ILogger logger)
diff --git a/PARR.DAL/NextRunServices/INextRunServiceV2.cs b/PARR.DAL/NextRunServices/INextRunService.cs
similarity index 98%
rename from PARR.DAL/NextRunServices/INextRunServiceV2.cs
rename to PARR.DAL/NextRunServices/INextRunService.cs
index 37d41a23..8d5fff32 100644
--- a/PARR.DAL/NextRunServices/INextRunServiceV2.cs
+++ b/PARR.DAL/NextRunServices/INextRunService.cs
@@ -4,7 +4,7 @@ using PARR.DAL.NextRunServices.Models;
namespace PARR.DAL.NextRunServices
{
- public interface INextRunServiceV2
+ public interface INextRunService
{
///
/// Распределить шаблоны в группе работ (получить список распределенных шаблонов с актуальными nextRun)
diff --git a/PARR.DAL/NextRunServices/NextRunServiceV2.cs b/PARR.DAL/NextRunServices/NextRunService.cs
similarity index 99%
rename from PARR.DAL/NextRunServices/NextRunServiceV2.cs
rename to PARR.DAL/NextRunServices/NextRunService.cs
index aa1dff2b..9e54bb29 100644
--- a/PARR.DAL/NextRunServices/NextRunServiceV2.cs
+++ b/PARR.DAL/NextRunServices/NextRunService.cs
@@ -13,23 +13,23 @@ using PARR.DAL.Services.Interfaces.Schedule;
namespace PARR.DAL.NextRunServices
{
- internal class NextRunServiceV2 : INextRunServiceV2
+ internal class NextRunService : INextRunService
{
- private readonly ILogger logger;
+ private readonly ILogger logger;
private readonly ITemplateService templateService;
private readonly IJobGroupService jobGroupService;
private readonly IEsppScheduleTransformService esppScheduleTransformService;
- private readonly ITemplateDistributorV2 templateDistributor;
+ private readonly ITemplateDistributor templateDistributor;
private readonly IShortcodesService shortcodesService;
private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService;
private readonly SettingsFromDb settingsFromDb;
- public NextRunServiceV2(
- ILogger logger,
+ public NextRunService(
+ ILogger logger,
ITemplateService templateService,
IJobGroupService jobGroupService,
IEsppScheduleTransformService esppScheduleTransformService,
- ITemplateDistributorV2 templateDistributor,
+ ITemplateDistributor templateDistributor,
IShortcodesService shortcodesService,
IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService,
SettingsFromDb settingsFromDb
diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs
index 6126e040..043fc44a 100644
--- a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs
+++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs
@@ -5,63 +5,47 @@ namespace PARR.DAL.NextRunServices.Subservices
internal interface ITemplateDistributor
{
///
- /// Распределяет все шаблоны на указанный период, учитывая план нагрузки.
- /// Возвращает список всех шаблонов с обновлёнными датами выполнения.
+ /// Распределить шаблоны
///
- /// Дата начала распределения
- /// Длительность распределения, дней
- /// ReferenceDate
- /// Спиоск шаблонов для распределения
- /// Исключать выходные из распределения
- ///
- Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends);
-
-
-
- ///
- /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки.
- /// Возвращает обновлённый шаблон с новой датой выполнения.
- ///
- ///
+ /// Дата начала, в любом часовом поясе
///
///
+ ///
+ ///
+ ///
+ ///
+ Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends);
+
+ ///
+ /// Метод проверяет, актуален ли текущий NextRun шаблона, если не актуален, возвращает актуальное значение
+ /// При проверке загружаются шаблоны из БД! Если хотим рассчитывать nextRun для НЕСКОЛЬКИХ шаблонов, то следует рассчитать nextRun для одного шаблона, сохранить в БД, затем рассчитать для следующего
+ ///
+ /// Дата начала, в любом часовом поясе
+ ///
+ ///
+ ///
///
///
///
- /// Новый шаблон
+ ///
///
- Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew);
-
-
+ Task GetNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew);
+
///
/// Получить список выходных дней
///
///
///
- ///
///
- Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate);
-
-
- /////
- ///// Получить список рабочих дней
- /////
- /////
- /////
- /////
- /////
- //Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends);
-
+ Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate);
///
- /// Получить список рабочих дней
+ /// Получить список рабочих дней в диапазоне, исключая выходные и праздники.
///
///
///
- ///
- ///
+ /// Исключить выходные и праздники
///
- Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate);
+ Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends);
}
}
-
diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs
deleted file mode 100644
index 57c10510..00000000
--- a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using PARR.DAL.NextRunServices.Models;
-
-namespace PARR.DAL.NextRunServices.Subservices
-{
- internal interface ITemplateDistributorV2
- {
- ///
- /// Распределить шаблоны
- ///
- /// Дата начала, в любом часовом поясе
- ///
- ///
- ///
- ///
- ///
- ///
- Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends);
-
- ///
- /// Метод проверяет, актуален ли текущий NextRun шаблона, если не актуален, возвращает актуальное значение
- /// При проверке загружаются шаблоны из БД! Если хотим рассчитывать nextRun для НЕСКОЛЬКИХ шаблонов, то следует рассчитать nextRun для одного шаблона, сохранить в БД, затем рассчитать для следующего
- ///
- /// Дата начала, в любом часовом поясе
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- Task GetNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew);
-
- ///
- /// Получить список выходных дней
- ///
- ///
- ///
- ///
- Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate);
-
- ///
- /// Получить список рабочих дней в диапазоне, исключая выходные и праздники.
- ///
- ///
- ///
- /// Исключить выходные и праздники
- ///
- Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends);
- }
-}
diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs
index 1430c248..e0eab027 100644
--- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs
+++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs
@@ -5,6 +5,11 @@ using PARR.DAL.Services.Interfaces;
namespace PARR.DAL.NextRunServices.Subservices
{
+ ///
+ /// Распределитель шаблонов с поддержкой часовых поясов.
+ /// Все входные даты интерпретируются в целевом часовом поясе (параметр offset).
+ /// Все результаты возвращаются строго в UTC (TimeSpan.Zero).
+ ///
internal class TemplateDistributor : ITemplateDistributor
{
private readonly ILogger logger;
@@ -15,141 +20,324 @@ namespace PARR.DAL.NextRunServices.Subservices
///
private const int offsetYearsIfErrorCalcNextRun = 5;
- public TemplateDistributor(
- ILogger logger,
- IWeekendDayService weekendDayService
- )
+ ///
+ /// Максимальное количество попыток расширения периода при отсутствии рабочих дней
+ ///
+ private const int maxPeriodExpansions = 2;
+
+ public TemplateDistributor(ILogger logger, IWeekendDayService weekendDayService)
{
this.logger = logger;
this.weekendDayService = weekendDayService;
}
- public async Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew)
+ public async Task GetNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew)
{
- //if (offset.HasValue)
- // referenceDate = referenceDate.ToOffset(offset.Value);
+ // Валидация входных параметров
+ if (duration <= 0)
+ throw new ArgumentException($"Duration must be positive, got {duration}", nameof(duration));
- //todo: allTemplates - пока не используется. Будет использоваться когда каждый раз будем строить план
+ if (allTemplates == null)
+ throw new ArgumentNullException(nameof(allTemplates));
- // Пока работает так:
- // Получаем список рабочих дней, если работа попадает в эти дни, то считаем дату валидной
- // Если нет, то перемещаем на последний день периода распределения или первый в зависимости от параметра isNew
+ logger.LogInformation("Начинаем проверку/расчёт NextRun для шаблона {templateId}, nextRun={nextRun}. isNew={isNew}, offset={offset}, referenceDate={referenceDate}", targetTemplate.Id, targetTemplate.NextRun, isNew, offset, referenceDate);
- var _startDate = GetStartPeriodDate(startDate, referenceDate);
+ // 1. Определяем целевой часовой пояс
+ var targetOffset = offset ?? TimeSpan.Zero;
+ var referenceDateInTargetTz = referenceDate.ToOffset(targetOffset);
+
+ logger.LogDebug("Целевой часовой пояс: {offset}. ReferenceDate в целевом поясе: {refDate}", targetOffset, referenceDateInTargetTz);
+
+
+ // 2. Рассчитываем период распределения в целевом поясе
+ var _startDate = GetStartPeriodDate(startDate, referenceDateInTargetTz, targetOffset);
var _endDate = GetEndPeriodDate(_startDate, duration);
- logger.LogDebug("Получаю актуальный nextRun для шаблона {templateId}. Текущий nextRun: {nextRun}. Период {duration} дней, с {_startDate} по {_endDate}. Исключить выходные и праздники: {excludeWeekends}. Для нового шаблона: {isNew}", targetTemplate.Id, targetTemplate.NextRun, duration, _startDate, _endDate, excludeWeekends, isNew);
+ logger.LogInformation("Период распределения: {_startDate} - {_endDate} (в поясе {offset})", _startDate, _endDate, targetOffset);
- // Получаем список рабочих дней (исключая выходные и праздники)
- //var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends);
- var workDays = await GetWorkDaysV2Async(_startDate, _endDate, excludeWeekends, referenceDate);
+ // 3. Получаем список рабочих дней с расширением периода при необходимости
+ var (expandedEndDate, workDays) = await GetWorkDaysWithExpansionAsync(_startDate, duration, excludeWeekends);
+
+ // Обновляем _endDate на расширенный период для дальнейших расчётов
+ _endDate = expandedEndDate;
if (workDays.Count == 0)
{
- logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем шаблон со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun);
-
- // смещаем nextRun вперед
- var result = FillTemplateResultWithOffsetError(targetTemplate, _startDate, referenceDate);
-
- logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld);
-
- return result;
+ logger.LogError("Не удалось найти рабочие дни даже после {maxAttempts} попыток расширения периода [{start}, {end}]. Возвращаем ошибку со смещением.", maxPeriodExpansions, _startDate, _endDate);
+ return FillTemplateResultWithOffsetError(targetTemplate, _startDate, referenceDateInTargetTz, targetOffset);
}
- // если isNew, то берем первый день периода
- if (isNew)
+ #region 4. Строим план нагрузки и рассчитываем текущую нагрузку
+
+ logger.LogDebug("Строим план распределения для шаблона {templateId}. Всего шаблонов: {totalTemplates}", targetTemplate.Id, allTemplates.Count);
+
+ // Исключаем текущий шаблон из расчёта
+ var templatesForPlan = allTemplates.Where(t => t.Id != targetTemplate.Id).ToList();
+
+ logger.LogDebug("Исключили текущий шаблон из расчёта. Осталось {count} шаблонов для плана", templatesForPlan.Count);
+
+ // Определяем тип плана (обычный или зеркальный)
+ bool useMirrorPlan = false;
+
+ // Определяем тип плана на основе контекста
+ if (!targetTemplate.NextRun.HasValue)
{
- var result = new TemplateNextRunResultDto
- (
- Id: targetTemplate.Id,
- NextRun: FindNextAvailableDateIgnoreLoad(workDays, referenceDate, true), //CreateDateTimeOffset(workDays.First(), referenceDate),
- NextRunOld: targetTemplate.NextRun
- );
-
- logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. Это было создание нового, поместили его в начало периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld);
-
- return result;
+ // Нет текущего NextRun — тип плана зависит от isNew
+ useMirrorPlan = !isNew;
+ logger.LogDebug("Нет текущего NextRun. Тип плана: {planType} (isNew={isNew})", useMirrorPlan ? "зеркальный" : "обычный", isNew);
}
-
- // есть ли NextRun и попадает ли он на рабочий день
- // проверяем чтоб было не меньше чем UtcNow
- //TODO: тут еще сделать проверку нагрузки на день
- if (targetTemplate.NextRun.HasValue && workDays.Contains(DateOnly.FromDateTime(targetTemplate.NextRun.Value.Date)) && targetTemplate.NextRun.Value > DateTimeOffset.UtcNow)
+ else
{
- var result = new TemplateNextRunResultDto
- (
- Id: targetTemplate.Id,
- NextRun: CreateDateTimeOffset(targetTemplate.NextRun.Value, referenceDate),
- NextRunOld: targetTemplate.NextRun
- );
+ // Есть текущий NextRun — проверяем, в периоде ли он
+ var _nextRunInTargetTz = targetTemplate.NextRun.Value.ToOffset(targetOffset);
+ var _nextRunDate = DateOnly.FromDateTime(_nextRunInTargetTz.Date);
- logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun актуальный, возвращаем его. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld);
+ bool _isInPeriod = _nextRunDate >= _startDate && _nextRunDate <= _endDate;
+ bool _isWorkDay = workDays.Contains(_nextRunDate);
- return result;
+ if (_isInPeriod && !_isWorkDay)
+ {
+ // В периоде, но нерабочий день — обычный план
+ useMirrorPlan = false;
+ logger.LogDebug("NextRun в периоде, но на нерабочем дне. Используем обычный план.");
+ }
+ else if (!_isInPeriod)
+ {
+ // Не в периоде — тип плана зависит от isNew
+ useMirrorPlan = !isNew;
+ logger.LogDebug("NextRun вне периода. Тип плана: {planType} (isNew={isNew})", useMirrorPlan ? "зеркальный" : "обычный", isNew);
+ }
+ else
+ {
+ // В периоде и рабочий день — будем проверять актуальность
+ useMirrorPlan = false;
+ logger.LogDebug("NextRun в периоде и на рабочем дне. Используем обычный план для проверки.");
+ }
}
+ // Строим план распределения
+ List plan;
+ if (useMirrorPlan)
+ {
+ // Зеркальный план
+ plan = GetDistributionPlan(templatesForPlan.Count + 1, workDays, mirror: true);
+ logger.LogDebug("Построен зеркальный план для {count} шаблонов", templatesForPlan.Count + 1);
+ }
+ else
+ {
+ // Обычный план
+ plan = GetDistributionPlan(templatesForPlan.Count + 1, workDays);
+ logger.LogDebug("Построен обычный план для {count} шаблонов", templatesForPlan.Count + 1);
+ }
- //TODO: Временный костыль, перемещаем на конец периода
+ // Считаем ожидаемую нагрузку из плана
+ var expectedDailyLoad = plan.GroupBy(d => d).ToDictionary(g => g.Key, g => g.Count());
- var endPeriodResult = new TemplateNextRunResultDto
- (
- Id: targetTemplate.Id,
- NextRun: FindNextAvailableDateIgnoreLoad(workDays, referenceDate, false),
- NextRunOld: targetTemplate.NextRun
- );
+ logger.LogDebug("Ожидаемая нагрузка по плану: {loadCount} дней", expectedDailyLoad.Count);
+ foreach (var kvp in expectedDailyLoad.OrderBy(x => x.Key))
+ logger.LogDebug(" {date}: ожидаемо {count} шаблонов", kvp.Key, kvp.Value);
- logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun перемещен на конец периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", endPeriodResult.Id, endPeriodResult.NextRun, endPeriodResult.NextRunOld);
+ // Считаем текущую нагрузку (только шаблоны внутри периода)
+ var currentDailyLoad = BuildCurrentLoad(templatesForPlan, workDays, _startDate, _endDate, targetOffset);
- return endPeriodResult;
+ logger.LogDebug("Текущая нагрузка (без учёта текущего шаблона): {loadCount} дней", currentDailyLoad.Count);
+ foreach (var kvp in currentDailyLoad.OrderBy(x => x.Key))
+ logger.LogDebug(" {date}: текущая {current}, ожидаемая {expected}", kvp.Key, kvp.Value, expectedDailyLoad.GetValueOrDefault(kvp.Key, 0));
+
+ #endregion
+
+ // 5. Случай: нет текущего NextRun
+ if (!targetTemplate.NextRun.HasValue)
+ {
+ logger.LogInformation("Шаблон {templateId} не имеет текущего NextRun. Ищем новое место...", targetTemplate.Id);
+
+ return HandleNoNextRun(
+ targetTemplate,
+ workDays,
+ currentDailyLoad,
+ expectedDailyLoad,
+ referenceDateInTargetTz,
+ targetOffset,
+ isNew);
+ }
+
+ #region 6. Случай: есть текущий NextRun — проверяем актуальность
+
+ logger.LogDebug("Шаблон {templateId} имеет текущий NextRun: {nextRun}. Проверяем актуальность...", targetTemplate.Id, targetTemplate.NextRun.Value);
+
+ var nextRunInTargetTz = targetTemplate.NextRun.Value.ToOffset(targetOffset);
+ var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);
+
+ logger.LogDebug("NextRun в целевом поясе {offset}: {nextRunTarget} (дата: {date})", targetOffset, nextRunInTargetTz, nextRunDate);
+
+ // Проверка 1: В периоде?
+ var isInPeriod = nextRunDate >= _startDate && nextRunDate <= _endDate;
+ logger.LogDebug("Проверка 1 (в периоде): {result} ({date} ∈ [{start}, {end}])", isInPeriod, nextRunDate, _startDate, _endDate);
+
+ // Проверка 2: Рабочий день?
+ var isWorkDay = workDays.Contains(nextRunDate);
+ logger.LogDebug("Проверка 2 (рабочий день): {result}", isWorkDay ? "да" : "нет");
+
+ // Проверка 3: В будущем?
+ var nextRunMoment = new DateTimeOffset(
+ nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
+ nextRunInTargetTz.Hour, nextRunInTargetTz.Minute, nextRunInTargetTz.Second,
+ targetOffset);
+
+ var isFuture = nextRunMoment >= DateTimeOffset.UtcNow;
+ logger.LogDebug("Проверка 3 (в будущем): {result} ({moment} >= {now})", isFuture, nextRunMoment, DateTimeOffset.UtcNow);
+
+ // Проверка 4: Время совпадает с referenceDate?
+ var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
+ nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
+ nextRunInTargetTz.Second == referenceDateInTargetTz.Second;
+ logger.LogDebug("Проверка 4 (время совпадает): {result} (NextRun: {nextRunHour}:{nextRunMin}:{nextRunSec} vs Reference: {refHour}:{refMin}:{refSec})",
+ hasMatchingTime,
+ nextRunInTargetTz.Hour, nextRunInTargetTz.Minute, nextRunInTargetTz.Second,
+ referenceDateInTargetTz.Hour, referenceDateInTargetTz.Minute, referenceDateInTargetTz.Second);
+
+ // СЛУЧАЙ А: Дата актуальна, но время не совпадает → корректируем ТОЛЬКО время
+ if (isInPeriod && isWorkDay && isFuture && !hasMatchingTime)
+ {
+ logger.LogDebug("Дата NextRun шаблона {templateId} актуальна, но время не совпадает. Корректируем время с {oldTime} на {newTime} (пояс {offset}).",
+ targetTemplate.Id,
+ $"{nextRunInTargetTz.Hour:D2}:{nextRunInTargetTz.Minute:D2}:{nextRunInTargetTz.Second:D2}",
+ $"{referenceDateInTargetTz.Hour:D2}:{referenceDateInTargetTz.Minute:D2}:{referenceDateInTargetTz.Second:D2}",
+ targetOffset);
+
+ // Сохраняем ту же дату, но меняем время на время из referenceDate
+ var correctedNextRunInTargetTz = new DateTimeOffset(
+ nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+
+ //скорректированный момент ещё в будущем?
+ if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
+ {
+ // Время актуально — возвращаем скорректированный NextRun
+ var correctedNextRunUtc = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);
+
+ logger.LogInformation("Скорректированный NextRun для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {nextRunOld}",
+ targetTemplate.Id, correctedNextRunInTargetTz, targetOffset, correctedNextRunUtc, targetTemplate.NextRun);
+
+ return new TemplateNextRunResultDto(
+ Id: targetTemplate.Id,
+ NextRun: correctedNextRunUtc,
+ NextRunOld: targetTemplate.NextRun);
+ }
+ else
+ {
+ // Скорректированное время уже прошло — считаем дату НЕАКТУАЛЬНОЙ
+ logger.LogDebug("Скорректированное время {correctedTime} уже прошло (сейчас {now}). Считаем дату неактуальной и передаём в основную логику поиска.",
+ correctedNextRunInTargetTz, DateTimeOffset.UtcNow);
+
+ // Помечаем дату как неактуальную по времени
+ isFuture = false;
+ // Дальнейшая логика сама вызовет HandleOutdatedNextRun с правильными параметрами
+ }
+ }
+
+ // СЛУЧАЙ Б: Все проверки пройдены (включая время) → возвращаем без изменений
+ if (isInPeriod && isWorkDay && isFuture && hasMatchingTime)
+ {
+ var nextRunUtc = targetTemplate.NextRun.Value.ToOffset(TimeSpan.Zero);
+ logger.LogInformation("NextRun шаблона {templateId} полностью актуален. Возвращаем без изменений: {nextRunUtc} (UTC)",
+ targetTemplate.Id, nextRunUtc);
+
+ return new TemplateNextRunResultDto(
+ Id: targetTemplate.Id,
+ NextRun: nextRunUtc,
+ NextRunOld: targetTemplate.NextRun);
+ }
+
+ // СЛУЧАЙ В: Дата неактуальна (не в периоде / нерабочий день / уже прошло) → ищем новое место
+ logger.LogDebug("NextRun шаблона {templateId} неактуален по дате (в периоде={inPeriod}, рабочий={workDay}, будущее={future}). Ищем новое место...",
+ targetTemplate.Id, isInPeriod, isWorkDay, isFuture);
+
+ #endregion
+
+ // 7. NextRun неактуален — ищем новое место
+ logger.LogDebug("NextRun шаблона {templateId} неактуален (в периоде={inPeriod}, рабочий={workDay}, будущее={future}). Ищем новое место...", targetTemplate.Id, isInPeriod, isWorkDay, isFuture);
+
+ return HandleOutdatedNextRun(
+ targetTemplate,
+ nextRunDate,
+ isInPeriod,
+ isWorkDay,
+ isNew,
+ workDays,
+ currentDailyLoad,
+ expectedDailyLoad,
+ referenceDateInTargetTz,
+ targetOffset,
+ _startDate,
+ _endDate);
}
- public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends)
+ ///
+ /// Распределить шаблоны
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends)
{
- //if (offset.HasValue)
- //referenceDate = referenceDate.ToOffset(offset.Value);
+ // Валидация входных параметров
+ if (duration <= 0)
+ throw new ArgumentException($"Duration must be positive, got {duration}", nameof(duration));
+
+ if (allTemplates == null)
+ throw new ArgumentNullException(nameof(allTemplates));
- var _startDate = GetStartPeriodDate(startDate, referenceDate);
+ // 1. Определяем целевой часовой пояс
+ var targetOffset = offset ?? TimeSpan.Zero;
+
+ // 2. Применяем оффсет к referenceDate для расчётов в целевом поясе
+ var referenceDateInTargetTz = referenceDate.ToOffset(targetOffset);
+
+ logger.LogDebug("Распределение шаблонов в часовом поясе {offset}. ReferenceDate в целевом поясе: {refDate}", targetOffset, referenceDateInTargetTz);
+
+ // 3. Рассчитываем период распределения С УЧЁТОМ ЧАСОВОГО ПОЯСА
+ var _startDate = GetStartPeriodDate(startDate, referenceDateInTargetTz, targetOffset);
var _endDate = GetEndPeriodDate(_startDate, duration);
- logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDate, excludeWeekends);
+ logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDateInTargetTz: {referenceDateInTargetTz} в целевом поясе: {targetOffset} ReferenceDate UTC: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDateInTargetTz, targetOffset, referenceDate, excludeWeekends);
- // Получаем список рабочих дней (исключая выходные и праздники)
- //var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends);
- var workDays = await GetWorkDaysV2Async(_startDate, _endDate, excludeWeekends, referenceDate);
+ // 4. Получаем рабочие дни с расширением периода при необходимости
+ var (expandedEndDate, workDays) = await GetWorkDaysWithExpansionAsync(_startDate, duration, excludeWeekends);
+
+ // Обновляем _endDate на расширенный период для дальнейших расчётов
+ _endDate = expandedEndDate;
if (workDays.Count == 0)
{
- logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun);
+ logger.LogError("Не удалось найти рабочие дни даже после {maxAttempts} попыток расширения периода [{start}, {end}]. Возвращаем шаблоны со смещением {years} лет.", maxPeriodExpansions, _startDate, _endDate, offsetYearsIfErrorCalcNextRun);
- // смещаем все nextRun вперед
- var result = allTemplates.Select(t =>
- FillTemplateResultWithOffsetError(t, _startDate, referenceDate)
- ).OrderBy(t => t.NextRun).ToList();
+ var errorResult = allTemplates.Select(t =>
+ FillTemplateResultWithOffsetError(t, _startDate, referenceDateInTargetTz, targetOffset)
+ ).OrderBy(t => t.NextRun).ToList();
- logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", result.Count, result.Count(t => t.NextRun == t.NextRunOld), result.Count(t => t.NextRun != t.NextRunOld));
-
- return result;
+ logger.LogInformation("Распределение завершено с ошибкой. Возвращаем {Count} записей.", errorResult.Count);
+ return errorResult;
}
- var totalWorks = allTemplates.Count;
-
- // интервал между шаблонами
- var interval = GetInterval(workDays.Count, totalWorks);
-
+ // 5. Строим план распределения
// строим план, дни на которые должны быть назанчены шаблоны
- var plan = GetPlan(totalWorks, workDays);
-
-
+ var plan = GetDistributionPlan(allTemplates.Count, workDays);
// Подсчитываем, сколько работ должно быть на каждый день
- var expectedDailyLoad = new Dictionary();
+ var expectedDailyLoad = plan.GroupBy(d => d)
+ .ToDictionary(g => g.Key, g => g.Count());
- foreach (var day in plan)
- expectedDailyLoad[day] = expectedDailyLoad.GetValueOrDefault(day, 0) + 1;
-
- #region Группируем шаблоны по дням, на которые они уже назначены
+ #region Группируем шаблоны по дням В ЦЕЛЕВОМ ПОЯСЕ, на которые они уже назначены
var templatesByDay = new Dictionary>();
@@ -157,37 +345,36 @@ namespace PARR.DAL.NextRunServices.Subservices
{
if (template.NextRun.HasValue)
{
- var day = DateOnly.FromDateTime(template.NextRun.Value.Date);
- if (!templatesByDay.ContainsKey(day))
+ // КОНВЕРТИРУЕМ В ЦЕЛЕВОЙ ПОЯС ДЛЯ ОПРЕДЕЛЕНИЯ ДАТЫ!
+ var dayInTargetTz = DateOnly.FromDateTime(template.NextRun.Value.ToOffset(targetOffset).Date);
+ //var day = DateOnly.FromDateTime(template.NextRun.Value.Date);
+
+ if (!templatesByDay.ContainsKey(dayInTargetTz))
{
- templatesByDay[day] = new List();
+ templatesByDay[dayInTargetTz] = new List();
}
- templatesByDay[day].Add(template);
+ templatesByDay[dayInTargetTz].Add(template);
+
+ logger.LogDebug("Шаблон {Id} с NextRun={nextRun} отнесён к дате {day} в поясе {offset}", template.Id, template.NextRun.Value, dayInTargetTz, targetOffset);
}
}
#endregion
- #region Подсчитываем текущую нагрузку на каждый день
+ #region 7. Подсчитываем текущую нагрузку на каждый день
- var currentDailyLoad = new Dictionary();
-
- foreach (var day in workDays)
- {
- currentDailyLoad[day] = 0;
- }
+ var currentDailyLoad = workDays.ToDictionary(day => day, _ => 0);
foreach (var kvp in templatesByDay)
{
- var day = kvp.Key;
- var count = kvp.Value.Count;
- currentDailyLoad[day] = count;
+ if (currentDailyLoad.ContainsKey(kvp.Key))
+ currentDailyLoad[kvp.Key] = kvp.Value.Count;
}
#endregion
- #region Определяем, какие шаблоны нужно оставить, а какие — переназначить
+ #region 8. Определяем, какие шаблоны нужно оставить, а какие — переназначить
var templatesToKeep = new List();
var templatesToReassign = new List();
@@ -212,31 +399,117 @@ namespace PARR.DAL.NextRunServices.Subservices
if (currentCount <= expectedCount)
{
- // Все шаблоны в этом дне можно оставить
+ // Все шаблоны в этом дне можно оставить, но нужно проверить/скорректировать время
foreach (var template in templates)
{
- templatesToKeep.Add(new TemplateNextRunResultDto
- (
+ // Конвертируем текущий NextRun в целевой пояс для проверки времени
+ var nextRunInTargetTz = template.NextRun.Value.ToOffset(targetOffset);
+ var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);
+
+ // Проверяем, совпадает ли время с referenceDate
+ var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
+ nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
+ nextRunInTargetTz.Second == referenceDateInTargetTz.Second;
+
+ DateTimeOffset newNextRun;
+
+ if (hasMatchingTime)
+ {
+ // Время совпадает — оставляем как есть (только конвертируем в чистый UTC)
+ newNextRun = template.NextRun.Value.ToOffset(TimeSpan.Zero);
+ logger.LogDebug("Шаблон {TemplateId} оставлен без изменений (время совпадает): {NextRun} (UTC)",
+ template.Id, newNextRun);
+ }
+ else
+ {
+ // Время НЕ совпадает — корректируем ТОЛЬКО время, сохраняя ту же дату
+ var correctedNextRunInTargetTz = new DateTimeOffset(
+ nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+
+ // Проверяем, что скорректированный момент ещё в будущем
+ if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
+ {
+ newNextRun = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);
+ logger.LogDebug("Шаблон {TemplateId} оставлен на дате {date}, время скорректировано с {oldTime} на {newTime} (UTC): {NextRun}",
+ template.Id, nextRunDate,
+ $"{nextRunInTargetTz.Hour:D2}:{nextRunInTargetTz.Minute:D2}:{nextRunInTargetTz.Second:D2}",
+ $"{referenceDateInTargetTz.Hour:D2}:{referenceDateInTargetTz.Minute:D2}:{referenceDateInTargetTz.Second:D2}",
+ newNextRun);
+ }
+ else
+ {
+ // Скорректированное время уже прошло — переназначаем шаблон
+ templatesToReassign.Add(template);
+ logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. скорректированное время {correctedTime} уже прошло",
+ template.Id, correctedNextRunInTargetTz);
+ continue;
+ }
+ }
+
+ templatesToKeep.Add(new TemplateNextRunResultDto(
Id: template.Id,
- NextRun: template.NextRun.Value,
+ NextRun: newNextRun,
NextRunOld: template.NextRun
));
- logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun);
}
}
else
{
// Оставляем только expectedCount штук, остальные — переназначить
+ // (для оставляемых тоже нужно проверить/скорректировать время)
for (int i = 0; i < expectedCount; i++)
{
var template = templates[i];
- templatesToKeep.Add(new TemplateNextRunResultDto
- (
+
+ // Конвертируем текущий NextRun в целевой пояс для проверки времени
+ var nextRunInTargetTz = template.NextRun.Value.ToOffset(targetOffset);
+ var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);
+
+ var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
+ nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
+ nextRunInTargetTz.Second == referenceDateInTargetTz.Second;
+
+ DateTimeOffset newNextRun;
+
+ if (hasMatchingTime)
+ {
+ newNextRun = template.NextRun.Value.ToOffset(TimeSpan.Zero);
+ logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) оставлен без изменений (время совпадает): {NextRun} (UTC)",
+ template.Id, newNextRun);
+ }
+ else
+ {
+ var correctedNextRunInTargetTz = new DateTimeOffset(
+ nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+
+ if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
+ {
+ newNextRun = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);
+ logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) оставлен на дате {date}, время скорректировано: {NextRun}",
+ template.Id, nextRunDate, newNextRun);
+ }
+ else
+ {
+ templatesToReassign.Add(template);
+ logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) переназначается, т.к. скорректированное время уже прошло",
+ template.Id);
+ continue;
+ }
+ }
+
+ templatesToKeep.Add(new TemplateNextRunResultDto(
Id: template.Id,
- NextRun: template.NextRun.Value,
+ NextRun: newNextRun,
NextRunOld: template.NextRun
));
- logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun);
}
for (int i = expectedCount; i < currentCount; i++)
@@ -247,10 +520,10 @@ namespace PARR.DAL.NextRunServices.Subservices
}
}
}
-
#endregion
- #region Добавляем шаблоны, у которых не было даты — тоже нужно распределить
+
+ #region 9. Добавляем шаблоны, у которых не было даты — тоже нужно распределить
foreach (var template in allTemplates)
{
@@ -263,125 +536,466 @@ namespace PARR.DAL.NextRunServices.Subservices
#endregion
- #region Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку
+ #region 10. Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку
var updatedTemplates = new List(templatesToKeep);
- foreach (var template in templatesToReassign)
+ foreach (var template in templatesToReassign.OrderBy(t => t.NextRun))
{
- var nextRun = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDate, expectedDailyLoad);
- if (nextRun.HasValue)
+ var nextRunInTargetTz = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDateInTargetTz, expectedDailyLoad, targetOffset);
+ if (nextRunInTargetTz.HasValue)
{
- var day = DateOnly.FromDateTime(nextRun.Value.Date);
+ var day = DateOnly.FromDateTime(nextRunInTargetTz.Value.Date);
currentDailyLoad[day]++;
+ // КОНВЕРТИРУЕМ В UTC ПЕРЕД ВОЗВРАТОМ!
updatedTemplates.Add(new TemplateNextRunResultDto
(
Id: template.Id,
- NextRun: nextRun.Value,
+ NextRun: nextRunInTargetTz.Value.ToOffset(TimeSpan.Zero), // ← В UTC!
NextRunOld: template.NextRun
));
- logger.LogDebug("Шаблону {TemplateId} назначен NextRun: {NextRun}, NextRunOld: {NextRunOld}", template.Id, nextRun, template.NextRun);
+ logger.LogDebug("Шаблону {Id} назначен NextRun: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {NextRunOld}", template.Id, nextRunInTargetTz.Value, targetOffset, nextRunInTargetTz.Value.ToOffset(TimeSpan.Zero), template.NextRun);
}
else
{
logger.LogWarning("Не удалось назначить дату для шаблона {TemplateId}", template.Id);
- // Возвращаем с ошибкой
- updatedTemplates.Add(new TemplateNextRunResultDto
- (
- Id: template.Id,
- NextRun: CreateDateTimeOffset(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
- NextRunOld: template.NextRun
- ));
+ // Возвращаем со смещением на несколько лет, так как ошибка
+ // конвертируем в UTC
+ updatedTemplates.Add(FillTemplateResultWithOffsetError(template, _startDate, referenceDateInTargetTz, targetOffset));
}
}
#endregion
+ // 11. Сортируем и возвращаем результат в UTC
- updatedTemplates = updatedTemplates.OrderBy(t => t.NextRun).ToList();
+ var result = updatedTemplates.OrderBy(t => t.NextRun).ToList();
logger.LogDebug("--- Результат распределения ---");
- foreach (var item in updatedTemplates)
- logger.LogDebug("Шаблон {templateId}, NextRun: {NextRun}, NextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld);
+ foreach (var item in result)
+ logger.LogDebug("Шаблон {templateId}, NextRun UTC: {NextRun}, NextRunOld UTC: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld);
- logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", updatedTemplates.Count, updatedTemplates.Count(t => t.NextRun == t.NextRunOld), updatedTemplates.Count(t => t.NextRun != t.NextRunOld));
-
- return updatedTemplates;
- }
-
-
- ///
- /// Заполнить TemplateNextRunResultDto, если нет рабочих дней со смещением
- ///
- ///
- ///
- private TemplateNextRunResultDto FillTemplateResultWithOffsetError(TemplateNextRunDto template, DateOnly startDate, DateTimeOffset referenceDate)
- {
- var result = new TemplateNextRunResultDto
- (
- Id: template.Id,
- //NextRun = template.NextRun.HasValue
- // // если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun
- // ? CreateNextRun(template.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate)
- // // если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun
- // : CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
- NextRun: CreateDateTimeOffset(startDate.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
- NextRunOld: template.NextRun
- );
-
- logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id} на {offsetYearsIfErrorCalcNextRun} лет вперед, nextRun: {NextRun}, nextRunOld: {NextRunOld}", result.Id, offsetYearsIfErrorCalcNextRun, result.NextRun, result.NextRunOld);
+ logger.LogInformation("Распределение завершено. Возвращаем {Count} записей в UTC. Изменено: {changedCount}. Остались прежние значения: {oldCount}", result.Count, result.Count(t => t.NextRun != t.NextRunOld), result.Count(t => t.NextRun == t.NextRunOld));
return result;
}
-
///
- /// Получает NextRun не учитывая нагрузку на день
+ /// Обрабатывает случай, когда у шаблона нет текущего NextRun.
///
- ///
- ///
- /// Искать с начала = true, false - искать с конца
- ///
- private DateTimeOffset FindNextAvailableDateIgnoreLoad(List workDays, DateTimeOffset referenseDate, bool isSearchFirst)
+ private TemplateNextRunResultDto HandleNoNextRun(TemplateNextRunDto targetTemplate, List workDays, Dictionary currentDailyLoad, Dictionary expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, bool isNew)
{
- // возможно это временная реализация метода, используется для GetValidNextRunForTemplateAsync
+ logger.LogDebug("Обработка случая 'нет текущего NextRun' для шаблона {templateId} (isNew={isNew})", targetTemplate.Id, isNew);
- if (!isSearchFirst)
+ DateOnly? foundDay = null;
+
+ if (isNew)
{
- // берем первый рабочий день с конца и возвращаем его
- var lastDate = CreateDateTimeOffset(workDays.Last(), referenseDate);
- logger.LogDebug("Берем первый рабочий день с конца. Сгенерированный NextRun: {lastDate}", lastDate);
- return lastDate;
+ // Ищем от начала периода к концу
+ logger.LogDebug("isNew=true: ищем от начала периода к концу");
+
+ foreach (var day in workDays)
+ {
+ if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
+ {
+ foundDay = day;
+ logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
+ break;
+ }
+ }
+
+ if (!foundDay.HasValue)
+ {
+ // Не нашли свободного места — берём первый день периода
+ foundDay = workDays.First();
+ logger.LogWarning("Все дни перегружены. Берём первый день периода: {day}", foundDay.Value);
+ }
+ }
+ else
+ {
+ // Ищем от конца периода к началу
+ logger.LogDebug("isNew=false: ищем от конца периода к началу");
+
+ foreach (var day in workDays.AsEnumerable().Reverse())
+ {
+ if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
+ {
+ foundDay = day;
+ logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
+ break;
+ }
+ }
+
+ if (!foundDay.HasValue)
+ {
+ // Не нашли свободного места — берём последний день периода
+ foundDay = workDays.Last();
+ logger.LogWarning("Все дни перегружены. Берём последний день периода: {day}", foundDay.Value);
+ }
}
- // будем искать сначала
- // берем первый день, и смотрим не прошло ли уже время
- var firstDate = CreateDateTimeOffset(workDays.First(), referenseDate);
- if (firstDate > DateTimeOffset.UtcNow)
- {
- logger.LogDebug("Берем первый день с начала. Сгенерированный NextRun: {firstDate}", firstDate);
- return firstDate;
- }
+ // Формируем результат
+ var nextRunInTargetTz = new DateTimeOffset(
+ foundDay.Value.Year, foundDay.Value.Month, foundDay.Value.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
- // тогда берем второй день
- if (workDays.Count > 1)
- {
- var secondDate = CreateDateTimeOffset(workDays.ElementAt(1), referenseDate);
- logger.LogDebug("Берем второй день сначала, так как у первого дня уже время прошло. Сгенерированный NextRun: {secondDate}", secondDate);
- return secondDate;
- }
- logger.LogWarning("Вернули nextRun: {firstDate} из первого дня распределения, хотя время уже просрочено. Всего рабочих дней: {workDays}", firstDate, workDays.Count);
- // если вдруг список рабочих дней был не >1, то тогда берем первый и вренем его. Так никогда не должно быть
- return firstDate;
+ // Корректируем найденный день, чтобы момент времени был в будущем
+ DateTimeOffset? adjustedNextRun;
+ TemplateNextRunResultDto? errorResult = null;
+
+ adjustedNextRun = AdjustNextRunToFuture(
+ foundDay.Value,
+ workDays,
+ currentDailyLoad,
+ expectedDailyLoad,
+ referenceDateInTargetTz,
+ targetOffset,
+ workDays.First(), // startDateForError для случая ошибки
+ targetTemplate,
+ out errorResult);
+
+ if (errorResult != null)
+ return errorResult;
+
+ //var nextRunUtc = nextRunInTargetTz.ToOffset(TimeSpan.Zero);
+ var nextRunUtc = adjustedNextRun.Value.ToOffset(TimeSpan.Zero);
+
+ logger.LogInformation("Найдено новое место для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC)", targetTemplate.Id, nextRunInTargetTz, targetOffset, nextRunUtc);
+
+ return new TemplateNextRunResultDto(
+ Id: targetTemplate.Id,
+ NextRun: nextRunUtc,
+ NextRunOld: targetTemplate.NextRun);
}
+ ///
+ /// Обрабатывает случай, когда текущий NextRun неактуален.
+ ///
+ private TemplateNextRunResultDto HandleOutdatedNextRun(TemplateNextRunDto targetTemplate, DateOnly nextRunDate, bool isInPeriod, bool isWorkDay, bool isNew, List workDays, Dictionary currentDailyLoad, Dictionary expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, DateOnly periodStart, DateOnly periodEnd)
+ {
+ logger.LogDebug("Обработка случая 'неактуальный NextRun' для шаблона {templateId}", targetTemplate.Id);
+
+ DateOnly? foundDay = null;
+
+ if (isInPeriod && !isWorkDay)
+ {
+ // СЦЕНАРИЙ А: В периоде, но нерабочий день
+ logger.LogDebug("Сценарий А: в периоде ({date}), но нерабочий день. Ищем слева...", nextRunDate);
+
+ // Вычисляем максимальную нагрузку по всему периоду
+ var maxLoadInPeriod = expectedDailyLoad.Values.Max();
+ logger.LogDebug("Максимальная нагрузка по периоду: {maxLoad}", maxLoadInPeriod);
+
+ // Берём все рабочие дни СТРОГО ЛЕВЕЕ исходной даты
+ var leftDays = workDays
+ .Where(d => d < nextRunDate)
+ .OrderByDescending(d => d) // Ближайший к исходной дате — первый
+ .ToList();
+
+ if (leftDays.Any())
+ {
+ logger.LogDebug("Слева найдено {count} рабочих дней. Ищем первый не перегруженный...", leftDays.Count);
+
+ // Ищем первый день где текущая_нагрузка < максимальная_нагрузка_периода
+ foreach (var day in leftDays)
+ {
+ if (currentDailyLoad[day] < maxLoadInPeriod) // ← КЛЮЧЕВОЕ ИЗМЕНЕНИЕ
+ {
+ foundDay = day;
+ logger.LogDebug("Нашли слева день с допустимой нагрузкой (< {maxLoad}): {day}", maxLoadInPeriod, day);
+ break;
+ }
+ }
+
+ //// Ищем первый не перегруженный
+ //foreach (var day in leftDays)
+ //{
+ // if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
+ // {
+ // foundDay = day;
+ // logger.LogDebug("Нашли слева не перегруженный день: {day}", day);
+ // break;
+ // }
+ //}
+
+ // Если не нашли — берём ближайший слева
+ if (!foundDay.HasValue)
+ {
+ foundDay = leftDays.First();
+ logger.LogWarning("Все дни слева перегружены. Берём ближайший: {day}", foundDay.Value);
+ }
+ }
+ else
+ {
+ // Слева нет рабочих дней — берём ближайший справа
+ var rightDays = workDays
+ .Where(d => d > nextRunDate)
+ .OrderBy(d => d) // Ближайший к исходной дате — первый
+ .ToList();
+
+ if (rightDays.Any())
+ {
+ foundDay = rightDays.First();
+ logger.LogDebug("Слева нет рабочих дней. Берём ближайший справа: {day}", foundDay.Value);
+ }
+ else
+ {
+ // Совсем нет рабочих дней — ошибка
+ logger.LogError("Нет рабочих дней ни слева, ни справа от {date}. Возвращаем ошибку.", nextRunDate);
+ return FillTemplateResultWithOffsetError(targetTemplate, periodStart, referenceDateInTargetTz, targetOffset);
+ }
+ }
+ }
+ else if (!isInPeriod)
+ {
+ // СЦЕНАРИЙ Б: Не в периоде
+ logger.LogDebug("Сценарий Б: не в периоде ({date}). Ищем в зависимости от isNew...", nextRunDate);
+
+ if (isNew)
+ {
+ // Ищем от начала периода к концу (обычный план)
+ logger.LogDebug("isNew=true: ищем от начала периода к концу");
+
+ foreach (var day in workDays)
+ {
+ if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
+ {
+ foundDay = day;
+ logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
+ break;
+ }
+ }
+
+ if (!foundDay.HasValue)
+ {
+ foundDay = workDays.First();
+ logger.LogWarning("Все дни перегружены. Берём первый день периода: {day}", foundDay.Value);
+ }
+ }
+ else
+ {
+ // Ищем от конца периода к началу (зеркальный план)
+ logger.LogDebug("isNew=false: ищем от конца периода к началу");
+
+ foreach (var day in workDays.AsEnumerable().Reverse())
+ {
+ if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
+ {
+ foundDay = day;
+ logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
+ break;
+ }
+ }
+
+ if (!foundDay.HasValue)
+ {
+ foundDay = workDays.Last();
+ logger.LogWarning("Все дни перегружены. Берём последний день периода: {day}", foundDay.Value);
+ }
+ }
+ }
+ else
+ {
+ // Другие случаи (например, время уже прошло)
+ logger.LogDebug("NextRun неактуален по другим причинам. Используем логику в зависимости от isNew...");
+
+ if (isNew)
+ {
+ foreach (var day in workDays)
+ {
+ if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
+ {
+ foundDay = day;
+ break;
+ }
+ }
+ foundDay ??= workDays.First();
+ }
+ else
+ {
+ foreach (var day in workDays.AsEnumerable().Reverse())
+ {
+ if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
+ {
+ foundDay = day;
+ break;
+ }
+ }
+ foundDay ??= workDays.Last();
+ }
+ }
+
+ // Формируем результат
+ var nextRunInTargetTz = new DateTimeOffset(
+ foundDay.Value.Year, foundDay.Value.Month, foundDay.Value.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+
+ // Корректируем найденный день, чтобы момент времени был в будущем
+ DateTimeOffset? adjustedNextRun;
+ TemplateNextRunResultDto? errorResult = null;
+
+ adjustedNextRun = AdjustNextRunToFuture(
+ foundDay.Value,
+ workDays,
+ currentDailyLoad,
+ expectedDailyLoad,
+ referenceDateInTargetTz,
+ targetOffset,
+ periodStart, // startDateForError для случая ошибки
+ targetTemplate,
+ out errorResult);
+
+ if (errorResult != null)
+ return errorResult;
+
+
+ //var nextRunUtc = nextRunInTargetTz.ToOffset(TimeSpan.Zero);
+ var nextRunUtc = adjustedNextRun.Value.ToOffset(TimeSpan.Zero);
+
+ logger.LogInformation("Найдено новое место для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {nextRunOld}", targetTemplate.Id, nextRunInTargetTz, targetOffset, nextRunUtc, targetTemplate.NextRun);
+
+ return new TemplateNextRunResultDto(
+ Id: targetTemplate.Id,
+ NextRun: nextRunUtc,
+ NextRunOld: targetTemplate.NextRun);
+ }
+
+
+ ///
+ /// Корректирует найденный день, чтобы момент времени был в будущем.
+ /// Если момент времени уже прошёл, ищет следующий рабочий день в будущем с учётом нагрузки.
+ /// В случае ошибки (нет будущих рабочих дней) возвращает ошибку через out-параметр.
+ ///
+ private DateTimeOffset? AdjustNextRunToFuture(DateOnly foundDay, List workDays, Dictionary currentDailyLoad, Dictionary expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, DateOnly startDateForError, TemplateNextRunDto targetTemplate, out TemplateNextRunResultDto? errorResult)
+ {
+ errorResult = null;
+
+ // Создаём момент времени для найденного дня
+ var nextRunInTargetTz = new DateTimeOffset(
+ foundDay.Year, foundDay.Month, foundDay.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+
+ // ФИНАЛЬНАЯ ПРОВЕРКА: момент времени должен быть в будущем
+ if (nextRunInTargetTz < DateTimeOffset.UtcNow)
+ {
+ logger.LogWarning("Найденный момент времени {nextRun} уже прошёл (сейчас {now}). Ищем следующий рабочий день в будущем.",
+ nextRunInTargetTz, DateTimeOffset.UtcNow);
+
+ // Берём все рабочие дни СТРОГО ПОСЛЕ найденного дня
+ var futureWorkDays = workDays.Where(d => d > foundDay).ToList();
+
+ if (futureWorkDays.Any())
+ {
+ // Ищем первый день с допустимой нагрузкой (текущая < ожидаемая)
+ var notOverloadedDays = futureWorkDays
+ .Where(d => currentDailyLoad[d] < expectedDailyLoad.GetValueOrDefault(d, 0))
+ .ToList();
+
+ var futureDay = notOverloadedDays.Any()
+ ? notOverloadedDays.First()
+ : futureWorkDays.First();
+
+ logger.LogDebug("Выбран следующий рабочий день в будущем: {day}", futureDay);
+
+ // Пересоздаём момент времени с новой датой
+ nextRunInTargetTz = new DateTimeOffset(
+ futureDay.Year, futureDay.Month, futureDay.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+ }
+ else
+ {
+ // Нет будущих рабочих дней — ошибка
+ logger.LogError("Нет будущих рабочих дней после {date} в периоде. Возвращаем ошибку со смещением.",
+ foundDay);
+
+ errorResult = FillTemplateResultWithOffsetError(targetTemplate, startDateForError, referenceDateInTargetTz, targetOffset);
+ return null;
+ }
+ }
+ else
+ {
+ logger.LogDebug("Момент времени {nextRun} актуален (в будущем). Коррекция не требуется.", nextRunInTargetTz);
+ }
+
+ return nextRunInTargetTz;
+ }
+
+
+ ///
+ /// Строит текущую нагрузку на дни, учитывая ТОЛЬКО шаблоны внутри периода.
+ /// Шаблоны вне периода игнорируются.
+ ///
+ private Dictionary BuildCurrentLoad(List templates, List workDays, DateOnly periodStart, DateOnly periodEnd, TimeSpan targetOffset)
+ {
+ var load = workDays.ToDictionary(day => day, _ => 0);
+
+ foreach (var template in templates)
+ {
+ if (!template.NextRun.HasValue)
+ continue;
+
+ // Конвертируем в целевой пояс для определения даты
+ var templateDate = DateOnly.FromDateTime(template.NextRun.Value.ToOffset(targetOffset).Date);
+
+ // Учитываем ТОЛЬКО шаблоны внутри периода
+ if (templateDate >= periodStart && templateDate <= periodEnd)
+ {
+ if (load.ContainsKey(templateDate))
+ load[templateDate]++;
+ }
+ }
+
+ return load;
+ }
+
+
+ ///
+ /// Заполнить TemplateNextRunResultDto, если нет рабочих дней со смещением несколько лет
+ ///
+ ///
+ ///
+ private TemplateNextRunResultDto FillTemplateResultWithOffsetError(TemplateNextRunDto template, DateOnly startDate, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset)
+ {
+ // Создаём дату в целевом поясе
+ var errorDateInTargetTz = new DateTimeOffset(
+ startDate.AddYears(offsetYearsIfErrorCalcNextRun).Year,
+ startDate.AddYears(offsetYearsIfErrorCalcNextRun).Month,
+ startDate.AddYears(offsetYearsIfErrorCalcNextRun).Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
+
+ // КОНВЕРТИРУЕМ В UTC ДЛЯ ВОЗВРАТА
+ var errorDateUtc = errorDateInTargetTz.ToOffset(TimeSpan.Zero);
+
+ logger.LogWarning("Шаблон {Id}: нет рабочих дней в периоде, сдвиг на {years} лет → {nextRun} (UTC)", template.Id, offsetYearsIfErrorCalcNextRun, errorDateUtc);
+
+ return new TemplateNextRunResultDto(
+ Id: template.Id,
+ NextRun: errorDateUtc,
+ NextRunOld: template.NextRun
+ );
+ }
///
@@ -392,7 +1006,7 @@ namespace PARR.DAL.NextRunServices.Subservices
///
///
///
- private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary currentDailyLoad, List workDays, DateTimeOffset referenceDate, Dictionary expectedDailyLoad)
+ private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary currentDailyLoad, List workDays, DateTimeOffset referenceDateInTargetTz, Dictionary expectedDailyLoad, TimeSpan targetOffset)
{
foreach (var day in workDays)
{
@@ -401,63 +1015,62 @@ namespace PARR.DAL.NextRunServices.Subservices
if (current < expected)
{
- return CreateDateTimeOffset(day, referenceDate);
+ // Создаём момент времени В ЦЕЛЕВОМ ПОЯСЕ
+ return new DateTimeOffset(
+ day.Year, day.Month, day.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
}
}
- // Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план
- foreach (var day in workDays)
- {
- var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
- var current = currentDailyLoad.GetValueOrDefault(day, 0);
-
- if (current < expected)
- {
- return CreateDateTimeOffset(day, referenceDate);
- }
- }
-
- // Если все дни уже загружены по плану — ищем день с минимальной нагрузкой
- var minLoad = currentDailyLoad.Values.Min();
- var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key;
-
- return CreateDateTimeOffset(minDay, referenceDate);
+ // Если все дни загружены — берём день с минимальной нагрузкой
+ var minDay = currentDailyLoad.MinBy(x => x.Value).Key;
+ return new DateTimeOffset(
+ minDay.Year, minDay.Month, minDay.Day,
+ referenceDateInTargetTz.Hour,
+ referenceDateInTargetTz.Minute,
+ referenceDateInTargetTz.Second,
+ targetOffset);
}
- //---------------------------------------------------------------------------------------------------------------------------------------------------------------------
///
- /// Получить план: дни, на которые должны быть назначены шаблоны
+ /// Получить план распределения шаблонов по рабочим дням.
///
- ///
- ///
- ///
- private List GetPlan(int totalWorks, List workDays)
+ /// Общее количество шаблонов для распределения
+ /// Список рабочих дней в периоде
+ /// Если true — строит зеркальный план (справа налево от конца периода), иначе обычный (слева направо от начала периода)
+ /// Список дат, на которые должны быть назначены шаблоны
+ private List GetDistributionPlan(int totalWorks, List workDays, bool mirror = false)
{
- var totalDays = workDays.Count();
-
- var interval = GetInterval(totalDays, totalWorks);
+ var totalDays = workDays.Count;
+ var interval = CalculateDistributionInterval(totalDays, totalWorks);
var plan = new List();
for (int i = 0; i < totalWorks; i++)
{
- #region Вынести в метод
- // Вычисляем индекс дня, на который нужно назначить i-ю работу
- //var dayIndex = (int)Math.Round(i * interval);
- var dayIndex = (int)Math.Floor(i * interval);
+ // Базовый индекс для обычного плана (слева → направо)
+ var baseIndex = (int)Math.Floor(i * interval);
- // Защита от возможного выхода за границы массива из-за округления
- if (dayIndex >= totalDays)
- dayIndex = totalDays - 1;
- #endregion
+ // Применяем зеркалирование, если нужно
+ var index = mirror
+ ? totalDays - 1 - baseIndex // Зеркальный индекс: от конца периода
+ : baseIndex; // Обычный индекс: от начала периода
- var date = workDays[dayIndex];
- plan.Add(date);
+ // Защита от выхода за границы массива
+ index = Math.Max(0, Math.Min(index, totalDays - 1));
+
+ plan.Add(workDays[index]);
}
- logger.LogDebug("--- План распределения ---");
- foreach (var item in plan.GroupBy(t => t))
+ // Логгирование
+ var planType = mirror ? "Зеркальный" : "Обычный";
+ logger.LogDebug("--- {planType} план распределения ({totalWorks} шаблонов, {totalDays} раб. дней) ---", planType, totalWorks, totalDays);
+
+ foreach (var item in plan.GroupBy(t => t).OrderBy(g => g.Key))
logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count());
return plan;
@@ -470,252 +1083,118 @@ namespace PARR.DAL.NextRunServices.Subservices
/// Кол-во рабочих дней
/// Кол-во шаблонов
///
- private double GetInterval(int totalDays, int totalWorks)
+ private double CalculateDistributionInterval(int totalDays, int totalWorks)
{
var interval = (double)totalDays / totalWorks;
- logger.LogDebug("Интевал между шаблонами: {interval}", interval);
+ logger.LogDebug("Интервал распределения: {interval} (дней на шаблон)", interval);
return interval;
}
///
- /// Создать DateTimeOffset (из DateTimeOffset + refDate)
+ /// Получить дату начала распределения с учётом целевого часового пояса.
+ /// Метод возвращает календарную дату (DateOnly), рассчитанную в контексте указанного оффсета.
///
- /// Откуда взять дату
- /// Откуда взять время
- ///
- private DateTimeOffset CreateDateTimeOffset(DateTimeOffset date, DateTimeOffset refDate)
+ /// Стартовая дата (интерпретируется в целевом часовом поясе)
+ /// Момент времени в целевом часовом поясе (после применения оффсета)
+ /// Целевой часовой пояс (оффсет)
+ /// Дата начала распределения
+ private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate, TimeSpan offset)
{
- return CreateDateTimeOffset(DateOnly.FromDateTime(date.Date), refDate);
- }
+ var dateStart = startDate;
- ///
- /// Создать DateTimeOffset (из DateOnly + refDate)
- ///
- ///
- ///
- ///
- private DateTimeOffset CreateDateTimeOffset(DateOnly date, DateTimeOffset refDate)
- {
- return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, refDate.Offset);
- }
-
-
- ///
- /// Получить дату начала распределения относительно StartDate, ReferenceDate и текущей даты
- ///
- ///
- ///
- ///
- private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate)
- {
- // дата начала не может быть раньше referenceDate
- // и не может быть раньше чем сегодня
-
- #region old
- //var dateStart = startDate;
-
- //// если 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);
-
- //// если текущее время > 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 (в его часовом поясе)
+ // 1. Дата начала не может быть раньше даты из referenceDate (в целевом поясе)
var refDateInTargetTz = DateOnly.FromDateTime(referenceDate.Date);
if (dateStart < refDateInTargetTz)
{
- logger.LogDebug("Дата начала {dateStart} раньше даты из referenceDate {refDate}. Используем дату из referenceDate.", dateStart, refDateInTargetTz);
+ logger.LogDebug("Дата начала {startDate} раньше даты из referenceDate в поясе {offset} ({refDate}). Используем {refDate}.",
+ dateStart, offset, refDateInTargetTz, refDateInTargetTz);
dateStart = refDateInTargetTz;
}
- // 3. Дата начала не может быть раньше "сегодня" в целевом часовом поясе
- var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Date);
+ // 2. Дата начала не может быть раньше "сегодня" в целевом часовом поясе
+ var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).Date);
if (dateStart < todayInTargetTz)
{
- logger.LogDebug("Дата начала {dateStart} раньше 'сегодня' в целевом поясе {today}. Используем 'сегодня'.", dateStart, todayInTargetTz);
+ logger.LogDebug("Дата начала {startDate} раньше 'сегодня' в поясе {offset} ({today}). Используем {today}.",
+ dateStart, offset, todayInTargetTz, todayInTargetTz);
dateStart = todayInTargetTz;
}
- // 4. Если момент времени "начало дня в целевом поясе" уже прошёл — добавляем 1 день
- var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate);
+ // 3. Если момент времени "начало дня в целевом поясе + время из referenceDate" уже прошёл — добавляем 1 день
+ // ВАЖНО: время берём из самого referenceDate (который уже в целевом поясе!)
+ var dateTimeStart = new DateTimeOffset(
+ dateStart.Year, dateStart.Month, dateStart.Day,
+ referenceDate.Hour, // ← Время из целевого пояса!
+ referenceDate.Minute,
+ referenceDate.Second,
+ offset); // ← Оффсет целевого пояса
+
+ // Сравниваем абсолютные моменты времени (работает корректно даже с разными оффсетами)
if (dateTimeStart < DateTimeOffset.UtcNow)
{
- logger.LogDebug("Момент времени {dateTimeStart} уже прошёл (сейчас {now}). Добавляем 1 день.", dateTimeStart, DateTimeOffset.UtcNow);
+ logger.LogDebug("Момент времени {dateTimeStart} (в поясе {offset}) уже прошёл (сейчас {now}). Добавляем 1 день.",
+ dateTimeStart, offset, DateTimeOffset.UtcNow);
dateStart = dateStart.AddDays(1);
}
- logger.LogDebug("Итоговая дата начала распределения: {dateStart} (в часовом поясе {offset})", dateStart, referenceDate.Offset);
-
+ logger.LogDebug("Итоговая дата начала распределения в поясе {offset}: {dateStart}", offset, dateStart);
return dateStart;
}
///
- /// Получить дату конца периода распределения
+ /// Получить дату окончания периода распределения.
+ /// Продолжительность включает стартовый день (например, duration=7 означает 7 дней: с 1 по 7 июня).
///
///
///
///
private DateOnly GetEndPeriodDate(DateOnly startDate, int duration)
{
+ if (duration <= 0)
+ {
+ logger.LogWarning("Некорректная продолжительность периода: {duration}. Используем минимальное значение 1.", duration);
+ duration = 1;
+ }
+
// duration — количество дней, включая startDate
// поэтому вычитаем 1, чтобы получить сдвиг
- return startDate.AddDays(duration - 1);
+ var endDate = startDate.AddDays(duration - 1);
+ logger.LogDebug("Расчёт даты окончания периода: startDate={startDate}, duration={duration}, endDate={endDate}", startDate, duration, endDate);
+
+ return endDate;
}
- /////
- ///// Получить список выходных дней
- /////
- /////
- /////
- /////
- //private async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate)
- //{
- // var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();
-
- // logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count);
- // foreach (var weekend in weekends)
- // logger.LogDebug("Не рабочий день: {weekend}", weekend);
-
- // return weekends.ToHashSet();
- //}
-
-
///
/// Получить список выходных дней
///
///
///
///
- public async Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TimeSpan? offset = null)
+ public async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate)
{
- if (offset.HasValue)
- referenceDate = referenceDate.ToOffset(offset.Value);
-
- // проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd
- //bool isPreviousDays = IsPreviousDays(referenceDate);
-
- //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);
- //}
-
+ //Возвращает все выходные дни включая сб и вск, и праздники
var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();
- var weekendsResult = new List();
-
- //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)
+ logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count);
+ foreach (var weekend in weekends)
logger.LogDebug("Не рабочий день: {weekend}", weekend);
- return weekendsResult.ToHashSet();
+ return weekends.ToHashSet();
}
- /////
- ///// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники.
- /////
- /////
- /////
- ///// Исключить выходные и праздники
- /////
- //public async Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends)
- //{
- // // Получаем список выходных/праздничных дней
- // var weekends = new HashSet();
- // if (excludeWeekends)
- // weekends = await GetWeekendsAsync(start, end);
-
- // var result = new List();
- // var date = start;
-
- // while (date <= end)
- // {
- // var dayOfWeek = date.DayOfWeek;
- // if (!weekends.Contains(date))
- // {
- // result.Add(date);
- // }
- // date = date.AddDays(1);
- // }
-
- // logger.LogDebug("Найдено рабочих дней: {Count}", result.Count);
- // foreach (var workDay in result)
- // logger.LogDebug("Рабочий день: {workDay}", workDay);
-
- // return result;
- //}
-
-
- public async Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate, TimeSpan? offset = null)
+ public async Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends)
{
- 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);
-
- // logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, start, end);
- //}
-
// Получаем список выходных/праздничных дней
var weekends = new HashSet();
if (excludeWeekends)
- weekends = await GetWeekendsV2Async(start, end, referenceDate);
+ weekends = await GetWeekendsAsync(start, end);
var result = new List();
var date = start;
@@ -737,45 +1216,44 @@ namespace PARR.DAL.NextRunServices.Subservices
return result;
}
-
- public Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate)
+ ///
+ /// Получает рабочие дни с автоматическим расширением периода при их отсутствии.
+ /// Расширяет период до maxPeriodExpansions раз: _startDate + duration * N (N = 1, 2, 3)
+ ///
+ private async Task<(DateOnly endDate, List workDays)> GetWorkDaysWithExpansionAsync(DateOnly startDate, int duration, bool excludeWeekends)
{
- throw new NotImplementedException();
+ // Попытка 0: исходный период [_startDate, _startDate + duration - 1]
+ var currentEndDate = startDate.AddDays(duration - 1);
+ var workDays = await GetWorkDaysAsync(startDate, currentEndDate, excludeWeekends);
+
+ if (workDays.Count > 0)
+ {
+ logger.LogDebug("Найдено {count} рабочих дней в исходном периоде [{start}, {end}]", workDays.Count, startDate, currentEndDate);
+ return (currentEndDate, workDays);
+ }
+
+ // Попытки расширения 1..maxPeriodExpansions
+ for (int attempt = 1; attempt <= maxPeriodExpansions; attempt++)
+ {
+ //расширяем период относительно ПРЕДЫДУЩЕГО конца
+ currentEndDate = currentEndDate.AddDays(duration);
+
+ logger.LogWarning("Попытка {attempt}/{maxAttempts} расширения периода: нет рабочих дней в [{start}, {end}]. Расширяем до {newEnd}.", attempt, maxPeriodExpansions, startDate, startDate.AddDays(duration - 1), currentEndDate);
+
+ workDays = await GetWorkDaysAsync(startDate, currentEndDate, excludeWeekends);
+
+ if (workDays.Count > 0)
+ {
+ logger.LogInformation("Рабочие дни найдены после {attempt} попытки расширения. Период: [{start}, {end}], кол-во рабочих дней: {count}", attempt, startDate, currentEndDate, workDays.Count);
+ return (currentEndDate, workDays);
+ }
+ }
+
+ // Все попытки исчерпаны — рабочих дней нет
+ logger.LogError("После {maxAttempts} попыток расширения рабочие дни не найдены в периоде [{start}, {end}]. Возвращаем ошибку.", maxPeriodExpansions, startDate, currentEndDate);
+
+ return (currentEndDate, new List()); // Пустой список для обработки ошибки вызывающим кодом
}
-
- public Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate)
- {
- throw new NotImplementedException();
- }
-
- /////
- ///// Это предыдущие сутки? Относительно referenceDate и МСК времени
- /////
- /////
- /////
- //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;
- // }
-
- //}
-
}
}
diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs
deleted file mode 100644
index 511b799b..00000000
--- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs
+++ /dev/null
@@ -1,1259 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging;
-using PARR.DAL.NextRunServices.Models;
-using PARR.DAL.Services.Interfaces;
-
-namespace PARR.DAL.NextRunServices.Subservices
-{
- ///
- /// Распределитель шаблонов с поддержкой часовых поясов.
- /// Все входные даты интерпретируются в целевом часовом поясе (параметр offset).
- /// Все результаты возвращаются строго в UTC (TimeSpan.Zero).
- ///
- internal class TemplateDistributorV2 : ITemplateDistributorV2
- {
- private readonly ILogger logger;
- private readonly IWeekendDayService weekendDayService;
-
- ///
- /// Смещение nextRun на несколько лет вперед, если не смог его рассчитать
- ///
- private const int offsetYearsIfErrorCalcNextRun = 5;
-
- ///
- /// Максимальное количество попыток расширения периода при отсутствии рабочих дней
- ///
- private const int maxPeriodExpansions = 2;
-
- public TemplateDistributorV2(ILogger logger, IWeekendDayService weekendDayService)
- {
- this.logger = logger;
- this.weekendDayService = weekendDayService;
- }
-
-
- public async Task GetNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew)
- {
- // Валидация входных параметров
- if (duration <= 0)
- throw new ArgumentException($"Duration must be positive, got {duration}", nameof(duration));
-
- if (allTemplates == null)
- throw new ArgumentNullException(nameof(allTemplates));
-
- logger.LogInformation("Начинаем проверку/расчёт NextRun для шаблона {templateId}, nextRun={nextRun}. isNew={isNew}, offset={offset}, referenceDate={referenceDate}", targetTemplate.Id, targetTemplate.NextRun, isNew, offset, referenceDate);
-
- // 1. Определяем целевой часовой пояс
- var targetOffset = offset ?? TimeSpan.Zero;
- var referenceDateInTargetTz = referenceDate.ToOffset(targetOffset);
-
- logger.LogDebug("Целевой часовой пояс: {offset}. ReferenceDate в целевом поясе: {refDate}", targetOffset, referenceDateInTargetTz);
-
-
- // 2. Рассчитываем период распределения в целевом поясе
- var _startDate = GetStartPeriodDate(startDate, referenceDateInTargetTz, targetOffset);
- var _endDate = GetEndPeriodDate(_startDate, duration);
-
- logger.LogInformation("Период распределения: {_startDate} - {_endDate} (в поясе {offset})", _startDate, _endDate, targetOffset);
-
- // 3. Получаем список рабочих дней с расширением периода при необходимости
- var (expandedEndDate, workDays) = await GetWorkDaysWithExpansionAsync(_startDate, duration, excludeWeekends);
-
- // Обновляем _endDate на расширенный период для дальнейших расчётов
- _endDate = expandedEndDate;
-
- if (workDays.Count == 0)
- {
- logger.LogError("Не удалось найти рабочие дни даже после {maxAttempts} попыток расширения периода [{start}, {end}]. Возвращаем ошибку со смещением.", maxPeriodExpansions, _startDate, _endDate);
- return FillTemplateResultWithOffsetError(targetTemplate, _startDate, referenceDateInTargetTz, targetOffset);
- }
-
- #region 4. Строим план нагрузки и рассчитываем текущую нагрузку
-
- logger.LogDebug("Строим план распределения для шаблона {templateId}. Всего шаблонов: {totalTemplates}", targetTemplate.Id, allTemplates.Count);
-
- // Исключаем текущий шаблон из расчёта
- var templatesForPlan = allTemplates.Where(t => t.Id != targetTemplate.Id).ToList();
-
- logger.LogDebug("Исключили текущий шаблон из расчёта. Осталось {count} шаблонов для плана", templatesForPlan.Count);
-
- // Определяем тип плана (обычный или зеркальный)
- bool useMirrorPlan = false;
-
- // Определяем тип плана на основе контекста
- if (!targetTemplate.NextRun.HasValue)
- {
- // Нет текущего NextRun — тип плана зависит от isNew
- useMirrorPlan = !isNew;
- logger.LogDebug("Нет текущего NextRun. Тип плана: {planType} (isNew={isNew})", useMirrorPlan ? "зеркальный" : "обычный", isNew);
- }
- else
- {
- // Есть текущий NextRun — проверяем, в периоде ли он
- var _nextRunInTargetTz = targetTemplate.NextRun.Value.ToOffset(targetOffset);
- var _nextRunDate = DateOnly.FromDateTime(_nextRunInTargetTz.Date);
-
- bool _isInPeriod = _nextRunDate >= _startDate && _nextRunDate <= _endDate;
- bool _isWorkDay = workDays.Contains(_nextRunDate);
-
- if (_isInPeriod && !_isWorkDay)
- {
- // В периоде, но нерабочий день — обычный план
- useMirrorPlan = false;
- logger.LogDebug("NextRun в периоде, но на нерабочем дне. Используем обычный план.");
- }
- else if (!_isInPeriod)
- {
- // Не в периоде — тип плана зависит от isNew
- useMirrorPlan = !isNew;
- logger.LogDebug("NextRun вне периода. Тип плана: {planType} (isNew={isNew})", useMirrorPlan ? "зеркальный" : "обычный", isNew);
- }
- else
- {
- // В периоде и рабочий день — будем проверять актуальность
- useMirrorPlan = false;
- logger.LogDebug("NextRun в периоде и на рабочем дне. Используем обычный план для проверки.");
- }
- }
-
- // Строим план распределения
- List plan;
- if (useMirrorPlan)
- {
- // Зеркальный план
- plan = GetDistributionPlan(templatesForPlan.Count + 1, workDays, mirror: true);
- logger.LogDebug("Построен зеркальный план для {count} шаблонов", templatesForPlan.Count + 1);
- }
- else
- {
- // Обычный план
- plan = GetDistributionPlan(templatesForPlan.Count + 1, workDays);
- logger.LogDebug("Построен обычный план для {count} шаблонов", templatesForPlan.Count + 1);
- }
-
- // Считаем ожидаемую нагрузку из плана
- var expectedDailyLoad = plan.GroupBy(d => d).ToDictionary(g => g.Key, g => g.Count());
-
- logger.LogDebug("Ожидаемая нагрузка по плану: {loadCount} дней", expectedDailyLoad.Count);
- foreach (var kvp in expectedDailyLoad.OrderBy(x => x.Key))
- logger.LogDebug(" {date}: ожидаемо {count} шаблонов", kvp.Key, kvp.Value);
-
- // Считаем текущую нагрузку (только шаблоны внутри периода)
- var currentDailyLoad = BuildCurrentLoad(templatesForPlan, workDays, _startDate, _endDate, targetOffset);
-
- logger.LogDebug("Текущая нагрузка (без учёта текущего шаблона): {loadCount} дней", currentDailyLoad.Count);
- foreach (var kvp in currentDailyLoad.OrderBy(x => x.Key))
- logger.LogDebug(" {date}: текущая {current}, ожидаемая {expected}", kvp.Key, kvp.Value, expectedDailyLoad.GetValueOrDefault(kvp.Key, 0));
-
- #endregion
-
- // 5. Случай: нет текущего NextRun
- if (!targetTemplate.NextRun.HasValue)
- {
- logger.LogInformation("Шаблон {templateId} не имеет текущего NextRun. Ищем новое место...", targetTemplate.Id);
-
- return HandleNoNextRun(
- targetTemplate,
- workDays,
- currentDailyLoad,
- expectedDailyLoad,
- referenceDateInTargetTz,
- targetOffset,
- isNew);
- }
-
- #region 6. Случай: есть текущий NextRun — проверяем актуальность
-
- logger.LogDebug("Шаблон {templateId} имеет текущий NextRun: {nextRun}. Проверяем актуальность...", targetTemplate.Id, targetTemplate.NextRun.Value);
-
- var nextRunInTargetTz = targetTemplate.NextRun.Value.ToOffset(targetOffset);
- var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);
-
- logger.LogDebug("NextRun в целевом поясе {offset}: {nextRunTarget} (дата: {date})", targetOffset, nextRunInTargetTz, nextRunDate);
-
- // Проверка 1: В периоде?
- var isInPeriod = nextRunDate >= _startDate && nextRunDate <= _endDate;
- logger.LogDebug("Проверка 1 (в периоде): {result} ({date} ∈ [{start}, {end}])", isInPeriod, nextRunDate, _startDate, _endDate);
-
- // Проверка 2: Рабочий день?
- var isWorkDay = workDays.Contains(nextRunDate);
- logger.LogDebug("Проверка 2 (рабочий день): {result}", isWorkDay ? "да" : "нет");
-
- // Проверка 3: В будущем?
- var nextRunMoment = new DateTimeOffset(
- nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
- nextRunInTargetTz.Hour, nextRunInTargetTz.Minute, nextRunInTargetTz.Second,
- targetOffset);
-
- var isFuture = nextRunMoment >= DateTimeOffset.UtcNow;
- logger.LogDebug("Проверка 3 (в будущем): {result} ({moment} >= {now})", isFuture, nextRunMoment, DateTimeOffset.UtcNow);
-
- // Проверка 4: Время совпадает с referenceDate?
- var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
- nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
- nextRunInTargetTz.Second == referenceDateInTargetTz.Second;
- logger.LogDebug("Проверка 4 (время совпадает): {result} (NextRun: {nextRunHour}:{nextRunMin}:{nextRunSec} vs Reference: {refHour}:{refMin}:{refSec})",
- hasMatchingTime,
- nextRunInTargetTz.Hour, nextRunInTargetTz.Minute, nextRunInTargetTz.Second,
- referenceDateInTargetTz.Hour, referenceDateInTargetTz.Minute, referenceDateInTargetTz.Second);
-
- // СЛУЧАЙ А: Дата актуальна, но время не совпадает → корректируем ТОЛЬКО время
- if (isInPeriod && isWorkDay && isFuture && !hasMatchingTime)
- {
- logger.LogDebug("Дата NextRun шаблона {templateId} актуальна, но время не совпадает. Корректируем время с {oldTime} на {newTime} (пояс {offset}).",
- targetTemplate.Id,
- $"{nextRunInTargetTz.Hour:D2}:{nextRunInTargetTz.Minute:D2}:{nextRunInTargetTz.Second:D2}",
- $"{referenceDateInTargetTz.Hour:D2}:{referenceDateInTargetTz.Minute:D2}:{referenceDateInTargetTz.Second:D2}",
- targetOffset);
-
- // Сохраняем ту же дату, но меняем время на время из referenceDate
- var correctedNextRunInTargetTz = new DateTimeOffset(
- nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
-
- //скорректированный момент ещё в будущем?
- if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
- {
- // Время актуально — возвращаем скорректированный NextRun
- var correctedNextRunUtc = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);
-
- logger.LogInformation("Скорректированный NextRun для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {nextRunOld}",
- targetTemplate.Id, correctedNextRunInTargetTz, targetOffset, correctedNextRunUtc, targetTemplate.NextRun);
-
- return new TemplateNextRunResultDto(
- Id: targetTemplate.Id,
- NextRun: correctedNextRunUtc,
- NextRunOld: targetTemplate.NextRun);
- }
- else
- {
- // Скорректированное время уже прошло — считаем дату НЕАКТУАЛЬНОЙ
- logger.LogDebug("Скорректированное время {correctedTime} уже прошло (сейчас {now}). Считаем дату неактуальной и передаём в основную логику поиска.",
- correctedNextRunInTargetTz, DateTimeOffset.UtcNow);
-
- // Помечаем дату как неактуальную по времени
- isFuture = false;
- // Дальнейшая логика сама вызовет HandleOutdatedNextRun с правильными параметрами
- }
- }
-
- // СЛУЧАЙ Б: Все проверки пройдены (включая время) → возвращаем без изменений
- if (isInPeriod && isWorkDay && isFuture && hasMatchingTime)
- {
- var nextRunUtc = targetTemplate.NextRun.Value.ToOffset(TimeSpan.Zero);
- logger.LogInformation("NextRun шаблона {templateId} полностью актуален. Возвращаем без изменений: {nextRunUtc} (UTC)",
- targetTemplate.Id, nextRunUtc);
-
- return new TemplateNextRunResultDto(
- Id: targetTemplate.Id,
- NextRun: nextRunUtc,
- NextRunOld: targetTemplate.NextRun);
- }
-
- // СЛУЧАЙ В: Дата неактуальна (не в периоде / нерабочий день / уже прошло) → ищем новое место
- logger.LogDebug("NextRun шаблона {templateId} неактуален по дате (в периоде={inPeriod}, рабочий={workDay}, будущее={future}). Ищем новое место...",
- targetTemplate.Id, isInPeriod, isWorkDay, isFuture);
-
- #endregion
-
- // 7. NextRun неактуален — ищем новое место
- logger.LogDebug("NextRun шаблона {templateId} неактуален (в периоде={inPeriod}, рабочий={workDay}, будущее={future}). Ищем новое место...", targetTemplate.Id, isInPeriod, isWorkDay, isFuture);
-
- return HandleOutdatedNextRun(
- targetTemplate,
- nextRunDate,
- isInPeriod,
- isWorkDay,
- isNew,
- workDays,
- currentDailyLoad,
- expectedDailyLoad,
- referenceDateInTargetTz,
- targetOffset,
- _startDate,
- _endDate);
- }
-
-
- ///
- /// Распределить шаблоны
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends)
- {
- // Валидация входных параметров
- if (duration <= 0)
- throw new ArgumentException($"Duration must be positive, got {duration}", nameof(duration));
-
- if (allTemplates == null)
- throw new ArgumentNullException(nameof(allTemplates));
-
-
- // 1. Определяем целевой часовой пояс
- var targetOffset = offset ?? TimeSpan.Zero;
-
- // 2. Применяем оффсет к referenceDate для расчётов в целевом поясе
- var referenceDateInTargetTz = referenceDate.ToOffset(targetOffset);
-
- logger.LogDebug("Распределение шаблонов в часовом поясе {offset}. ReferenceDate в целевом поясе: {refDate}", targetOffset, referenceDateInTargetTz);
-
- // 3. Рассчитываем период распределения С УЧЁТОМ ЧАСОВОГО ПОЯСА
- var _startDate = GetStartPeriodDate(startDate, referenceDateInTargetTz, targetOffset);
- var _endDate = GetEndPeriodDate(_startDate, duration);
-
- logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDateInTargetTz: {referenceDateInTargetTz} в целевом поясе: {targetOffset} ReferenceDate UTC: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDateInTargetTz, targetOffset, referenceDate, excludeWeekends);
-
- // 4. Получаем рабочие дни с расширением периода при необходимости
- var (expandedEndDate, workDays) = await GetWorkDaysWithExpansionAsync(_startDate, duration, excludeWeekends);
-
- // Обновляем _endDate на расширенный период для дальнейших расчётов
- _endDate = expandedEndDate;
-
- if (workDays.Count == 0)
- {
- logger.LogError("Не удалось найти рабочие дни даже после {maxAttempts} попыток расширения периода [{start}, {end}]. Возвращаем шаблоны со смещением {years} лет.", maxPeriodExpansions, _startDate, _endDate, offsetYearsIfErrorCalcNextRun);
-
- var errorResult = allTemplates.Select(t =>
- FillTemplateResultWithOffsetError(t, _startDate, referenceDateInTargetTz, targetOffset)
- ).OrderBy(t => t.NextRun).ToList();
-
- logger.LogInformation("Распределение завершено с ошибкой. Возвращаем {Count} записей.", errorResult.Count);
- return errorResult;
- }
-
- // 5. Строим план распределения
- // строим план, дни на которые должны быть назанчены шаблоны
- var plan = GetDistributionPlan(allTemplates.Count, workDays);
- // Подсчитываем, сколько работ должно быть на каждый день
- var expectedDailyLoad = plan.GroupBy(d => d)
- .ToDictionary(g => g.Key, g => g.Count());
-
- #region Группируем шаблоны по дням В ЦЕЛЕВОМ ПОЯСЕ, на которые они уже назначены
-
- var templatesByDay = new Dictionary>();
-
- foreach (var template in allTemplates)
- {
- if (template.NextRun.HasValue)
- {
- // КОНВЕРТИРУЕМ В ЦЕЛЕВОЙ ПОЯС ДЛЯ ОПРЕДЕЛЕНИЯ ДАТЫ!
- var dayInTargetTz = DateOnly.FromDateTime(template.NextRun.Value.ToOffset(targetOffset).Date);
- //var day = DateOnly.FromDateTime(template.NextRun.Value.Date);
-
- if (!templatesByDay.ContainsKey(dayInTargetTz))
- {
- templatesByDay[dayInTargetTz] = new List();
- }
- templatesByDay[dayInTargetTz].Add(template);
-
- logger.LogDebug("Шаблон {Id} с NextRun={nextRun} отнесён к дате {day} в поясе {offset}", template.Id, template.NextRun.Value, dayInTargetTz, targetOffset);
- }
- }
-
- #endregion
-
- #region 7. Подсчитываем текущую нагрузку на каждый день
-
- var currentDailyLoad = workDays.ToDictionary(day => day, _ => 0);
-
- foreach (var kvp in templatesByDay)
- {
- if (currentDailyLoad.ContainsKey(kvp.Key))
- currentDailyLoad[kvp.Key] = kvp.Value.Count;
- }
-
- #endregion
-
-
- #region 8. Определяем, какие шаблоны нужно оставить, а какие — переназначить
- var templatesToKeep = new List();
- var templatesToReassign = new List();
-
- foreach (var kvp in templatesByDay)
- {
- var day = kvp.Key;
- var templates = kvp.Value;
-
- // Если день — нерабочий, то все шаблоны нужно переназначить
- if (!workDays.Contains(day))
- {
- foreach (var template in templates)
- {
- templatesToReassign.Add(template);
- logger.LogDebug("Шаблон {TemplateId} назначен на нерабочий день {NextRun} — будет переназначен.", template.Id, template.NextRun);
- }
- continue;
- }
-
- var expectedCount = expectedDailyLoad.GetValueOrDefault(day, 0);
- var currentCount = templates.Count;
-
- if (currentCount <= expectedCount)
- {
- // Все шаблоны в этом дне можно оставить, но нужно проверить/скорректировать время
- foreach (var template in templates)
- {
- // Конвертируем текущий NextRun в целевой пояс для проверки времени
- var nextRunInTargetTz = template.NextRun.Value.ToOffset(targetOffset);
- var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);
-
- // Проверяем, совпадает ли время с referenceDate
- var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
- nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
- nextRunInTargetTz.Second == referenceDateInTargetTz.Second;
-
- DateTimeOffset newNextRun;
-
- if (hasMatchingTime)
- {
- // Время совпадает — оставляем как есть (только конвертируем в чистый UTC)
- newNextRun = template.NextRun.Value.ToOffset(TimeSpan.Zero);
- logger.LogDebug("Шаблон {TemplateId} оставлен без изменений (время совпадает): {NextRun} (UTC)",
- template.Id, newNextRun);
- }
- else
- {
- // Время НЕ совпадает — корректируем ТОЛЬКО время, сохраняя ту же дату
- var correctedNextRunInTargetTz = new DateTimeOffset(
- nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
-
- // Проверяем, что скорректированный момент ещё в будущем
- if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
- {
- newNextRun = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);
- logger.LogDebug("Шаблон {TemplateId} оставлен на дате {date}, время скорректировано с {oldTime} на {newTime} (UTC): {NextRun}",
- template.Id, nextRunDate,
- $"{nextRunInTargetTz.Hour:D2}:{nextRunInTargetTz.Minute:D2}:{nextRunInTargetTz.Second:D2}",
- $"{referenceDateInTargetTz.Hour:D2}:{referenceDateInTargetTz.Minute:D2}:{referenceDateInTargetTz.Second:D2}",
- newNextRun);
- }
- else
- {
- // Скорректированное время уже прошло — переназначаем шаблон
- templatesToReassign.Add(template);
- logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. скорректированное время {correctedTime} уже прошло",
- template.Id, correctedNextRunInTargetTz);
- continue;
- }
- }
-
- templatesToKeep.Add(new TemplateNextRunResultDto(
- Id: template.Id,
- NextRun: newNextRun,
- NextRunOld: template.NextRun
- ));
- }
- }
- else
- {
- // Оставляем только expectedCount штук, остальные — переназначить
- // (для оставляемых тоже нужно проверить/скорректировать время)
- for (int i = 0; i < expectedCount; i++)
- {
- var template = templates[i];
-
- // Конвертируем текущий NextRun в целевой пояс для проверки времени
- var nextRunInTargetTz = template.NextRun.Value.ToOffset(targetOffset);
- var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);
-
- var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
- nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
- nextRunInTargetTz.Second == referenceDateInTargetTz.Second;
-
- DateTimeOffset newNextRun;
-
- if (hasMatchingTime)
- {
- newNextRun = template.NextRun.Value.ToOffset(TimeSpan.Zero);
- logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) оставлен без изменений (время совпадает): {NextRun} (UTC)",
- template.Id, newNextRun);
- }
- else
- {
- var correctedNextRunInTargetTz = new DateTimeOffset(
- nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
-
- if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
- {
- newNextRun = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);
- logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) оставлен на дате {date}, время скорректировано: {NextRun}",
- template.Id, nextRunDate, newNextRun);
- }
- else
- {
- templatesToReassign.Add(template);
- logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) переназначается, т.к. скорректированное время уже прошло",
- template.Id);
- continue;
- }
- }
-
- templatesToKeep.Add(new TemplateNextRunResultDto(
- Id: template.Id,
- NextRun: newNextRun,
- NextRunOld: template.NextRun
- ));
- }
-
- for (int i = expectedCount; i < currentCount; i++)
- {
- var template = templates[i];
- templatesToReassign.Add(template);
- logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. нагрузка в день превышена.", template.Id);
- }
- }
- }
- #endregion
-
-
- #region 9. Добавляем шаблоны, у которых не было даты — тоже нужно распределить
-
- foreach (var template in allTemplates)
- {
- if (!template.NextRun.HasValue)
- {
- templatesToReassign.Add(template);
- logger.LogDebug("Шаблон {TemplateId} не имел даты — будет распределён.", template.Id);
- }
- }
-
- #endregion
-
- #region 10. Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку
-
- var updatedTemplates = new List(templatesToKeep);
-
- foreach (var template in templatesToReassign.OrderBy(t => t.NextRun))
- {
- var nextRunInTargetTz = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDateInTargetTz, expectedDailyLoad, targetOffset);
- if (nextRunInTargetTz.HasValue)
- {
- var day = DateOnly.FromDateTime(nextRunInTargetTz.Value.Date);
- currentDailyLoad[day]++;
-
- // КОНВЕРТИРУЕМ В UTC ПЕРЕД ВОЗВРАТОМ!
- updatedTemplates.Add(new TemplateNextRunResultDto
- (
- Id: template.Id,
- NextRun: nextRunInTargetTz.Value.ToOffset(TimeSpan.Zero), // ← В UTC!
- NextRunOld: template.NextRun
- ));
-
- logger.LogDebug("Шаблону {Id} назначен NextRun: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {NextRunOld}", template.Id, nextRunInTargetTz.Value, targetOffset, nextRunInTargetTz.Value.ToOffset(TimeSpan.Zero), template.NextRun);
- }
- else
- {
- logger.LogWarning("Не удалось назначить дату для шаблона {TemplateId}", template.Id);
-
- // Возвращаем со смещением на несколько лет, так как ошибка
- // конвертируем в UTC
- updatedTemplates.Add(FillTemplateResultWithOffsetError(template, _startDate, referenceDateInTargetTz, targetOffset));
- }
- }
-
- #endregion
-
- // 11. Сортируем и возвращаем результат в UTC
-
- var result = updatedTemplates.OrderBy(t => t.NextRun).ToList();
-
- logger.LogDebug("--- Результат распределения ---");
- foreach (var item in result)
- logger.LogDebug("Шаблон {templateId}, NextRun UTC: {NextRun}, NextRunOld UTC: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld);
-
- logger.LogInformation("Распределение завершено. Возвращаем {Count} записей в UTC. Изменено: {changedCount}. Остались прежние значения: {oldCount}", result.Count, result.Count(t => t.NextRun != t.NextRunOld), result.Count(t => t.NextRun == t.NextRunOld));
-
- return result;
- }
-
-
- ///
- /// Обрабатывает случай, когда у шаблона нет текущего NextRun.
- ///
- private TemplateNextRunResultDto HandleNoNextRun(TemplateNextRunDto targetTemplate, List workDays, Dictionary currentDailyLoad, Dictionary expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, bool isNew)
- {
- logger.LogDebug("Обработка случая 'нет текущего NextRun' для шаблона {templateId} (isNew={isNew})", targetTemplate.Id, isNew);
-
- DateOnly? foundDay = null;
-
- if (isNew)
- {
- // Ищем от начала периода к концу
- logger.LogDebug("isNew=true: ищем от начала периода к концу");
-
- foreach (var day in workDays)
- {
- if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
- {
- foundDay = day;
- logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
- break;
- }
- }
-
- if (!foundDay.HasValue)
- {
- // Не нашли свободного места — берём первый день периода
- foundDay = workDays.First();
- logger.LogWarning("Все дни перегружены. Берём первый день периода: {day}", foundDay.Value);
- }
- }
- else
- {
- // Ищем от конца периода к началу
- logger.LogDebug("isNew=false: ищем от конца периода к началу");
-
- foreach (var day in workDays.AsEnumerable().Reverse())
- {
- if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
- {
- foundDay = day;
- logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
- break;
- }
- }
-
- if (!foundDay.HasValue)
- {
- // Не нашли свободного места — берём последний день периода
- foundDay = workDays.Last();
- logger.LogWarning("Все дни перегружены. Берём последний день периода: {day}", foundDay.Value);
- }
- }
-
- // Формируем результат
- var nextRunInTargetTz = new DateTimeOffset(
- foundDay.Value.Year, foundDay.Value.Month, foundDay.Value.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
-
-
-
- // Корректируем найденный день, чтобы момент времени был в будущем
- DateTimeOffset? adjustedNextRun;
- TemplateNextRunResultDto? errorResult = null;
-
- adjustedNextRun = AdjustNextRunToFuture(
- foundDay.Value,
- workDays,
- currentDailyLoad,
- expectedDailyLoad,
- referenceDateInTargetTz,
- targetOffset,
- workDays.First(), // startDateForError для случая ошибки
- targetTemplate,
- out errorResult);
-
- if (errorResult != null)
- return errorResult;
-
- //var nextRunUtc = nextRunInTargetTz.ToOffset(TimeSpan.Zero);
- var nextRunUtc = adjustedNextRun.Value.ToOffset(TimeSpan.Zero);
-
- logger.LogInformation("Найдено новое место для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC)", targetTemplate.Id, nextRunInTargetTz, targetOffset, nextRunUtc);
-
- return new TemplateNextRunResultDto(
- Id: targetTemplate.Id,
- NextRun: nextRunUtc,
- NextRunOld: targetTemplate.NextRun);
- }
-
-
- ///
- /// Обрабатывает случай, когда текущий NextRun неактуален.
- ///
- private TemplateNextRunResultDto HandleOutdatedNextRun(TemplateNextRunDto targetTemplate, DateOnly nextRunDate, bool isInPeriod, bool isWorkDay, bool isNew, List workDays, Dictionary currentDailyLoad, Dictionary expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, DateOnly periodStart, DateOnly periodEnd)
- {
- logger.LogDebug("Обработка случая 'неактуальный NextRun' для шаблона {templateId}", targetTemplate.Id);
-
- DateOnly? foundDay = null;
-
- if (isInPeriod && !isWorkDay)
- {
- // СЦЕНАРИЙ А: В периоде, но нерабочий день
- logger.LogDebug("Сценарий А: в периоде ({date}), но нерабочий день. Ищем слева...", nextRunDate);
-
- // Вычисляем максимальную нагрузку по всему периоду
- var maxLoadInPeriod = expectedDailyLoad.Values.Max();
- logger.LogDebug("Максимальная нагрузка по периоду: {maxLoad}", maxLoadInPeriod);
-
- // Берём все рабочие дни СТРОГО ЛЕВЕЕ исходной даты
- var leftDays = workDays
- .Where(d => d < nextRunDate)
- .OrderByDescending(d => d) // Ближайший к исходной дате — первый
- .ToList();
-
- if (leftDays.Any())
- {
- logger.LogDebug("Слева найдено {count} рабочих дней. Ищем первый не перегруженный...", leftDays.Count);
-
- // Ищем первый день где текущая_нагрузка < максимальная_нагрузка_периода
- foreach (var day in leftDays)
- {
- if (currentDailyLoad[day] < maxLoadInPeriod) // ← КЛЮЧЕВОЕ ИЗМЕНЕНИЕ
- {
- foundDay = day;
- logger.LogDebug("Нашли слева день с допустимой нагрузкой (< {maxLoad}): {day}", maxLoadInPeriod, day);
- break;
- }
- }
-
- //// Ищем первый не перегруженный
- //foreach (var day in leftDays)
- //{
- // if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
- // {
- // foundDay = day;
- // logger.LogDebug("Нашли слева не перегруженный день: {day}", day);
- // break;
- // }
- //}
-
- // Если не нашли — берём ближайший слева
- if (!foundDay.HasValue)
- {
- foundDay = leftDays.First();
- logger.LogWarning("Все дни слева перегружены. Берём ближайший: {day}", foundDay.Value);
- }
- }
- else
- {
- // Слева нет рабочих дней — берём ближайший справа
- var rightDays = workDays
- .Where(d => d > nextRunDate)
- .OrderBy(d => d) // Ближайший к исходной дате — первый
- .ToList();
-
- if (rightDays.Any())
- {
- foundDay = rightDays.First();
- logger.LogDebug("Слева нет рабочих дней. Берём ближайший справа: {day}", foundDay.Value);
- }
- else
- {
- // Совсем нет рабочих дней — ошибка
- logger.LogError("Нет рабочих дней ни слева, ни справа от {date}. Возвращаем ошибку.", nextRunDate);
- return FillTemplateResultWithOffsetError(targetTemplate, periodStart, referenceDateInTargetTz, targetOffset);
- }
- }
- }
- else if (!isInPeriod)
- {
- // СЦЕНАРИЙ Б: Не в периоде
- logger.LogDebug("Сценарий Б: не в периоде ({date}). Ищем в зависимости от isNew...", nextRunDate);
-
- if (isNew)
- {
- // Ищем от начала периода к концу (обычный план)
- logger.LogDebug("isNew=true: ищем от начала периода к концу");
-
- foreach (var day in workDays)
- {
- if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
- {
- foundDay = day;
- logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
- break;
- }
- }
-
- if (!foundDay.HasValue)
- {
- foundDay = workDays.First();
- logger.LogWarning("Все дни перегружены. Берём первый день периода: {day}", foundDay.Value);
- }
- }
- else
- {
- // Ищем от конца периода к началу (зеркальный план)
- logger.LogDebug("isNew=false: ищем от конца периода к началу");
-
- foreach (var day in workDays.AsEnumerable().Reverse())
- {
- if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
- {
- foundDay = day;
- logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
- break;
- }
- }
-
- if (!foundDay.HasValue)
- {
- foundDay = workDays.Last();
- logger.LogWarning("Все дни перегружены. Берём последний день периода: {day}", foundDay.Value);
- }
- }
- }
- else
- {
- // Другие случаи (например, время уже прошло)
- logger.LogDebug("NextRun неактуален по другим причинам. Используем логику в зависимости от isNew...");
-
- if (isNew)
- {
- foreach (var day in workDays)
- {
- if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
- {
- foundDay = day;
- break;
- }
- }
- foundDay ??= workDays.First();
- }
- else
- {
- foreach (var day in workDays.AsEnumerable().Reverse())
- {
- if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
- {
- foundDay = day;
- break;
- }
- }
- foundDay ??= workDays.Last();
- }
- }
-
- // Формируем результат
- var nextRunInTargetTz = new DateTimeOffset(
- foundDay.Value.Year, foundDay.Value.Month, foundDay.Value.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
-
- // Корректируем найденный день, чтобы момент времени был в будущем
- DateTimeOffset? adjustedNextRun;
- TemplateNextRunResultDto? errorResult = null;
-
- adjustedNextRun = AdjustNextRunToFuture(
- foundDay.Value,
- workDays,
- currentDailyLoad,
- expectedDailyLoad,
- referenceDateInTargetTz,
- targetOffset,
- periodStart, // startDateForError для случая ошибки
- targetTemplate,
- out errorResult);
-
- if (errorResult != null)
- return errorResult;
-
-
- //var nextRunUtc = nextRunInTargetTz.ToOffset(TimeSpan.Zero);
- var nextRunUtc = adjustedNextRun.Value.ToOffset(TimeSpan.Zero);
-
- logger.LogInformation("Найдено новое место для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {nextRunOld}", targetTemplate.Id, nextRunInTargetTz, targetOffset, nextRunUtc, targetTemplate.NextRun);
-
- return new TemplateNextRunResultDto(
- Id: targetTemplate.Id,
- NextRun: nextRunUtc,
- NextRunOld: targetTemplate.NextRun);
- }
-
-
- ///
- /// Корректирует найденный день, чтобы момент времени был в будущем.
- /// Если момент времени уже прошёл, ищет следующий рабочий день в будущем с учётом нагрузки.
- /// В случае ошибки (нет будущих рабочих дней) возвращает ошибку через out-параметр.
- ///
- private DateTimeOffset? AdjustNextRunToFuture(DateOnly foundDay, List workDays, Dictionary currentDailyLoad, Dictionary expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, DateOnly startDateForError, TemplateNextRunDto targetTemplate, out TemplateNextRunResultDto? errorResult)
- {
- errorResult = null;
-
- // Создаём момент времени для найденного дня
- var nextRunInTargetTz = new DateTimeOffset(
- foundDay.Year, foundDay.Month, foundDay.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
-
- // ФИНАЛЬНАЯ ПРОВЕРКА: момент времени должен быть в будущем
- if (nextRunInTargetTz < DateTimeOffset.UtcNow)
- {
- logger.LogWarning("Найденный момент времени {nextRun} уже прошёл (сейчас {now}). Ищем следующий рабочий день в будущем.",
- nextRunInTargetTz, DateTimeOffset.UtcNow);
-
- // Берём все рабочие дни СТРОГО ПОСЛЕ найденного дня
- var futureWorkDays = workDays.Where(d => d > foundDay).ToList();
-
- if (futureWorkDays.Any())
- {
- // Ищем первый день с допустимой нагрузкой (текущая < ожидаемая)
- var notOverloadedDays = futureWorkDays
- .Where(d => currentDailyLoad[d] < expectedDailyLoad.GetValueOrDefault(d, 0))
- .ToList();
-
- var futureDay = notOverloadedDays.Any()
- ? notOverloadedDays.First()
- : futureWorkDays.First();
-
- logger.LogDebug("Выбран следующий рабочий день в будущем: {day}", futureDay);
-
- // Пересоздаём момент времени с новой датой
- nextRunInTargetTz = new DateTimeOffset(
- futureDay.Year, futureDay.Month, futureDay.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
- }
- else
- {
- // Нет будущих рабочих дней — ошибка
- logger.LogError("Нет будущих рабочих дней после {date} в периоде. Возвращаем ошибку со смещением.",
- foundDay);
-
- errorResult = FillTemplateResultWithOffsetError(targetTemplate, startDateForError, referenceDateInTargetTz, targetOffset);
- return null;
- }
- }
- else
- {
- logger.LogDebug("Момент времени {nextRun} актуален (в будущем). Коррекция не требуется.", nextRunInTargetTz);
- }
-
- return nextRunInTargetTz;
- }
-
-
- ///
- /// Строит текущую нагрузку на дни, учитывая ТОЛЬКО шаблоны внутри периода.
- /// Шаблоны вне периода игнорируются.
- ///
- private Dictionary BuildCurrentLoad(List templates, List workDays, DateOnly periodStart, DateOnly periodEnd, TimeSpan targetOffset)
- {
- var load = workDays.ToDictionary(day => day, _ => 0);
-
- foreach (var template in templates)
- {
- if (!template.NextRun.HasValue)
- continue;
-
- // Конвертируем в целевой пояс для определения даты
- var templateDate = DateOnly.FromDateTime(template.NextRun.Value.ToOffset(targetOffset).Date);
-
- // Учитываем ТОЛЬКО шаблоны внутри периода
- if (templateDate >= periodStart && templateDate <= periodEnd)
- {
- if (load.ContainsKey(templateDate))
- load[templateDate]++;
- }
- }
-
- return load;
- }
-
-
- ///
- /// Заполнить TemplateNextRunResultDto, если нет рабочих дней со смещением несколько лет
- ///
- ///
- ///
- private TemplateNextRunResultDto FillTemplateResultWithOffsetError(TemplateNextRunDto template, DateOnly startDate, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset)
- {
- // Создаём дату в целевом поясе
- var errorDateInTargetTz = new DateTimeOffset(
- startDate.AddYears(offsetYearsIfErrorCalcNextRun).Year,
- startDate.AddYears(offsetYearsIfErrorCalcNextRun).Month,
- startDate.AddYears(offsetYearsIfErrorCalcNextRun).Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
-
- // КОНВЕРТИРУЕМ В UTC ДЛЯ ВОЗВРАТА
- var errorDateUtc = errorDateInTargetTz.ToOffset(TimeSpan.Zero);
-
- logger.LogWarning("Шаблон {Id}: нет рабочих дней в периоде, сдвиг на {years} лет → {nextRun} (UTC)", template.Id, offsetYearsIfErrorCalcNextRun, errorDateUtc);
-
- return new TemplateNextRunResultDto(
- Id: template.Id,
- NextRun: errorDateUtc,
- NextRunOld: template.NextRun
- );
- }
-
-
- ///
- /// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad
- ///
- ///
- ///
- ///
- ///
- ///
- private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary currentDailyLoad, List workDays, DateTimeOffset referenceDateInTargetTz, Dictionary expectedDailyLoad, TimeSpan targetOffset)
- {
- foreach (var day in workDays)
- {
- var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
- var current = currentDailyLoad.GetValueOrDefault(day, 0);
-
- if (current < expected)
- {
- // Создаём момент времени В ЦЕЛЕВОМ ПОЯСЕ
- return new DateTimeOffset(
- day.Year, day.Month, day.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
- }
- }
-
- // Если все дни загружены — берём день с минимальной нагрузкой
- var minDay = currentDailyLoad.MinBy(x => x.Value).Key;
- return new DateTimeOffset(
- minDay.Year, minDay.Month, minDay.Day,
- referenceDateInTargetTz.Hour,
- referenceDateInTargetTz.Minute,
- referenceDateInTargetTz.Second,
- targetOffset);
- }
-
-
- ///
- /// Получить план распределения шаблонов по рабочим дням.
- ///
- /// Общее количество шаблонов для распределения
- /// Список рабочих дней в периоде
- /// Если true — строит зеркальный план (справа налево от конца периода), иначе обычный (слева направо от начала периода)
- /// Список дат, на которые должны быть назначены шаблоны
- private List GetDistributionPlan(int totalWorks, List workDays, bool mirror = false)
- {
- var totalDays = workDays.Count;
- var interval = CalculateDistributionInterval(totalDays, totalWorks);
-
- var plan = new List();
-
- for (int i = 0; i < totalWorks; i++)
- {
- // Базовый индекс для обычного плана (слева → направо)
- var baseIndex = (int)Math.Floor(i * interval);
-
- // Применяем зеркалирование, если нужно
- var index = mirror
- ? totalDays - 1 - baseIndex // Зеркальный индекс: от конца периода
- : baseIndex; // Обычный индекс: от начала периода
-
- // Защита от выхода за границы массива
- index = Math.Max(0, Math.Min(index, totalDays - 1));
-
- plan.Add(workDays[index]);
- }
-
- // Логгирование
- var planType = mirror ? "Зеркальный" : "Обычный";
- logger.LogDebug("--- {planType} план распределения ({totalWorks} шаблонов, {totalDays} раб. дней) ---", planType, totalWorks, totalDays);
-
- foreach (var item in plan.GroupBy(t => t).OrderBy(g => g.Key))
- logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count());
-
- return plan;
- }
-
-
- ///
- /// Рассчитать интервал между шаблонами
- ///
- /// Кол-во рабочих дней
- /// Кол-во шаблонов
- ///
- private double CalculateDistributionInterval(int totalDays, int totalWorks)
- {
- var interval = (double)totalDays / totalWorks;
-
- logger.LogDebug("Интервал распределения: {interval} (дней на шаблон)", interval);
-
- return interval;
- }
-
-
- ///
- /// Получить дату начала распределения с учётом целевого часового пояса.
- /// Метод возвращает календарную дату (DateOnly), рассчитанную в контексте указанного оффсета.
- ///
- /// Стартовая дата (интерпретируется в целевом часовом поясе)
- /// Момент времени в целевом часовом поясе (после применения оффсета)
- /// Целевой часовой пояс (оффсет)
- /// Дата начала распределения
- private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate, TimeSpan offset)
- {
- var dateStart = startDate;
-
- // 1. Дата начала не может быть раньше даты из referenceDate (в целевом поясе)
- var refDateInTargetTz = DateOnly.FromDateTime(referenceDate.Date);
- if (dateStart < refDateInTargetTz)
- {
- logger.LogDebug("Дата начала {startDate} раньше даты из referenceDate в поясе {offset} ({refDate}). Используем {refDate}.",
- dateStart, offset, refDateInTargetTz, refDateInTargetTz);
- dateStart = refDateInTargetTz;
- }
-
- // 2. Дата начала не может быть раньше "сегодня" в целевом часовом поясе
- var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).Date);
- if (dateStart < todayInTargetTz)
- {
- logger.LogDebug("Дата начала {startDate} раньше 'сегодня' в поясе {offset} ({today}). Используем {today}.",
- dateStart, offset, todayInTargetTz, todayInTargetTz);
- dateStart = todayInTargetTz;
- }
-
- // 3. Если момент времени "начало дня в целевом поясе + время из referenceDate" уже прошёл — добавляем 1 день
- // ВАЖНО: время берём из самого referenceDate (который уже в целевом поясе!)
- var dateTimeStart = new DateTimeOffset(
- dateStart.Year, dateStart.Month, dateStart.Day,
- referenceDate.Hour, // ← Время из целевого пояса!
- referenceDate.Minute,
- referenceDate.Second,
- offset); // ← Оффсет целевого пояса
-
- // Сравниваем абсолютные моменты времени (работает корректно даже с разными оффсетами)
- if (dateTimeStart < DateTimeOffset.UtcNow)
- {
- logger.LogDebug("Момент времени {dateTimeStart} (в поясе {offset}) уже прошёл (сейчас {now}). Добавляем 1 день.",
- dateTimeStart, offset, DateTimeOffset.UtcNow);
- dateStart = dateStart.AddDays(1);
- }
-
- logger.LogDebug("Итоговая дата начала распределения в поясе {offset}: {dateStart}", offset, dateStart);
- return dateStart;
- }
-
-
- ///
- /// Получить дату окончания периода распределения.
- /// Продолжительность включает стартовый день (например, duration=7 означает 7 дней: с 1 по 7 июня).
- ///
- ///
- ///
- ///
- private DateOnly GetEndPeriodDate(DateOnly startDate, int duration)
- {
- if (duration <= 0)
- {
- logger.LogWarning("Некорректная продолжительность периода: {duration}. Используем минимальное значение 1.", duration);
- duration = 1;
- }
-
- // duration — количество дней, включая startDate
- // поэтому вычитаем 1, чтобы получить сдвиг
-
- var endDate = startDate.AddDays(duration - 1);
- logger.LogDebug("Расчёт даты окончания периода: startDate={startDate}, duration={duration}, endDate={endDate}", startDate, duration, endDate);
-
- return endDate;
- }
-
-
- ///
- /// Получить список выходных дней
- ///
- ///
- ///
- ///
- public async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate)
- {
- //Возвращает все выходные дни включая сб и вск, и праздники
- var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();
-
- logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count);
- foreach (var weekend in weekends)
- logger.LogDebug("Не рабочий день: {weekend}", weekend);
-
- return weekends.ToHashSet();
- }
-
-
- public async Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends)
- {
- // Получаем список выходных/праздничных дней
- var weekends = new HashSet();
- if (excludeWeekends)
- weekends = await GetWeekendsAsync(start, end);
-
- var result = new List();
- var date = start;
-
- while (date <= end)
- {
- var dayOfWeek = date.DayOfWeek;
- if (!weekends.Contains(date))
- {
- result.Add(date);
- }
- date = date.AddDays(1);
- }
-
- logger.LogDebug("Найдено рабочих дней: {Count}", result.Count);
- foreach (var workDay in result)
- logger.LogDebug("Рабочий день: {workDay}", workDay);
-
- return result;
- }
-
-
- ///
- /// Получает рабочие дни с автоматическим расширением периода при их отсутствии.
- /// Расширяет период до maxPeriodExpansions раз: _startDate + duration * N (N = 1, 2, 3)
- ///
- private async Task<(DateOnly endDate, List workDays)> GetWorkDaysWithExpansionAsync(DateOnly startDate, int duration, bool excludeWeekends)
- {
- // Попытка 0: исходный период [_startDate, _startDate + duration - 1]
- var currentEndDate = startDate.AddDays(duration - 1);
- var workDays = await GetWorkDaysAsync(startDate, currentEndDate, excludeWeekends);
-
- if (workDays.Count > 0)
- {
- logger.LogDebug("Найдено {count} рабочих дней в исходном периоде [{start}, {end}]", workDays.Count, startDate, currentEndDate);
- return (currentEndDate, workDays);
- }
-
- // Попытки расширения 1..maxPeriodExpansions
- for (int attempt = 1; attempt <= maxPeriodExpansions; attempt++)
- {
- //расширяем период относительно ПРЕДЫДУЩЕГО конца
- currentEndDate = currentEndDate.AddDays(duration);
-
- logger.LogWarning("Попытка {attempt}/{maxAttempts} расширения периода: нет рабочих дней в [{start}, {end}]. Расширяем до {newEnd}.", attempt, maxPeriodExpansions, startDate, startDate.AddDays(duration - 1), currentEndDate);
-
- workDays = await GetWorkDaysAsync(startDate, currentEndDate, excludeWeekends);
-
- if (workDays.Count > 0)
- {
- logger.LogInformation("Рабочие дни найдены после {attempt} попытки расширения. Период: [{start}, {end}], кол-во рабочих дней: {count}", attempt, startDate, currentEndDate, workDays.Count);
- return (currentEndDate, workDays);
- }
- }
-
- // Все попытки исчерпаны — рабочих дней нет
- logger.LogError("После {maxAttempts} попыток расширения рабочие дни не найдены в периоде [{start}, {end}]. Возвращаем ошибку.", maxPeriodExpansions, startDate, currentEndDate);
-
- return (currentEndDate, new List()); // Пустой список для обработки ошибки вызывающим кодом
- }
- }
-}
diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs
index aa952cab..eb5c539e 100644
--- a/PARR.DAL/ParrDalInstaller.cs
+++ b/PARR.DAL/ParrDalInstaller.cs
@@ -141,8 +141,7 @@ namespace PARR.DAL
services.AddTransient();
services.AddTransient();
- services.AddTransient();
- services.AddTransient();
+ services.AddTransient();
#endregion
diff --git a/PARR.EsppScheduleSync/ScheduleSyncher.cs b/PARR.EsppScheduleSync/ScheduleSyncher.cs
index d3410738..9477ef58 100644
--- a/PARR.EsppScheduleSync/ScheduleSyncher.cs
+++ b/PARR.EsppScheduleSync/ScheduleSyncher.cs
@@ -226,7 +226,7 @@ namespace PARR.EsppScheduleSync
//nextRunWithTimeZone = nextRunModifierService.GetNextRunByAccountRobotTimeZone(template.NextRun);
- var nextRunService = scope.ServiceProvider.GetRequiredService();
+ var nextRunService = scope.ServiceProvider.GetRequiredService();
//var shortcodeService = scope.ServiceProvider.GetRequiredService();
//responseArea = shortcodeService.ApplyShortcodesAsync(template.Job!.ResponseAreaMask, template).GetAwaiter().GetResult();
@@ -327,7 +327,7 @@ namespace PARR.EsppScheduleSync
private async Task CalcNextRunWithRobotTzAsync(Guid templateId)
{
using var scope = serviceProvider.CreateScope();
- var nextRunService = scope.ServiceProvider.GetRequiredService();
+ var nextRunService = scope.ServiceProvider.GetRequiredService();
var nextRun = await nextRunService.GetNextRunForTemplateAsync(templateId, isNew: false);
diff --git a/PARR.NextRun/NextRunIntervalService.cs b/PARR.NextRun/NextRunIntervalService.cs
index 4cccea50..beaa4b9e 100644
--- a/PARR.NextRun/NextRunIntervalService.cs
+++ b/PARR.NextRun/NextRunIntervalService.cs
@@ -46,10 +46,10 @@ namespace PARR.NextRun
//var esppSchService = scope.ServiceProvider.GetService();
var templateService = scope.ServiceProvider.GetService();
var jobGroupService = scope.ServiceProvider.GetService();
- var nextRunService = scope.ServiceProvider.GetService();
+ var nextRunService = scope.ServiceProvider.GetService();
if (templateService == null || nextRunService == null || jobGroupService == null)
- throw new Exception($"Не смог получить серивс {nameof(ITemplateService)} или {nameof(INextRunServiceV2)} или {nameof(IJobGroupService)}");
+ throw new Exception($"Не смог получить серивс {nameof(ITemplateService)} или {nameof(INextRunService)} или {nameof(IJobGroupService)}");
await HandlerAsync(templateService, nextRunService, jobGroupService);
}
@@ -61,7 +61,7 @@ namespace PARR.NextRun
/// Рассчет следующей даты срабатываения
///
///
- private async Task HandlerAsync(ITemplateService templateService, INextRunServiceV2 nextRunService, IJobGroupService jobGroupService)
+ private async Task HandlerAsync(ITemplateService templateService, INextRunService nextRunService, IJobGroupService jobGroupService)
{
logger.LogInformation("Начинаю обновлять NextRun по расписанию");
@@ -77,7 +77,7 @@ namespace PARR.NextRun
}
- private async Task UpdateNextRunAsync(ITemplateService templateService, INextRunServiceV2 nextRunService, IJobGroupService jobGroupService)
+ private async Task UpdateNextRunAsync(ITemplateService templateService, INextRunService nextRunService, IJobGroupService jobGroupService)
{
// выбираем все шаблоны с просроченным nextRun в статусе Used
diff --git a/PARR.NextRun/NextRunRabbitService.cs b/PARR.NextRun/NextRunRabbitService.cs
index 6a465ee3..df58beb9 100644
--- a/PARR.NextRun/NextRunRabbitService.cs
+++ b/PARR.NextRun/NextRunRabbitService.cs
@@ -100,7 +100,7 @@ namespace PARR.NextRun
using var scope = serviceProvider.CreateScope();
var templateService = scope.ServiceProvider.GetRequiredService();
- var nextRunService = scope.ServiceProvider.GetRequiredService();
+ var nextRunService = scope.ServiceProvider.GetRequiredService();
// Берем шаблоны только в статусе Used
var templates = await templateService.Get()
diff --git a/PARR.TemplateDistributor/TemplateDistributor.cs b/PARR.TemplateDistributor/TemplateDistributor.cs
index a2d1dc37..6a497c29 100644
--- a/PARR.TemplateDistributor/TemplateDistributor.cs
+++ b/PARR.TemplateDistributor/TemplateDistributor.cs
@@ -12,14 +12,14 @@ namespace PARR.TemplateDistributor
{
private readonly ILogger logger;
//private readonly INextRunService nextRunService;
- private readonly INextRunServiceV2 nextRunService;
+ private readonly INextRunService nextRunService;
private readonly ITemplateService templateService;
private readonly IRobotConfigurationService robotConfigurationService;
public TemplateDistributor(
ILogger logger,
//INextRunService nextRunService,
- INextRunServiceV2 nextRunService,
+ INextRunService nextRunService,
ITemplateService templateService,
IRobotConfigurationService robotConfigurationService
)
diff --git a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs
index 2d7fa3d0..fee27c1c 100644
--- a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs
+++ b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs
@@ -20,7 +20,7 @@ namespace PARR.TemplateGeneratorWorker
private readonly IJobService jobService;
private readonly IShortcodesService shortcodesService;
private readonly ITemplateService templateService;
- private readonly INextRunServiceV2 nextRunService;
+ private readonly INextRunService nextRunService;
//private readonly INextRunService nextRunService;
@@ -33,7 +33,7 @@ namespace PARR.TemplateGeneratorWorker
IJobService jobService,
IShortcodesService shortcodesService,
ITemplateService templateService,
- INextRunServiceV2 nextRunService
+ INextRunService nextRunService
//INextRunService nextRunService
//IEsppScheduleTransformService esppScheduleTransformService
)
diff --git a/PARR.TemplateUpdater/Services/TemplateUpdaterService.cs b/PARR.TemplateUpdater/Services/TemplateUpdaterService.cs
index 4f0009b3..bf78fabe 100644
--- a/PARR.TemplateUpdater/Services/TemplateUpdaterService.cs
+++ b/PARR.TemplateUpdater/Services/TemplateUpdaterService.cs
@@ -19,7 +19,7 @@ namespace PARR.TemplateUpdater.Services
private readonly IJobService jobService;
private readonly IUnitService unitService;
private readonly IRobotConfigurationService robotConfigurationService;
- private readonly INextRunServiceV2 nextRunService;
+ private readonly INextRunService nextRunService;
public TemplateUpdaterService(
ILogger logger,
@@ -27,7 +27,7 @@ namespace PARR.TemplateUpdater.Services
IJobService jobService,
IUnitService unitService,
IRobotConfigurationService robotConfigurationService,
- INextRunServiceV2 nextRunService
+ INextRunService nextRunService
)
{
this.logger = logger;
diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs
index e01a2850..29f25322 100644
--- a/PARR.Test/NextRun/NextRunTest.cs
+++ b/PARR.Test/NextRun/NextRunTest.cs
@@ -14,7 +14,7 @@ namespace PARR.Test.NextRun
private readonly ITemplateService templateService;
private readonly IShortcodesService shortcodesService;
//private readonly ITemplateDistributorV2 templateDistributorV2;
- private readonly INextRunServiceV2 nextRunServiceV2;
+ private readonly INextRunService nextRunServiceV2;
public NextRunTest(
/*ITemplateDistributor templateDistributor, */
@@ -22,7 +22,7 @@ namespace PARR.Test.NextRun
ITemplateService templateService,
IShortcodesService shortcodesService,
//ITemplateDistributorV2 templateDistributorV2,
- INextRunServiceV2 nextRunServiceV2
+ INextRunService nextRunServiceV2
)
{
//this.templateDistributor = templateDistributor;
diff --git a/PARR.Test/Worker.cs b/PARR.Test/Worker.cs
index 7ed9581a..b10ef5c2 100644
--- a/PARR.Test/Worker.cs
+++ b/PARR.Test/Worker.cs
@@ -15,21 +15,21 @@ namespace PARR.Test
private readonly ILogger _logger;
private readonly IEsppApiService esppApiService;
private readonly IIntervalService intervalService;
- private readonly ICalendarService calendarService;
+ //private readonly ICalendarService calendarService;
private readonly IServiceProvider serviceProvider;
public Worker(
ILogger logger,
IEsppApiService esppApiService,
IIntervalService intervalService,
- ICalendarService calendarService,
+ //ICalendarService calendarService,
IServiceProvider serviceProvider
)
{
_logger = logger;
this.esppApiService = esppApiService;
this.intervalService = intervalService;
- this.calendarService = calendarService;
+ //this.calendarService = calendarService;
this.serviceProvider = serviceProvider;
}