using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.BLL.Domain.Mq; using PARR.BLL.Services.Interfaces; using PARR.Common.Domain; using PARR.Constants; using PARR.DAL.Cache.Models; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.UnitFilterService; using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.Models.Unit; using PARR.DAL.NextRunServices; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.TransformServices; using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Settings; using System.Text.Json; namespace PARR.TemplateMatcher.Services.Implementations; internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { private const bool DefaultUsedTemplateState = false; private const bool DefaultUsedScheduleState = false; 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 IJobGroupService jobGroupService; private readonly ITemplateReuser templateReuser; //private readonly IEsppScheduleTransformService esppScheduleTransformService; private readonly IUnitRegionalEkPtkGroupService regionalEkPtkGroupService; private readonly IUnitFieldService unitFieldService; private readonly ITemplateDeactivator templateDeactivator; private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly IMatchingStatusService matchingStatusService; private readonly INextRunService nextRunService; public GroupedTemplateSynchronizer( ILogger logger, IUnitFilterService unitFilterService, IUnitInUnitService unitInUnitService, IUnitInValueService unitInValueService, IUnitService unitService, MqSettings mqSettings, IMqService mqService, ITemplateService templateService, IJobGroupService jobGroupService, ITemplateReuser templateReuser, //IEsppScheduleTransformService esppScheduleTransformService, IUnitRegionalEkPtkGroupService regionalEkPtkGroupService, IUnitFieldService unitFieldService, ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, ITemplateUpdaterMqSender templateUpdaterMqSender, IMatchingStatusService matchingStatusService, INextRunService nextRunService ) { 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.jobGroupService = jobGroupService; this.templateReuser = templateReuser; //this.esppScheduleTransformService = esppScheduleTransformService; this.regionalEkPtkGroupService = regionalEkPtkGroupService; this.unitFieldService = unitFieldService; this.templateDeactivator = templateDeactivator; this.templateNameNormalizer = templateNameNormalizer; this.templateUpdaterMqSender = templateUpdaterMqSender; this.matchingStatusService = matchingStatusService; this.nextRunService = nextRunService; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); } public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); // === Проверка: уже запущена? === var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); if (existingStatus.DetailsJobGroups?.Any() == true) { logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId); return; } // === Устанавливаем статус "в процессе" === var initialStatus = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = "Начало синхронизации" }; await matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup, new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) }, TimeSpan.FromMinutes(35) ); try { // 1. Получить JobGroup и связанные Job'ы var jobGroup = await jobGroupService.Get() .AsNoTracking() .Include(jg => jg.GroupType) .Include(jg => jg.Jobs) .ThenInclude(j => j.AutoControl) .Include(jg => jg.Jobs) .ThenInclude(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) .Include(jg => jg.Jobs) .ThenInclude(jg => jg.Tnk) .FirstOrDefaultAsync(jg => jg.Id == jobGroupId); if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) { logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст"); return; } var jobsInGroup = jobGroup.Jobs.ToList(); // --- Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"); if (workGroupField == null) { logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено в справочнике полей. Синхронизация прервана."); await UpdateMatchingStatusAsync(jobGroupId, "Ошибка: поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено"); return; } var workGroupFieldId = workGroupField.Id; var relationshipGroupValueIds = regionalEkPtkGroupService.Get() .Select(g => g.FieldValueId) .ToList(); logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", relationshipGroupValueIds.Count); // 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); await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships"); return; } if (maxJob.UnitFilters == null) { logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id); } logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships); // 3. Использовать фильтры maxJob для получения отфильтрованных юнитов var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id); if (filteredUnits == null || !filteredUnits.Any()) { logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов"); return; } // Извлекаем ID юнитов для последующих операций var expectedUnitIds = filteredUnits.Select(u => u.Id).ToList(); // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId if (!jobGroup.GroupingUnitFieldId.HasValue) { logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Отсутствует GroupingUnitFieldId"); return; } var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; // Фильтруем юниты по GroupingUnitFieldId используя данные из DTO var unitIdsWithValidGroupingFieldSet = filteredUnits .Where(u => u.Values.Any(v => v.FieldId == groupingFieldId && !string.IsNullOrEmpty(v.Value))) .Select(u => u.Id) .ToList(); logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); if (!unitIdsWithValidGroupingFieldSet.Any()) { logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId"); return; } // Аналогично для фильтрации по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" var unitIdsWithValidWorkGroupFieldSet = filteredUnits .Where(u => u.Values.Any(v => v.FieldId == workGroupFieldId && relationshipGroupValueIds.Contains(v.FieldId) && !string.IsNullOrEmpty(v.Value))) .Select(u => u.Id) .ToList(); logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count); if (!unitIdsWithValidWorkGroupFieldSet.Any()) { logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'"); return; } unitIdsWithValidGroupingFieldSet = unitIdsWithValidWorkGroupFieldSet; // 5. Получить RelationshipFilters из maxJob var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List(); logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию..."); // Используем unitIdsWithValidGroupingFieldSet для получения связей var potentialUnitInUnitLinks = await unitInUnitService.Get() .AsNoTracking() .Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId)) .ToListAsync(); logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count); // Подготавливаем данные для фильтрации связей var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet(); var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet(); // Фильтруем юниты для получения родительских и дочерних значений var parentUnits = filteredUnits.Where(u => allParentIds.Contains(u.Id)).ToList(); var childUnits = filteredUnits.Where(u => allChildIds.Contains(u.Id)).ToList(); // Создаем словари значений для родителей и детей var parentValuesMap = parentUnits .ToDictionary( u => u.Id, u => u.Values.ToDictionary(v => v.FieldId, v => v.Value) ); var childValuesMap = childUnits .ToDictionary( u => u.Id, u => u.Values.ToDictionary(v => v.FieldId, v => v.Value) ); logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count); var filteredUnitInUnitLinks = new List(); foreach (var link in potentialUnitInUnitLinks) { bool linkMatchesAllFilters = true; foreach (var rf in relationshipFilters) { bool filterMatch; if (rf.IsParent) { // Родительские значения if (parentValuesMap.TryGetValue(link.ParentUnitId, out var parentValues) && parentValues.TryGetValue(rf.FieldId, out var parentValue) && parentValue != null) { filterMatch = parentValue.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase); } else { filterMatch = false; } } else { // Дочерние значения if (childValuesMap.TryGetValue(link.ChildUnitId, out var childValues) && childValues.TryGetValue(rf.FieldId, out var childValue) && childValue != null) { filterMatch = childValue.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase); } else { filterMatch = false; } } // Учитываем инверсию фильтра if (rf.IsInverse) filterMatch = !filterMatch; if (!filterMatch) { linkMatchesAllFilters = false; break; } } if (linkMatchesAllFilters) { filteredUnitInUnitLinks.Add(link); } } logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count); // --- Сгруппировать юниты --- var groupedRelationships = new Dictionary>(); foreach (var link in filteredUnitInUnitLinks) { var parentUnitId = link.ParentUnitId; var childUnitId = link.ChildUnitId; if (unitIdsWithValidGroupingFieldSet.Contains(parentUnitId)) { if (!groupedRelationships.ContainsKey(childUnitId)) { groupedRelationships[childUnitId] = new List(); } groupedRelationships[childUnitId].Add(parentUnitId); } else if (unitIdsWithValidGroupingFieldSet.Contains(childUnitId)) { if (!groupedRelationships.ContainsKey(parentUnitId)) { groupedRelationships[parentUnitId] = new List(); } groupedRelationships[parentUnitId].Add(childUnitId); } } logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count); // --- Разрешение конфликта с детерминированной сортировкой --- var unitToKeys = new Dictionary>(); foreach (var kvp in groupedRelationships) { var key = kvp.Key; var units = kvp.Value; foreach (var unitId in units) { if (!unitToKeys.ContainsKey(unitId)) { unitToKeys[unitId] = new List(); } unitToKeys[unitId].Add(key); } } // Получаем имена всех конфликтующих ключей var conflictKeys = unitToKeys .Where(kvp => kvp.Value.Count > 1) .SelectMany(kvp => kvp.Value) .Distinct() .ToList(); // Создаем словарь имен для конфликтующих ключей из отфильтрованных юнитов var unitNamesMap = filteredUnits .Where(u => conflictKeys.Contains(u.Id)) .ToDictionary(u => u.Id, u => u.Name); foreach (var conflictedUnitEntry in unitToKeys.Where(kvp => kvp.Value.Count > 1)) { var unitId = conflictedUnitEntry.Key; var keysForUnit = conflictedUnitEntry.Value; // Сортируем по: 1) кол-во юнитов (убывание), 2) имя ключа (возрастание) var sortedKeys = keysForUnit .Select(key => ( key, count: groupedRelationships[key].Count, name: unitNamesMap.GetValueOrDefault(key, "") )) .OrderByDescending(x => x.count) .ThenBy(x => x.name) .ToList(); var bestKey = sortedKeys.First().key; // Удаляем юнит из ВСЕХ групп, кроме лучшей foreach (var key in keysForUnit) { if (key != bestKey && groupedRelationships.ContainsKey(key)) { groupedRelationships[key].Remove(unitId); } } } // Удаляем пустые группы var emptyKeys = groupedRelationships .Where(kvp => !kvp.Value.Any()) .Select(kvp => kvp.Key) .ToList(); foreach (var key in emptyKeys) { groupedRelationships.Remove(key); } logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count); // === Основной цикл обработки === foreach (var kvp in groupedRelationships) { var relationshipUnitId = kvp.Key; var childUnitIds = kvp.Value; if (childUnitIds.Count == 0) continue; logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count); // Получаем имена юнитов из отфильтрованных данных var childUnitNameMap = filteredUnits .Where(u => childUnitIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => u.Name); var sortedChildUnitIds = childUnitIds .OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString())) .ToList(); int maxValueForSplitting = maxJob.MaxValueRelationships!.Value; var childUnitGroups = sortedChildUnitIds .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .GroupBy(x => x.groupIndex) .Select(g => g.Select(x => x.id).ToList()) .ToList(); logger.LogDebug("Связанный юнит {RelationshipUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count); // Индекс начинается с 1 for (int i = 1; i <= childUnitGroups.Count; i++) { var subGroup = childUnitGroups[i - 1]; // корректируем индекс для доступа к коллекции var subGroupSize = subGroup.Count; logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RelationshipUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize); Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); var existingTemplatesForRelationship = await templateService.Get() .AsNoTracking() .Include(t => t.Unit) .Include(t => t.Job) .ThenInclude(t => t!.Tnk) .Include(t => t.Job) .ThenInclude(t => t!.Group) .ThenInclude(t => t!.GroupType) .Include(t => t.UnitsInTemplate) .ThenInclude(uit => uit.Unit) .Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToListAsync(); var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault(); if (existingTemplateForSubGroup != null) { // === 1. Получаем текущие и новые ID юнитов === var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); var proposedUnitIds = subGroup.ToList(); // === 2. Сравниваем детерминированно с сортировкой по имени === var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList(); // Получаем имена юнитов из отфильтрованных данных var unitNamesForSort = filteredUnits .Where(u => allUnitIdsForSort.Contains(u.Id)) .ToDictionary(u => u.Id, u => u.Name); var sortedCurrentUnitIds = currentUnitIds .OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString())) .ToList(); var sortedProposedUnitIds = proposedUnitIds .OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString())) .ToList(); bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds); if (unitsAreEqual) { logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id); existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList(); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup); if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id); //var nextRun = await GetNextRunAsync(targetJob, existingTemplateForSubGroup.NextRun); var nextRun = await nextRunService.GetNextRunForTemplateAsync(existingTemplateForSubGroup.Id, false); var updateRequest = new TemplateUpdaterMq { TemplateId = existingTemplateForSubGroup.Id, JobId = targetJob.Id, UnitId = relationshipUnitId, Name = expectedName, IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate, IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule, LastRun = existingTemplateForSubGroup.LastRun, NextRun = nextRun, Index = i, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = sortedProposedUnitIds }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } else { logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id); } } else { logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов или их порядка (после сортировки).", existingTemplateForSubGroup.Id); var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); if (newTargetJob.Id != existingTemplateForSubGroup.JobId) { logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id); } await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, sortedProposedUnitIds, newTargetJob, initiator, i); } } else { var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, relationshipUnitId, initiator); if (reusableTemplate != null) { logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i); var tempTemplateForName = new Template { Id = reusableTemplate.Id, Name = reusableTemplate.Name, JobId = targetJob.Id, UnitId = relationshipUnitId, Index = i, Job = targetJob, Unit = reusableTemplate.Unit, UnitsInTemplate = subGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList() }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); //var nextRun = await GetNextRunAsync(targetJob); var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true); var updateRequest = new TemplateUpdaterMq { TemplateId = reusableTemplate.Id, JobId = targetJob.Id, UnitId = relationshipUnitId, Name = expectedName, IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, NextRun = nextRun, Index = i, UnitsInTemplate = subGroup }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } else { logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count); await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator); } } } } // === Деактивация === var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); foreach (var kvp in groupedRelationships) { var relationshipUnitId = kvp.Key; var childUnitIds = kvp.Value; // Получаем имена юнитов из отфильтрованных данных var childUnitNameMapForDeactivate = filteredUnits .Where(u => childUnitIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => u.Name); var sortedChildUnitIdsForDeactivate = childUnitIds .OrderBy(id => childUnitNameMapForDeactivate.GetValueOrDefault(id, id.ToString())) .ToList(); int maxValueForSplitting = maxJob.MaxValueRelationships!.Value; var childUnitGroups = sortedChildUnitIdsForDeactivate .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .GroupBy(x => x.groupIndex) .Select(g => g.Select(x => x.id).ToList()) .ToList(); // Индекс начинается с 1 for (int i = 1; i <= childUnitGroups.Count; i++) { var subGroup = childUnitGroups[i - 1]; // корректируем индекс для доступа к коллекции var subGroupSize = subGroup.Count; Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); expectedTemplateKeys.Add((targetJobForExpectedKey.Id, relationshipUnitId, i)); } } // Получаем ВСЕ шаблоны для JobGroup (не только для текущих relationshipUnitIds) var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); var allExistingTemplatesInGroup = await templateService.Get() .AsNoTracking() .Include(t => t.Unit) .Include(t => t.UnitsInTemplate) .Where(t => allJobIdsInGroup.Contains(t.JobId) && t.StatusTypeId == TemplateStatusTypeEnum.Used && t.Job!.GroupId == jobGroupId) .ToListAsync(); foreach (var existingTemplate in allExistingTemplatesInGroup) { var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); if (!expectedTemplateKeys.Contains(key)) { logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index); await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator); } } // === Успешное завершение === await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно"); await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); } catch (Exception ex) { logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}"); throw; } } public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); return; } private Job SelectTargetJob(List jobsInGroup, int subGroupSize, Job maxJob) { Job? 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; logger.LogDebug("Для подгруппы размером {Size} не найден подходящий Job, используем maxJob {MaxJobId}.", subGroupSize, maxJob.Id); } else { logger.LogDebug("Для подгруппы размером {Size} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", subGroupSize, targetJob.Id, targetJob.MaxValueRelationships); } return targetJob; } private async Task UpdateTemplateUnitsAsync(Template template, List newUnitIds, Job targetJob, HistoryInitiator initiator, int newIndex) { template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); return; } var tempTemplateForName = new Template { Id = template.Id, Name = template.Name, JobId = targetJob.Id, UnitId = template.UnitId, Index = newIndex, Job = targetJob, Unit = template.Unit, UnitsInTemplate = newUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList() }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); //var nextRun = await GetNextRunAsync(targetJob, template.NextRun); var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = targetJob.Id, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, LastRun = template.LastRun, NextRun = nextRun, Index = newIndex, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = newUnitIds }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } private async Task CreateGroupedTemplateAsync(Guid jobId, Guid relationshipUnitId, List unitIds, int index, HistoryInitiator initiator) { logger.LogInformation("Создание нового группового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", jobId, relationshipUnitId, index, unitIds.Count); var mqRequest = new TemplateGeneratorMq { JobId = jobId, UnitId = relationshipUnitId, UnitsInTemplate = unitIds, 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}, связанного юнита {RelationshipUnitId}, Index {Index}.", jobId, relationshipUnitId, index); } //private async Task GetNextRunAsync(Job targetJob, DateTimeOffset? currentNextRun = null) //{ // var referenceDate = targetJob.Group?.ReferenceDate ?? DateTimeOffset.UtcNow; // return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate); //} private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment) { var status = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = comment }; await matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup, new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) }, TimeSpan.FromMinutes(30) ); } }