From 897dd4c4e33f148991c3daf1a0f84bb51a5dcc8d Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Mon, 9 Feb 2026 15:48:30 +1000 Subject: [PATCH 01/14] =?UTF-8?q?feat(api,dal):=20=D0=B2=20JobGroup=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81?= =?UTF-8?q?=D0=B2=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=BE=20UserTimeZoneOffset?= =?UTF-8?q?Minutes,=20=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5?= =?UTF-8?q?=D1=80.=20EsppScheduleTransformService=20-=20=D0=B4=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D1=81=20=D1=83=D1=87?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=BC=20offset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contracts/V1/Requests/JobGroupRequest.cs | 2 + .../V1/Responses/JobGroupResponse.cs | 2 + PARR.API/Controllers/V1/JobGroupController.cs | 5 +- PARR.API/Validators/JobGroupValidator.cs | 9 + ...upAddUserTimeZoneOffsetMinutes.Designer.cs | 4148 +++++++++++++++++ ...tblJobGroupAddUserTimeZoneOffsetMinutes.cs | 31 + .../Migrations/DataContextModelSnapshot.cs | 4 + PARR.DAL/Models/Job/JobGroup.cs | 15 + PARR.DAL/NextRunServices/INextRunService.cs | 14 +- PARR.DAL/NextRunServices/NextRunService.cs | 18 +- .../EsppScheduleTransformService.cs | 232 +- .../IEsppScheduleTransformService.cs | 3 +- 12 files changed, 4309 insertions(+), 174 deletions(-) create mode 100644 PARR.DAL/Migrations/20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes.Designer.cs create mode 100644 PARR.DAL/Migrations/20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes.cs diff --git a/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs b/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs index 9380f968..8f6b06ba 100644 --- a/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs +++ b/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs @@ -23,6 +23,8 @@ public DateTimeOffset ReferenceDate { get; set; } + public int? UserTimeZoneOffsetMinutes { get; set; } + /// /// Использовать таймзону рабочей группы ответственного за ЭК шаблона /// diff --git a/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs b/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs index 0935433e..50a3dd4c 100644 --- a/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs +++ b/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs @@ -18,6 +18,8 @@ public DateTimeOffset ReferenceDate { get; set; } + public int? UserTimeZoneOffsetMinutes { get; set; } + public bool IsWorkGroupTimezone { get; set; } public required JobGroupTypeResponse GroupType { get; set; } diff --git a/PARR.API/Controllers/V1/JobGroupController.cs b/PARR.API/Controllers/V1/JobGroupController.cs index a9608703..532fd288 100644 --- a/PARR.API/Controllers/V1/JobGroupController.cs +++ b/PARR.API/Controllers/V1/JobGroupController.cs @@ -39,7 +39,6 @@ namespace PARR.API.Controllers.V1 private readonly IEsppSchTypeConfigService esppConfigService; private readonly IValidator validator; private readonly IJobGroupTypeService jobGroupTypeService; - private readonly SettingsFromDb settingsFromDb; private readonly IMatchingStatusService matchingStatusService; private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; @@ -52,7 +51,6 @@ namespace PARR.API.Controllers.V1 IEsppSchTypeConfigService esppConfigService, IValidator validator, IJobGroupTypeService jobGroupTypeService, - SettingsFromDb settingsFromDb, IMatchingStatusService matchingStatusService, IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService ) @@ -65,7 +63,6 @@ namespace PARR.API.Controllers.V1 this.esppConfigService = esppConfigService; this.validator = validator; this.jobGroupTypeService = jobGroupTypeService; - this.settingsFromDb = settingsFromDb; this.matchingStatusService = matchingStatusService; this.scheduleResponseAreaTimeOffsetService = scheduleResponseAreaTimeOffsetService; } @@ -168,6 +165,7 @@ namespace PARR.API.Controllers.V1 Solution = request.Solution.Trim(), TemplateDuration = request.TemplateDuration.Trim(), ReferenceDate = request.ReferenceDate, + UserTimeZoneOffsetMinutes = request.UserTimeZoneOffsetMinutes, ScheduleExcludeTypeId = request.ScheduleExcludeTypeId, ScheduleExcludeTypeCalendarId = request.ScheduleExcludeTypeCalendarId, IsAutoDistributionEnabled = request.IsAutoDistributionEnabled, @@ -262,6 +260,7 @@ namespace PARR.API.Controllers.V1 orig.Solution = request.Solution.Trim(); orig.TemplateDuration = request.TemplateDuration.Trim(); orig.ReferenceDate = request.ReferenceDate; + orig.UserTimeZoneOffsetMinutes = request.UserTimeZoneOffsetMinutes; orig.ScheduleExcludeTypeId = request.ScheduleExcludeTypeId; orig.ScheduleExcludeTypeCalendarId = request.ScheduleExcludeTypeCalendarId; orig.IsAutoDistributionEnabled = request.IsAutoDistributionEnabled; diff --git a/PARR.API/Validators/JobGroupValidator.cs b/PARR.API/Validators/JobGroupValidator.cs index 1823937c..7e5afb1d 100644 --- a/PARR.API/Validators/JobGroupValidator.cs +++ b/PARR.API/Validators/JobGroupValidator.cs @@ -1,5 +1,6 @@ using FluentValidation; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; using PARR.API.Contracts.V1.Requests; using PARR.DAL.Contracts; using PARR.DAL.Services.Interfaces; @@ -123,6 +124,14 @@ namespace PARR.API.Validators return true; }).WithMessage("Некорректное значение периода распределения"); + RuleFor(t => t.UserTimeZoneOffsetMinutes) + .Must((entity, value, c) => + { + // если IsResponseAreaTimezone == true, то поле UserTimeZoneOffsetMinutes обязательно. + // если IsResponseAreaTimezone == false, то UserTimeZoneOffsetMinutes должно быть null + return (entity.IsWorkGroupTimezone && value != null) || (!entity.IsWorkGroupTimezone && value == null); + }).WithMessage("Некорректное значение."); ; + } } } diff --git a/PARR.DAL/Migrations/20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes.Designer.cs b/PARR.DAL/Migrations/20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes.Designer.cs new file mode 100644 index 00000000..9bc07bf9 --- /dev/null +++ b/PARR.DAL/Migrations/20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes.Designer.cs @@ -0,0 +1,4148 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PARR.DAL.Context; + +#nullable disable + +namespace PARR.DAL.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes")] + partial class tblJobGroupAddUserTimeZoneOffsetMinutes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PARR.DAL.Models.AgentHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("HistoryLevelId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("HistoryLevelId"); + + b.HasIndex("OrderId"); + + b.HasIndex("TemplateId"); + + b.ToTable("AgentHistories"); + }); + + modelBuilder.Entity("PARR.DAL.Models.AgentHistoryLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AgentHistoryLevels"); + + b.HasData( + new + { + Id = 1, + Description = "Агент начал выполнять задание", + Name = "Start" + }, + new + { + Id = 5, + Description = "Агент завершил выполнение задания", + Name = "End" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.AppInWorkInWorkGroup", b => + { + b.Property("ApplicationsInWorkId") + .HasColumnType("uuid"); + + b.Property("WorkGroupId") + .HasColumnType("uuid"); + + b.HasKey("ApplicationsInWorkId", "WorkGroupId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("AppInWorkInWorkGroups"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicationTypeId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationTypeId"); + + b.HasIndex("Name", "ApplicationTypeId") + .IsUnique(); + + b.ToTable("Applications"); + }); + + modelBuilder.Entity("PARR.DAL.Models.ApplicationInHost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("HostId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("HostId"); + + b.ToTable("ApplicationsInHost"); + }); + + modelBuilder.Entity("PARR.DAL.Models.ApplicationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ApplicationTypes"); + + b.HasData( + new + { + Id = new Guid("32c28386-6f13-4f7b-8508-be165b7fabdb"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Сервер приложений", + Name = "APP" + }, + new + { + Id = new Guid("7848a96c-cdee-48c1-a786-de9cb889723a"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "ОС", + Name = "OS" + }, + new + { + Id = new Guid("aae2636f-b93a-42dc-873e-0764a90a0a40"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "СУБД", + Name = "DB" + }, + new + { + Id = new Guid("749f4c34-b883-4f28-90dd-c161dd3c4270"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "ЦК БС", + Name = "CKBS" + }, + new + { + Id = new Guid("0bd96f9b-d36b-4dfb-bd8a-9c356bc6912b"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "ИБ", + Name = "IB" + }, + new + { + Id = new Guid("4466148b-510d-4423-a58f-ce878152ff01"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Инфраструктура", + Name = "SI" + }, + new + { + Id = new Guid("725a96db-358e-489b-a7c9-a84b06ec15df"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Мониторинг", + Name = "SM" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.ApplicationsInWork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentName") + .HasColumnType("text"); + + b.Property("AgentScript") + .HasColumnType("text"); + + b.Property("AgentTimeOutSec") + .HasColumnType("integer"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("FullDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAgent") + .HasColumnType("boolean"); + + b.Property("IsAutoDistributionEnabled") + .HasColumnType("boolean"); + + b.Property("ReferenceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Solution") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateDuration") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("WorkId", "ApplicationId") + .IsUnique(); + + b.ToTable("ApplicationsInWorks"); + }); + + modelBuilder.Entity("PARR.DAL.Models.DistributionPeriod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Duration") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Type"); + + b.ToTable("DistributionPeriods", "schedule"); + + b.HasData( + new + { + Id = new Guid("9bd0bfa8-c316-480b-a001-e09ffe8c9340"), + Duration = "7", + Name = "Еженедельно", + Type = "Day" + }, + new + { + Id = new Guid("3a7341a8-085c-461c-babc-6b78dd5d2c67"), + Duration = "30", + Name = "Ежемесячно", + Type = "Day" + }, + new + { + Id = new Guid("0ad2dfdb-3833-46d9-979a-c408b7eadc6c"), + Duration = "90", + Name = "Каждые 90 дней", + Type = "Day" + }, + new + { + Id = new Guid("31dd3947-8471-4f09-852c-a4f075f1ba7f"), + Duration = "182", + Name = "Каждые пол года", + Type = "Day" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.DistributionPeriodType", b => + { + b.Property("Type") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Type"); + + b.ToTable("DistributionPeriodTypes", "schedule"); + + b.HasData( + new + { + Type = "Day", + Description = "День" + }, + new + { + Type = "Month", + Description = "Месяц" + }, + new + { + Type = "Year", + Description = "Год" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.EkStatus", b => + { + b.Property("Code") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Code")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Code"); + + b.ToTable("EkStatuses"); + + b.HasData( + new + { + Code = 1, + Name = "1-Новый" + }, + new + { + Code = 2, + Name = "2-Подготовка к эксплуатации" + }, + new + { + Code = 3, + Name = "3-В эксплуатации" + }, + new + { + Code = 4, + Name = "4-В ремонте" + }, + new + { + Code = 5, + Name = "5-В резерве" + }, + new + { + Code = 6, + Name = "6-Выведен из эксплуатации" + }, + new + { + Code = 7, + Name = "7-Тестовый" + }, + new + { + Code = 9, + Name = "9-В разработке" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EsppSchTypes", "schedule"); + + b.HasData( + new + { + Id = 1, + Description = "Через интервал", + Name = "Interval" + }, + new + { + Id = 2, + Description = "День недели: пнд, вт...", + Name = "DayOfWeek" + }, + new + { + Id = 3, + Description = "Число месяца: 1,2,3,4", + Name = "DayOfMonth" + }, + new + { + Id = 4, + Description = "Каждый: первый, второй, третий, четвертый, последний", + Name = "Order" + }, + new + { + Id = 5, + Description = "Месяц: январь, февраль...", + Name = "Month" + }, + new + { + Id = 6, + Description = "Месяц (родительный падеж): января, февраля...", + Name = "MonthGenitive" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchTypeConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("TypeId") + .HasColumnType("integer"); + + b.Property("TypeScheduleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.HasIndex("TypeScheduleId", "TypeId") + .IsUnique(); + + b.ToTable("EsppSchTypeConfigs", "schedule"); + + b.HasData( + new + { + Id = new Guid("d2693562-b3eb-41e9-97db-c6b5d2ef1bea"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 0, + TypeId = 1, + TypeScheduleId = 1 + }, + new + { + Id = new Guid("06b2e7aa-6927-4fbf-99fb-67c92a4fce8a"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 0, + TypeId = 2, + TypeScheduleId = 2 + }, + new + { + Id = new Guid("78079071-ff80-417e-9bb0-d928cec853e7"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 0, + TypeId = 3, + TypeScheduleId = 3 + }, + new + { + Id = new Guid("accb1d46-e1c1-4396-9182-4c0766155921"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 0, + TypeId = 4, + TypeScheduleId = 4 + }, + new + { + Id = new Guid("162ffe73-2428-4abe-8f83-235c2593664c"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 1, + TypeId = 2, + TypeScheduleId = 4 + }, + new + { + Id = new Guid("8470d1ef-165d-42fc-ab05-8203e4d263cf"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 0, + TypeId = 5, + TypeScheduleId = 5 + }, + new + { + Id = new Guid("006900b5-f65c-413e-bc67-09e6dae60b7c"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 1, + TypeId = 3, + TypeScheduleId = 5 + }, + new + { + Id = new Guid("85635bc8-f17a-46b8-9c15-8615761be3d9"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 0, + TypeId = 4, + TypeScheduleId = 6 + }, + new + { + Id = new Guid("439460c2-deca-4b54-80e7-2f20806696ec"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 1, + TypeId = 2, + TypeScheduleId = 6 + }, + new + { + Id = new Guid("5c63ad8c-e9b4-43fd-9d24-5c2eed80dadd"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Order = 2, + TypeId = 6, + TypeScheduleId = 6 + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchTypeSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EsppSchTypeSchedules", "schedule"); + + b.HasData( + new + { + Id = 1, + Description = "Регулярно", + Name = "Regularly" + }, + new + { + Id = 2, + Description = "Еженедельно", + Name = "Weekly" + }, + new + { + Id = 3, + Description = "Ежемесячно", + Name = "Monthly" + }, + new + { + Id = 4, + Description = "Ежемесячно-2", + Name = "Monthly2" + }, + new + { + Id = 5, + Description = "Ежегодно", + Name = "Annually" + }, + new + { + Id = 6, + Description = "Ежегодно-2", + Name = "Annually2" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchTypeValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("EsppExportValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("TypeId") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.ToTable("EsppSchTypeValues", "schedule"); + + b.HasData( + new + { + Id = new Guid("6e3b5dd2-b2f7-40bc-bace-15bfa6bbd4ff"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "01:00:00", + Order = 0, + TypeId = 1, + Value = "Каждый час" + }, + new + { + Id = new Guid("4df04834-f14f-43d9-8984-334f080f4107"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "02:00:00", + Order = 1, + TypeId = 1, + Value = "Каждые 2 часа" + }, + new + { + Id = new Guid("dab6e3f9-2385-4966-b77c-133ad233c592"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "03:00:00", + Order = 2, + TypeId = 1, + Value = "Каждые 3 часа" + }, + new + { + Id = new Guid("035f485d-8451-47df-bfaa-ba4dd36cf146"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "04:00:00", + Order = 3, + TypeId = 1, + Value = "Каждые 4 часа" + }, + new + { + Id = new Guid("d627daa7-3eed-4571-acae-ffbb3c5bedb9"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "06:00:00", + Order = 4, + TypeId = 1, + Value = "Каждые 6 часов" + }, + new + { + Id = new Guid("ae0a0015-0f1e-4496-b71e-6244f7e321e7"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "12:00:00", + Order = 5, + TypeId = 1, + Value = "Каждые 12 часов" + }, + new + { + Id = new Guid("8df097aa-9860-4a62-9675-a8eecd3f2ce7"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "1 00:00:00", + Order = 6, + TypeId = 1, + Value = "Ежедневно" + }, + new + { + Id = new Guid("dee32e60-c1a1-4206-98bc-aa03deabfdf6"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "3 00:00:00", + Order = 7, + TypeId = 1, + Value = "Каждые 72 часа" + }, + new + { + Id = new Guid("e0465fbf-e3a5-4486-9f53-5bb85c6feeca"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "4 00:00:00", + Order = 8, + TypeId = 1, + Value = "Каждые 96 часов" + }, + new + { + Id = new Guid("4e3f73d1-a0f3-4dd1-bca9-68ada0dd5ce2"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "14 00:00:00", + Order = 9, + TypeId = 1, + Value = "Каждые 2 недели" + }, + new + { + Id = new Guid("55833417-6a24-42f6-bcc2-5d032f883202"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "60 00:00:00", + Order = 10, + TypeId = 1, + Value = "Каждые 60 дней" + }, + new + { + Id = new Guid("7fefa052-29db-4ce5-9c57-2fd9ff855f21"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "80 00:00:00", + Order = 11, + TypeId = 1, + Value = "Каждые 80 дней" + }, + new + { + Id = new Guid("b0ca99f9-5ee0-45b3-bb80-948f82b1fcb1"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "90 00:00:00", + Order = 12, + TypeId = 1, + Value = "Каждые 90 дней" + }, + new + { + Id = new Guid("086f7a37-9848-423a-a3cc-36dbb5ad43e3"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "182 00:00:00", + Order = 13, + TypeId = 1, + Value = "Каждые полгода" + }, + new + { + Id = new Guid("f2da9e02-d619-4517-a598-374880a8c8e8"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "540 00:00:00", + Order = 14, + TypeId = 1, + Value = "Каждые 1,5 года" + }, + new + { + Id = new Guid("f38d7d30-8923-4ea0-aa7c-5b251e19ab61"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "1095 00:00:00", + Order = 15, + TypeId = 1, + Value = "Каждые 3 года" + }, + new + { + Id = new Guid("b7303461-9a30-43fa-8e55-305aa13f186f"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "1", + Order = 0, + TypeId = 2, + Value = "Понедельник" + }, + new + { + Id = new Guid("e951135e-71f2-4262-9342-df4d5315ab6c"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "2", + Order = 1, + TypeId = 2, + Value = "Вторник" + }, + new + { + Id = new Guid("c34d5375-70e7-4632-b3af-30e279a0621a"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "3", + Order = 2, + TypeId = 2, + Value = "Среда" + }, + new + { + Id = new Guid("ec9540df-c2de-4bda-a063-34d02d4b6e03"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "4", + Order = 3, + TypeId = 2, + Value = "Четверг" + }, + new + { + Id = new Guid("2ed85534-4684-4eb2-9ccb-e264d212c945"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "5", + Order = 4, + TypeId = 2, + Value = "Пятница" + }, + new + { + Id = new Guid("a6804315-96ef-484e-82ee-c5795694468b"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "6", + Order = 5, + TypeId = 2, + Value = "Суббота" + }, + new + { + Id = new Guid("eb71e699-937f-4894-9688-f7a7f95e5e58"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "7", + Order = 6, + TypeId = 2, + Value = "Воскресенье" + }, + new + { + Id = new Guid("e86adbfb-3345-4e0c-aa3b-b4418f9cfe88"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "1", + Order = 0, + TypeId = 3, + Value = "1" + }, + new + { + Id = new Guid("5b4d7ca6-31e7-475b-b7c8-13c6680c3dc3"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "2", + Order = 1, + TypeId = 3, + Value = "2" + }, + new + { + Id = new Guid("0da03db0-9404-425d-bea1-5da0c9b60b59"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "3", + Order = 2, + TypeId = 3, + Value = "3" + }, + new + { + Id = new Guid("199474c8-0a77-47e5-aab8-ba33c216cc9e"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "4", + Order = 3, + TypeId = 3, + Value = "4" + }, + new + { + Id = new Guid("e6619da1-f7e8-45b9-bbd3-9c17bec2ccd3"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "5", + Order = 4, + TypeId = 3, + Value = "5" + }, + new + { + Id = new Guid("a8708763-c838-49a5-95fe-bf4f73518d71"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "6", + Order = 5, + TypeId = 3, + Value = "6" + }, + new + { + Id = new Guid("7ec056aa-820c-4900-96cb-4564d2ea6398"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "7", + Order = 6, + TypeId = 3, + Value = "7" + }, + new + { + Id = new Guid("554d7d0c-91b1-4b81-b94d-1ee864192962"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "8", + Order = 7, + TypeId = 3, + Value = "8" + }, + new + { + Id = new Guid("8b7847a4-23de-4199-8aad-3b5c0323b5a2"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "9", + Order = 8, + TypeId = 3, + Value = "9" + }, + new + { + Id = new Guid("fb7f41ca-950b-4dc1-a0a6-bd9a221ab2ad"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "10", + Order = 9, + TypeId = 3, + Value = "10" + }, + new + { + Id = new Guid("a28b530f-ea38-4a2c-a16b-0b7fa3770d3c"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "11", + Order = 10, + TypeId = 3, + Value = "11" + }, + new + { + Id = new Guid("7a004a11-32e1-40ae-ab35-0d3dc3a69785"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "12", + Order = 11, + TypeId = 3, + Value = "12" + }, + new + { + Id = new Guid("e0f2f4d1-4a5e-4997-8336-83c3c35f38df"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "13", + Order = 12, + TypeId = 3, + Value = "13" + }, + new + { + Id = new Guid("9c5aaf48-88ec-4c67-b936-2be45550f8bf"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "14", + Order = 13, + TypeId = 3, + Value = "14" + }, + new + { + Id = new Guid("8dbd680c-ce49-4d2e-8a1a-05619685e240"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "15", + Order = 14, + TypeId = 3, + Value = "15" + }, + new + { + Id = new Guid("2f476f70-b1ef-419d-aad5-3911f1e3231f"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "16", + Order = 15, + TypeId = 3, + Value = "16" + }, + new + { + Id = new Guid("2c635ce2-fc18-47be-b3e9-98c211abf312"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "17", + Order = 16, + TypeId = 3, + Value = "17" + }, + new + { + Id = new Guid("f9d5b03b-624a-4f9f-8da3-1490ea5d2914"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "18", + Order = 17, + TypeId = 3, + Value = "18" + }, + new + { + Id = new Guid("4f4b249c-db60-4582-86ed-75cfee4edd0c"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "19", + Order = 18, + TypeId = 3, + Value = "19" + }, + new + { + Id = new Guid("220a1464-4d39-416c-b4b2-3093eb007298"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "20", + Order = 19, + TypeId = 3, + Value = "20" + }, + new + { + Id = new Guid("c887ca60-b05d-4d3c-ad1f-223c879b1a6d"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "21", + Order = 20, + TypeId = 3, + Value = "21" + }, + new + { + Id = new Guid("2905765d-63fb-41fe-a111-ee2f1d03d62e"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "22", + Order = 21, + TypeId = 3, + Value = "22" + }, + new + { + Id = new Guid("dbbe704a-8d6d-40e7-bd30-6974035b1fab"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "23", + Order = 22, + TypeId = 3, + Value = "23" + }, + new + { + Id = new Guid("cf7b9645-52ed-43a2-8267-267e0aed19b1"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "24", + Order = 23, + TypeId = 3, + Value = "24" + }, + new + { + Id = new Guid("454788d6-d0ad-4c07-878c-da3fd3cd05e1"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "25", + Order = 24, + TypeId = 3, + Value = "25" + }, + new + { + Id = new Guid("40d70d73-8582-4be6-a9e6-503b96ae3d49"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "26", + Order = 25, + TypeId = 3, + Value = "26" + }, + new + { + Id = new Guid("8f6610dc-ef46-4842-a57e-18d7aaa75931"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "27", + Order = 26, + TypeId = 3, + Value = "27" + }, + new + { + Id = new Guid("03858472-d4ce-47fd-9497-8051728766e4"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "28", + Order = 27, + TypeId = 3, + Value = "28" + }, + new + { + Id = new Guid("b1018e07-e90e-42ee-b399-19f3d0c14dd6"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "29", + Order = 28, + TypeId = 3, + Value = "29" + }, + new + { + Id = new Guid("a1669207-aa31-40fe-98d0-d941008d1655"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "30", + Order = 29, + TypeId = 3, + Value = "30" + }, + new + { + Id = new Guid("0936aa99-73e0-43de-b6df-334c1228a226"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "31", + Order = 30, + TypeId = 3, + Value = "31" + }, + new + { + Id = new Guid("626ca076-f09e-4a7a-8610-b7de6337b24a"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "1", + Order = 0, + TypeId = 4, + Value = "Первый" + }, + new + { + Id = new Guid("032fbd02-67d0-4b8c-bed0-42629ab7113b"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "2", + Order = 1, + TypeId = 4, + Value = "Второй" + }, + new + { + Id = new Guid("fa8a8f60-e41e-495e-ac86-35174d9976bf"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "3", + Order = 2, + TypeId = 4, + Value = "Третий" + }, + new + { + Id = new Guid("d2f90168-2b49-42f2-820b-79db2c522adf"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "4", + Order = 3, + TypeId = 4, + Value = "Четвертый" + }, + new + { + Id = new Guid("12bb8db2-cf7f-4113-baff-6883770f1ba5"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "5", + Order = 4, + TypeId = 4, + Value = "Последний" + }, + new + { + Id = new Guid("18260e91-3fbe-4401-9edc-78dbc419370e"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "1", + Order = 0, + TypeId = 5, + Value = "Январь" + }, + new + { + Id = new Guid("361526aa-3e1c-452e-bb44-0ade8521830d"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "2", + Order = 1, + TypeId = 5, + Value = "Февраль" + }, + new + { + Id = new Guid("fad4bdb0-6c51-4810-a30b-6d031a0d4c46"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "3", + Order = 2, + TypeId = 5, + Value = "Март" + }, + new + { + Id = new Guid("06ab835d-3d3e-4057-8422-a553b6b40995"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "4", + Order = 3, + TypeId = 5, + Value = "Апрель" + }, + new + { + Id = new Guid("83f56b5d-b16b-4aa5-89d7-a253fee4e4ad"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "5", + Order = 4, + TypeId = 5, + Value = "Май" + }, + new + { + Id = new Guid("e679ec84-a9ee-4bed-a051-2b14edddcb18"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "6", + Order = 5, + TypeId = 5, + Value = "Июнь" + }, + new + { + Id = new Guid("d439808a-6a10-409b-8809-a755b7cca60b"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "7", + Order = 6, + TypeId = 5, + Value = "Июль" + }, + new + { + Id = new Guid("6e9972c4-6bf7-4fe4-b4ea-bbb333fb69c8"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "8", + Order = 7, + TypeId = 5, + Value = "Август" + }, + new + { + Id = new Guid("0883aac2-9d5b-4098-b672-c684d2a3a0c9"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "9", + Order = 8, + TypeId = 5, + Value = "Сентябрь" + }, + new + { + Id = new Guid("5e6592bc-acb2-45a4-979b-fdecf8457453"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "10", + Order = 9, + TypeId = 5, + Value = "Октябрь" + }, + new + { + Id = new Guid("edd7cb49-d6a0-4969-b2d6-1967da69b336"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "11", + Order = 10, + TypeId = 5, + Value = "Ноябрь" + }, + new + { + Id = new Guid("dd695fb2-4ad0-4cda-a4df-e15059c56b24"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "12", + Order = 11, + TypeId = 5, + Value = "Декабрь" + }, + new + { + Id = new Guid("2f473624-4eb3-49b4-a4b9-cc7fc08a50f5"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "1", + Order = 0, + TypeId = 6, + Value = "Января" + }, + new + { + Id = new Guid("f0003490-a249-44bd-814f-4566999915d8"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "2", + Order = 1, + TypeId = 6, + Value = "Февраля" + }, + new + { + Id = new Guid("f4c3fe38-aeaa-42f7-a9ba-190ea2dfe339"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "3", + Order = 2, + TypeId = 6, + Value = "Марта" + }, + new + { + Id = new Guid("06a8bd2f-68e1-42f5-961c-ac3b98a2181e"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "4", + Order = 3, + TypeId = 6, + Value = "Апреля" + }, + new + { + Id = new Guid("963b9c41-d607-444a-9fe0-fe429590e326"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "5", + Order = 4, + TypeId = 6, + Value = "Мая" + }, + new + { + Id = new Guid("ec8554c8-dee4-49a1-b1b7-474ad8ec1473"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "6", + Order = 5, + TypeId = 6, + Value = "Июня" + }, + new + { + Id = new Guid("e15c7c63-e29f-41a3-9c82-f70eb6c24fde"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "7", + Order = 6, + TypeId = 6, + Value = "Июля" + }, + new + { + Id = new Guid("0b1f3cea-301a-4d74-9ce1-2cd93b41897d"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "8", + Order = 7, + TypeId = 6, + Value = "Августа" + }, + new + { + Id = new Guid("e2fa3769-f66f-4753-a161-35adad7717a5"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "9", + Order = 8, + TypeId = 6, + Value = "Сентября" + }, + new + { + Id = new Guid("c1de4aca-9f49-48ae-9279-cf9ec4092112"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "10", + Order = 9, + TypeId = 6, + Value = "Октября" + }, + new + { + Id = new Guid("a74b9ebd-bcd8-4537-b371-194de2fe0a4d"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "11", + Order = 10, + TypeId = 6, + Value = "Ноября" + }, + new + { + Id = new Guid("852a5ebd-4545-494b-bc69-c27409a41adc"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppExportValue = "12", + Order = 11, + TypeId = 6, + Value = "Декабря" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchValue", b => + { + b.Property("JobGroupId") + .HasColumnType("uuid"); + + b.Property("TypeValueId") + .HasColumnType("uuid"); + + b.Property("TypeConfigId") + .HasColumnType("uuid"); + + b.HasKey("JobGroupId", "TypeValueId", "TypeConfigId"); + + b.HasIndex("TypeConfigId"); + + b.HasIndex("TypeValueId"); + + b.ToTable("EsppSchValues", "schedule"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Host", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Ek") + .IsRequired() + .HasColumnType("text"); + + b.Property("EkStatusCode") + .HasColumnType("integer"); + + b.Property("IP") + .HasColumnType("text"); + + b.Property("LastLogon") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseAreaCode") + .HasColumnType("integer"); + + b.Property("WorkGroupId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Ek") + .IsUnique(); + + b.HasIndex("EkStatusCode"); + + b.HasIndex("IP"); + + b.HasIndex("ResponseAreaCode"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Hosts"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IsParentRelationships") + .HasColumnType("boolean"); + + b.Property("MaxValueRelationships") + .HasColumnType("integer"); + + b.Property("MinValueRelationships") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResponseAreaMask") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateNameMask") + .IsRequired() + .HasColumnType("text"); + + b.Property("TnkId") + .HasColumnType("uuid"); + + b.Property("WorkGroupMask") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("TnkId"); + + b.ToTable("Jobs", "job", t => + { + t.HasComment("Таблица видов работ"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobAutoControl", b => + { + b.Property("JobId") + .HasColumnType("uuid"); + + b.Property("InitUsedScheduleState") + .HasColumnType("boolean"); + + b.Property("InitUsedTemplateState") + .HasColumnType("boolean"); + + b.Property("IsEnable") + .HasColumnType("boolean"); + + b.HasKey("JobId"); + + b.ToTable("AutoControls", "job", t => + { + t.HasComment("Таблица управления автоконтролем для работ"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobFieldFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("FieldId") + .HasColumnType("uuid"); + + b.Property("IsInverse") + .HasColumnType("boolean"); + + b.Property("UnitFilterId") + .HasColumnType("uuid"); + + b.Property("ValueMask") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FieldId"); + + b.HasIndex("UnitFilterId"); + + b.ToTable("FieldFilters", "job", t => + { + t.HasComment("Таблица описания критериев выборки аттрибутов ЭК"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentName") + .HasColumnType("text"); + + b.Property("AgentScript") + .HasColumnType("text"); + + b.Property("AgentTimeOutSec") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("FullDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GroupTypeId") + .HasColumnType("uuid"); + + b.Property("GroupingUnitFieldId") + .HasColumnType("uuid"); + + b.Property("IsAgent") + .HasColumnType("boolean"); + + b.Property("IsAutoDistributionEnabled") + .HasColumnType("boolean"); + + b.Property("IsResponseAreaTimezone") + .HasColumnType("boolean"); + + b.Property("ReferenceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ScheduleExcludeTypeCalendarId") + .HasColumnType("uuid"); + + b.Property("ScheduleExcludeTypeId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Solution") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateDuration") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserTimeZoneOffsetMinutes") + .HasColumnType("integer") + .HasComment("Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate"); + + b.HasKey("Id"); + + b.HasIndex("GroupTypeId"); + + b.HasIndex("GroupingUnitFieldId"); + + b.HasIndex("ScheduleExcludeTypeCalendarId"); + + b.HasIndex("ScheduleExcludeTypeId"); + + b.ToTable("Groups", "job", t => + { + t.HasComment("Таблица описания групп работ, для реализации зонтиков"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobGroupDistributionConfig", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("DistributionPeriodId") + .HasColumnType("uuid"); + + b.Property("IsExcludeWeekends") + .HasColumnType("boolean"); + + b.Property("IsGroupingByWorkGroup") + .HasColumnType("boolean"); + + b.HasKey("GroupId"); + + b.HasIndex("DistributionPeriodId"); + + b.ToTable("GroupDistributionConfigs", "job", t => + { + t.HasComment("Настройки автораспределения для группы работ"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobGroupType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("GroupTypes", "job", t => + { + t.HasComment("Таблица типов групп работ"); + }); + + b.HasData( + new + { + Id = new Guid("4fa62e79-86bb-47c2-be1a-72a716a170fa"), + Code = 0, + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Обычный", + Name = "Simple" + }, + new + { + Id = new Guid("6ec58b1a-5c40-47b9-b036-4fa490e8d503"), + Code = 1, + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Зонтик", + Name = "Umbrella" + }, + new + { + Id = new Guid("318625e5-f833-436a-99df-2a382a1e71c7"), + Code = 2, + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Сгруппированный", + Name = "Group" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobRelationshipFilter", b => + { + b.Property("UnitFilterId") + .HasColumnType("uuid"); + + b.Property("FieldId") + .HasColumnType("uuid"); + + b.Property("IsFullMatch") + .HasColumnType("boolean"); + + b.Property("IsInverse") + .HasColumnType("boolean"); + + b.Property("IsParent") + .HasColumnType("boolean"); + + b.Property("ValueMask") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UnitFilterId", "FieldId"); + + b.HasIndex("FieldId"); + + b.ToTable("RelationshipFilters", "job", t => + { + t.HasComment("Таблица фильтров связей ЭК"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobUnitFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("JobId") + .HasColumnType("uuid"); + + b.Property("UnitFilter") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.ToTable("UnitFilters", "job", t => + { + t.HasComment("Таблица описания критериев выборки ЭК, описание полей в АСУ ЕСПП"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GenerateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextStatusCode") + .HasColumnType("integer"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("WorkGroup") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NextStatusCode"); + + b.HasIndex("Number") + .IsUnique(); + + b.HasIndex("StatusCode"); + + b.HasIndex("TemplateId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("PARR.DAL.Models.OrderStatus", b => + { + b.Property("Code") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Code")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Code"); + + b.ToTable("OrderStatuses"); + + b.HasData( + new + { + Code = 1, + Description = "1-Направлен в группу", + Name = "New" + }, + new + { + Code = 2, + Description = "2-В работе", + Name = "InWork" + }, + new + { + Code = 3, + Description = "3-Приостановлен", + Name = "Stop" + }, + new + { + Code = 4, + Description = "4-Выполнен", + Name = "Complete" + }, + new + { + Code = 5, + Description = "5-Закрыт", + Name = "Closed" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.ParrComponent", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ParrComponents"); + + b.HasData( + new + { + Id = 0, + Description = "API", + Name = "Api" + }, + new + { + Id = 1, + Description = "Загрузчик из АИХИТ", + Name = "AihitLoader" + }, + new + { + Id = 2, + Description = "Синхронизатор АИХИТ", + Name = "AihitSyncer" + }, + new + { + Id = 3, + Description = "Загрузчик нарядов из ЕСПП", + Name = "EsppOrderLoader" + }, + new + { + Id = 4, + Description = "Управление нарядами ЕСПП", + Name = "EsppOrderManager" + }, + new + { + Id = 5, + Description = "Синхронизатор расписаний", + Name = "EsppScheduleSync" + }, + new + { + Id = 6, + Description = "Синхронизатор шаблонов", + Name = "EsppTemplateSync" + }, + new + { + Id = 7, + Description = "Генератор шаблонов", + Name = "GeneratorTemplates" + }, + new + { + Id = 8, + Description = "Авто-контроль РР", + Name = "JobAutoControl" + }, + new + { + Id = 9, + Description = "Связывание нарядов ЕСПП с ПАРР (шаблонами, агентами)", + Name = "Master" + }, + new + { + Id = 10, + Description = "Рассчет даты следующего срабатывания", + Name = "NextRun" + }, + new + { + Id = 11, + Description = "Активатор шаблонов / расписаний", + Name = "TemplateActivator" + }, + new + { + Id = 12, + Description = "Автораспределение РР", + Name = "TemplateDistributor" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Process", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("EsppId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EsppId") + .IsUnique(); + + b.ToTable("Processes"); + }); + + modelBuilder.Entity("PARR.DAL.Models.ResponseArea", b => + { + b.Property("Code") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Code")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Code"); + + b.ToTable("ResponseAreas"); + + b.HasData( + new + { + Code = 96, + Name = "96-ДВС" + }, + new + { + Code = 94, + Name = "94-ЗАБ" + }, + new + { + Code = 92, + Name = "92-ВСИБ" + }, + new + { + Code = 88, + Name = "88-КРАСН" + }, + new + { + Code = 83, + Name = "83-ЗСИБ" + }, + new + { + Code = 80, + Name = "80-ЮУР" + }, + new + { + Code = 76, + Name = "76-СВРД" + }, + new + { + Code = 63, + Name = "63-КБШ" + }, + new + { + Code = 61, + Name = "61-ПРИВ" + }, + new + { + Code = 58, + Name = "58-ЮВСТ" + }, + new + { + Code = 51, + Name = "51-СКВ" + }, + new + { + Code = 28, + Name = "28-СЕВ" + }, + new + { + Code = 24, + Name = "24-ГОР" + }, + new + { + Code = 17, + Name = "17-МСК" + }, + new + { + Code = 10, + Name = "10-КЛГ" + }, + new + { + Code = 1, + Name = "01-ОКТ" + }, + new + { + Code = 99, + Name = "00-ГВЦ" + }, + new + { + Code = 100, + Name = "ОБЩЕЕ" + }, + new + { + Code = 110, + Name = "ВП" + }, + new + { + Code = 111, + Name = "ОСК" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Robot", b => + { + b.Property("Code") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Code")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Robots"); + + b.HasData( + new + { + Code = 1, + Description = "Робот по созданию/изменению шаблона наряда ЕСПП", + Name = "TemplateOrder" + }, + new + { + Code = 2, + Description = "Робот по созданию/изменению расписания шаблона наряда в ЕСПП", + Name = "ScheduleOrder" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptsNumber") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("LastRobotStatusUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("RobotCode") + .HasColumnType("integer"); + + b.Property("RobotStatusCode") + .HasColumnType("integer"); + + b.Property("TaskStatusCode") + .HasColumnType("integer"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RobotCode"); + + b.HasIndex("RobotStatusCode"); + + b.HasIndex("TaskStatusCode"); + + b.HasIndex("TemplateId", "RobotCode") + .IsUnique(); + + b.HasIndex("TemplateId", "RobotStatusCode"); + + b.HasIndex("TemplateId", "TaskStatusCode"); + + b.ToTable("RobotConfigurations"); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("EsppMessage") + .HasColumnType("text"); + + b.Property("HistoryLevel") + .HasColumnType("integer"); + + b.Property("RobotConfigurationId") + .HasColumnType("uuid"); + + b.Property("RobotIp") + .HasColumnType("text"); + + b.Property("RobotMessage") + .HasColumnType("text"); + + b.Property("TaskStatusCode") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("HistoryLevel"); + + b.HasIndex("RobotConfigurationId"); + + b.HasIndex("TaskStatusCode"); + + b.ToTable("RobotHistories"); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotHistoryLevel", b => + { + b.Property("Level") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Level")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Level"); + + b.ToTable("RobotHistoryLevels"); + + b.HasData( + new + { + Level = 1, + Description = "Робот начал работу ", + Name = "Start" + }, + new + { + Level = 5, + Description = "Информация", + Name = "Information" + }, + new + { + Level = 10, + Description = "Ошибка", + Name = "Error" + }, + new + { + Level = 15, + Description = "Успешно завершил работу", + Name = "Complete" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotStatus", b => + { + b.Property("Code") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Code")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Code"); + + b.ToTable("RobotStatuses"); + + b.HasData( + new + { + Code = 11, + Description = "Ожидание, ждет пока робот возьмет в работу.", + Name = "Wait" + }, + new + { + Code = 22, + Description = "Робот взял в работу.", + Name = "InProgress" + }, + new + { + Code = 33, + Description = "Ошибка отработки роботом. Требует ручного вмешательства.", + Name = "Error" + }, + new + { + Code = 44, + Description = "Робот успешно отработал.", + Name = "Complete" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + + b.HasData( + new + { + Id = new Guid("c8f2f144-e6c2-45d7-ba66-5e9bbd376376"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Администратор", + Name = "administrator" + }, + new + { + Id = new Guid("13bca225-7fa4-42fa-9d80-d2d46de261fe"), + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Робот ЕСПП", + Name = "espp-robot" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Schedule.ScheduleExcludeType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("EsppName") + .IsRequired() + .HasColumnType("text"); + + b.Property("EsppValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ExcludeTypes", "schedule", t => + { + t.HasComment("Расписание регламентной работы - Тип исключения"); + }); + + b.HasData( + new + { + Id = new Guid("0595b8e0-f322-4661-b5e3-064af8fbe7cd"), + Code = "None", + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppName = "Нет исключений", + EsppValue = "NONE", + Title = "Нет исключений" + }, + new + { + Id = new Guid("2b7a4356-9aaf-4e00-b444-42f0b5010e25"), + Code = "Only", + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppName = "Выполнить ТОЛЬКО В указанном календаре", + EsppValue = "ONLY", + Title = "Выполнить ТОЛЬКО В указанном календаре" + }, + new + { + Id = new Guid("38cd9e64-27c1-4672-a805-2211952a48bd"), + Code = "Except", + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppName = "Выполнить везде, КРОМЕ указанного календаря", + EsppValue = "EXCEPT", + Title = "Выполнить везде, КРОМЕ указанного календаря" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Schedule.ScheduleExcludeTypeCalendar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("EsppName") + .IsRequired() + .HasColumnType("text"); + + b.Property("EsppValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ExcludeTypeCalendars", "schedule", t => + { + t.HasComment("Расписание регламентной работы, исключение - Календарь"); + }); + + b.HasData( + new + { + Id = new Guid("a26edb88-0267-4fbf-b7ec-d172b0a86c9c"), + Code = "24x5", + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppName = "24x5", + EsppValue = "24x5", + Title = "24x5" + }, + new + { + Id = new Guid("6834f1d5-ac51-43d5-974e-4c759d7bbf9f"), + Code = "24x7", + DateCreated = new DateTimeOffset(new DateTime(2023, 5, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EsppName = "24x7", + EsppValue = "24x7", + Title = "24x7" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Schedule.ScheduleResponseAreaTimeOffset", b => + { + b.Property("ResponseArea") + .HasColumnType("text"); + + b.Property("EsppValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("UtcTimeOffset") + .HasColumnType("interval"); + + b.HasKey("ResponseArea"); + + b.ToTable("ResponseAreaTimeOffsets", "schedule", t => + { + t.HasComment("Расписание в ЕСПП. Смещение часового пояса относительно МСК для зоны ответственности рабочей группы"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Setting", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Name"); + + b.ToTable("Settings"); + + b.HasData( + new + { + Name = "Initiator", + Description = "Инициатор регламентной работы, указывается при создании шаблона в ЕСПП.", + Value = "ОВЧАРЕНКО АЛЕКСЕЙ ВИТАЛЬЕВИЧ (OVCHARENKOAV@GVC.OAO.RZD)" + }, + new + { + Name = "ClosingCode", + Description = "Код закрытия регламентной работы, указывается при создании шаблона в ЕСПП.", + Value = "выполнен" + }, + new + { + Name = "Category", + Description = "Категория создаваемого объекта в ЕСПП", + Value = "регламентная работа" + }, + new + { + Name = "TemplatePrefixName", + Description = "Префикс имени шаблона в ЕСПП", + Value = "%PREFIX%-ЭИТИ-ПТК-ПАРР" + }, + new + { + Name = "RobotAttemptsNumber", + Description = "Количество попыток выполнения задания роботом", + Value = "3" + }, + new + { + Name = "RobotWaitTime", + Description = "Время ожидания выполнения роботом задания", + Value = "00:15:00" + }, + new + { + Name = "ScheduleRepeatRange", + Description = "Расписание регламентной работы - Диапазн повторов", + Value = "Отсутствует дата завершения" + }, + new + { + Name = "OrderSearchDeltaDate", + Description = "Промежуток времени для поиска нарядов в ЕСПП", + Value = "01:30:00" + }, + new + { + Name = "EsppRobotAccountTimeZoneHour", + Description = "Таймзона УЗ роботов в ЕСПП, в часах (может быть положительная и отрицательная)", + Value = "3" + }, + new + { + Name = "WeekendCacheTtl", + Description = "Время хранения в кэше данных о выходных и рабочих днях", + Value = "01:00:00" + }, + new + { + Name = "TemplateNameConstantParts", + Description = "Список неизменных частей имени шаблона", + Value = "[{\"Name\":\"П-1\",\"Value\":\"ЭИТИ-ПАРР\"}]" + }, + new + { + Name = "EsppUnitTag", + Description = "Префикс тега в поле ЭК \"Дополнительная информация\"", + Value = "ПАРР_" + }, + new + { + Name = "JobIdForUnusedTemplates", + Description = "Job в который перемещаем ниспользованные шаблоны", + Value = "8f85a91c-a223-4686-bb69-1f0ee73624f2" + }, + new + { + Name = "DefaultResponseAreaToTimeOffset", + Description = "Зона ответственности для получения тайм зоны по умолчанию для расписаний", + Value = "17-МСК" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Subprocess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("EsppId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProcessId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EsppId") + .IsUnique(); + + b.HasIndex("ProcessId"); + + b.ToTable("Subprocesses"); + }); + + modelBuilder.Entity("PARR.DAL.Models.TaskStatus", b => + { + b.Property("Code") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Code")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Code"); + + b.ToTable("TaskStatuses"); + + b.HasData( + new + { + Code = 10, + Description = "Требуется создание объекта в ЕСПП", + Name = "Creating" + }, + new + { + Code = 20, + Description = "Требуется обновление объекта в ЕСПП", + Name = "Updating" + }, + new + { + Code = 30, + Description = "Нормальное состояние объекта в ЕСПП и ПАРР. Объект в ПАРР соответствует объекту в ЕСПП", + Name = "Ok" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Template", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("InitiatorComment") + .HasColumnType("text"); + + b.Property("InitiatorIp") + .HasColumnType("text"); + + b.Property("InitiatorParrComponentId") + .HasColumnType("integer"); + + b.Property("IsActiveSchedule") + .HasColumnType("boolean"); + + b.Property("IsActiveTemplate") + .HasColumnType("boolean"); + + b.Property("JobId") + .HasColumnType("uuid"); + + b.Property("LastRun") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextRun") + .HasColumnType("timestamp with time zone"); + + b.Property("ScheduleEsppId") + .HasColumnType("text"); + + b.Property("StatusTypeId") + .HasColumnType("integer"); + + b.Property("UnitId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("StatusTypeId"); + + b.HasIndex("UnitId"); + + b.HasIndex("Name", "Index") + .IsUnique(); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("PARR.DAL.Models.TemplateHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateAddedToHistory") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("InitiatorComment") + .HasColumnType("text"); + + b.Property("InitiatorIp") + .HasColumnType("text"); + + b.Property("InitiatorParrComponentId") + .HasColumnType("integer"); + + b.Property("IsActiveSchedule") + .HasColumnType("boolean"); + + b.Property("IsActiveTemplate") + .HasColumnType("boolean"); + + b.Property("LastRun") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextRun") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("StatusTypeId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("TemplateHistories"); + }); + + modelBuilder.Entity("PARR.DAL.Models.TemplateStatusType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateStatusTypes", t => + { + t.HasComment("Таблица описания критериев выборки аттрибутов ЭК"); + }); + + b.HasData( + new + { + Id = 0, + Description = "В работе", + Name = "Used" + }, + new + { + Id = 1, + Description = "Не используется", + Name = "Unused" + }, + new + { + Id = 2, + Description = "Обновляется", + Name = "Updating" + }, + new + { + Id = 3, + Description = "Ошибка", + Name = "Error" + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Tnk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("EsppId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("SubprocessId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubprocessId"); + + b.ToTable("Tnks"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.Unit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLogon") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Units", "unit", t => + { + t.HasComment("Таблица с ЭК"); + + t.HasCheckConstraint("CK_Units_Name_Upper", "\"Name\" = UPPER(\"Name\") AND \"Name\" IS NOT NULL AND LENGTH(\"Name\")>0"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AihitName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("EsppName") + .HasColumnType("text"); + + b.Property("IsMultipleValue") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AihitName"); + + b.HasIndex("EsppName"); + + b.ToTable("Fields", "unit", t => + { + t.HasComment("Справочник полей ЭК"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitFieldInUnitFieldValue", b => + { + b.Property("FieldId") + .HasColumnType("uuid"); + + b.Property("FieldValueId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("FieldId", "FieldValueId"); + + b.HasIndex("FieldValueId"); + + b.ToTable("FieldInFieldValues", "unit", t => + { + t.HasComment("Значения полей ЭК"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Value") + .IsUnique(); + + b.ToTable("FieldValues", "unit", t => + { + t.HasComment("Значения полей ЭК"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitInField", b => + { + b.Property("UnitId") + .HasColumnType("uuid"); + + b.Property("FieldId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UnitId", "FieldId"); + + b.HasIndex("FieldId"); + + b.ToTable("UnitInFields", "unit", t => + { + t.HasComment("Справочник полей ЭК"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitInUnit", b => + { + b.Property("ParentUnitId") + .HasColumnType("uuid"); + + b.Property("ChildUnitId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateSynced") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ParentUnitId", "ChildUnitId"); + + b.HasIndex("ChildUnitId"); + + b.ToTable("UnitInUnits", "unit", t => + { + t.HasComment("Таблица связей иерархии между ЭК"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitInValue", b => + { + b.Property("UnitId") + .HasColumnType("uuid"); + + b.Property("FieldId") + .HasColumnType("uuid"); + + b.Property("ValueId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UnitId", "FieldId", "ValueId"); + + b.HasIndex("ValueId"); + + b.HasIndex("FieldId", "UnitId"); + + b.HasIndex("FieldId", "ValueId"); + + b.HasIndex("UnitId", "FieldId"); + + b.ToTable("UnitInValues", "unit", t => + { + t.HasComment("Таблица связи ЭК с полями и со значениями"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitKiiUnit", b => + { + b.Property("UnitId") + .HasColumnType("uuid"); + + b.HasKey("UnitId"); + + b.ToTable("KiiUnits", "unit", t => + { + t.HasComment("Таблица - Список ЭК КИИ, которые учавствуют в групповых работах. Создавалась как временная"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitRegionalEkPtkGroup", b => + { + b.Property("FieldValueId") + .HasColumnType("uuid"); + + b.HasKey("FieldValueId"); + + b.ToTable("RegionalEkPtkGroups", "unit", t => + { + t.HasComment("Таблица - региональные группы ПТК"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.UnitsInTemplate", b => + { + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("UnitId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("TemplateId", "UnitId"); + + b.HasIndex("UnitId"); + + b.ToTable("UnitsInTemplates", "job", t => + { + t.HasComment("Таблица связи ЭК в шаблонах"); + }); + }); + + modelBuilder.Entity("PARR.DAL.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastLogon") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Ip") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PARR.DAL.Models.UsersInRole", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UsersInRoles"); + }); + + modelBuilder.Entity("PARR.DAL.Models.WeekendDay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.HasKey("Id"); + + b.HasIndex("Date") + .IsUnique(); + + b.ToTable("WeekendDays", "schedule"); + }); + + modelBuilder.Entity("PARR.DAL.Models.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResponseAreaCode") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ResponseAreaCode"); + + b.ToTable("WorkGroups"); + }); + + modelBuilder.Entity("PARR.DAL.Models.AgentHistory", b => + { + b.HasOne("PARR.DAL.Models.AgentHistoryLevel", "AgentHistoryLevel") + .WithMany("AgentHistories") + .HasForeignKey("HistoryLevelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Order", "Order") + .WithMany("AgentHistories") + .HasForeignKey("OrderId"); + + b.HasOne("PARR.DAL.Models.Template", "Template") + .WithMany("AgentHistories") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AgentHistoryLevel"); + + b.Navigation("Order"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("PARR.DAL.Models.AppInWorkInWorkGroup", b => + { + b.HasOne("PARR.DAL.Models.ApplicationsInWork", "ApplicationsInWork") + .WithMany("WorkGroups") + .HasForeignKey("ApplicationsInWorkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.WorkGroup", "WorkGroup") + .WithMany("AppInWorks") + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationsInWork"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Application", b => + { + b.HasOne("PARR.DAL.Models.ApplicationType", "ApplicationType") + .WithMany("Applications") + .HasForeignKey("ApplicationTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationType"); + }); + + modelBuilder.Entity("PARR.DAL.Models.ApplicationInHost", b => + { + b.HasOne("PARR.DAL.Models.Application", "Application") + .WithMany("ApplicationsInHosts") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Host", "Host") + .WithMany("ApplicationsInHosts") + .HasForeignKey("HostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Host"); + }); + + modelBuilder.Entity("PARR.DAL.Models.ApplicationsInWork", b => + { + b.HasOne("PARR.DAL.Models.Application", "Application") + .WithMany("ApplicationsInWorks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("PARR.DAL.Models.DistributionPeriod", b => + { + b.HasOne("PARR.DAL.Models.DistributionPeriodType", "DistributionPeriodType") + .WithMany("Periods") + .HasForeignKey("Type") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DistributionPeriodType"); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchTypeConfig", b => + { + b.HasOne("PARR.DAL.Models.EsppSchType", "EsppSchType") + .WithMany("EsppSchTypeConfigs") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.EsppSchTypeSchedule", "EsppSchTypeSchedule") + .WithMany("EsppSchTypeConfigs") + .HasForeignKey("TypeScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EsppSchType"); + + b.Navigation("EsppSchTypeSchedule"); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchTypeValue", b => + { + b.HasOne("PARR.DAL.Models.EsppSchType", "EsppSchType") + .WithMany("EsppSchTypeValues") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EsppSchType"); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchValue", b => + { + b.HasOne("PARR.DAL.Models.Job.JobGroup", "JobGroup") + .WithMany("EsppSchValues") + .HasForeignKey("JobGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.EsppSchTypeConfig", "EsppSchTypeConfig") + .WithMany("EsppSchValues") + .HasForeignKey("TypeConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.EsppSchTypeValue", "EsppSchTypeValue") + .WithMany("EsppSchValues") + .HasForeignKey("TypeValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EsppSchTypeConfig"); + + b.Navigation("EsppSchTypeValue"); + + b.Navigation("JobGroup"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Host", b => + { + b.HasOne("PARR.DAL.Models.EkStatus", "EkStatus") + .WithMany("Hosts") + .HasForeignKey("EkStatusCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.ResponseArea", "ResponseArea") + .WithMany("Hosts") + .HasForeignKey("ResponseAreaCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.WorkGroup", "WorkGroup") + .WithMany("Hosts") + .HasForeignKey("WorkGroupId"); + + b.Navigation("EkStatus"); + + b.Navigation("ResponseArea"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.Job", b => + { + b.HasOne("PARR.DAL.Models.Job.JobGroup", "Group") + .WithMany("Jobs") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Tnk", "Tnk") + .WithMany("Jobs") + .HasForeignKey("TnkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Tnk"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobAutoControl", b => + { + b.HasOne("PARR.DAL.Models.Job.Job", "Job") + .WithOne("AutoControl") + .HasForeignKey("PARR.DAL.Models.Job.JobAutoControl", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobFieldFilter", b => + { + b.HasOne("PARR.DAL.Models.Unit.UnitField", "UnitField") + .WithMany() + .HasForeignKey("FieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Job.JobUnitFilter", "UnitFilter") + .WithMany("FieldFilters") + .HasForeignKey("UnitFilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UnitField"); + + b.Navigation("UnitFilter"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobGroup", b => + { + b.HasOne("PARR.DAL.Models.Job.JobGroupType", "GroupType") + .WithMany("JobGroups") + .HasForeignKey("GroupTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Unit.UnitField", "GroupingUnitField") + .WithMany("JobGroupWithGrouping") + .HasForeignKey("GroupingUnitFieldId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PARR.DAL.Models.Schedule.ScheduleExcludeTypeCalendar", "ScheduleExcludeTypeCalendar") + .WithMany("JobGroups") + .HasForeignKey("ScheduleExcludeTypeCalendarId"); + + b.HasOne("PARR.DAL.Models.Schedule.ScheduleExcludeType", "ScheduleExcludeType") + .WithMany("JobGroups") + .HasForeignKey("ScheduleExcludeTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GroupType"); + + b.Navigation("GroupingUnitField"); + + b.Navigation("ScheduleExcludeType"); + + b.Navigation("ScheduleExcludeTypeCalendar"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobGroupDistributionConfig", b => + { + b.HasOne("PARR.DAL.Models.DistributionPeriod", "DistributionPeriod") + .WithMany("GroupDistributionConfig") + .HasForeignKey("DistributionPeriodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Job.JobGroup", "Group") + .WithOne("DistributionConfig") + .HasForeignKey("PARR.DAL.Models.Job.JobGroupDistributionConfig", "GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DistributionPeriod"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobRelationshipFilter", b => + { + b.HasOne("PARR.DAL.Models.Unit.UnitField", "UnitField") + .WithMany("RelationshipFilters") + .HasForeignKey("FieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Job.JobUnitFilter", "UnitFilter") + .WithMany("RelationshipFilters") + .HasForeignKey("UnitFilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UnitField"); + + b.Navigation("UnitFilter"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobUnitFilter", b => + { + b.HasOne("PARR.DAL.Models.Job.Job", "Job") + .WithMany("UnitFilters") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Order", b => + { + b.HasOne("PARR.DAL.Models.OrderStatus", "NextStatus") + .WithMany("OrdersNext") + .HasForeignKey("NextStatusCode"); + + b.HasOne("PARR.DAL.Models.OrderStatus", "OrderStatus") + .WithMany("Orders") + .HasForeignKey("StatusCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Template", "Template") + .WithMany("Orders") + .HasForeignKey("TemplateId"); + + b.Navigation("NextStatus"); + + b.Navigation("OrderStatus"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotConfiguration", b => + { + b.HasOne("PARR.DAL.Models.Robot", "Robot") + .WithMany("RobotConfigurations") + .HasForeignKey("RobotCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.RobotStatus", "RobotStatus") + .WithMany("RobotConfigurations") + .HasForeignKey("RobotStatusCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.TaskStatus", "TaskStatus") + .WithMany("RobotConfigurations") + .HasForeignKey("TaskStatusCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Template", "Template") + .WithMany("RobotConfigurations") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Robot"); + + b.Navigation("RobotStatus"); + + b.Navigation("TaskStatus"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotHistory", b => + { + b.HasOne("PARR.DAL.Models.RobotHistoryLevel", "RobotHistoryLevel") + .WithMany("RobotHistories") + .HasForeignKey("HistoryLevel") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.RobotConfiguration", "RobotConfiguration") + .WithMany("RobotHistories") + .HasForeignKey("RobotConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.TaskStatus", "StatusTask") + .WithMany("RobotHistories") + .HasForeignKey("TaskStatusCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RobotConfiguration"); + + b.Navigation("RobotHistoryLevel"); + + b.Navigation("StatusTask"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Subprocess", b => + { + b.HasOne("PARR.DAL.Models.Process", "Process") + .WithMany("Subprocesses") + .HasForeignKey("ProcessId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Process"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Template", b => + { + b.HasOne("PARR.DAL.Models.Job.Job", "Job") + .WithMany("Templates") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.TemplateStatusType", "StatusType") + .WithMany("Templates") + .HasForeignKey("StatusTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Unit.Unit", "Unit") + .WithMany("Templates") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("StatusType"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("PARR.DAL.Models.TemplateHistory", b => + { + b.HasOne("PARR.DAL.Models.Template", "Template") + .WithMany("TemplateHistories") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Tnk", b => + { + b.HasOne("PARR.DAL.Models.Subprocess", "Subprocess") + .WithMany("Tnks") + .HasForeignKey("SubprocessId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subprocess"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitFieldInUnitFieldValue", b => + { + b.HasOne("PARR.DAL.Models.Unit.UnitField", "Field") + .WithMany("UnitFieldValues") + .HasForeignKey("FieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Unit.UnitFieldValue", "FieldValue") + .WithMany("FieldValues") + .HasForeignKey("FieldValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Field"); + + b.Navigation("FieldValue"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitInField", b => + { + b.HasOne("PARR.DAL.Models.Unit.UnitField", "UnitField") + .WithMany("Units") + .HasForeignKey("FieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Unit.Unit", "Unit") + .WithMany("UnitFields") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Unit"); + + b.Navigation("UnitField"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitInUnit", b => + { + b.HasOne("PARR.DAL.Models.Unit.Unit", "ChildUnit") + .WithMany("ParentUnits") + .HasForeignKey("ChildUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Unit.Unit", "ParentUnit") + .WithMany("ChildUnits") + .HasForeignKey("ParentUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChildUnit"); + + b.Navigation("ParentUnit"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitInValue", b => + { + b.HasOne("PARR.DAL.Models.Unit.UnitField", "Field") + .WithMany("UnitInValues") + .HasForeignKey("FieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Unit.Unit", "Unit") + .WithMany("UnitValues") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Unit.UnitFieldValue", "Value") + .WithMany("UnitInValues") + .HasForeignKey("ValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Field"); + + b.Navigation("Unit"); + + b.Navigation("Value"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitKiiUnit", b => + { + b.HasOne("PARR.DAL.Models.Unit.Unit", "Unit") + .WithOne("UnitKii") + .HasForeignKey("PARR.DAL.Models.Unit.UnitKiiUnit", "UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitRegionalEkPtkGroup", b => + { + b.HasOne("PARR.DAL.Models.Unit.UnitFieldValue", "FieldValue") + .WithOne("RegionalEkPtkGroup") + .HasForeignKey("PARR.DAL.Models.Unit.UnitRegionalEkPtkGroup", "FieldValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FieldValue"); + }); + + modelBuilder.Entity("PARR.DAL.Models.UnitsInTemplate", b => + { + b.HasOne("PARR.DAL.Models.Template", "Template") + .WithMany("UnitsInTemplate") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.Unit.Unit", "Unit") + .WithMany("TemplatesInUnit") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("PARR.DAL.Models.UsersInRole", b => + { + b.HasOne("PARR.DAL.Models.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PARR.DAL.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PARR.DAL.Models.WorkGroup", b => + { + b.HasOne("PARR.DAL.Models.ResponseArea", "ResponseArea") + .WithMany("WorkGroups") + .HasForeignKey("ResponseAreaCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ResponseArea"); + }); + + modelBuilder.Entity("PARR.DAL.Models.AgentHistoryLevel", b => + { + b.Navigation("AgentHistories"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Application", b => + { + b.Navigation("ApplicationsInHosts"); + + b.Navigation("ApplicationsInWorks"); + }); + + modelBuilder.Entity("PARR.DAL.Models.ApplicationType", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("PARR.DAL.Models.ApplicationsInWork", b => + { + b.Navigation("WorkGroups"); + }); + + modelBuilder.Entity("PARR.DAL.Models.DistributionPeriod", b => + { + b.Navigation("GroupDistributionConfig"); + }); + + modelBuilder.Entity("PARR.DAL.Models.DistributionPeriodType", b => + { + b.Navigation("Periods"); + }); + + modelBuilder.Entity("PARR.DAL.Models.EkStatus", b => + { + b.Navigation("Hosts"); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchType", b => + { + b.Navigation("EsppSchTypeConfigs"); + + b.Navigation("EsppSchTypeValues"); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchTypeConfig", b => + { + b.Navigation("EsppSchValues"); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchTypeSchedule", b => + { + b.Navigation("EsppSchTypeConfigs"); + }); + + modelBuilder.Entity("PARR.DAL.Models.EsppSchTypeValue", b => + { + b.Navigation("EsppSchValues"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Host", b => + { + b.Navigation("ApplicationsInHosts"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.Job", b => + { + b.Navigation("AutoControl"); + + b.Navigation("Templates"); + + b.Navigation("UnitFilters"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobGroup", b => + { + b.Navigation("DistributionConfig"); + + b.Navigation("EsppSchValues"); + + b.Navigation("Jobs"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobGroupType", b => + { + b.Navigation("JobGroups"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Job.JobUnitFilter", b => + { + b.Navigation("FieldFilters"); + + b.Navigation("RelationshipFilters"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Order", b => + { + b.Navigation("AgentHistories"); + }); + + modelBuilder.Entity("PARR.DAL.Models.OrderStatus", b => + { + b.Navigation("Orders"); + + b.Navigation("OrdersNext"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Process", b => + { + b.Navigation("Subprocesses"); + }); + + modelBuilder.Entity("PARR.DAL.Models.ResponseArea", b => + { + b.Navigation("Hosts"); + + b.Navigation("WorkGroups"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Robot", b => + { + b.Navigation("RobotConfigurations"); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotConfiguration", b => + { + b.Navigation("RobotHistories"); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotHistoryLevel", b => + { + b.Navigation("RobotHistories"); + }); + + modelBuilder.Entity("PARR.DAL.Models.RobotStatus", b => + { + b.Navigation("RobotConfigurations"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Schedule.ScheduleExcludeType", b => + { + b.Navigation("JobGroups"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Schedule.ScheduleExcludeTypeCalendar", b => + { + b.Navigation("JobGroups"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Subprocess", b => + { + b.Navigation("Tnks"); + }); + + modelBuilder.Entity("PARR.DAL.Models.TaskStatus", b => + { + b.Navigation("RobotConfigurations"); + + b.Navigation("RobotHistories"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Template", b => + { + b.Navigation("AgentHistories"); + + b.Navigation("Orders"); + + b.Navigation("RobotConfigurations"); + + b.Navigation("TemplateHistories"); + + b.Navigation("UnitsInTemplate"); + }); + + modelBuilder.Entity("PARR.DAL.Models.TemplateStatusType", b => + { + b.Navigation("Templates"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Tnk", b => + { + b.Navigation("Jobs"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.Unit", b => + { + b.Navigation("ChildUnits"); + + b.Navigation("ParentUnits"); + + b.Navigation("Templates"); + + b.Navigation("TemplatesInUnit"); + + b.Navigation("UnitFields"); + + b.Navigation("UnitKii"); + + b.Navigation("UnitValues"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitField", b => + { + b.Navigation("JobGroupWithGrouping"); + + b.Navigation("RelationshipFilters"); + + b.Navigation("UnitFieldValues"); + + b.Navigation("UnitInValues"); + + b.Navigation("Units"); + }); + + modelBuilder.Entity("PARR.DAL.Models.Unit.UnitFieldValue", b => + { + b.Navigation("FieldValues"); + + b.Navigation("RegionalEkPtkGroup"); + + b.Navigation("UnitInValues"); + }); + + modelBuilder.Entity("PARR.DAL.Models.User", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("PARR.DAL.Models.WorkGroup", b => + { + b.Navigation("AppInWorks"); + + b.Navigation("Hosts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PARR.DAL/Migrations/20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes.cs b/PARR.DAL/Migrations/20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes.cs new file mode 100644 index 00000000..919bc4ce --- /dev/null +++ b/PARR.DAL/Migrations/20260209010657_tblJobGroupAddUserTimeZoneOffsetMinutes.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PARR.DAL.Migrations +{ + /// + public partial class tblJobGroupAddUserTimeZoneOffsetMinutes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UserTimeZoneOffsetMinutes", + schema: "job", + table: "Groups", + type: "integer", + nullable: true, + comment: "Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UserTimeZoneOffsetMinutes", + schema: "job", + table: "Groups"); + } + } +} diff --git a/PARR.DAL/Migrations/DataContextModelSnapshot.cs b/PARR.DAL/Migrations/DataContextModelSnapshot.cs index 1f5a115d..4b8fe7ad 100644 --- a/PARR.DAL/Migrations/DataContextModelSnapshot.cs +++ b/PARR.DAL/Migrations/DataContextModelSnapshot.cs @@ -1702,6 +1702,10 @@ namespace PARR.DAL.Migrations .IsRequired() .HasColumnType("text"); + b.Property("UserTimeZoneOffsetMinutes") + .HasColumnType("integer") + .HasComment("Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate"); + b.HasKey("Id"); b.HasIndex("GroupTypeId"); diff --git a/PARR.DAL/Models/Job/JobGroup.cs b/PARR.DAL/Models/Job/JobGroup.cs index defc3e95..66d544e0 100644 --- a/PARR.DAL/Models/Job/JobGroup.cs +++ b/PARR.DAL/Models/Job/JobGroup.cs @@ -92,6 +92,21 @@ namespace PARR.DAL.Models.Job public DateTimeOffset ReferenceDate { get; set; } + /// + /// Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate + /// + [Comment("Смещение часового пояса пользователя в минутах относительно UTC на момент ReferenceDate")] + public int? UserTimeZoneOffsetMinutes { get; set; } + + /// + /// Смещение часового пояса пользователя относительно UTC на момент ReferenceDate, рассчитывается из поля UserTimeZoneOffsetMinutes + /// + [NotMapped] + public TimeSpan? UserTimeZoneOffset => + UserTimeZoneOffsetMinutes.HasValue + ? TimeSpan.FromMinutes(UserTimeZoneOffsetMinutes.Value) + : null; + /// /// Использовать таймзону рабочей группы ответственного за ЭК шаблона /// diff --git a/PARR.DAL/NextRunServices/INextRunService.cs b/PARR.DAL/NextRunServices/INextRunService.cs index 8ad74a75..fdb0bb93 100644 --- a/PARR.DAL/NextRunServices/INextRunService.cs +++ b/PARR.DAL/NextRunServices/INextRunService.cs @@ -72,12 +72,12 @@ namespace PARR.DAL.NextRunServices /// DateTimeOffset GetNextRunWithTimezoneEsppAndResponseArea(DateTimeOffset nextRun, bool? isResponseAreaTimezone = null, string? responseArea = null); - /// - /// Получить NextRun согласно таймзоны ЗО - /// - /// - /// - /// - DateTimeOffset GetNextRunWithResponseAreaOffset(DateTimeOffset nextRun, string responseArea); + ///// + ///// Получить NextRun согласно таймзоны ЗО + ///// + ///// + ///// + ///// + //DateTimeOffset GetNextRunWithResponseAreaOffset(DateTimeOffset nextRun, string responseArea); } } diff --git a/PARR.DAL/NextRunServices/NextRunService.cs b/PARR.DAL/NextRunServices/NextRunService.cs index 2009576e..eb7f0ee5 100644 --- a/PARR.DAL/NextRunServices/NextRunService.cs +++ b/PARR.DAL/NextRunServices/NextRunService.cs @@ -295,6 +295,7 @@ namespace PARR.DAL.NextRunServices } + #region Ok /// /// Получить продолжительность в днях @@ -331,6 +332,8 @@ namespace PARR.DAL.NextRunServices return periodDays; } + #endregion + /// /// Получить дату начала распределения @@ -438,14 +441,15 @@ namespace PARR.DAL.NextRunServices //return resultNextRun; } - public DateTimeOffset GetNextRunWithResponseAreaOffset(DateTimeOffset nextRun, string responseArea) - { - var responseAreaOffset = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea).UtcTimeOffset; - var resulstNextRun = nextRun.Add(responseAreaOffset); - logger.LogDebug("Смещение nextRun для ЗО {responseArea}, входящий: {nextRun}, смещение: {offset}, результат: {result}", responseArea, nextRun, responseAreaOffset, resulstNextRun); + //// был паблик + //private DateTimeOffset GetNextRunWithResponseAreaOffset(DateTimeOffset nextRun, string responseArea) + //{ + // var responseAreaOffset = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea).UtcTimeOffset; + // var resulstNextRun = nextRun.Add(responseAreaOffset); + // logger.LogDebug("Смещение nextRun для ЗО {responseArea}, входящий: {nextRun}, смещение: {offset}, результат: {result}", responseArea, nextRun, responseAreaOffset, resulstNextRun); - return resulstNextRun; - } + // return resulstNextRun; + //} /// diff --git a/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs b/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs index 069a992b..b4a949ea 100644 --- a/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs +++ b/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs @@ -2,14 +2,12 @@ using PARR.DAL.Contracts; using PARR.DAL.DomainModels; using PARR.DAL.Services.Interfaces; -using PARR.DAL.TransformServices; namespace PARR.DAL.NextRunServices.Subservices { internal class EsppScheduleTransformService : IEsppScheduleTransformService { private readonly IEsppSchTypeConfigService esppSchTypeConfigService; - //private readonly INextRunModifierService nextRunModifierService; private readonly ILogger logger; private static readonly Dictionary monthDict = new Dictionary() @@ -52,11 +50,9 @@ namespace PARR.DAL.NextRunServices.Subservices public EsppScheduleTransformService( ILogger logger, IEsppSchTypeConfigService esppSchTypeConfigService - //INextRunModifierService nextRunModifierService ) { this.esppSchTypeConfigService = esppSchTypeConfigService; - //this.nextRunModifierService = nextRunModifierService; this.logger = logger; } @@ -69,9 +65,12 @@ namespace PARR.DAL.NextRunServices.Subservices private DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate, TimeSpan? offset) { + //offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК + var nextRun = referenceDate; - var finalOffset = new TimeSpan(3, 0, 0) + offset ?? TimeSpan.Zero; + //var finalOffset = new TimeSpan(3, 0, 0) + (offset ?? TimeSpan.Zero); + var finalOffset = offset ?? TimeSpan.Zero; var offsetReferenceDate = referenceDate.ToOffset(finalOffset); @@ -115,64 +114,12 @@ namespace PARR.DAL.NextRunServices.Subservices } - //private List GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate) - //{ - // // расписание на оставшееся на сегодня время. его так мало... - // referenceDate = referenceDate.UtcDateTime; - // var schedules = new List(); - // var curTime = referenceDate; - - // //Если ещё не произошло то добавляем расписание, возможно это новое AiW и он ещё ниразу не запускался. - // //lastRun это будущее - // if (referenceDate >= DateTimeOffset.UtcNow && referenceDate <= DateTimeOffset.UtcNow.EndOfDay()) - // schedules.Add(referenceDate); - - // while (curTime < DateTimeOffset.UtcNow.EndOfDay()) - // { - // switch (esppSchedule.TypeSchedule.Id) - // { - // case (int)EsppSchTypeScheduleEnum.Regularly: - // curTime = GetNextDateRegularly(esppSchedule.Values, curTime); - // break; - // case (int)EsppSchTypeScheduleEnum.Weekly: - // curTime = GetNextDateWeekly(esppSchedule.Values, curTime); - // break; - // case (int)EsppSchTypeScheduleEnum.Monthly: - // curTime = GetNextDateMonthly(esppSchedule.Values, curTime); - // break; - // case (int)EsppSchTypeScheduleEnum.Monthly2: - // curTime = GetNextDateMonthly2(esppSchedule.Values, curTime); - // break; - // case (int)EsppSchTypeScheduleEnum.Annually: - // curTime = GetNextDateAnnually(esppSchedule.Values, curTime); - // break; - // case (int)EsppSchTypeScheduleEnum.Annually2: - // curTime = GetNextDateAnnually2(esppSchedule.Values, curTime); - // break; - // } - - // schedules.Add(curTime); - // } - // schedules.RemoveAll(s => s > DateTimeOffset.UtcNow.EndOfDay()); - - // return schedules.OrderBy(t => t).ToList(); - //} - - - - - - //public async Task> GetNextScheduleAsync(Guid jobGroupId, DateTimeOffset referenceDate) - //{ - // //TODO: тут не проверен переход через выходные дни!!! Переход не используется, так как тут не рассчитываем NextRun. - // //В общем проверить, когда будем тестировать агента, что с датами все ок - // var esppSchedule = await GetEsppScheduleAsync(jobGroupId); - // var nextSchedule = GetNextSchedule(esppSchedule, referenceDate); - - // return nextSchedule; - //} - - + /// + /// Получить расписание из БД + /// + /// + /// + /// private async Task GetEsppScheduleAsync(Guid jobGroupId) { var esppSchedule = await esppSchTypeConfigService.GetEsppScheduleDtoAsync(jobGroupId); @@ -236,7 +183,22 @@ namespace PARR.DAL.NextRunServices.Subservices var calcDay = referenceDate; if (calcDay < DateTimeOffset.UtcNow) - calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, DateTimeOffset.UtcNow.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + { + var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + calcDay = new DateTimeOffset( + nowInTargetTz.Year, + //DateTimeOffset.UtcNow.Year, + nowInTargetTz.Month, + //DateTimeOffset.UtcNow.Month, + nowInTargetTz.Day, + //DateTimeOffset.UtcNow.Day, + referenceDate.Hour, + referenceDate.Minute, + referenceDate.Second, + referenceDate.Offset + ); + } + if (((int)calcDay.DayOfWeek) != dwNum) calcDay = OffsetToDayOfWeek(dwNum, calcDay); @@ -260,7 +222,20 @@ namespace PARR.DAL.NextRunServices.Subservices var calcDay = new DateTimeOffset(referenceDate.Year, referenceDate.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); if (referenceDate < DateTimeOffset.UtcNow) - calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + { + var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + calcDay = new DateTimeOffset( + nowInTargetTz.Year, + nowInTargetTz.Month, + dayNum, + referenceDate.Hour, + referenceDate.Minute, + referenceDate.Second, + referenceDate.Offset + ); + //calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + } + //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию @@ -285,9 +260,29 @@ namespace PARR.DAL.NextRunServices.Subservices throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}"); } + //var startDay = referenceDate < DateTimeOffset.UtcNow + // ? new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset) + // : new DateTimeOffset(referenceDate.Year, referenceDate.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + var startDay = referenceDate < DateTimeOffset.UtcNow - ? new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset) - : new DateTimeOffset(referenceDate.Year, referenceDate.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + ? new DateTimeOffset( + DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Year, + DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Month, + 1, + referenceDate.Hour, + referenceDate.Minute, + referenceDate.Second, + referenceDate.Offset + ) + : new DateTimeOffset( + referenceDate.Year, + referenceDate.Month, + 1, + referenceDate.Hour, + referenceDate.Minute, + referenceDate.Second, + referenceDate.Offset + ); var daysList = FindAllDaysInMonth(dwNum, startDay); @@ -330,7 +325,12 @@ namespace PARR.DAL.NextRunServices.Subservices throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); } - var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year; + //var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year; + //сравниваем года в одном часовом поясе + var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + var year = (referenceDate.Year < nowInTargetTz.Year) + ? nowInTargetTz.Year + : referenceDate.Year; var calcDay = new DateTimeOffset( year, monthNum, dayNum, @@ -379,9 +379,15 @@ namespace PARR.DAL.NextRunServices.Subservices throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}"); } - var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year; + //var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year; + // сравниваем года в одном часовом поясе + var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + var year = (referenceDate.Year < nowInTargetTz.Year) + ? nowInTargetTz.Year + : referenceDate.Year; - if (year == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.Month) + //if (year == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.Month) + if (year == nowInTargetTz.Year && referenceDate.Month < nowInTargetTz.Month) year += 1; var calcDay = new DateTimeOffset( @@ -432,91 +438,5 @@ namespace PARR.DAL.NextRunServices.Subservices return calcDay.AddDays(daysOffset); } - - //public DateTimeOffset GetNextDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod) - //{ - // var nextRun = lastRun; - - // switch (periodType) - // { - // case (DistributionPeriodTypeEnum.Day): - // nextRun = lastRun.AddDays(ParseInt(distributionPeriod)); - // break; - // case (DistributionPeriodTypeEnum.Month): - // nextRun = lastRun.AddMonths(ParseInt(distributionPeriod)); - // break; - // case (DistributionPeriodTypeEnum.Year): - // nextRun = lastRun.AddYears(ParseInt(distributionPeriod)); - // break; - // } - - // return nextRunModifierService.GetWorkDayAsync(nextRun).GetAwaiter().GetResult(); - //} - - - //private DateTimeOffset GetPrevDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod) - //{ - // switch (periodType) - // { - // case (DistributionPeriodTypeEnum.Day): - // return lastRun.AddDays(-ParseInt(distributionPeriod)); - - // case (DistributionPeriodTypeEnum.Month): - // return lastRun.AddMonths(-ParseInt(distributionPeriod)); - - // case (DistributionPeriodTypeEnum.Year): - // return lastRun.AddYears(-ParseInt(distributionPeriod)); - // } - - // return lastRun; - //} - - - //public async Task GetStartPeriodForDateAsync(Guid jobGroupId, DateTimeOffset date, DateTimeOffset referenceDate, DistributionPeriodTypeEnum periodType, string distributionPeriod) - //{ - // var esppSchedule = await GetEsppScheduleAsync(jobGroupId); - - // var currentStartPeriod = referenceDate; - // var currentEndPeriod = GetNextDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod); - - // while (!(date >= currentStartPeriod && date < currentEndPeriod)) - // { - // if (date > currentEndPeriod) - // { - // currentStartPeriod = currentEndPeriod; - // currentEndPeriod = GetNextDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod); - // } - // else - // { - // currentEndPeriod = currentStartPeriod; - // currentStartPeriod = GetPrevDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod); - // } - // } - - // return DateOnly.FromDateTime(currentStartPeriod.DateTime); - //} - - - //private int ParseInt(string value) - //{ - // try - // { - // return int.Parse(value); - // } - // catch (Exception ex) - // { - // logger.LogError(ex, $"{nameof(this.GetType)}, получение конца периода. Не смог распарсить string в int для значения {value}."); - // return 0; - // } - //} - - - //private DistributionPeriodTypeEnum ParseDistributionPeriodType(string value) - //{ - // var result = (DistributionPeriodTypeEnum)Enum.Parse(typeof(DistributionPeriodTypeEnum), value); - - // return result; - //} - } } diff --git a/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs b/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs index 4a197589..82b4a70e 100644 --- a/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs +++ b/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs @@ -26,8 +26,9 @@ /// /// /// + /// offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК /// - Task GetNextDateAsync(Guid jobGroupId, DateTimeOffset lastRun, TimeSpan? offset = null); + Task GetNextDateAsync(Guid jobGroupId, DateTimeOffset referenceDate, TimeSpan? offset = null); ///// ///// Получить расписание по jobGroupId From 7c8f8bbf9712b0112dfa9bb063c885cfc3f789a9 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Tue, 10 Feb 2026 15:50:30 +1000 Subject: [PATCH 02/14] =?UTF-8?q?feat(dal):=20ITemplateDistributorV2=20-?= =?UTF-8?q?=20=D1=80=D0=B0=D1=81=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D1=82=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D1=8B=20?= =?UTF-8?q?=D1=81=20=D1=83=D1=87=D0=B5=D1=82=D0=BE=D0=BC=20=D1=87=D0=B0?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=D1=8F=D1=81?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EsppScheduleTransformService.cs | 9 + .../IEsppScheduleTransformService.cs | 45 +- .../Subservices/ITemplateDistributorV2.cs | 37 ++ .../Subservices/TemplateDistributor.cs | 206 +++++--- .../Subservices/TemplateDistributorV2.cs | 495 ++++++++++++++++++ PARR.DAL/ParrDalInstaller.cs | 1 + PARR.Test/NextRun/NextRunTest.cs | 38 +- 7 files changed, 707 insertions(+), 124 deletions(-) create mode 100644 PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs create mode 100644 PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs diff --git a/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs b/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs index b4a949ea..f94f9291 100644 --- a/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs +++ b/PARR.DAL/NextRunServices/Subservices/EsppScheduleTransformService.cs @@ -2,6 +2,7 @@ using PARR.DAL.Contracts; using PARR.DAL.DomainModels; using PARR.DAL.Services.Interfaces; +using System.Text.Json; namespace PARR.DAL.NextRunServices.Subservices { @@ -67,6 +68,8 @@ namespace PARR.DAL.NextRunServices.Subservices { //offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК + logger.LogDebug("Начинаю рассчет nextRun по расписанию ЕСПП. Входные данные: date: {referenceDate}, offset: {offset}, esppSchedule: {esppSchedule}", referenceDate, offset, JsonSerializer.Serialize(esppSchedule)); + var nextRun = referenceDate; //var finalOffset = new TimeSpan(3, 0, 0) + (offset ?? TimeSpan.Zero); @@ -74,6 +77,8 @@ namespace PARR.DAL.NextRunServices.Subservices var offsetReferenceDate = referenceDate.ToOffset(finalOffset); + logger.LogDebug("Дата после добавления offset - {offsetReferenceDate}", offsetReferenceDate); + ////--------- //// Для рассчета по МСК времени, потому что в ЮТС может быть еще ВСК, а по МСК это уже ПНД //var offsetReferenceDate = referenceDate.ToOffset(new TimeSpan(3, 0, 0)); @@ -107,9 +112,13 @@ namespace PARR.DAL.NextRunServices.Subservices break; } + logger.LogDebug("Рассчитанная дата, с учетом offset: {nextRun}", nextRun); + // Возвращаем обратно в ЮТС nextRun = nextRun.ToOffset(TimeSpan.Zero); + logger.LogDebug("Рассчитанная дата с нулевым offset: {nextRun}", nextRun); + return nextRun; } diff --git a/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs b/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs index 82b4a70e..fc4e7d75 100644 --- a/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs +++ b/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs @@ -5,56 +5,13 @@ /// internal interface IEsppScheduleTransformService { - ///// - ///// Получить следующую дату согласно расписания - ///// - ///// - ///// - ///// - //DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset lastRun); - - ///// - ///// Получить расписание - ///// - ///// - ///// - ///// - //List GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset lastRun); - /// /// Получить следующую дату по jobGroupId /// /// - /// + /// referenceDate или nextRun /// offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК /// Task GetNextDateAsync(Guid jobGroupId, DateTimeOffset referenceDate, TimeSpan? offset = null); - - ///// - ///// Получить расписание по jobGroupId - ///// - ///// - ///// - ///// - //Task> GetNextScheduleAsync(Guid jobGroupId, DateTimeOffset lastRun); - - - ///// - ///// Получить следующую дату срабатывания в указанном периоде - ///// - ///// - ///// - ///// - ///// - //DateTimeOffset GetNextDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod); - - - ///// - ///// Получить дату начала периода распределения относительно опорной даты (Reference Date) - ///// - ///// - ///// - ///// - //Task GetStartPeriodForDateAsync(Guid jobGroupId, DateTimeOffset date, DateTimeOffset refrenceDate, DistributionPeriodTypeEnum periodType, string distributionPeriod); } } diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs new file mode 100644 index 00000000..98137dd8 --- /dev/null +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs @@ -0,0 +1,37 @@ +using PARR.DAL.NextRunServices.Models; + +namespace PARR.DAL.NextRunServices.Subservices +{ + //todo: -> internal + public interface ITemplateDistributorV2 + { + /// + /// Распределить шаблоны + /// + /// + /// + /// + /// + /// + /// + /// + Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends); + + /// + /// Получить список выходных дней + /// + /// + /// + /// + 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 e93c5dd9..1430c248 100644 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs @@ -27,6 +27,9 @@ namespace PARR.DAL.NextRunServices.Subservices public async Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew) { + //if (offset.HasValue) + // referenceDate = referenceDate.ToOffset(offset.Value); + //todo: allTemplates - пока не используется. Будет использоваться когда каждый раз будем строить план // Пока работает так: @@ -104,6 +107,10 @@ namespace PARR.DAL.NextRunServices.Subservices public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends) { + //if (offset.HasValue) + //referenceDate = referenceDate.ToOffset(offset.Value); + + var _startDate = GetStartPeriodDate(startDate, referenceDate); var _endDate = GetEndPeriodDate(_startDate, duration); @@ -417,7 +424,7 @@ namespace PARR.DAL.NextRunServices.Subservices return CreateDateTimeOffset(minDay, referenceDate); } - + //--------------------------------------------------------------------------------------------------------------------------------------------------------------------- /// /// Получить план: дни, на которые должны быть назначены шаблоны @@ -492,7 +499,7 @@ namespace PARR.DAL.NextRunServices.Subservices /// private DateTimeOffset CreateDateTimeOffset(DateOnly date, DateTimeOffset refDate) { - return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero); + return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, refDate.Offset); } @@ -507,20 +514,61 @@ namespace PARR.DAL.NextRunServices.Subservices // дата начала не может быть раньше referenceDate // и не может быть раньше чем сегодня - var dateStart = startDate; + #region old + //var dateStart = startDate; - // если dateStart < чем refDate, то берем refDate - if (dateStart < DateOnly.FromDateTime(referenceDate.Date)) - dateStart = DateOnly.FromDateTime(referenceDate.Date); + //// если dateStart < чем refDate, то берем refDate + //if (dateStart < DateOnly.FromDateTime(referenceDate.Date)) + // dateStart = DateOnly.FromDateTime(referenceDate.Date); - // если dateStart < чем текущая дата, то берем текущую дату - if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date)) - dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date); + //// если dateStart < чем текущая дата, то берем текущую дату + //if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date)) + // dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date); - // если текущее время > referenceDate, то прибавляем 1 день + //// если текущее время > referenceDate, то прибавляем 1 день + //var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate); + //if (dateTimeStart < DateTimeOffset.UtcNow) + // dateStart = dateStart.AddDays(1); + + //return dateStart; + #endregion + + // 1. Конвертируем startDate (UTC) в целевой часовой пояс + var startDateUtc = new DateTimeOffset( + startDate.Year, startDate.Month, startDate.Day, + 0, 0, 0, + TimeSpan.Zero); + + var startDateInTargetTz = startDateUtc.ToOffset(referenceDate.Offset); + var dateStart = DateOnly.FromDateTime(startDateInTargetTz.DateTime); + + logger.LogDebug("startDate UTC ({startDate} 00:00 UTC) конвертирована в целевой часовой пояс {offset}: {dateStart}", startDate, referenceDate.Offset, dateStart); + + // 2. Дата начала не может быть раньше даты из referenceDate (в его часовом поясе) + var refDateInTargetTz = DateOnly.FromDateTime(referenceDate.Date); + if (dateStart < refDateInTargetTz) + { + logger.LogDebug("Дата начала {dateStart} раньше даты из referenceDate {refDate}. Используем дату из referenceDate.", dateStart, refDateInTargetTz); + dateStart = refDateInTargetTz; + } + + // 3. Дата начала не может быть раньше "сегодня" в целевом часовом поясе + var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Date); + if (dateStart < todayInTargetTz) + { + logger.LogDebug("Дата начала {dateStart} раньше 'сегодня' в целевом поясе {today}. Используем 'сегодня'.", dateStart, todayInTargetTz); + dateStart = todayInTargetTz; + } + + // 4. Если момент времени "начало дня в целевом поясе" уже прошёл — добавляем 1 день var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate); if (dateTimeStart < DateTimeOffset.UtcNow) + { + logger.LogDebug("Момент времени {dateTimeStart} уже прошёл (сейчас {now}). Добавляем 1 день.", dateTimeStart, DateTimeOffset.UtcNow); dateStart = dateStart.AddDays(1); + } + + logger.LogDebug("Итоговая дата начала распределения: {dateStart} (в часовом поясе {offset})", dateStart, referenceDate.Offset); return dateStart; } @@ -565,41 +613,44 @@ namespace PARR.DAL.NextRunServices.Subservices /// /// /// - public async Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate) + public async Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TimeSpan? offset = null) { + if (offset.HasValue) + referenceDate = referenceDate.ToOffset(offset.Value); + // проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd - bool isPreviousDays = IsPreviousDays(referenceDate); + //bool isPreviousDays = IsPreviousDays(referenceDate); - if (isPreviousDays) - { - // Это предыдущие сутки, корректируем - startDate = startDate.AddDays(-1); - // так как мы потом будем сдвигать выходные дни влево, то надо взять на один день дальше, чтоб знать какой он - // вроде бы это не костыль - endDate = endDate.AddDays(1); - //endDate = endDate.AddDays(-1); + //if (isPreviousDays) + //{ + // // Это предыдущие сутки, корректируем + // startDate = startDate.AddDays(-1); + // // так как мы потом будем сдвигать выходные дни влево, то надо взять на один день дальше, чтоб знать какой он + // // вроде бы это не костыль + // endDate = endDate.AddDays(1); + // //endDate = endDate.AddDays(-1); - logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты для получения выходных дней, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, startDate, endDate); - } + // logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты для получения выходных дней, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, startDate, endDate); + //} var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); var weekendsResult = new List(); - if (isPreviousDays) - { - // так как должны быть предыдущие дни, отнимаем 1 день - foreach (var weekend in weekends) - { - var previous = weekend.AddDays(-1); - logger.LogDebug("Выходной день, -1, так как день должен быть предыдущий, старое значение {weekend}, новое {previous}", weekend, previous); - weekendsResult.Add(previous); - } - } - else - { - weekendsResult = weekends; - } + //if (isPreviousDays) + //{ + // // так как должны быть предыдущие дни, отнимаем 1 день + // foreach (var weekend in weekends) + // { + // var previous = weekend.AddDays(-1); + // logger.LogDebug("Выходной день, -1, так как день должен быть предыдущий, старое значение {weekend}, новое {previous}", weekend, previous); + // weekendsResult.Add(previous); + // } + //} + //else + //{ + // weekendsResult = weekends; + //} logger.LogDebug("Найдено не рабочих дней: {Count}", weekendsResult.Count); foreach (var weekend in weekendsResult) @@ -644,19 +695,22 @@ namespace PARR.DAL.NextRunServices.Subservices //} - public async Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate) + public async Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate, TimeSpan? offset = null) { + if (offset.HasValue) + referenceDate = referenceDate.ToOffset(offset.Value); + logger.LogDebug("Формирование списка рабочих дней, start: {start}, end: {end}, excludeWeekends: {excludeWeekends}, referenceDate: {referenceDate}", start, end, excludeWeekends, referenceDate); - // проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd - if (IsPreviousDays(referenceDate)) - { - // Это предыдущие сутки, корректируем - start = start.AddDays(-1); - end = end.AddDays(-1); + //// проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd + //if (IsPreviousDays(referenceDate)) + //{ + // // Это предыдущие сутки, корректируем + // start = start.AddDays(-1); + // end = end.AddDays(-1); - logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, start, end); - } + // logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, start, end); + //} // Получаем список выходных/праздничных дней var weekends = new HashSet(); @@ -683,33 +737,45 @@ namespace PARR.DAL.NextRunServices.Subservices return result; } - /// - /// Это предыдущие сутки? Относительно referenceDate и МСК времени - /// - /// - /// - private bool IsPreviousDays(DateTimeOffset referenceDate) + + + public Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate) { - // смещение часов относительно UTC - MSK - int offsetMskHour = 3; - - // если время больше 21 часа, то это текущие сутки, если меньше, то прошлые - int maxHour = 24 - offsetMskHour; - - if (referenceDate.Hour >= maxHour && referenceDate.Hour <= 23) - { - // это прошлые сутки - logger.LogDebug("ReferenceDate {refDate} - прошлые стуки", referenceDate); - return true; - } - else - { - // это текущие сутки - logger.LogDebug("ReferenceDate {refDate} - текущие стуки", referenceDate); - return false; - } - + throw new NotImplementedException(); } + public Task> 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 new file mode 100644 index 00000000..d9f76269 --- /dev/null +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs @@ -0,0 +1,495 @@ +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; + + public TemplateDistributorV2( + ILogger logger, + IWeekendDayService weekendDayService + ) + { + this.logger = logger; + this.weekendDayService = weekendDayService; + } + + public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends) + { + // 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: {referenceDateInTargetTz} в целевом поясе: {targetOffset}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDateInTargetTz, targetOffset, excludeWeekends); + + // 4. Получаем рабочие дни (календарные даты, не зависят от пояса) + var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); + + if (workDays.Count == 0) + { + logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate} (пояс {offset}). Возвращаем шаблоны со смещением {years} лет.", _startDate, _endDate, targetOffset, offsetYearsIfErrorCalcNextRun); + + // смещаем все nextRun вперед + var errorResult = allTemplates.Select(t => + FillTemplateResultWithOffsetError(t, _startDate, referenceDateInTargetTz, targetOffset) + ).OrderBy(t => t.NextRun).ToList(); + + logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", errorResult.Count, errorResult.Count(t => t.NextRun == t.NextRunOld), errorResult.Count(t => t.NextRun != t.NextRunOld)); + + return errorResult; + } + + + // 5. Строим план распределения + // строим план, дни на которые должны быть назанчены шаблоны + var plan = GetPlan(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) + { + // КОНВЕРТИРУЕМ В UTC ПЕРЕД СОХРАНЕНИЕМ! + templatesToKeep.Add(new TemplateNextRunResultDto + ( + Id: template.Id, + NextRun: template.NextRun.Value.ToOffset(TimeSpan.Zero), // ← В UTC! + NextRunOld: template.NextRun + )); + logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun} (UTC), т.к. нагрузка в день не превышена.", template.Id, template.NextRun.Value.ToOffset(TimeSpan.Zero)); + } + } + else + { + // Оставляем только expectedCount штук, остальные — переназначить + for (int i = 0; i < expectedCount; i++) + { + var template = templates[i]; + // КОНВЕРТИРУЕМ В UTC ПЕРЕД СОХРАНЕНИЕМ! + templatesToKeep.Add(new TemplateNextRunResultDto + ( + Id: template.Id, + NextRun: template.NextRun.Value.ToOffset(TimeSpan.Zero), // ← В UTC! + NextRunOld: template.NextRun + )); + logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun} (UTC), т.к. нагрузка в день не превышена.", template.Id, template.NextRun.Value.ToOffset(TimeSpan.Zero)); + } + + 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) + { + 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; + } + + + + /// + /// Заполнить 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); + } + + + + + + + /// + /// Получить план: дни, на которые должны быть назначены шаблоны + /// + /// + /// + /// + private List GetPlan(int totalWorks, List workDays) + { + var totalDays = workDays.Count(); + + var interval = GetInterval(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); + + // Защита от возможного выхода за границы массива из-за округления + if (dayIndex >= totalDays) + dayIndex = totalDays - 1; + #endregion + + var date = workDays[dayIndex]; + plan.Add(date); + } + + logger.LogDebug("--- План распределения ---"); + foreach (var item in plan.GroupBy(t => t)) + logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count()); + + return plan; + } + + + /// + /// Рассчитать интервал между шаблонами + /// + /// Кол-во рабочих дней + /// Кол-во шаблонов + /// + private double GetInterval(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; + } + + + } +} diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index 21a686e1..0d12389e 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -141,6 +141,7 @@ namespace PARR.DAL services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); #endregion diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index afaa3c1a..377468d1 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -14,20 +14,38 @@ namespace PARR.Test.NextRun private readonly INextRunService nextRunService; private readonly ITemplateService templateService; private readonly IShortcodesService shortcodesService; + private readonly ITemplateDistributorV2 templateDistributorV2; - public NextRunTest(/*ITemplateDistributor templateDistributor, */INextRunService nextRunService, ITemplateService templateService, IShortcodesService shortcodesService) + public NextRunTest( + /*ITemplateDistributor templateDistributor, */ + INextRunService nextRunService, + ITemplateService templateService, + IShortcodesService shortcodesService, + ITemplateDistributorV2 templateDistributorV2 + ) { //this.templateDistributor = templateDistributor; this.nextRunService = nextRunService; this.templateService = templateService; this.shortcodesService = shortcodesService; + this.templateDistributorV2 = templateDistributorV2; } - int periodDays = 10; - DateTimeOffset referenceDate = new DateTimeOffset(2025, 1, 20, 3, 0, 0, TimeSpan.Zero); + public async Task Test() { + var duration = 10; + var offset = TimeSpan.FromHours(10); + + var startDate = new DateOnly(2026, 2, 10); + var referenceDate = new DateTimeOffset(2026, 2, 10, 4, 0, 0, TimeSpan.Zero); + + var templates = GetTemplates(); + + var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); + + //var template = await templateService.Get().Include(t => t.Job).ThenInclude(t => t.Group).FirstAsync(t => t.Name == "ЭИТИ-ПАРР_ЦКИТ-ГВЦ_СХД ТО-1_СХД-AERODISK-432-2-4-U26-EN4SAG022-ГВЦ"); //var responseAreae = await shortcodesService.ApplyShortcodesAsync(template.Job.ResponseAreaMask, template); //var nextRunWithTimezone = nextRunService.GetNextRunWithTimezoneEsppAndResponseArea(template.NextRun, template.Job?.Group?.IsResponseAreaTimezone, responseAreae); @@ -117,14 +135,14 @@ namespace PARR.Test.NextRun } - private DateOnly GetDateStart() - { - var today = DateOnly.FromDateTime(DateTime.UtcNow.Date); + //private DateOnly GetDateStart() + //{ + // var today = DateOnly.FromDateTime(DateTime.UtcNow.Date); - var refDate = DateOnly.FromDateTime(referenceDate.Date); + // var refDate = DateOnly.FromDateTime(referenceDate.Date); - return today < refDate ? refDate : today; - } + // return today < refDate ? refDate : today; + //} //private DateOnly GetDateEnd() //{ @@ -143,7 +161,7 @@ namespace PARR.Test.NextRun templates.Add(new TemplateNextRunDto(Id: Guid.Parse("B397F1FE-DF2B-4B50-B8D6-2DABECD3D5A1"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("619D9621-2B45-4108-AF54-CF96862A8E9B"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("D74D831C-D435-46BF-9B5F-A24884AAA7DC"), NextRun: new DateTimeOffset(2025, 12, 25, 3, 0, 0, TimeSpan.Zero))); - templates.Add(new TemplateNextRunDto(Id: Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun: new DateTimeOffset(2026, 2, 13, 4, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("94A7375E-D1BD-49B7-9186-D26E85F1CF74"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("B3681F5F-10A5-425C-BBEA-EA2B85FE0967"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("3259B408-48A3-4995-AF9A-097477ECAB81"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); From e857248142c2d09813207a46eb2211771179174a Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Wed, 11 Feb 2026 17:01:23 +1000 Subject: [PATCH 03/14] =?UTF-8?q?feat(dal):=20ITemplateDistributorV2=20-?= =?UTF-8?q?=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20=D0=BF=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20nextRun?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IEsppScheduleTransformService.cs | 3 +- .../Subservices/ITemplateDistributorV2.cs | 18 +- .../Subservices/TemplateDistributorV2.cs | 680 +++++++++++++++++- PARR.Test/NextRun/NextRunTest.cs | 11 +- 4 files changed, 679 insertions(+), 33 deletions(-) diff --git a/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs b/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs index fc4e7d75..b7b5f1fd 100644 --- a/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs +++ b/PARR.DAL/NextRunServices/Subservices/IEsppScheduleTransformService.cs @@ -1,9 +1,10 @@ namespace PARR.DAL.NextRunServices.Subservices { + //todo: public->internal /// /// Сервис трансформации расписания ЕСПП в дату/расписание /// - internal interface IEsppScheduleTransformService + public interface IEsppScheduleTransformService { /// /// Получить следующую дату по jobGroupId diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs index 98137dd8..53aeb825 100644 --- a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs @@ -16,7 +16,21 @@ namespace PARR.DAL.NextRunServices.Subservices /// /// Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List allTemplates, bool excludeWeekends); - + + /// + /// Метод проверяет, актуален ли текущий NextRun шаблона, если не актуален, возвращает актуальное значение + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task GetNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew); + /// /// Получить список выходных дней /// @@ -24,7 +38,7 @@ namespace PARR.DAL.NextRunServices.Subservices /// /// Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate); - + /// /// Получить список рабочих дней /// diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs index d9f76269..c640b2ad 100644 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs @@ -29,6 +29,255 @@ namespace PARR.DAL.NextRunServices.Subservices this.weekendDayService = weekendDayService; } + + + public async Task GetNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew) + { + 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 workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); + + if (workDays.Count == 0) + { + logger.LogWarning("Нет рабочих дней в периоде {_startDate} - {_endDate} (пояс {offset}). Возвращаем ошибку со смещением.", _startDate, _endDate, targetOffset); + + 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) { // 1. Определяем целевой часовой пояс @@ -65,7 +314,7 @@ namespace PARR.DAL.NextRunServices.Subservices // 5. Строим план распределения // строим план, дни на которые должны быть назанчены шаблоны - var plan = GetPlan(allTemplates.Count, workDays); + var plan = GetDistributionPlan(allTemplates.Count, workDays); // Подсчитываем, сколько работ должно быть на каждый день var expectedDailyLoad = plan.GroupBy(d => d) .ToDictionary(g => g.Key, g => g.Count()); @@ -233,6 +482,383 @@ namespace PARR.DAL.NextRunServices.Subservices } + /// + /// Обрабатывает случай, когда у шаблона нет текущего 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 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] < 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, если нет рабочих дней со смещением несколько лет @@ -302,42 +928,41 @@ namespace PARR.DAL.NextRunServices.Subservices } - - - - /// - /// Получить план: дни, на которые должны быть назначены шаблоны + /// Получить план распределения шаблонов по рабочим дням. /// - /// - /// - /// - 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; @@ -350,11 +975,11 @@ 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; } @@ -490,6 +1115,5 @@ namespace PARR.DAL.NextRunServices.Subservices return result; } - } } diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index 377468d1..7b852db3 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -4,6 +4,7 @@ using PARR.DAL.NextRunServices; using PARR.DAL.NextRunServices.Models; using PARR.DAL.NextRunServices.Subservices; using PARR.DAL.Services.Interfaces; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace PARR.Test.NextRun @@ -39,11 +40,17 @@ namespace PARR.Test.NextRun var offset = TimeSpan.FromHours(10); var startDate = new DateOnly(2026, 2, 10); - var referenceDate = new DateTimeOffset(2026, 2, 10, 4, 0, 0, TimeSpan.Zero); + var referenceDate = new DateTimeOffset(2026, 2, 10, 6, 0, 0, TimeSpan.Zero); var templates = GetTemplates(); + var templatesEmpty = new List(); - var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); + // распределить шаблоны + //var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); + + // получить актуальный nextRun + var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 14, 5, 0, 0, TimeSpan.Zero)); + var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesEmpty, true, isNew: false); //var template = await templateService.Get().Include(t => t.Job).ThenInclude(t => t.Group).FirstAsync(t => t.Name == "ЭИТИ-ПАРР_ЦКИТ-ГВЦ_СХД ТО-1_СХД-AERODISK-432-2-4-U26-EN4SAG022-ГВЦ"); From a581db5787bd4ccb2629b9d2fff20dc105cf1d78 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Thu, 12 Feb 2026 10:07:53 +1000 Subject: [PATCH 04/14] =?UTF-8?q?feat(dal):=20ITemplateDistributorV2=20-?= =?UTF-8?q?=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20nextRun=20=D0=B5=D1=81=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=BE=D0=BD=20=D0=BD=D0=B0=D1=85=D0=BE=D0=B4=D0=B8=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5,=20=D0=BD=D0=BE=20=D0=B2=20=D0=B2=D1=8B=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=BD=D0=BE=D0=B9=20=D0=B4=D0=B5=D0=BD=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Subservices/TemplateDistributorV2.cs | 21 ++++++++++++++++--- PARR.Test/NextRun/NextRunTest.cs | 7 ++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs index c640b2ad..88efad7f 100644 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs @@ -590,6 +590,10 @@ namespace PARR.DAL.NextRunServices.Subservices // СЦЕНАРИЙ А: В периоде, но нерабочий день logger.LogDebug("Сценарий А: в периоде ({date}), но нерабочий день. Ищем слева...", nextRunDate); + // Вычисляем максимальную нагрузку по всему периоду + var maxLoadInPeriod = expectedDailyLoad.Values.Max(); + logger.LogDebug("Максимальная нагрузка по периоду: {maxLoad}", maxLoadInPeriod); + // Берём все рабочие дни СТРОГО ЛЕВЕЕ исходной даты var leftDays = workDays .Where(d => d < nextRunDate) @@ -600,17 +604,28 @@ namespace PARR.DAL.NextRunServices.Subservices { logger.LogDebug("Слева найдено {count} рабочих дней. Ищем первый не перегруженный...", leftDays.Count); - // Ищем первый не перегруженный + // Ищем первый день где текущая_нагрузка < максимальная_нагрузка_периода foreach (var day in leftDays) { - if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0)) + if (currentDailyLoad[day] < maxLoadInPeriod) // ← КЛЮЧЕВОЕ ИЗМЕНЕНИЕ { foundDay = day; - logger.LogDebug("Нашли слева не перегруженный день: {day}", 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) { diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index 7b852db3..344166d8 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -40,17 +40,18 @@ namespace PARR.Test.NextRun var offset = TimeSpan.FromHours(10); var startDate = new DateOnly(2026, 2, 10); - var referenceDate = new DateTimeOffset(2026, 2, 10, 6, 0, 0, TimeSpan.Zero); + var referenceDate = new DateTimeOffset(2026, 2, 12, 23, 0, 0, TimeSpan.Zero); var templates = GetTemplates(); var templatesEmpty = new List(); + var templatesToHandeler = templates; // templatesEmpty // распределить шаблоны //var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); // получить актуальный nextRun - var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 14, 5, 0, 0, TimeSpan.Zero)); - var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesEmpty, true, isNew: false); + var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 14, 23, 30, 0, TimeSpan.Zero)); + var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesToHandeler, true, isNew: false); //var template = await templateService.Get().Include(t => t.Job).ThenInclude(t => t.Group).FirstAsync(t => t.Name == "ЭИТИ-ПАРР_ЦКИТ-ГВЦ_СХД ТО-1_СХД-AERODISK-432-2-4-U26-EN4SAG022-ГВЦ"); From 999da9bf84b446be38491717ee24c8d4ada63892 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Thu, 12 Feb 2026 12:01:36 +1000 Subject: [PATCH 05/14] =?UTF-8?q?feat(dal):=20TemplateDistributorV2=20-=20?= =?UTF-8?q?=D0=B5=D1=81=D0=BB=D0=B8=20=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=20=D1=80=D0=B0=D1=81=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B5=D1=82=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B8=D1=85=20=D0=B4=D0=BD=D0=B5?= =?UTF-8?q?=D0=B9,=20=D1=82=D0=BE=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B6=D0=B5=D1=82=20=D1=80=D0=B0=D1=81=D1=88?= =?UTF-8?q?=D0=B8=D1=80=D0=B8=D1=82=D1=8C=D1=81=D1=8F=20=D0=B5=D1=89=D0=B5?= =?UTF-8?q?=202=20=D1=80=D0=B0=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Subservices/ITemplateDistributorV2.cs | 10 +- .../Subservices/TemplateDistributorV2.cs | 218 ++++++++++++++---- PARR.Test/NextRun/NextRunTest.cs | 21 +- 3 files changed, 185 insertions(+), 64 deletions(-) diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs index 53aeb825..b98dbdc9 100644 --- a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributorV2.cs @@ -19,6 +19,7 @@ namespace PARR.DAL.NextRunServices.Subservices /// /// Метод проверяет, актуален ли текущий NextRun шаблона, если не актуален, возвращает актуальное значение + /// При проверке загружаются шаблоны из БД! Если хотим рассчитывать nextRun для НЕСКОЛЬКИХ шаблонов, то следует рассчитать nextRun для одного шаблона, сохранить в БД, затем рассчитать для следующего /// /// /// @@ -38,14 +39,5 @@ namespace PARR.DAL.NextRunServices.Subservices /// /// Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate); - - /// - /// Получить список рабочих дней - /// - /// - /// - /// - /// - Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends); } } diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs index 88efad7f..f22b34fe 100644 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributorV2.cs @@ -20,19 +20,27 @@ namespace PARR.DAL.NextRunServices.Subservices /// private const int offsetYearsIfErrorCalcNextRun = 5; - public TemplateDistributorV2( - ILogger logger, - IWeekendDayService weekendDayService - ) + /// + /// Максимальное количество попыток расширения периода при отсутствии рабочих дней + /// + 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. Определяем целевой часовой пояс @@ -48,13 +56,15 @@ namespace PARR.DAL.NextRunServices.Subservices logger.LogInformation("Период распределения: {_startDate} - {_endDate} (в поясе {offset})", _startDate, _endDate, targetOffset); - // 3. Получаем список рабочих дней в периоде - var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); + // 3. Получаем список рабочих дней с расширением периода при необходимости + var (expandedEndDate, workDays) = await GetWorkDaysWithExpansionAsync(_startDate, duration, excludeWeekends); + + // Обновляем _endDate на расширенный период для дальнейших расчётов + _endDate = expandedEndDate; if (workDays.Count == 0) { - logger.LogWarning("Нет рабочих дней в периоде {_startDate} - {_endDate} (пояс {offset}). Возвращаем ошибку со смещением.", _startDate, _endDate, targetOffset); - + logger.LogError("Не удалось найти рабочие дни даже после {maxAttempts} попыток расширения периода [{start}, {end}]. Возвращаем ошибку со смещением.", maxPeriodExpansions, _startDate, _endDate); return FillTemplateResultWithOffsetError(targetTemplate, _startDate, referenceDateInTargetTz, targetOffset); } @@ -280,6 +290,14 @@ namespace PARR.DAL.NextRunServices.Subservices /// 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; @@ -292,26 +310,26 @@ namespace PARR.DAL.NextRunServices.Subservices var _startDate = GetStartPeriodDate(startDate, referenceDateInTargetTz, targetOffset); var _endDate = GetEndPeriodDate(_startDate, duration); - logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDateInTargetTz} в целевом поясе: {targetOffset}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDateInTargetTz, targetOffset, excludeWeekends); + logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDateInTargetTz: {referenceDateInTargetTz} в целевом поясе: {targetOffset} ReferenceDate UTC: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDateInTargetTz, targetOffset, referenceDate, excludeWeekends); - // 4. Получаем рабочие дни (календарные даты, не зависят от пояса) - var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); + // 4. Получаем рабочие дни с расширением периода при необходимости + var (expandedEndDate, workDays) = await GetWorkDaysWithExpansionAsync(_startDate, duration, excludeWeekends); + + // Обновляем _endDate на расширенный период для дальнейших расчётов + _endDate = expandedEndDate; if (workDays.Count == 0) { - logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate} (пояс {offset}). Возвращаем шаблоны со смещением {years} лет.", _startDate, _endDate, targetOffset, offsetYearsIfErrorCalcNextRun); + logger.LogError("Не удалось найти рабочие дни даже после {maxAttempts} попыток расширения периода [{start}, {end}]. Возвращаем шаблоны со смещением {years} лет.", maxPeriodExpansions, _startDate, _endDate, offsetYearsIfErrorCalcNextRun); - // смещаем все nextRun вперед var errorResult = allTemplates.Select(t => - FillTemplateResultWithOffsetError(t, _startDate, referenceDateInTargetTz, targetOffset) - ).OrderBy(t => t.NextRun).ToList(); - - logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", errorResult.Count, errorResult.Count(t => t.NextRun == t.NextRunOld), errorResult.Count(t => t.NextRun != t.NextRunOld)); + FillTemplateResultWithOffsetError(t, _startDate, referenceDateInTargetTz, targetOffset) + ).OrderBy(t => t.NextRun).ToList(); + logger.LogInformation("Распределение завершено с ошибкой. Возвращаем {Count} записей.", errorResult.Count); return errorResult; } - // 5. Строим план распределения // строим план, дни на которые должны быть назанчены шаблоны var plan = GetDistributionPlan(allTemplates.Count, workDays); @@ -355,8 +373,8 @@ namespace PARR.DAL.NextRunServices.Subservices #endregion - #region 8. Определяем, какие шаблоны нужно оставить, а какие — переназначить + #region 8. Определяем, какие шаблоны нужно оставить, а какие — переназначить var templatesToKeep = new List(); var templatesToReassign = new List(); @@ -381,33 +399,117 @@ namespace PARR.DAL.NextRunServices.Subservices if (currentCount <= expectedCount) { - // Все шаблоны в этом дне можно оставить + // Все шаблоны в этом дне можно оставить, но нужно проверить/скорректировать время foreach (var template in templates) { - // КОНВЕРТИРУЕМ В UTC ПЕРЕД СОХРАНЕНИЕМ! - 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.ToOffset(TimeSpan.Zero), // ← В UTC! + NextRun: newNextRun, NextRunOld: template.NextRun )); - logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun} (UTC), т.к. нагрузка в день не превышена.", template.Id, template.NextRun.Value.ToOffset(TimeSpan.Zero)); } } else { // Оставляем только expectedCount штук, остальные — переназначить + // (для оставляемых тоже нужно проверить/скорректировать время) for (int i = 0; i < expectedCount; i++) { var template = templates[i]; - // КОНВЕРТИРУЕМ В UTC ПЕРЕД СОХРАНЕНИЕМ! - 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.ToOffset(TimeSpan.Zero), // ← В UTC! + NextRun: newNextRun, NextRunOld: template.NextRun )); - logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun} (UTC), т.к. нагрузка в день не превышена.", template.Id, template.NextRun.Value.ToOffset(TimeSpan.Zero)); } for (int i = expectedCount; i < currentCount; i++) @@ -418,9 +520,9 @@ namespace PARR.DAL.NextRunServices.Subservices } } } - #endregion + #region 9. Добавляем шаблоны, у которых не было даты — тоже нужно распределить foreach (var template in allTemplates) @@ -777,16 +879,7 @@ namespace PARR.DAL.NextRunServices.Subservices /// Если момент времени уже прошёл, ищет следующий рабочий день в будущем с учётом нагрузки. /// В случае ошибки (нет будущих рабочих дней) возвращает ошибку через out-параметр. /// - private DateTimeOffset? AdjustNextRunToFuture( - DateOnly foundDay, - List workDays, - Dictionary currentDailyLoad, - Dictionary expectedDailyLoad, - DateTimeOffset referenceDateInTargetTz, - TimeSpan targetOffset, - DateOnly startDateForError, - TemplateNextRunDto targetTemplate, - out TemplateNextRunResultDto? errorResult) + private DateTimeOffset? AdjustNextRunToFuture(DateOnly foundDay, List workDays, Dictionary currentDailyLoad, Dictionary expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, DateOnly startDateForError, TemplateNextRunDto targetTemplate, out TemplateNextRunResultDto? errorResult) { errorResult = null; @@ -1103,7 +1196,7 @@ namespace PARR.DAL.NextRunServices.Subservices /// /// Исключить выходные и праздники /// - public async Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends) + private async Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends) { // Получаем список выходных/праздничных дней var weekends = new HashSet(); @@ -1130,5 +1223,44 @@ namespace PARR.DAL.NextRunServices.Subservices 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.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index 344166d8..2444d03a 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -1,11 +1,8 @@ -using Microsoft.EntityFrameworkCore; -using PARR.DAL.DomainServices.Shortcodes; +using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.NextRunServices; using PARR.DAL.NextRunServices.Models; using PARR.DAL.NextRunServices.Subservices; using PARR.DAL.Services.Interfaces; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; namespace PARR.Test.NextRun { @@ -36,22 +33,22 @@ namespace PARR.Test.NextRun public async Task Test() { - var duration = 10; - var offset = TimeSpan.FromHours(10); + var duration = 2; + var offset = TimeSpan.FromHours(3); var startDate = new DateOnly(2026, 2, 10); - var referenceDate = new DateTimeOffset(2026, 2, 12, 23, 0, 0, TimeSpan.Zero); + var referenceDate = new DateTimeOffset(2026, 2, 13, 23, 0, 0, TimeSpan.Zero); var templates = GetTemplates(); var templatesEmpty = new List(); - var templatesToHandeler = templates; // templatesEmpty + var templatesToHandeler = templatesEmpty; // templatesEmpty // templates // распределить шаблоны - //var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); + var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); // получить актуальный nextRun - var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 14, 23, 30, 0, TimeSpan.Zero)); - var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesToHandeler, true, isNew: false); + var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 15, 23, 30, 0, TimeSpan.Zero)); + //var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesToHandeler, true, isNew: false); //var template = await templateService.Get().Include(t => t.Job).ThenInclude(t => t.Group).FirstAsync(t => t.Name == "ЭИТИ-ПАРР_ЦКИТ-ГВЦ_СХД ТО-1_СХД-AERODISK-432-2-4-U26-EN4SAG022-ГВЦ"); @@ -169,7 +166,7 @@ namespace PARR.Test.NextRun templates.Add(new TemplateNextRunDto(Id: Guid.Parse("B397F1FE-DF2B-4B50-B8D6-2DABECD3D5A1"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("619D9621-2B45-4108-AF54-CF96862A8E9B"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("D74D831C-D435-46BF-9B5F-A24884AAA7DC"), NextRun: new DateTimeOffset(2025, 12, 25, 3, 0, 0, TimeSpan.Zero))); - templates.Add(new TemplateNextRunDto(Id: Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun: new DateTimeOffset(2026, 2, 13, 4, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun: new DateTimeOffset(2026, 2, 16, 23, 30, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("94A7375E-D1BD-49B7-9186-D26E85F1CF74"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("B3681F5F-10A5-425C-BBEA-EA2B85FE0967"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); templates.Add(new TemplateNextRunDto(Id: Guid.Parse("3259B408-48A3-4995-AF9A-097477ECAB81"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); From 858dd8c31cdd926a31b5cfc954d129e58a462245 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Thu, 12 Feb 2026 16:15:36 +1000 Subject: [PATCH 06/14] =?UTF-8?q?feat(dal):=20NextRunServiceV2=20-=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B2=20=D1=80=D0=B0=D0=BC=D0=BA=D0=B0=D1=85=20JobGroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.DAL/NextRunServices/INextRunServiceV2.cs | 14 + PARR.DAL/NextRunServices/NextRunServiceV2.cs | 240 ++++++++++++++++++ PARR.DAL/ParrDalInstaller.cs | 1 + .../ScheduleResponseAreaTimeOffsetService.cs | 20 +- PARR.Test/NextRun/NextRunTest.cs | 39 ++- 5 files changed, 299 insertions(+), 15 deletions(-) create mode 100644 PARR.DAL/NextRunServices/INextRunServiceV2.cs create mode 100644 PARR.DAL/NextRunServices/NextRunServiceV2.cs diff --git a/PARR.DAL/NextRunServices/INextRunServiceV2.cs b/PARR.DAL/NextRunServices/INextRunServiceV2.cs new file mode 100644 index 00000000..b8bdbb86 --- /dev/null +++ b/PARR.DAL/NextRunServices/INextRunServiceV2.cs @@ -0,0 +1,14 @@ +using PARR.DAL.NextRunServices.Models; + +namespace PARR.DAL.NextRunServices +{ + public interface INextRunServiceV2 + { + /// + /// Распределить шаблоны в группе работ (получить список распределенных шаблонов с актуальными nextRun) + /// + /// + /// Список TemplateId с NextRun и NextRunOld + Task?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId); + } +} diff --git a/PARR.DAL/NextRunServices/NextRunServiceV2.cs b/PARR.DAL/NextRunServices/NextRunServiceV2.cs new file mode 100644 index 00000000..515102e2 --- /dev/null +++ b/PARR.DAL/NextRunServices/NextRunServiceV2.cs @@ -0,0 +1,240 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging; +using NodaTime; +using PARR.Constants; +using PARR.DAL.Contracts; +using PARR.DAL.DomainServices.Shortcodes; +using PARR.DAL.Models; +using PARR.DAL.Models.Job; +using PARR.DAL.NextRunServices.Models; +using PARR.DAL.NextRunServices.Subservices; +using PARR.DAL.Services.Interfaces; +using PARR.DAL.Services.Interfaces.Job; +using PARR.DAL.Services.Interfaces.Schedule; + +namespace PARR.DAL.NextRunServices +{ + internal class NextRunServiceV2 : INextRunServiceV2 + { + private readonly ILogger logger; + private readonly ITemplateService templateService; + private readonly IJobGroupService jobGroupService; + private readonly IEsppScheduleTransformService esppScheduleTransformService; + private readonly ITemplateDistributorV2 templateDistributor; + private readonly IShortcodesService shortcodesService; + private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; + private readonly SettingsFromDb settingsFromDb; + + public NextRunServiceV2( + ILogger logger, + ITemplateService templateService, + IJobGroupService jobGroupService, + IEsppScheduleTransformService esppScheduleTransformService, + ITemplateDistributorV2 templateDistributor, + IShortcodesService shortcodesService, + IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService, + SettingsFromDb settingsFromDb + ) + { + this.logger = logger; + this.templateService = templateService; + this.jobGroupService = jobGroupService; + this.esppScheduleTransformService = esppScheduleTransformService; + this.templateDistributor = templateDistributor; + this.shortcodesService = shortcodesService; + this.scheduleResponseAreaTimeOffsetService = scheduleResponseAreaTimeOffsetService; + this.settingsFromDb = settingsFromDb; + } + + public async Task?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) + { + logger.LogInformation("Начинаю распределять шаблоны для группы работ {gobGroupId}", jobGroupId); + + var jobGroup = await jobGroupService.Get() + .Include(t => t.DistributionConfig) + .ThenInclude(t => t.DistributionPeriod) + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Id == jobGroupId); + + if (jobGroup == null) + { + logger.LogError("Не найдена группа работа с id: {id}", jobGroupId); + return null; + } + + if (!jobGroup.IsAutoDistributionEnabled || jobGroup.DistributionConfig == null) + { + logger.LogError("Группа работ id: {id} не подходит для автораспределения, у нее или отсутствуют настройки или не включено автораспределение. IsAutoDistributionEnabled: {IsAutoDistributionEnabled}. Есть конфиг: {DistributionConfig}", jobGroupId, jobGroup.IsAutoDistributionEnabled, jobGroup.DistributionConfig != null); + return null; + } + + logger.LogInformation("Параметры распределения. groupId: {groupId}, name: {groupName}, referenceDate: {referenceDate}, " + + "distributionPeriodName: {distributionPeriodName}, distributionPeriodDuration: {distributionPeriodDuration}, " + + "distributionPeriodType: {distributionPeriodType}, IsExcludeWeekends: {IsExcludeWeekends}, IsGroupingByWorkGroup: {IsGroupingByWorkGroup}, IsResponseAreaTimezone: {IsResponseAreaTimezone}", + jobGroupId, jobGroup.GroupName, jobGroup.ReferenceDate, jobGroup.DistributionConfig.DistributionPeriod.Name, jobGroup.DistributionConfig.DistributionPeriod.Duration, jobGroup.DistributionConfig.DistributionPeriod.Type, + jobGroup.DistributionConfig.IsExcludeWeekends, jobGroup.DistributionConfig.IsGroupingByWorkGroup, jobGroup.IsResponseAreaTimezone); + + //получаем шаблоны только в статусе Used + var allTemplates = await templateService.Get() + .AsNoTracking() + .Include(t => t.Job) + .Where(t => + t.Job!.GroupId == jobGroupId + && t.StatusTypeId == TemplateStatusTypeEnum.Used + ).ToListAsync(); + if (!allTemplates.Any()) + { + logger.LogInformation("В группе c ИД {jobGroupId} отсутствуют шаблоны в статусе Used", jobGroupId); + return null; + } + + logger.LogDebug("Всего шаблонов для распределения в статусе Used: {count} шт.", allTemplates.Count); + + + // продолжительность в днях + var durationDays = GetDurationDays(jobGroup.DistributionConfig); + + // группировать по РГ + var isGroupingByWorkGroup = jobGroup.DistributionConfig.IsGroupingByWorkGroup; + + // использовать часовой пояс РГ + var isResponseAreaTimeZone = jobGroup.IsResponseAreaTimezone; + + var distributedTemplates = new List(); + + if (isGroupingByWorkGroup) + { + // группировать по РГ + + if (isResponseAreaTimeZone) + { + // использовать часовой пояс РГ + } + else + { + // не нужно использовать часовой пояс РГ (используем часовой пояс УЗ ЕСПП) + } + } + else + { + //не нужно группировать по РГ + + if (isResponseAreaTimeZone) + { + // использовать ЧАСОВОЙ ПОЯС РГ (нужно сгруппировать шаблоны по часовым поясам, затем отдельно распределить каждую группу) + } + else + { + // не нужно использовать часовой пояс РГ + + var offset = GetOffsetForDistribution(); + var templatesForDistribute = allTemplates.Select(t => new TemplateNextRunDto(t.Id, t.NextRun)).ToList(); + var dateStart = GetDateStart(offset); + + logger.LogInformation("Буду распределать шаблоны {count} шт, их НЕ НУЖНО группировать по РГ, НЕ НУЖНО использовать часовой пояс РГ. dateStart в целевом часовом поясе: {dateStart}, offset: {offset}", + templatesForDistribute.Count, dateStart, offset); + + distributedTemplates = await templateDistributor.DistributeTemplatesAsync(dateStart, durationDays, jobGroup.ReferenceDate, offset, templatesForDistribute, jobGroup.DistributionConfig.IsExcludeWeekends); + } + } + + return distributedTemplates; + } + + + /// + /// Получить продолжительность в днях + /// + /// + /// + private int GetDurationDays(JobGroupDistributionConfig config) + { + var period = config.DistributionPeriod; + var periodType = period!.Type; + int.TryParse(period.Duration, out var duration); + + var periodDays = duration; + + if (periodType == DistributionPeriodTypeEnum.Day.ToString()) + periodDays = duration; + + + if (periodType == DistributionPeriodTypeEnum.Month.ToString()) + // в месяце 30 дней, duration*30 + periodDays = duration * 30; + + + if (periodType == DistributionPeriodTypeEnum.Year.ToString()) + // в году 365 дней, duration*365 + periodDays = duration * 365; + + + logger.LogDebug("Период распределения: {name}, duration: {duration}, type: {type}. Итого в днях: {days}", period.Name, period.Duration, period.Type, periodDays); + + if (periodDays == 0) + { + periodDays = 1; + logger.LogWarning("Полученный период распределения 0 дней. Неверный конфиг распределения в таблице {table}. {name}, duration: {duration}, type: {type}. Устанавливаем минимальный период распределения {periodDays} дней.", nameof(DistributionPeriod), period.Name, period.Duration, period.Type, periodDays); + } + + return periodDays; + } + + + /// + /// Получить часовую зону для распределения + /// + /// + private TimeSpan GetOffsetForDistribution(string? responseArea = null) + { + // или часова зона робота, или часовая зона РГ + if (responseArea != null) + { + // возвращаем часовой пояс ЗО + var responseAreaOffset = scheduleResponseAreaTimeOffsetService.GetByResponseAreaOrDefault(responseArea).UtcTimeOffset; + logger.LogDebug("Получил часовой пояс для ЗО: {responseArea}, UtcTimeOffset: {responseAreaOffset}", responseArea, responseAreaOffset); + + return responseAreaOffset; + } + else + { + // возвращаем часовой пояс робота + var esppOffset = GetEsppAccountOffset(); + logger.LogDebug("Не передана ЗО, использую часовой пояс УЗ ЕСПП, UtcTimeOffset: {UtcTimeOffset}", esppOffset); + + return esppOffset; + } + } + + + /// + /// Получить часовой пояс УЗ ЕСПП + /// + /// + private TimeSpan GetEsppAccountOffset() + { + var esppOffset = TimeSpan.FromHours(settingsFromDb.EsppRobotAccountTimeZoneHour); + logger.LogDebug("Оффсет УЗ ЕСПП: {esppOffset}", esppOffset); + + return esppOffset; + } + + + /// + /// Получить дату начала распределения в целевом часовом поясе + /// + /// + /// + private DateOnly GetDateStart(TimeSpan offset) + { + var dateStart = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).Date); + logger.LogDebug("Дата начала распределения (сегодня) в целевом часовом поясе {dateStart}, offset: {offset}", dateStart, offset); + + return dateStart; + } + + + + } +} diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index 0d12389e..e329d200 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -142,6 +142,7 @@ namespace PARR.DAL services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); #endregion diff --git a/PARR.DAL/Services/Implementations/Schedule/ScheduleResponseAreaTimeOffsetService.cs b/PARR.DAL/Services/Implementations/Schedule/ScheduleResponseAreaTimeOffsetService.cs index 5a6a9ac2..8a05b428 100644 --- a/PARR.DAL/Services/Implementations/Schedule/ScheduleResponseAreaTimeOffsetService.cs +++ b/PARR.DAL/Services/Implementations/Schedule/ScheduleResponseAreaTimeOffsetService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using PARR.DAL.Context; using PARR.DAL.Contracts; using PARR.DAL.Models.Schedule; @@ -17,8 +18,9 @@ namespace PARR.DAL.Services.Implementations.Schedule /// Настройки, если не нашли в offsetList /// private readonly ScheduleResponseAreaTimeOffset defaultOffset; + private readonly ILogger logger; - public ScheduleResponseAreaTimeOffsetService(DataContext dataContext, SettingsFromDb settingsFromDb) + public ScheduleResponseAreaTimeOffsetService(DataContext dataContext, SettingsFromDb settingsFromDb, ILogger logger) { offsetList = dataContext.ScheduleResponseAreaTimeOffsets .AsNoTracking() @@ -36,6 +38,7 @@ namespace PARR.DAL.Services.Implementations.Schedule EsppValue = "", UtcTimeOffset = new TimeSpan(3, 0, 0) }; + this.logger = logger; } @@ -45,7 +48,20 @@ namespace PARR.DAL.Services.Implementations.Schedule public ScheduleResponseAreaTimeOffset GetByResponseAreaOrDefault(string responseArea) { - return offsetList.TryGetValue(responseArea, out var value) ? value : defaultOffset; + offsetList.TryGetValue(responseArea, out var value); + + if (value != null) + { + logger.LogDebug("Получил часовой пояс по ЗО '{responseArea}', esppValue: {EsppValue}, utcTimeOffset: {utcTimeOffset}", responseArea, value.EsppValue, value.UtcTimeOffset); + return value; + } + else + { + logger.LogWarning("Не смог получить часовой пояс по ЗО '{responseArea}', вернул часово пояс по умолчанию: name: {name}, esppValue: {EsppValue}, utcTimeOffset: {utcTimeOffset}", + responseArea, defaultOffset.ResponseArea, defaultOffset.EsppValue, defaultOffset.UtcTimeOffset); + return defaultOffset; + } + //return offsetList.TryGetValue(responseArea, out var value) ? value : defaultOffset; } diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index 2444d03a..a960e90c 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -13,13 +13,15 @@ namespace PARR.Test.NextRun private readonly ITemplateService templateService; private readonly IShortcodesService shortcodesService; private readonly ITemplateDistributorV2 templateDistributorV2; + private readonly INextRunServiceV2 nextRunServiceV2; public NextRunTest( /*ITemplateDistributor templateDistributor, */ INextRunService nextRunService, ITemplateService templateService, IShortcodesService shortcodesService, - ITemplateDistributorV2 templateDistributorV2 + ITemplateDistributorV2 templateDistributorV2, + INextRunServiceV2 nextRunServiceV2 ) { //this.templateDistributor = templateDistributor; @@ -27,29 +29,40 @@ namespace PARR.Test.NextRun this.templateService = templateService; this.shortcodesService = shortcodesService; this.templateDistributorV2 = templateDistributorV2; + this.nextRunServiceV2 = nextRunServiceV2; } public async Task Test() { - var duration = 2; - var offset = TimeSpan.FromHours(3); + #region тестирование INextRunServiceV2 - var startDate = new DateOnly(2026, 2, 10); - var referenceDate = new DateTimeOffset(2026, 2, 13, 23, 0, 0, TimeSpan.Zero); + var distributeResult = await nextRunServiceV2.GetNextRunForJobGroupWithAutoDistributionAsync(Guid.Parse("d25d8a9e-898f-417a-9bec-ef3a356e7c94")); - var templates = GetTemplates(); - var templatesEmpty = new List(); - var templatesToHandeler = templatesEmpty; // templatesEmpty // templates + #endregion - // распределить шаблоны - var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); - // получить актуальный nextRun - var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 15, 23, 30, 0, TimeSpan.Zero)); - //var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesToHandeler, true, isNew: false); + #region Тестирование templateDistributorV2 + //var duration = 2; + //var offset = TimeSpan.FromHours(3); + + //var startDate = new DateOnly(2026, 2, 10); + //var referenceDate = new DateTimeOffset(2026, 2, 13, 23, 0, 0, TimeSpan.Zero); + + //var templates = GetTemplates(); + //var templatesEmpty = new List(); + //var templatesToHandeler = templatesEmpty; // templatesEmpty // templates + + //// распределить шаблоны + //var distributedTemplates = await templateDistributorV2.DistributeTemplatesAsync(startDate, duration, referenceDate, offset, templates, true); + + //// получить актуальный nextRun + //var targetTemplate = new TemplateNextRunDto(Guid.Parse("DA151719-2742-4260-BFFE-012B61591053"), new DateTimeOffset(2026, 2, 15, 23, 30, 0, TimeSpan.Zero)); + ////var validNextRun = await templateDistributorV2.GetNextRunForTemplateAsync(startDate, duration, referenceDate, offset, targetTemplate, templatesToHandeler, true, isNew: false); + + #endregion //var template = await templateService.Get().Include(t => t.Job).ThenInclude(t => t.Group).FirstAsync(t => t.Name == "ЭИТИ-ПАРР_ЦКИТ-ГВЦ_СХД ТО-1_СХД-AERODISK-432-2-4-U26-EN4SAG022-ГВЦ"); //var responseAreae = await shortcodesService.ApplyShortcodesAsync(template.Job.ResponseAreaMask, template); From 89b6cdf73171de4ddaf2f9a94d8982420c85a455 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Fri, 13 Feb 2026 15:39:21 +1000 Subject: [PATCH 07/14] =?UTF-8?q?feat(dal,=20TemplateDistributorWorker):?= =?UTF-8?q?=20NextRunServiceV2=20-=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20Te?= =?UTF-8?q?mplateDistributorWorker=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B9=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.DAL/NextRunServices/INextRunServiceV2.cs | 8 +- PARR.DAL/NextRunServices/NextRunServiceV2.cs | 237 +++++++++++++++--- .../Subservices/ITemplateDistributorV2.cs | 4 +- .../Subservices/TemplateDistributorV2.cs | 2 +- PARR.DAL/ParrDalInstaller.cs | 4 +- .../TemplateDistributor.cs | 14 +- PARR.Test/NextRun/NextRunTest.cs | 11 +- 7 files changed, 234 insertions(+), 46 deletions(-) diff --git a/PARR.DAL/NextRunServices/INextRunServiceV2.cs b/PARR.DAL/NextRunServices/INextRunServiceV2.cs index b8bdbb86..f101651e 100644 --- a/PARR.DAL/NextRunServices/INextRunServiceV2.cs +++ b/PARR.DAL/NextRunServices/INextRunServiceV2.cs @@ -1,4 +1,5 @@ -using PARR.DAL.NextRunServices.Models; +using PARR.Constants; +using PARR.DAL.NextRunServices.Models; namespace PARR.DAL.NextRunServices { @@ -7,8 +8,9 @@ namespace PARR.DAL.NextRunServices /// /// Распределить шаблоны в группе работ (получить список распределенных шаблонов с актуальными nextRun) /// - /// + /// ИД группы работ + /// Статус шаблона, если не передать, берутся все шаблоны группы /// Список TemplateId с NextRun и NextRunOld - Task?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId); + Task?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId, TemplateStatusTypeEnum? templateStatusType); } } diff --git a/PARR.DAL/NextRunServices/NextRunServiceV2.cs b/PARR.DAL/NextRunServices/NextRunServiceV2.cs index 515102e2..97d0638c 100644 --- a/PARR.DAL/NextRunServices/NextRunServiceV2.cs +++ b/PARR.DAL/NextRunServices/NextRunServiceV2.cs @@ -1,7 +1,5 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; -using NodaTime; using PARR.Constants; using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Shortcodes; @@ -47,14 +45,15 @@ namespace PARR.DAL.NextRunServices this.settingsFromDb = settingsFromDb; } - public async Task?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) + + public async Task?> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId, TemplateStatusTypeEnum? templateStatusType) { - logger.LogInformation("Начинаю распределять шаблоны для группы работ {gobGroupId}", jobGroupId); + logger.LogInformation("Начинаю распределять шаблоны для группы работ {gobGroupId}, templateStatusType: {templateStatusType}", jobGroupId, templateStatusType); var jobGroup = await jobGroupService.Get() + .AsNoTracking() .Include(t => t.DistributionConfig) .ThenInclude(t => t.DistributionPeriod) - .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == jobGroupId); if (jobGroup == null) @@ -76,13 +75,20 @@ namespace PARR.DAL.NextRunServices jobGroup.DistributionConfig.IsExcludeWeekends, jobGroup.DistributionConfig.IsGroupingByWorkGroup, jobGroup.IsResponseAreaTimezone); //получаем шаблоны только в статусе Used - var allTemplates = await templateService.Get() - .AsNoTracking() - .Include(t => t.Job) - .Where(t => - t.Job!.GroupId == jobGroupId - && t.StatusTypeId == TemplateStatusTypeEnum.Used - ).ToListAsync(); + var templateQuery = templateService.Get() + .AsNoTracking() + .Include(t => t.Job) + .Where(t => + t.Job!.GroupId == jobGroupId + //&& t.StatusTypeId == TemplateStatusTypeEnum.Used + ); + + if (templateStatusType.HasValue) + templateQuery = templateQuery.Where(t => t.StatusTypeId == templateStatusType); + + + var allTemplates = await templateQuery.ToListAsync(); + if (!allTemplates.Any()) { logger.LogInformation("В группе c ИД {jobGroupId} отсутствуют шаблоны в статусе Used", jobGroupId); @@ -106,39 +112,150 @@ namespace PARR.DAL.NextRunServices if (isGroupingByWorkGroup) { // группировать по РГ + logger.LogDebug("Нужно группировать по РГ"); - if (isResponseAreaTimeZone) + // получаем для каждого шаблона РГ и сразу группируем по РГ + var templatesByWorkGroup = new Dictionary>(); + foreach (var template in allTemplates) { - // использовать часовой пояс РГ + var workGroupName = await shortcodesService.ApplyShortcodesAsync(template.Job!.WorkGroupMask, template); + // Если даже workGroupName == null, ну и ладно, сгруппируем по null, и распределим шаблоны в рамках этого null + if (workGroupName == null) + logger.LogWarning("Для шаблона {id}, с маской РГ '{workGroupMask}', не смог с помощью шорткода определить рабочую группу, шорткод сервис вернул РГ: '{workGroupName}'", + template.Id, template.Job!.WorkGroupMask, workGroupName); + + // Добавляем шаблон в группу + if (!templatesByWorkGroup.TryGetValue(workGroupName, out var templatesList)) + { + templatesList = new List