diff --git a/PARR.BLL/Domain/Mq/TemplateGeneratorWorkerMq.cs b/PARR.BLL/Domain/Mq/TemplateGeneratorMq.cs similarity index 71% rename from PARR.BLL/Domain/Mq/TemplateGeneratorWorkerMq.cs rename to PARR.BLL/Domain/Mq/TemplateGeneratorMq.cs index 9029aca1..372c2ffc 100644 --- a/PARR.BLL/Domain/Mq/TemplateGeneratorWorkerMq.cs +++ b/PARR.BLL/Domain/Mq/TemplateGeneratorMq.cs @@ -5,7 +5,7 @@ namespace PARR.BLL.Domain.Mq /// /// Модель в MQ, простого создания Template /// - public class TemplateGeneratorWorkerMq + public class TemplateGeneratorMq { /// /// Id регламентной работы @@ -32,5 +32,15 @@ namespace PARR.BLL.Domain.Mq /// public HistoryInitiator? HistoryInitiator { get; set; } + /// + /// Связанные ЭК для сгруппированного типа JobGroup + /// + public required List UnitsInTemplate { get; set; } + + /// + /// Индекс, используется в групповых шаблонах + /// + public int? Index { get; set; } + } } diff --git a/PARR.BLL/Domain/Mq/TemplateUpdaterMq.cs b/PARR.BLL/Domain/Mq/TemplateUpdaterMq.cs index 8c83ea01..d2de59aa 100644 --- a/PARR.BLL/Domain/Mq/TemplateUpdaterMq.cs +++ b/PARR.BLL/Domain/Mq/TemplateUpdaterMq.cs @@ -7,24 +7,47 @@ namespace PARR.BLL.Domain.Mq { public Guid TemplateId { get; set; } + /// + /// Id регламентной работы + /// public Guid JobId { get; set; } public required string Name { get; set; } + /// + /// Актировать шаблон при инициализации + /// public bool IsActiveTemplate { get; set; } + /// + /// Актировать расписание при инициализации + /// public bool IsActiveSchedule { get; set; } public DateTimeOffset? LastRun { get; set; } public DateTimeOffset NextRun { get; set; } + /// + /// Id Юнита(единицы обслуживания)/ЭК + /// public Guid UnitId { get; set; } + /// + /// Индекс, используется в групповых шаблонах + /// public int? Index { get; set; } public TemplateStatusTypeEnum StatusTypeId { get; set; } + /// + /// Инициатор запроса к генератору + /// public required HistoryInitiator Initiator { get; set; } + + /// + /// Связанные ЭК для сгруппированного типа JobGroup + /// + public required List UnitsInTemplate { get; set; } } } diff --git a/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs b/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs index df9a78c1..7b9f482e 100644 --- a/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs +++ b/PARR.DAL/DomainServices/Implementations/ShortcodesService.cs @@ -48,7 +48,7 @@ namespace PARR.DAL.DomainServices.Implementations this.unitFilterService = unitFilterService; } - public async Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId) + public async Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId, int? index = null) { logger.LogDebug("Начата подстановка шорткодов. Вход: '{Input}', unitId={UnitId}, jobId={JobId}", str, unitId, jobId); @@ -95,7 +95,7 @@ namespace PARR.DAL.DomainServices.Implementations // Делаем замену var oldResult = resultName; - resultName = ReplaceStandardShortcodes(job, unit, resultName); + resultName = ReplaceStandardShortcodes(job, unit, resultName, index); iteration++; // Защита от "бесполезных" итераций (строка не изменилась) @@ -262,14 +262,15 @@ namespace PARR.DAL.DomainServices.Implementations } - private static string ReplaceStandardShortcodes(Job job, Unit unit, string input) + private static string ReplaceStandardShortcodes(Job job, Unit unit, string input, int? index = null) { return input .Replace("%ЭК%", unit.Name, StringComparison.OrdinalIgnoreCase) .Replace("%ГРУППА_РАБОТ%", job.Group?.GroupName ?? "", StringComparison.OrdinalIgnoreCase) .Replace("%РАБОТА%", job.WorkName, StringComparison.OrdinalIgnoreCase) .Replace("%ТНК%", job.Tnk?.Name ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("%ТНК-КРАТКО%", job.Tnk?.ShortName ?? "", StringComparison.OrdinalIgnoreCase); + .Replace("%ТНК-КРАТКО%", job.Tnk?.ShortName ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("%ИНДЕКС%", index?.ToString() ?? "", StringComparison.OrdinalIgnoreCase); } diff --git a/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs b/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs index cca7e67f..c57fc0ad 100644 --- a/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs +++ b/PARR.DAL/DomainServices/Interfaces/IShortcodesService.cs @@ -4,7 +4,7 @@ namespace PARR.DAL.DomainServices.Interfaces { public interface IShortcodesService { - Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId); + Task ApplyShortcodesAsync(string str, Guid unitId, Guid jobId, int? index = null); bool IsAnyShortcodes(string str); diff --git a/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs b/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs index cd80eabc..a5db2d68 100644 --- a/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs +++ b/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs @@ -21,6 +21,11 @@ namespace PARR.DAL.Services.Implementations.Unit } + public IQueryable Get() + { + return dataContext.UnitInUnits; + } + public Task> GetByParentIdAsync(Guid parentId) { @@ -29,6 +34,7 @@ namespace PARR.DAL.Services.Implementations.Unit .ToListAsync(); } + public Task> GetByChildIdAsync(Guid childId) { return dataContext.UnitInUnits diff --git a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs index a3426b97..eec6f078 100644 --- a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs +++ b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs @@ -80,5 +80,10 @@ namespace PARR.DAL.Services.Implementations.Unit .Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId)) .ToListAsync(); } + + public IQueryable Get() + { + return dataContext.UnitInValues; + } } } diff --git a/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs b/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs index 969d0e74..fcc6ee68 100644 --- a/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs +++ b/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs @@ -16,5 +16,8 @@ namespace PARR.DAL.Services.Interfaces.Unit /// Получает связи, где ParentUnitId unitIds (для IsParent=False). /// Task> GetChildLinksByParentIdsAsync(IEnumerable parentUnitIds); + + + IQueryable Get(); } } diff --git a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs index 5b56ed49..e4f011b7 100644 --- a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs +++ b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs @@ -12,6 +12,6 @@ namespace PARR.DAL.Services.Interfaces.Unit /// Получает UnitInValue (с Value) для заданных UnitId и FieldId. /// Task> GetByUnitIdsAndFieldIdsAsync(IEnumerable unitIds, IEnumerable fieldIds); - + IQueryable Get(); } } diff --git a/PARR.TemplateGeneratorWorker/Services/IValidatorService.cs b/PARR.TemplateGeneratorWorker/Services/IValidatorService.cs index 34a57370..3d70937b 100644 --- a/PARR.TemplateGeneratorWorker/Services/IValidatorService.cs +++ b/PARR.TemplateGeneratorWorker/Services/IValidatorService.cs @@ -2,6 +2,6 @@ { public interface IValidatorService { - Task IsValidAsync(Guid jobId, Guid unitId); + Task IsValidAsync(Guid jobId, Guid unitId, int? index, List? unitsInTemplate = null); } } diff --git a/PARR.TemplateGeneratorWorker/Services/ValidatorService.cs b/PARR.TemplateGeneratorWorker/Services/ValidatorService.cs index a0bb92e4..9309f96f 100644 --- a/PARR.TemplateGeneratorWorker/Services/ValidatorService.cs +++ b/PARR.TemplateGeneratorWorker/Services/ValidatorService.cs @@ -1,4 +1,6 @@ -using Microsoft.EntityFrameworkCore; +// ValidatorService.cs +using Microsoft.EntityFrameworkCore; +using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; @@ -9,22 +11,25 @@ namespace PARR.TemplateGeneratorWorker.Services private readonly ILogger logger; private readonly IJobService jobService; private readonly IUnitService unitService; + private readonly ITemplateService templateService; public ValidatorService( ILogger logger, IJobService jobService, - IUnitService unitService + IUnitService unitService, + ITemplateService templateService ) { this.logger = logger; this.jobService = jobService; this.unitService = unitService; + this.templateService = templateService; } - public async Task IsValidAsync(Guid jobId, Guid unitId) + public async Task IsValidAsync(Guid jobId, Guid unitId, int? index, List? unitsInTemplate = null) { - + // Проверяем JobId var job = await jobService .Get().AsNoTracking() .FirstOrDefaultAsync(t => t.Id == jobId); @@ -35,6 +40,7 @@ namespace PARR.TemplateGeneratorWorker.Services return false; } + // Проверяем UnitId (UnitId - это ID регионального юнита) var unit = await unitService .Get().AsNoTracking() .FirstOrDefaultAsync(t => t.Id == unitId); @@ -45,7 +51,35 @@ namespace PARR.TemplateGeneratorWorker.Services return false; } + // Проверяем уникальность (JobId, UnitId, Index) + var existingTemplate = await templateService + .Get().AsNoTracking() + .FirstOrDefaultAsync(t => t.JobId == jobId && t.UnitId == unitId && t.Index == index); + + if (existingTemplate != null) + { + logger.LogError("Шаблон с JobId={JobId}, UnitId={UnitId}, Index={Index} уже существует.", jobId, unitId, index); + return false; + } + + // Проверяем, что все UnitId в UnitsInTemplate существуют (если список не null и не пуст) + if (unitsInTemplate != null && unitsInTemplate.Any()) + { + var unitIdsToCheck = unitsInTemplate.ToHashSet(); + var existingUnitIdsCount = await unitService.Get() + .AsNoTracking() + .Where(u => unitIdsToCheck.Contains(u.Id)) + .Select(u => u.Id) + .CountAsync(); + + if (existingUnitIdsCount != unitIdsToCheck.Count) + { + logger.LogError("Не все UnitId из UnitsInTemplate существуют в базе данных. Ожидается: {ExpectedCount}, Найдено: {FoundCount}", unitIdsToCheck.Count, existingUnitIdsCount); + return false; + } + } + return true; } } -} +} \ No newline at end of file diff --git a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs index 3e67cde5..37cfc4fe 100644 --- a/PARR.TemplateGeneratorWorker/TemplateGenerator.cs +++ b/PARR.TemplateGeneratorWorker/TemplateGenerator.cs @@ -1,5 +1,4 @@ - -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using PARR.BLL.Domain.Mq; using PARR.BLL.Services.Interfaces; using PARR.Common.Domain; @@ -43,31 +42,23 @@ namespace PARR.TemplateGeneratorWorker this.templateService = templateService; this.esppScheduleTransformService = esppScheduleTransformService; } + public async Task GenerateTemplateAsync(string msg) { logger.LogInformation($"Получили запрос: {msg}"); - var query = transformService.GetModelFromJson(msg); + var query = transformService.GetModelFromJson(msg); if (query == null) - return; - - - if (!await validatorService.IsValidAsync(query.JobId, query.UnitId)) { - logger.LogError($"Некорректные параметры регламентной работы или Unit {nameof(Job)}: {query.JobId}, {nameof(Unit)}: {query.UnitId}"); + logger.LogError("Не удалось десериализовать запрос: {Message}", msg); return; } - var existing = await templateService - .Get() - .AsNoTracking() - .FirstOrDefaultAsync(t => t.JobId == query.JobId && t.UnitId == query.UnitId); - - - if (existing != null) + // Проверяем всё + if (!await validatorService.IsValidAsync(query.JobId, query.UnitId, query.Index, query.UnitsInTemplate)) { - logger.LogWarning("Шаблон уже существует (Job={JobId}, Unit={UnitId}) → пропускаем Create", query.JobId, query.UnitId); + logger.LogError($"Параметры запроса не прошли валидацию: JobId={query.JobId}, UnitId={query.UnitId}, Index={query.Index}, UnitsInTemplateCount={query.UnitsInTemplate?.Count ?? 0}"); return; } @@ -101,22 +92,32 @@ namespace PARR.TemplateGeneratorWorker IsActiveTemplate = query.IsActiveTemplate ?? false, IsActiveSchedule = query.IsActiveSchedule ?? false, NextRun = nextRun, - //IsUnused = false, StatusTypeId = TemplateStatusTypeEnum.Used, InitiatorComment = query.HistoryInitiator?.InitiatorComment, - InitiatorParrComponentId = query.HistoryInitiator?.InitiatorParrComponentId + InitiatorParrComponentId = query.HistoryInitiator?.InitiatorParrComponentId, + Index = query.Index }; - if (await templateService.CreateAsync(template) && await templateService.CommitAsync(new HistoryInitiator { InitiatorComment = "Запрос на генерацию с тестового шаблона", InitiatorParrComponentId = ParrComponentsEnum.TemplateTaskGenerator })) + // Устанавливаем UnitsInTemplate + if (query.UnitsInTemplate != null && query.UnitsInTemplate.Any()) { - logger.LogInformation("Создан шаблон: Id={TemplateId}, Name={Name}, Job={JobId}, Unit={UnitId}", - template.Id, template.Name, query.JobId, query.UnitId); + template.UnitsInTemplate = query.UnitsInTemplate.Select(unitId => new UnitsInTemplate { UnitId = unitId }).ToList(); } else { - logger.LogError("Ошибка создания шаблона: Name={Name}", templateName); + // Если список пуст, все равно инициализируем коллекцию, чтобы избежать NullReferenceException при сохранении (если это не nullable) + template.UnitsInTemplate = new List(); + } + + if (await templateService.CreateAsync(template) && await templateService.CommitAsync(new HistoryInitiator { InitiatorComment = "Запрос на генерацию с тестового шаблона", InitiatorParrComponentId = ParrComponentsEnum.TemplateTaskGenerator })) + { + logger.LogInformation("Создан шаблон: Id={TemplateId}, Name={Name}, Job={JobId}, Unit={UnitId}, Index={Index}, UnitsInTemplateCount={UnitsCount}", + template.Id, template.Name, query.JobId, query.UnitId, query.Index, template.UnitsInTemplate.Count); + } + else + { + logger.LogError("Ошибка создания шаблона: Name={Name}, JobId={JobId}, UnitId={UnitId}, Index={Index}", templateName, query.JobId, query.UnitId, query.Index); } } - } -} +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/ITemplateMatcher.cs b/PARR.TemplateMatcher/ITemplateMatcher.cs index c9f18af0..c923797e 100644 --- a/PARR.TemplateMatcher/ITemplateMatcher.cs +++ b/PARR.TemplateMatcher/ITemplateMatcher.cs @@ -2,9 +2,10 @@ namespace PARR.TemplateMatcher { - internal interface ITemplateMatcher + public interface ITemplateMatcher { Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator); Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator); + Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator); } } diff --git a/PARR.TemplateMatcher/MqTemplateMatcher.cs b/PARR.TemplateMatcher/MqTemplateMatcher.cs index 5af00a58..8361a7d8 100644 --- a/PARR.TemplateMatcher/MqTemplateMatcher.cs +++ b/PARR.TemplateMatcher/MqTemplateMatcher.cs @@ -59,8 +59,8 @@ namespace PARR.TemplateMatcher switch (query.EntityType) { case SyncTaskEntityTypeEnum.Job: - var validatorService = GetServiceInScope(scope); - if (!await validatorService.IsValidAsync(query.Id)) + var jobValidatorService = GetServiceInScope(scope); + if (!await jobValidatorService.IsValidJobAsync(query.Id)) { logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id); return; @@ -83,9 +83,29 @@ namespace PARR.TemplateMatcher break; case SyncTaskEntityTypeEnum.JobGroup: - logger.LogWarning("Обработка EntityType JobGroup не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action); - break; + var jobGroupValidatorService = GetServiceInScope(scope); + if (!await jobGroupValidatorService.IsValidJobGroupAsync(query.Id)) + { + logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id); + return; + } + switch (query.Action) + { + case TemplateMatcherActionEnum.Sync: + // Создать недостающие шаблоны, привязать к нужному Job, включить/выключить по фильтрам + await templateMatcherService.SyncTemplatesForJobGroup(query.Id, query.Initiator); + break; + case TemplateMatcherActionEnum.Update: + // Обновить существующие шаблоны: имя, привязка к Job, вкл/выкл по фильтрам + //await templateMatcherService.UpdateTemplatesForJob(query.Id, query.Initiator); + logger.LogWarning("Обработка EntityType JobGroup и TemplateMatcherActionEnum.Update не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action); + break; + default: + logger.LogWarning("Неизвестное действие для {EntityType}: {Action}", query.EntityType, query.Action); + break; + } + break; case SyncTaskEntityTypeEnum.Template: logger.LogWarning("Обработка EntityType Template не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action); break; diff --git a/PARR.TemplateMatcher/Services/Implemetaions/JobGroupValidatorService.cs b/PARR.TemplateMatcher/Services/Implemetaions/JobGroupValidatorService.cs new file mode 100644 index 00000000..4e5057b3 --- /dev/null +++ b/PARR.TemplateMatcher/Services/Implemetaions/JobGroupValidatorService.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using PARR.DAL.Services.Interfaces.Job; +using PARR.TemplateMatcher.Services.Interfaces; + +namespace PARR.TemplateMatcher.Services.Implemetaions +{ + internal class JobGroupValidatorService : IJobGroupValidatorService + { + private readonly ILogger logger; + private readonly IJobGroupService jobGroupService; + + public JobGroupValidatorService( + ILogger logger, + IJobGroupService jobGroupService + ) + { + this.logger = logger; + this.jobGroupService = jobGroupService; + } + public async Task IsValidJobGroupAsync(Guid jobGroupId) + { + var isExist = await jobGroupService.GetAsync(jobGroupId); + + if (isExist == null) + { + logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}"); + return false; + } + + return true; + } + } +} diff --git a/PARR.TemplateMatcher/Services/Implemetaions/JobValidatorService.cs b/PARR.TemplateMatcher/Services/Implemetaions/JobValidatorService.cs index 741e761b..99038eac 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/JobValidatorService.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/JobValidatorService.cs @@ -17,7 +17,7 @@ namespace PARR.TemplateMatcher.Services.Implemetaions this.logger = logger; this.jobService = jobService; } - public async Task IsValidAsync(Guid jobId) + public async Task IsValidJobAsync(Guid jobId) { var isExist = await jobService.GetAsync(jobId); diff --git a/PARR.TemplateMatcher/Services/Interfaces/IJobGroupValidatorService.cs b/PARR.TemplateMatcher/Services/Interfaces/IJobGroupValidatorService.cs new file mode 100644 index 00000000..ca32fdf2 --- /dev/null +++ b/PARR.TemplateMatcher/Services/Interfaces/IJobGroupValidatorService.cs @@ -0,0 +1,12 @@ +namespace PARR.TemplateMatcher.Services.Interfaces +{ + internal interface IJobGroupValidatorService + { + /// + /// Проверяет существование группы регламентной работы + /// + /// + /// + Task IsValidJobGroupAsync(Guid jobGroupId); + } +} diff --git a/PARR.TemplateMatcher/Services/Interfaces/IJobValidatorService.cs b/PARR.TemplateMatcher/Services/Interfaces/IJobValidatorService.cs index 8fa90b2e..9586b88d 100644 --- a/PARR.TemplateMatcher/Services/Interfaces/IJobValidatorService.cs +++ b/PARR.TemplateMatcher/Services/Interfaces/IJobValidatorService.cs @@ -7,6 +7,6 @@ /// /// /// - Task IsValidAsync(Guid jobId); + Task IsValidJobAsync(Guid jobId); } } diff --git a/PARR.TemplateMatcher/TemplateMatcher.cs b/PARR.TemplateMatcher/TemplateMatcher.cs index d32a1d21..b299ac0f 100644 --- a/PARR.TemplateMatcher/TemplateMatcher.cs +++ b/PARR.TemplateMatcher/TemplateMatcher.cs @@ -4,11 +4,14 @@ using PARR.BLL.Domain.Mq; using PARR.BLL.Services.Interfaces; using PARR.Common.Domain; using PARR.Constants; +using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.Models; using PARR.DAL.Models.Job; +using PARR.DAL.Models.Unit; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; +using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.TransformServices; using PARR.TemplateMatcher.Settings; using System.Text.Json; @@ -25,29 +28,41 @@ namespace PARR.TemplateMatcher private readonly ILogger logger; private readonly IUnitFilterService unitFilterService; + private readonly IUnitInUnitService unitInUnitService; + private readonly IUnitInValueService unitInValueService; // Добавлено + private readonly IUnitService unitService; // Добавлено private readonly MqSettings mqSettings; private readonly IMqService mqService; private readonly ITemplateService templateService; private readonly IJobService jobService; + private readonly IJobGroupService jobGroupService; private readonly IShortcodesService shortcodesService; private readonly IEsppScheduleTransformService esppScheduleTransformService; public TemplateMatcher( ILogger logger, IUnitFilterService unitFilterService, + IUnitInUnitService unitInUnitService, + IUnitInValueService unitInValueService, + IUnitService unitService, MqSettings mqSettings, IMqService mqService, ITemplateService templateService, IJobService jobService, + IJobGroupService jobGroupService, IShortcodesService shortcodesService, IEsppScheduleTransformService esppScheduleTransformService) { this.logger = logger; this.unitFilterService = unitFilterService; + this.unitInUnitService = unitInUnitService; + this.unitInValueService = unitInValueService; + this.unitService = unitService; this.mqSettings = mqSettings; this.mqService = mqService; this.templateService = templateService; this.jobService = jobService; + this.jobGroupService = jobGroupService; this.shortcodesService = shortcodesService; this.esppScheduleTransformService = esppScheduleTransformService; } @@ -66,12 +81,393 @@ namespace PARR.TemplateMatcher return; } + // Проверяем, является ли Job "групповым" + bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group; + + if (isGroupJob && job.Group.GroupingUnitFieldId.HasValue) + { + logger.LogInformation("Job {JobId} является групповым. Используйте SyncTemplatesForJobGroup для синхронизации.", jobId); + return; // Ничего не делаем для группового Job + } + else + { + await SyncSimpleTemplatesAsync(job, expectedUnitIds, initiator); + } + + logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); + } + + public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) + { + logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); + + // 1. Получить JobGroup и связанные Job'ы + var jobGroup = await jobGroupService.Get() + .AsNoTracking() // Добавлено + .Include(jg => jg.Jobs) + .ThenInclude(j => j.AutoControl) + .Include(jg => jg.Jobs) + .ThenInclude(j => j.UnitFilters) + .ThenInclude(uf => uf.RelationshipFilters) + .FirstOrDefaultAsync(jg => jg.Id == jobGroupId); + + if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) + { + logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); + return; + } + + var jobsInGroup = jobGroup.Jobs.ToList(); + + // 2. Найти Job с максимальным MaxValueRelationships + var maxJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue) + .OrderByDescending(j => j.MaxValueRelationships) + .FirstOrDefault(); + + if (maxJob == null) + { + logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); + // Возможно, нужно обработать случай, когда MaxValueRelationships не установлено ни у одного Job. + // Пока просто выйдем. + return; + } + + // Проверяем, что UnitFilters и RelationshipFilters загружены + if (maxJob.UnitFilters == null) + { + logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id); + // Продолжить с пустыми фильтрами или выйти? + // Пока продолжим с пустым списком. + } + + logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships); + + // 3. Использовать фильтры maxJob для получения expectedUnitIds + var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id); + if (expectedUnitIds == null || !expectedUnitIds.Any()) + { + logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId); + // Деактивировать все шаблоны для всех Job в группе? + // Пока просто выйдем. + return; + } + + // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId + if (!jobGroup.GroupingUnitFieldId.HasValue) + { + logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); + return; + } + + var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; + + // --- ИСПРАВЛЕНИЕ: Разбиваем запрос на части --- + // Загрузить UnitValues для отфильтрованных юнитов, чтобы проверить GroupingUnitFieldId + var filteredUnits = await unitService.Get() + .AsNoTracking() // Добавлено + .AsSplitQuery() // Добавлено + .Include(t => t.UnitValues) // Добавлено + .ThenInclude(t => t.Value) // Добавлено + .Where(u => expectedUnitIds.Contains(u.Id)) + .ToListAsync(); // Сначала загружаем Unit'ы + + // Затем фильтруем их UnitValues и собираем UnitId + var unitIdsWithValidGroupingFieldSet = filteredUnits + .Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))) + .Select(u => u.Id) + .ToHashSet(); // Создаем HashSet + + logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); + + if (!unitIdsWithValidGroupingFieldSet.Any()) + { + logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); + return; + } + + // --- НОВАЯ ЛОГИКА: Применение RelationshipFilters --- + // Найти связи UnitInUnit для unitIdsWithValidGroupingFieldSet + var potentialUnitInUnitLinks = await unitInUnitService.Get() + .AsNoTracking() // Добавлено + .Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId)) + .ToListAsync(); + + logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit до применения RelationshipFilters.", potentialUnitInUnitLinks.Count); + + // Получить RelationshipFilters из maxJob + var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List(); + + if (relationshipFilters.Any()) + { + // Загрузить UnitInValue для ParentUnitId и ChildUnitId из potentialUnitInUnitLinks + var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet(); + var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet(); + + var parentUnitValues = await unitInValueService.Get() + .AsNoTracking() // Добавлено + .Include(uv => uv.Field) // Добавлено + .Include(uv => uv.Value) // Добавлено + .Where(uv => allParentIds.Contains(uv.UnitId)) + .ToListAsync(); + + var childUnitValues = await unitInValueService.Get() + .AsNoTracking() // Добавлено + .Include(uv => uv.Field) // Добавлено + .Include(uv => uv.Value) // Добавлено + .Where(uv => allChildIds.Contains(uv.UnitId)) + .ToListAsync(); + + // Сгруппировать значения по UnitId для быстрого доступа + var parentValuesMap = parentUnitValues + .GroupBy(uv => uv.UnitId) + .ToDictionary(g => g.Key, g => g.ToList()); + + var childValuesMap = childUnitValues + .GroupBy(uv => uv.UnitId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // Применить фильтры к связям + var filteredUnitInUnitLinks = new List(); + foreach (var link in potentialUnitInUnitLinks) + { + bool linkMatchesAllFilters = true; + + foreach (var rf in relationshipFilters) + { + var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List()); + + bool filterMatch = valuesToCheck.Any(uv => + uv.FieldId == rf.FieldId && + uv.Value != null && + uv.Value.Value != null && + uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase) + ); + + if (rf.IsInverse) + filterMatch = !filterMatch; + + if (!filterMatch) + { + linkMatchesAllFilters = false; + break; // Не подходит под один из фильтров + } + } + + if (linkMatchesAllFilters) + { + filteredUnitInUnitLinks.Add(link); + } + } + + potentialUnitInUnitLinks = filteredUnitInUnitLinks; // Заменяем на отфильтрованные + } + + logger.LogDebug("Осталось {Count} связей UnitInUnit после применения RelationshipFilters.", potentialUnitInUnitLinks.Count); + + // 6. Сгруппировать ChildUnitId по ParentUnitId (региональный ЭК) из ОТФИЛЬТРОВАННЫХ связей + var groupedByRegional = potentialUnitInUnitLinks + .GroupBy(link => link.ParentUnitId) + .ToDictionary(g => g.Key, g => g.Select(l => l.ChildUnitId).ToList()); + + logger.LogDebug("Сформировано {Count} групп по региональным юнитам.", groupedByRegional.Count); + + // 7. Разбить каждую группу и сопоставить с Job + // Для каждого регионального юнита и его дочерних юнитов: + foreach (var kvp in groupedByRegional) + { + var regionalUnitId = kvp.Key; + var childUnitIds = kvp.Value; + + logger.LogDebug("Обработка регионального юнита {RegionalUnitId} с {Count} дочерними юнитами.", regionalUnitId, childUnitIds.Count); + + // Применяем ограничение MaxValueRelationships maxJob + int maxValueForSplitting = maxJob.MaxValueRelationships.Value; // Уже проверили, что не null + var childUnitGroups = childUnitIds + .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) + .GroupBy(x => x.groupIndex) + .Select(g => g.Select(x => x.id).ToList()) + .ToList(); + + logger.LogDebug("Региональный юнит {RegionalUnitId}: разбит на {GroupCount} подгрупп.", regionalUnitId, childUnitGroups.Count); + + // Для каждой подгруппы: + for (int i = 0; i < childUnitGroups.Count; i++) + { + var subGroup = childUnitGroups[i]; + var subGroupSize = subGroup.Count; + + logger.LogDebug("Обработка подгруппы {Index} регионального юнита {RegionalUnitId}, размер {Size}.", i, regionalUnitId, subGroupSize); + + // 8. Найти подходящий Job для подгруппы + // Попробовать найти Job с MaxValueRelationships, равным размеру подгруппы + var targetJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) + .FirstOrDefault(); + + if (targetJob == null) + { + // Найти Job с MaxValueRelationships >= размеру подгруппы, но минимально подходящее + targetJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) + .OrderBy(j => j.MaxValueRelationships.Value) + .FirstOrDefault(); + } + + if (targetJob == null) + { + // Если подходящий Job не найден, используем maxJob + targetJob = maxJob; + logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} не найден подходящий Job, используем maxJob {MaxJobId}.", i, regionalUnitId, maxJob.Id); + } + else + { + logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", i, regionalUnitId, targetJob.Id, targetJob.MaxValueRelationships); + } + + // 9. Загрузить существующие шаблоны для targetJob, связанные с regionalUnitId + var existingTemplatesForRegional = await templateService.Get() + .AsNoTracking() // Добавлено + .Include(t => t.UnitsInTemplate) + .Where(t => t.JobId == targetJob.Id && t.UnitId == regionalUnitId && t.Index == i) + .ToListAsync(); + + Template existingTemplateForSubGroup = existingTemplatesForRegional.FirstOrDefault(); + + if (existingTemplateForSubGroup != null) + { + // Проверить, изменились ли юниты + var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet(); + var newUnitIds = subGroup.ToHashSet(); + + if (existingUnitIds.SetEquals(newUnitIds)) + { + logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) актуален.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i); + // Возможно, нужно обновить имя или статус, если изменились фильтры или AutoControl + // Пока оставим как есть, если структура не изменилась. + } + else + { + logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i); + // Обновляем существующий шаблон + await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, targetJob, initiator); + } + } + else + { + // --- ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ МЕТОД TryReuseOneUnusedTemplateAsync --- + var reusableTemplate = await TryReuseOneUnusedTemplateAsync(targetJob.Id, regionalUnitId, initiator); // передаём regionalUnitId как unitId для старого метода + + if (reusableTemplate != null) // если захват успешен + { + logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, регионального юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, regionalUnitId, i); + + // Подготовить сообщение для TemplateUpdater с новыми параметрами + var expectedName = await GetNormalizedTemplateNameAsync(targetJob, regionalUnitId, i); + var nextRun = await GetNextRunAsync(targetJob); // всегда пересчитываем для нового назначения + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = reusableTemplate.Id, // ID захваченного шаблона + JobId = targetJob.Id, // Новый JobId + UnitId = regionalUnitId, // Новый UnitId (региональный) + Name = expectedName, + IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, + IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator, + NextRun = nextRun, + Index = i, // Новый Index + UnitsInTemplate = subGroup // Новые UnitsInTemplate + }; + + await SendTemplateUpdateMessage(updateRequest); + } + else + { + // Создать новый шаблон + logger.LogDebug("Создание нового шаблона для Job {JobId}, Regional {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, regionalUnitId, i, subGroup.Count); + await CreateGroupedTemplateAsync(targetJob.Id, regionalUnitId, subGroup, i, initiator); + } + } + } + } + + // 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе + // Это требует сбора всех ожидаемых (JobId, UnitId, Index) и сравнения с существующими. + // Соберем ожидаемые комбинации + var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); + foreach (var kvp in groupedByRegional) + { + var regionalUnitId = kvp.Key; + var childUnitIds = kvp.Value; + int maxValueForSplitting = maxJob.MaxValueRelationships.Value; + var childUnitGroups = childUnitIds + .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) + .GroupBy(x => x.groupIndex) + .Select(g => g.Select(x => x.id).ToList()) + .ToList(); + + for (int i = 0; i < childUnitGroups.Count; i++) + { + var subGroup = childUnitGroups[i]; + var subGroupSize = subGroup.Count; + + var targetJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) + .FirstOrDefault(); + + if (targetJob == null) + { + targetJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) + .OrderBy(j => j.MaxValueRelationships.Value) + .FirstOrDefault(); + } + + if (targetJob == null) targetJob = maxJob; + + // Исправлено: используем конкретные типы для кортежа + expectedTemplateKeys.Add((targetJob.Id, regionalUnitId, i)); + } + } + + // Загрузить *все* шаблоны для всех Job в группе, связанные с региональными юнитами из групп + var allRegionalUnitIds = groupedByRegional.Keys.ToHashSet(); + var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); + + var allExistingTemplatesInGroup = await templateService.Get() + .AsNoTracking() // Добавлено + .Include(t => t.UnitsInTemplate) + .Where(t => allJobIdsInGroup.Contains(t.JobId) && allRegionalUnitIds.Contains(t.UnitId)) + .ToListAsync(); + + foreach (var existingTemplate in allExistingTemplatesInGroup) + { + // Исправлено: используем конкретные типы для ключа + var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); + if (!expectedTemplateKeys.Contains(key)) + { + logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Regional {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index); + await DeactivateTemplateAsync(existingTemplate, existingTemplate.JobId, initiator); + } + } + + logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); + } + + + private async Task SyncSimpleTemplatesAsync(Job job, HashSet expectedUnitIds, HistoryInitiator initiator) + { + logger.LogDebug("Синхронизация обычных шаблонов для JobId {JobId}", job.Id); + var existingTemplates = await templateService.Get() - .Where(t => t.JobId == jobId) + .AsNoTracking() // Добавлено + .Where(t => t.JobId == job.Id) .ToListAsync(); logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.", - jobId, expectedUnitIds.Count, existingTemplates.Count); + job.Id, expectedUnitIds.Count, existingTemplates.Count); // Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны if (!expectedUnitIds.Any()) @@ -79,19 +475,19 @@ namespace PARR.TemplateMatcher if (existingTemplates.Any()) { logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.", - jobId, existingTemplates.Count); + job.Id, existingTemplates.Count); foreach (var template in existingTemplates) { - await DeactivateTemplateAsync(template, jobId, initiator); + await DeactivateTemplateAsync(template, job.Id, initiator); } } else { - logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", jobId); + logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", job.Id); } - logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", jobId); + logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", job.Id); return; } @@ -102,12 +498,13 @@ namespace PARR.TemplateMatcher foreach (var template in templatesToDeactivate) { - await DeactivateTemplateAsync(template, jobId, initiator); + await DeactivateTemplateAsync(template, job.Id, initiator); } // Перечитываем шаблоны после деактивации existingTemplates = await templateService.Get() - .Where(t => t.JobId == jobId) + .AsNoTracking() // Добавлено + .Where(t => t.JobId == job.Id) .ToListAsync(); var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t); @@ -121,7 +518,7 @@ namespace PARR.TemplateMatcher foreach (var unitId in unitIdsMissingTemplates) { - var reused = await TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); + var reused = await TryReuseOneUnusedTemplateAsync(job.Id, unitId, initiator); if (reused != null) { logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId); @@ -132,14 +529,15 @@ namespace PARR.TemplateMatcher var updateRequest = new TemplateUpdaterMq { TemplateId = reused.Id, - JobId = jobId, + JobId = job.Id, UnitId = unitId, Name = expectedName, IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, - NextRun = nextRun + NextRun = nextRun, + UnitsInTemplate = new List() }; await SendTemplateUpdateMessage(updateRequest); @@ -165,17 +563,17 @@ namespace PARR.TemplateMatcher // Создание новых шаблонов foreach (var unitId in unitIdsToCreateFresh) { - await SendTemplateGeneratorMessageAsync(jobId, unitId, initiator); + await SendTemplateGeneratorMessageAsync(job.Id, unitId, initiator); } - - logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); } + public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); var existingTemplates = await templateService.Get() + .AsNoTracking() // Добавлено .Where(t => t.JobId == jobId) .ToListAsync(); @@ -251,7 +649,8 @@ namespace PARR.TemplateMatcher NextRun = nextRun, Index = template.Index, StatusTypeId = targetStatus, - Initiator = initiator + Initiator = initiator, + UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() }; await SendTemplateUpdateMessage(updateRequest); @@ -261,6 +660,66 @@ namespace PARR.TemplateMatcher } + private async Task UpdateTemplateUnitsAsync(Template template, List newUnitIds, Job job, HistoryInitiator initiator) + { + // Обновляем шаблон как "Updating" + template.StatusTypeId = TemplateStatusTypeEnum.Updating; + template.DateModified = DateTimeOffset.UtcNow; + + if (!await templateService.CommitAsync(initiator)) + { + logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); + return; + } + + // Здесь нужно обновить UnitsInTemplate. + // Это может быть сделано через TemplateUpdaterMq, если он поддерживает передачу нового списка юнитов. + // Или напрямую в сервисе шаблонов, если логика обновления простая. + // Пока отправим сообщение в TemplateUpdater. + + var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); + var nextRun = await GetNextRunAsync(job, template.NextRun); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = job.Id, + UnitId = template.UnitId, + Name = expectedName, + IsActiveTemplate = template.IsActiveTemplate, + IsActiveSchedule = template.IsActiveSchedule, + LastRun = template.LastRun, + NextRun = nextRun, + Index = template.Index, + StatusTypeId = TemplateStatusTypeEnum.Used, // Предполагаем, что используется + Initiator = initiator, + UnitsInTemplate = newUnitIds // Передаем обновленный список юнитов + }; + + await SendTemplateUpdateMessage(updateRequest); + } + + private async Task CreateGroupedTemplateAsync(Guid jobId, Guid regionalUnitId, List unitIds, int index, HistoryInitiator initiator) + { + logger.LogInformation("Создание нового группового шаблона для Job {JobId}, регионального юнита {RegionalUnitId}, Index {Index}, с {Count} юнитами.", jobId, regionalUnitId, index, unitIds.Count); + + // Предполагаем, что TemplateGeneratorMq может обрабатывать UnitsInTemplate и Index + var mqRequest = new TemplateGeneratorMq + { + JobId = jobId, + UnitId = regionalUnitId, // UnitId шаблона + UnitsInTemplate = unitIds, // Юниты для UnitsInTemplate + Index = index, // Индекс шаблона + HistoryInitiator = initiator + }; + + var msg = JsonSerializer.Serialize(mqRequest); + var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); + + if (!result.IsSuccess) + logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, регионального юнита {RegionalUnitId}, Index {Index}.", jobId, regionalUnitId, index); + } + private async Task DeactivateTemplateAsync( Template template, Guid jobId, @@ -293,7 +752,8 @@ namespace PARR.TemplateMatcher NextRun = template.NextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Unused, - Initiator = initiator + Initiator = initiator, + UnitsInTemplate = new List() }; await SendTemplateUpdateMessage(updateRequest); @@ -339,7 +799,8 @@ namespace PARR.TemplateMatcher NextRun = nextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, - Initiator = initiator + Initiator = initiator, + UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() }; await SendTemplateUpdateMessage(updateRequest); @@ -352,11 +813,12 @@ namespace PARR.TemplateMatcher HistoryInitiator initiator) { logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId); - var mqRequest = new TemplateGeneratorWorkerMq + var mqRequest = new TemplateGeneratorMq { JobId = jobId, UnitId = unitId, - HistoryInitiator = initiator + HistoryInitiator = initiator, + UnitsInTemplate = new List() }; var msg = JsonSerializer.Serialize(mqRequest); @@ -387,9 +849,10 @@ namespace PARR.TemplateMatcher } } + // --- ИЗМЕНЕННЫЙ МЕТОД: Теперь используется как для простых, так и для групповых шаблонов --- private async Task TryReuseOneUnusedTemplateAsync( - Guid jobId, - Guid unitId, + Guid jobId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync) + Guid unitId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync) HistoryInitiator initiator, int maxAttempts = 3) { @@ -398,6 +861,7 @@ namespace PARR.TemplateMatcher try { var unusedCandidates = await templateService.Get() + .AsNoTracking() // Добавлено .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) .OrderBy(t => t.DateModified ?? t.DateCreated) .Take(UnusedCandidateBatchSize) @@ -421,9 +885,9 @@ namespace PARR.TemplateMatcher if (await templateService.CommitAsync(initiator)) { - logger.LogInformation("Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}).", - candidate.Id, unitId, attempt); - return candidate; + logger.LogInformation("Успешно захвачен шаблон {TemplateId} для переиспользования (попытка {Attempt}).", + candidate.Id, attempt); + return candidate; // Возвращаем захваченный шаблон } // Откат при неудаче @@ -462,7 +926,9 @@ namespace PARR.TemplateMatcher private async Task GetJobWithGroupAndAutoControlAsync(Guid jobId) { return await jobService.Get() + .AsNoTracking() // Добавлено .Include(j => j.Group) + .ThenInclude(j => j.GroupType) .Include(j => j.AutoControl) .FirstOrDefaultAsync(j => j.Id == jobId); } @@ -473,9 +939,9 @@ namespace PARR.TemplateMatcher return units?.ToHashSet() ?? new HashSet(); } - private async Task GetNormalizedTemplateNameAsync(Job job, Guid unitId) + private async Task GetNormalizedTemplateNameAsync(Job job, Guid unitId, int? index = null) { - var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id); + var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id, index); return rawName.ToUpper(); } diff --git a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs index 941bbee5..e22ae913 100644 --- a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs +++ b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs @@ -21,6 +21,7 @@ namespace PARR.TemplateMatcher services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); } diff --git a/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs b/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs index b3e0922f..7d3453d0 100644 --- a/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs +++ b/PARR.TemplateTaskGenerator/TemplateTaskGenerator.cs @@ -41,7 +41,7 @@ namespace PARR.TemplateTaskGenerator foreach (var unitId in unitIdsToCreateTemplate) { - var mqRequest = new TemplateGeneratorWorkerMq + var mqRequest = new TemplateGeneratorMq { JobId = jobId, UnitId = unitId,