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.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.Services.Interfaces; using PARR.TemplateMatcher.Settings; using System.Configuration; using System.Text.Json; namespace PARR.TemplateMatcher.Services.Implementations; internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { #if DEBUG private readonly Guid targetUnitId = Guid.Parse("d4322a08-246b-4380-8953-8ce4a8446235"); #endif 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; 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, SettingsFromDb settingsFromDb ) { 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; } 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); // 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); return; } var jobsInGroup = jobGroup.Jobs.ToList(); // --- НОВАЯ ЛОГИКА: Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"); if (workGroupField == null) { logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено в справочнике полей. Синхронизация прервана."); return; } var workGroupFieldId = workGroupField.Id; var regionalGroupValueIds = regionalEkPtkGroupService.Get() .Select(g => g.FieldValueId) .ToList(); // Получаем список UnitFieldValue.Id logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", regionalGroupValueIds.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); return; } // Проверяем, что UnitFilters и RelationshipFilters загружены (если используется для выбора targetJob) 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); return; } #if DEBUG // Отладка: проверить, есть ли юнит в expectedUnitIds if (expectedUnitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в expectedUnitIds.", targetUnitId); //return; // ❌ юнит отсеялся на этом этапе } #endif // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId (дополнительный фильтр) if (!jobGroup.GroupingUnitFieldId.HasValue) { logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); return; } var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; // Загрузить UnitValues для юнитов из expectedUnitIds, чтобы проверить GroupingUnitFieldId var expectedUnitsWithGroupingField = await unitService.Get() .AsNoTracking() .AsSplitQuery() // Для Unit -> UnitValues .Include(u => u.UnitValues) .ThenInclude(uv => uv.Value) .Where(u => expectedUnitIds.Contains(u.Id)) .ToListAsync(); var unitIdsWithValidGroupingFieldSet = expectedUnitsWithGroupingField .Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))) .Select(u => u.Id) .ToHashSet(); #if DEBUG // Отладка: проверить, есть ли юнит в unitIdsWithValidGroupingFieldSet if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidGroupingFieldSet.", targetUnitId); //return; // ❌ юнит отсеялся на этом этапе } #endif logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); if (!unitIdsWithValidGroupingFieldSet.Any()) { logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); return; } // --- Дополнительная фильтрация по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- var unitIdsWithValidWorkGroupFieldSet = expectedUnitsWithGroupingField .Where(u => unitIdsWithValidGroupingFieldSet.Contains(u.Id) && // Убедимся, что юнит уже прошёл фильтр по GroupingFieldId u.UnitValues.Any(uv => uv.FieldId == workGroupFieldId && // Поле "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" uv.Value != null && // Значение существует regionalGroupValueIds.Contains(uv.Value.Id) // Значение в списке разрешённых )) .Select(u => u.Id) // Выбираем Id юнита .ToHashSet(); // И снова в HashSet #if DEBUG // Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId); //return; // ❌ юнит отсеялся на этом этапе } #endif logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count); if (!unitIdsWithValidWorkGroupFieldSet.Any()) { logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); return; } // Обновляем список юнитов, прошедших оба фильтра unitIdsWithValidGroupingFieldSet = unitIdsWithValidWorkGroupFieldSet; // 5. Получить RelationshipFilters из maxJob var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List(); // --- Найти и отфильтровать UnitInUnit связи --- logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию по GroupingUnitFieldId и 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'."); var potentialUnitInUnitLinks = await unitInUnitService.Get() .AsNoTracking() .Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId)) .ToListAsync(); #if DEBUG // Отладка: проверить, юнит участвует в potentialUnitInUnitLinks var potentialLinksContainingTarget = potentialUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList(); if (potentialLinksContainingTarget.Any()) { logger.LogDebug("Юнит {TargetUnitId} участвует в {Count} потенциальных связях UnitInUnit.", targetUnitId, potentialLinksContainingTarget.Count); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ участвует в потенциальных связях UnitInUnit.", targetUnitId); // ❌ юнит отсеялся на этом этапе, если связи не требовались } #endif logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count); // Загрузить 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()); // Применить фильтры к связям logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count); 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); } } #if DEBUG // Отладка: проверить, юнит участвует в filteredUnitInUnitLinks var filteredLinksContainingTarget = filteredUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList(); if (filteredLinksContainingTarget.Any()) { logger.LogDebug("Юнит {TargetUnitId} участвует в {Count} отфильтрованных связях UnitInUnit.", targetUnitId, filteredLinksContainingTarget.Count); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ участвует в отфильтрованных связях UnitInUnit.", targetUnitId); // ❌ юнит отсеялся на этом этапе, если связи требовались } #endif logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count); // --- Сгруппировать юниты из unitIdsWithValidGroupingFieldSet по связанному юниту --- 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); #if DEBUG // Отладка: проверить, юнит есть в groupedRelationships.Values var allUnitsInGroups = groupedRelationships.Values.SelectMany(x => x).ToList(); if (allUnitsInGroups.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в groupedRelationships.Values до разрешения конфликтов.", targetUnitId); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в groupedRelationships.Values до разрешения конфликтов.", targetUnitId); // ❌ юнит отсеялся на этапе группировки } logger.LogDebug("Содержимое groupedRelationships до разрешения конфликтов: [{Groups}]", string.Join(", ", groupedRelationships.Select(kvp => $"Key: {kvp.Key}, Values: [{string.Join(", ", kvp.Value)}]"))); #endif // --- Разрешение конфликта - один юнит из unitIdsWithValidGroupingFieldSet только в одном списке значений --- 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); } } #if DEBUG // Отладка: проверить, юнит есть в unitToKeys до поиска конфликтов if (unitToKeys.ContainsKey(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitToKeys до поиска конфликтов, находится в {Count} группах: [{Groups}]", targetUnitId, unitToKeys[targetUnitId].Count, string.Join(", ", unitToKeys[targetUnitId])); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitToKeys до поиска конфликтов.", targetUnitId); } #endif // Найти юниты, которые находятся в нескольких списках var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList(); #if DEBUG // Отладка: проверить, юнит в conflictedUnits var targetConflictedEntry = conflictedUnits.FirstOrDefault(c => c.Key == targetUnitId); if (targetConflictedEntry.Key != default) { logger.LogDebug("Юнит {TargetUnitId} находится в {Count} группах (конфликт).", targetUnitId, targetConflictedEntry.Value.Count); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ находится в конфликте (не в нескольких группах).", targetUnitId); } #endif foreach (var conflictedUnitEntry in conflictedUnits) { var unitId = conflictedUnitEntry.Key; var keysForUnit = conflictedUnitEntry.Value; Guid bestKey = keysForUnit[0]; // Инициализируем первым ключом int maxCount = groupedRelationships[bestKey].Count; for (int i = 1; i < keysForUnit.Count; i++) { var currentKey = keysForUnit[i]; var currentCount = groupedRelationships[currentKey].Count; if (currentCount > maxCount) { bestKey = currentKey; maxCount = currentCount; } } // Удалить юнит из списков всех ключей, кроме bestKey foreach (var key in keysForUnit) { if (key != bestKey) { groupedRelationships[key].Remove(unitId); logger.LogDebug("Юнит {UnitId} перемещён из группы {OldKey} в группу {BestKey} (по кол-ву).", unitId, key, bestKey); } } } #if DEBUG // Отладка: проверить, юнит есть в groupedRelationships.Values после разрешения конфликтов var allUnitsInGroupsAfterConflictResolution = groupedRelationships.Values.SelectMany(x => x).ToList(); if (allUnitsInGroupsAfterConflictResolution.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в groupedRelationships.Values после разрешения конфликтов.", targetUnitId); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в groupedRelationships.Values после разрешения конфликтов.", targetUnitId); // ❌ юнит отсеялся на этапе разрешения конфликтов } #endif // Удаляем ключи, у которых список стал пустым после разрешения конфликтов var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList(); foreach (var key in keysToRemove) { groupedRelationships.Remove(key); logger.LogDebug("Ключ {Key} удалён, так как его список юнитов стал пустым после разрешения конфликтов.", key); } logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count); #if DEBUG logger.LogDebug("Содержимое groupedRelationships после разрешения конфликтов: [{Groups}]", string.Join(", ", groupedRelationships.Select(kvp => $"Key: {kvp.Key}, Values: [{string.Join(", ", kvp.Value)}]"))); #endif // 7. Разбить каждую группу и сопоставить с Job foreach (var kvp in groupedRelationships) { var relationshipUnitId = kvp.Key; // Связанный юнит (не из unitIdsWithValidGroupingFieldSet) var childUnitIds = kvp.Value; // Юниты из unitIdsWithValidGroupingFieldSet, связанные с regionalUnitId if (childUnitIds.Count == 0) continue; #if DEBUG // Отладка: проверить, есть ли ВРТ-AOS-05-ДВС в childUnitIds var childUnitNames = childUnitIds.Select(id => unitService.Get().AsNoTracking().Where(u => u.Id == id).Select(u => u.Name).FirstOrDefaultAsync().GetAwaiter().GetResult() ?? id.ToString()).ToList(); if (childUnitNames.Contains("ВРТ-AOS-05-ДВС")) { var relationshipUnitName = await unitService.Get().AsNoTracking().Where(u => u.Id == relationshipUnitId).Select(u => u.Name).FirstOrDefaultAsync(); logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юнит 'ВРТ-AOS-05-ДВС' в childUnitIds: [{ChildUnitNames}]", relationshipUnitId, relationshipUnitName, string.Join(", ", childUnitNames)); } #endif logger.LogDebug("Обработка связанного юнита {RegionalUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count); // Применяем ограничение MaxValueRelationships maxJob if (!maxJob.MaxValueRelationships.HasValue) { logger.LogWarning("Job {JobId} не заполнено MaxValueRelationships.", maxJob.Id); return; } 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(); logger.LogDebug("Связанный юнит {RegionalUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count); // Для каждой подгруппы: for (int i = 0; i < childUnitGroups.Count; i++) { var subGroup = childUnitGroups[i]; var subGroupSize = subGroup.Count; logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RegionalUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize); // Выбираем targetJob для подгруппы Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); // 9. Загрузить существующие шаблоны для targetJob, связанные с relationshipUnitId var existingTemplatesForRelationship = await templateService.Get() .AsNoTracking() .Include(t => t.UnitsInTemplate) .Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToListAsync(); var existingTemplateForSubGroup = existingTemplatesForRelationship.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, relationshipUnitId, i); // Проверить, изменилось ли имя шаблона (например, из-за %МАКС:...% или %ТНК-КРАТКО%) var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup); if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.Name, expectedName); var nextRun = await GetNextRunAsync(targetJob, existingTemplateForSubGroup.NextRun); 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 = subGroup }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } else { logger.LogDebug("Шаблон {TemplateId} полностью актуален (юниты и имя).", existingTemplateForSubGroup.Id); } } else { logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i); // Пересчитать targetJob для нового размера subGroup var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); // Если Job изменился — нужно обновить шаблон с новым JobId if (newTargetJob.Id != existingTemplateForSubGroup.JobId) { logger.LogDebug("Job для шаблона {TemplateId} изменился: старый {OldJobId}, новый {NewJobId}.", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.JobId, newTargetJob.Id); } await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, newTargetJob, initiator); } } else { var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, relationshipUnitId, initiator); if (reusableTemplate != null) { logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup); var nextRun = await GetNextRunAsync(targetJob); 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}, связанного юнита {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count); await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator); } } } } // 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе (логика без изменений) var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); foreach (var kvp in groupedRelationships) { 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; Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); expectedTemplateKeys.Add((targetJobForExpectedKey.Id, regionalUnitId, i)); } } var allRegionalUnitIds = groupedRelationships.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 templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator); } } logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); } public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); return; } // --- Вспомогательные методы --- /// /// Выбирает Job, соответствующий размеру подгруппы /// /// Список Job'ов в группе /// Размер подгруппы /// Job с максимальным MaxValueRelationships /// Найденный Job или maxJob, если не найден подходящий 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; // maxJob уже проверен на null ранее 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) { template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); return; } var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, template.UnitId, template.Index, newUnitIds); var nextRun = await GetNextRunAsync(targetJob, template.NextRun); 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 = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = newUnitIds }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(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); 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 GetNextRunAsync(Job targetJob, DateTimeOffset? currentNextRun = null) { var referenceDate = targetJob.Group?.ReferenceDate ?? DateTimeOffset.UtcNow; return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate); } }