diff --git a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs index d3f73c12..a8d9516f 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs @@ -22,7 +22,8 @@ namespace PARR.TemplateMatcher.Services.Implementations; internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { #if DEBUG - private readonly Guid targetUnitId = Guid.Parse("87fc4c36-1ea8-4163-983f-1605fee1de99"); + private readonly string[] debugWatchUnitNames = { "МОНИТОР-АСОУП-3-ВСИБ", "ЦЕКОН-ВСИБ", "ВРТ-ASOUP2-ВСИБ" }; + private Dictionary debugWatchUnitIds = new Dictionary(); #endif private const bool DefaultUsedTemplateState = false; @@ -64,7 +65,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer ITemplateNameNormalizer templateNameNormalizer, ITemplateUpdaterMqSender templateUpdaterMqSender, IMatchingStatusService matchingStatusService - ) + ) { this.logger = logger; this.unitFilterService = unitFilterService; @@ -85,6 +86,36 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer this.matchingStatusService = matchingStatusService; } +#if DEBUG + private async Task InitializeDebugUnitsAsync() + { + try + { + var watchUnits = await unitService.Get() + .AsNoTracking() + .Where(u => debugWatchUnitNames.Contains(u.Name)) + .ToDictionaryAsync(u => u.Name, u => u.Id); + + foreach (var name in debugWatchUnitNames) + { + if (watchUnits.TryGetValue(name, out var id)) + { + debugWatchUnitIds[name] = id; + logger.LogWarning("[DEBUG] Найден юнит для отслеживания '{UnitName}' с ID: {UnitId}", name, id); + } + else + { + logger.LogWarning("[DEBUG] Юнит для отслеживания '{UnitName}' не найден в БД", name); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "[DEBUG] Ошибка при поиске юнитов для отслеживания"); + } + } +#endif + public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); @@ -92,6 +123,10 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { +#if DEBUG + await InitializeDebugUnitsAsync(); +#endif + logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); // === Проверка: уже запущена? === @@ -149,11 +184,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer return; } var workGroupFieldId = workGroupField.Id; - var regionalGroupValueIds = regionalEkPtkGroupService.Get() .Select(g => g.FieldValueId) .ToList(); - logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", regionalGroupValueIds.Count); // 2. Найти Job с максимальным MaxValueRelationships @@ -186,13 +219,21 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } #if DEBUG - if (expectedUnitIds.Contains(targetUnitId)) + var debugMonitorUnitId = debugWatchUnitIds.GetValueOrDefault("МОНИТОР-АСОУП-3-ВСИБ", Guid.Empty); + var debugCekonUnitId = debugWatchUnitIds.GetValueOrDefault("ЦЕКОН-ВСИБ", Guid.Empty); + var debugVrtUnitId = debugWatchUnitIds.GetValueOrDefault("ВРТ-ASOUP2-ВСИБ", Guid.Empty); + + if (debugMonitorUnitId != Guid.Empty && expectedUnitIds.Contains(debugMonitorUnitId)) { - logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId); + logger.LogDebug("Юнит МОНИТОР-АСОУП-3-ВСИБ найден в expectedUnitIds."); } - else + if (debugCekonUnitId != Guid.Empty && expectedUnitIds.Contains(debugCekonUnitId)) { - logger.LogDebug("Юнит {TargetUnitId} НЕ найден в expectedUnitIds.", targetUnitId); + logger.LogDebug("Юнит ЦЕКОН-ВСИБ найден в expectedUnitIds."); + } + if (debugVrtUnitId != Guid.Empty && expectedUnitIds.Contains(debugVrtUnitId)) + { + logger.LogDebug("Юнит ВРТ-ASOUP2-ВСИБ найден в expectedUnitIds."); } #endif @@ -205,7 +246,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; - var expectedUnitsWithGroupingField = await unitService.Get() .AsNoTracking() .Include(u => u.UnitValues) @@ -218,17 +258,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .Select(u => u.Id) .ToHashSet(); -#if DEBUG - if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId)) - { - logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId); - } - else - { - logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidGroupingFieldSet.", targetUnitId); - } -#endif - logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); if (!unitIdsWithValidGroupingFieldSet.Any()) @@ -246,17 +275,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer regionalGroupValueIds ); -#if DEBUG - if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId)) - { - logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId); - } - else - { - logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId); - } -#endif - logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count); if (!unitIdsWithValidWorkGroupFieldSet.Any()) @@ -305,11 +323,11 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .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()); @@ -366,7 +384,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count); - // --- Разрешение конфликта --- + // === DEBUG: СПЕЦИАЛЬНАЯ ПРОВЕРКА ДУБЛИРОВАНИЯ === +#if DEBUG + await LogGroupingDebugInfo(jobGroup, groupedRelationships); +#endif + // =============================================== + + // --- Разрешение конфликта с детерминированной сортировкой --- var unitToKeys = new Dictionary>(); foreach (var kvp in groupedRelationships) { @@ -382,163 +406,66 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } } - var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList(); - foreach (var conflictedUnitEntry in conflictedUnits) + // Получаем имена всех конфликтующих ключей + var conflictKeys = unitToKeys + .Where(kvp => kvp.Value.Count > 1) + .SelectMany(kvp => kvp.Value) + .Distinct() + .ToList(); + + var unitNamesMap = await unitService.Get() + .Where(u => conflictKeys.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u.Name ?? string.Empty); + + foreach (var conflictedUnitEntry in unitToKeys.Where(kvp => kvp.Value.Count > 1)) { var unitId = conflictedUnitEntry.Key; var keysForUnit = conflictedUnitEntry.Value; - Guid bestKey = keysForUnit[0]; - int maxCount = groupedRelationships[bestKey].Count; + // Сортируем по: 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(); - for (int i = 1; i < keysForUnit.Count; i++) - { - var currentKey = keysForUnit[i]; - var currentCount = groupedRelationships[currentKey].Count; - if (currentCount > maxCount) - { - bestKey = currentKey; - maxCount = currentCount; - } - } + var bestKey = sortedKeys.First().key; + // Удаляем юнит из ВСЕХ групп, кроме лучшей foreach (var key in keysForUnit) { - if (key != bestKey) + if (key != bestKey && groupedRelationships.ContainsKey(key)) { groupedRelationships[key].Remove(unitId); } } } - var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList(); - foreach (var key in keysToRemove) + // Удаляем пустые группы + 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); - // === Подсчёт операций === - int toCreate = 0; - int toUpdate = 0; - int toDeactivate = 0; - - foreach (var kvp in groupedRelationships) - { - var childUnitIds = kvp.Value; - if (childUnitIds.Count == 0) continue; - - var childUnitNameMap = await unitService.Get() - .AsNoTracking() - .Where(u => childUnitIds.Contains(u.Id)) - .ToDictionaryAsync(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(); - - for (int i = 0; i < childUnitGroups.Count; i++) - { - var subGroup = childUnitGroups[i]; - var subGroupSize = subGroup.Count; - Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); - - var existingTemplatesForRelationship = await templateService.Get() - .AsNoTracking() - .Include(t=>t.Unit) - .Include(t=>t.Job) - .ThenInclude(t=>t.Group) - .ThenInclude(t=>t.GroupType) - .Include(t => t.Job) - .ThenInclude(t => t.Tnk) - .Include(t => t.UnitsInTemplate) - .Where(t => t.JobId == targetJob.Id && t.UnitId == kvp.Key && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) - .ToListAsync(); - - if (existingTemplatesForRelationship.Any()) - { - var existingTemplate = existingTemplatesForRelationship.First(); - var existingUnitIds = existingTemplate.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet(); - var newUnitIds = subGroup.ToHashSet(); - - if (!existingUnitIds.SetEquals(newUnitIds)) - toUpdate++; - else - { - // Обновляем UnitsInTemplate для корректной генерации имени - existingTemplate.UnitsInTemplate = subGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList(); - var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate); - if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase)) - toUpdate++; - } - } - else - { - toCreate++; - } - } - } - - // Подсчёт деактивации - 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 = await unitService.Get() - .AsNoTracking() - .Where(u => childUnitIds.Contains(u.Id)) - .ToDictionaryAsync(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(); - - 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, relationshipUnitId, i)); - } - } - - var allRelationshipUnitIds = 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) && allRelationshipUnitIds.Contains(t.UnitId)) - .ToListAsync(); - - foreach (var existingTemplate in allExistingTemplatesInGroup) - { - var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); - if (!expectedTemplateKeys.Contains(key)) - toDeactivate++; - } - - await UpdateMatchingStatusAsync(jobGroupId, $"Осталось: создать={toCreate}, обновить={toUpdate}, деактивировать={toDeactivate}"); + // === DEBUG: ЛОГИРОВАНИЕ ПОСЛЕ РАЗРЕШЕНИЯ КОНФЛИКТОВ === +#if DEBUG + await LogGroupingDebugInfo(jobGroup, groupedRelationships, true); +#endif + // ====================================================== // === Основной цикл обработки === - int created = 0, updated = 0, deactivated = 0; foreach (var kvp in groupedRelationships) { var relationshipUnitId = kvp.Key; @@ -551,13 +478,11 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .AsNoTracking() .Where(u => debugUnitIds.Contains(u.Id)) .ToDictionaryAsync(u => u.Id, u => u.Name); - var childUnitNamesForDebug = childUnitIds.Select(id => debugUnits.GetValueOrDefault(id, id.ToString())).ToList(); var relationshipUnitName = debugUnits.GetValueOrDefault(relationshipUnitId, relationshipUnitId.ToString()); - - if (childUnitNamesForDebug.Contains("ВРТ-AOS-05-ДВС")) + if (debugWatchUnitIds.Values.Contains(relationshipUnitId) || childUnitNamesForDebug.Contains("ВРТ-ASOUP2-ВСИБ")) { - logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юнит 'ВРТ-AOS-05-ДВС' в childUnitIds: [{ChildUnitNames}]", relationshipUnitId, relationshipUnitName, string.Join(", ", childUnitNamesForDebug)); + logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юниты: [{ChildUnitNames}]", relationshipUnitId, relationshipUnitName, string.Join(", ", childUnitNamesForDebug)); } #endif @@ -585,7 +510,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { var subGroup = childUnitGroups[i]; var subGroupSize = subGroup.Count; - logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RelationshipUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize); Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); @@ -598,29 +522,47 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .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(); + // === DEBUG: ДЕТАЛЬНОЕ СРАВНЕНИЕ UnitsInTemplate === +#if DEBUG if (existingTemplateForSubGroup != null) { - var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet(); - var newUnitIds = subGroup.ToHashSet(); - - if (existingUnitIds.SetEquals(newUnitIds)) + var relationshipUnit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == relationshipUnitId); + if (relationshipUnit != null && + (relationshipUnit.Name == "МОНИТОР-АСОУП-3-ВСИБ" || relationshipUnit.Name == "ЦЕКОН-ВСИБ") && + subGroup.Contains(debugWatchUnitIds.GetValueOrDefault("ВРТ-ASOUP2-ВСИБ", Guid.Empty))) { - logger.LogDebug("Шаблон {TemplateId} актуален по юнитам.", existingTemplateForSubGroup.Id); + await LogUnitComparisonDebugAsync(existingTemplateForSubGroup, subGroup, relationshipUnit.Name); + } + } +#endif + // =========================================== + if (existingTemplateForSubGroup != null) + { + var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); + var newUnitIds = subGroup.ToList(); + + // Сравниваем не только наборы, но и порядок + bool areUnitsEqual = existingUnitIds.SequenceEqual(newUnitIds); + + if (areUnitsEqual) + { + logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку.", existingTemplateForSubGroup.Id); // Обновляем UnitsInTemplate для генерации имени existingTemplateForSubGroup.UnitsInTemplate = subGroup.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 updateRequest = new TemplateUpdaterMq { TemplateId = existingTemplateForSubGroup.Id, @@ -636,10 +578,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer Initiator = initiator, UnitsInTemplate = subGroup }; - await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); - updated++; - await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } else { @@ -648,23 +587,18 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } else { - logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов.", existingTemplateForSubGroup.Id); - + 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, subGroup, newTargetJob, initiator); - updated++; - await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } } 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); @@ -700,30 +634,63 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); - created++; - await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } else { logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count); await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator); - created++; - await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } } } } // === Деактивация === + 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 = await unitService.Get() + .AsNoTracking() + .Where(u => childUnitIds.Contains(u.Id)) + .ToDictionaryAsync(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(); + 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, 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); + logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {UnitId}, Index {Index}).", + existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index); await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator); - deactivated++; - await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } } @@ -740,6 +707,160 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } } +#if DEBUG + private async Task LogGroupingDebugInfo(JobGroup jobGroup, Dictionary> groupedRelationships, bool isAfterConflictResolution = false) + { + logger.LogDebug("===[ DEBUG: {Stage} ГРУППИРОВКА ДЛЯ ОТСЛЕЖИВАНИЯ ДУБЛИРОВАНИЯ ВРТ-ASOUP2-ВСИБ ]===", + isAfterConflictResolution ? "ПОСЛЕ РАЗРЕШЕНИЯ КОНФЛИКТОВ" : "ДО РАЗРЕШЕНИЯ КОНФЛИКТОВ"); + + // Проверяем, есть ли в группах наши целевые юниты + var watchUnitsInGroups = groupedRelationships + .Where(g => debugWatchUnitIds.Values.Contains(g.Key) || g.Value.Any(id => debugWatchUnitIds.Values.Contains(id))) + .ToList(); + + if (!watchUnitsInGroups.Any()) + { + logger.LogDebug("Целевые юниты не найдены в группах"); + return; + } + + // Загружаем имена для всех юнитов в группах + var allUnitIds = watchUnitsInGroups.SelectMany(g => new[] { g.Key }.Concat(g.Value)).Distinct().ToList(); + var unitNames = await unitService.Get() + .AsNoTracking() + .Where(u => allUnitIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u.Name ?? $"(ID={u.Id})"); + + // Ищем все группы, содержащие ВРТ-ASOUP2-ВСИБ + var vrtUnitId = debugWatchUnitIds.GetValueOrDefault("ВРТ-ASOUP2-ВСИБ", Guid.Empty); + if (vrtUnitId != Guid.Empty) + { + var groupsWithVrt = groupedRelationships + .Where(g => g.Key == vrtUnitId || g.Value.Contains(vrtUnitId)) + .ToList(); + + if (groupsWithVrt.Any()) + { + logger.LogWarning("===[ DEBUG: ВРТ-ASOUP2-ВСИБ НАЙДЕН В СЛЕДУЮЩИХ ГРУППАХ ]==="); + foreach (var group in groupsWithVrt) + { + var groupName = unitNames.GetValueOrDefault(group.Key, $"(ID={group.Key})"); + var members = string.Join(", ", group.Value.Select(id => unitNames.GetValueOrDefault(id, $"(ID={id})"))); + logger.LogWarning("Группа [Ключ: {GroupName}]: Участники [{Members}]", groupName, members); + } + } + else + { + logger.LogDebug("ВРТ-ASOUP2-ВСИБ не найден в группах"); + } + } + + // Дополнительно: проверяем принадлежность ВРТ-ASOUP2-ВСИБ к целевым группам + var monitorGroupId = debugWatchUnitIds.GetValueOrDefault("МОНИТОР-АСОУП-3-ВСИБ", Guid.Empty); + var cekonGroupId = debugWatchUnitIds.GetValueOrDefault("ЦЕКОН-ВСИБ", Guid.Empty); + + if (vrtUnitId != Guid.Empty && (monitorGroupId != Guid.Empty || cekonGroupId != Guid.Empty)) + { + bool vrtInMonitorGroup = false; + if (monitorGroupId != Guid.Empty && groupedRelationships.TryGetValue(monitorGroupId, out var monitorGroup)) + { + vrtInMonitorGroup = monitorGroup.Contains(vrtUnitId); + } + + bool vrtInCekonGroup = false; + if (cekonGroupId != Guid.Empty && groupedRelationships.TryGetValue(cekonGroupId, out var cekonGroup)) + { + vrtInCekonGroup = cekonGroup.Contains(vrtUnitId); + } + + logger.LogWarning("=== АНАЛИЗ ПРИНАДЛЕЖНОСТИ ВРТ-ASOUP2-ВСИБ ==="); + logger.LogWarning("ВРТ-ASOUP2-ВСИБ в группе МОНИТОР-АСОУП-3-ВСИБ: {InGroup}", vrtInMonitorGroup); + logger.LogWarning("ВРТ-ASOUP2-ВСИБ в группе ЦЕКОН-ВСИБ: {InGroup}", vrtInCekonGroup); + + if (vrtInMonitorGroup && vrtInCekonGroup) + { + logger.LogError("!!! КРИТИЧНО: ВРТ-ASOUP2-ВСИБ ПРИНАДЛЕЖИТ ОБЕИМ ГРУППАМ !!!"); + } + } + + // Дополнительная информация о составе целевых групп + if (monitorGroupId != Guid.Empty || cekonGroupId != Guid.Empty) + { + logger.LogWarning("=== СОСТАВ ЦЕЛЕВЫХ ГРУПП ==="); + + if (monitorGroupId != Guid.Empty && groupedRelationships.TryGetValue(monitorGroupId, out var monitorGroup)) + { + var members = string.Join(", ", monitorGroup.Select(id => unitNames.GetValueOrDefault(id, $"(ID={id})"))); + logger.LogWarning("Группа МОНИТОР-АСОУП-3-ВСИБ ({Count} участников): [{Members}]", + monitorGroup.Count, members); + } + + if (cekonGroupId != Guid.Empty && groupedRelationships.TryGetValue(cekonGroupId, out var cekonGroup)) + { + var members = string.Join(", ", cekonGroup.Select(id => unitNames.GetValueOrDefault(id, $"(ID={id})"))); + logger.LogWarning("Группа ЦЕКОН-ВСИБ ({Count} участников): [{Members}]", + cekonGroup.Count, members); + } + } + } + + private async Task LogUnitComparisonDebugAsync(Template template, List newUnitIds, string groupName) + { + logger.LogWarning("===[ DEBUG: СРАВНЕНИЕ UnitsInTemplate ДЛЯ {GroupName} ]===", groupName); + + // Загружаем ИМЕНА для юнитов из базы + var dbUnitIds = template.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); + var dbUnitsMap = await unitService.Get() + .AsNoTracking() + .Where(u => dbUnitIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u.Name); + + var dbUnitNames = template.UnitsInTemplate + .Select(uit => dbUnitsMap.GetValueOrDefault(uit.UnitId, $"(ID={uit.UnitId})")) + .ToList(); + + // Загружаем ИМЕНА для новых юнитов + var newUnitsMap = await unitService.Get() + .AsNoTracking() + .Where(u => newUnitIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u.Name); + + var newUnitNames = newUnitIds + .Select(id => newUnitsMap.GetValueOrDefault(id, $"(ID={id})")) + .ToList(); + + logger.LogWarning("Шаблон ID={TemplateId}, Имя='{TemplateName}'", + template.Id, template.Name); + + logger.LogWarning("Юниты в базе ({Count}): [{Units}]", + dbUnitNames.Count, string.Join(", ", dbUnitNames)); + + logger.LogWarning("Новые юниты ({Count}): [{Units}]", + newUnitNames.Count, string.Join(", ", newUnitNames)); + + // Сравниваем ID + var existingUnitIds = template.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); + var newUnitIdsList = newUnitIds.ToList(); + + bool idSetsEqual = existingUnitIds.ToHashSet().SetEquals(newUnitIdsList.ToHashSet()); + bool orderEqual = existingUnitIds.SequenceEqual(newUnitIdsList); + + logger.LogWarning("Сравнение: ID-наборы совпадают={IdMatch}, Порядок совпадает={OrderMatch}", + idSetsEqual, orderEqual); + + if (!orderEqual) + { + logger.LogWarning("ПОРЯДОК ОТЛИЧАЕТСЯ:"); + logger.LogWarning(" База: [{Order}]", string.Join(", ", dbUnitNames)); + logger.LogWarning(" Новое: [{Order}]", string.Join(", ", newUnitNames)); + + // Дополнительно: показываем ID для точного сравнения + logger.LogWarning(" ID базы: [{Ids}]", string.Join(", ", existingUnitIds)); + logger.LogWarning(" ID новых: [{Ids}]", string.Join(", ", newUnitIdsList)); + } + } +#endif + public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); @@ -747,7 +868,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } // --- Вспомогательные методы --- - private async Task> FilterByWorkGroupFieldAsync( List units, HashSet candidateUnitIds, @@ -756,10 +876,10 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { return units .Where(u => candidateUnitIds.Contains(u.Id) && - u.UnitValues.Any(uv => - uv.FieldId == workGroupFieldId && - uv.Value != null && - regionalGroupValueIds.Contains(uv.Value.Id))) + u.UnitValues.Any(uv => + uv.FieldId == workGroupFieldId && + uv.Value != null && + regionalGroupValueIds.Contains(uv.Value.Id))) .Select(u => u.Id) .ToHashSet(); } @@ -795,7 +915,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; - if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); @@ -816,7 +935,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); var nextRun = await GetNextRunAsync(targetJob, template.NextRun); - var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, @@ -870,6 +988,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer Action = TemplateMatcherActionEnum.Sync, Comment = comment }; + await matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup,