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.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.Text.Json; namespace PARR.TemplateMatcher.Services.Implementations; internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { #if DEBUG private readonly string[] debugWatchUnitNames = { "МОНИТОР-АСОУП-3-ВСИБ", "ЦЕКОН-ВСИБ", "ВРТ-ASOUP2-ВСИБ" }; private Dictionary debugWatchUnitIds = new Dictionary(); #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; private readonly IMatchingStatusService matchingStatusService; 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 ) { 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; } #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); } public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { #if DEBUG await InitializeDebugUnitsAsync(); #endif 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 regionalGroupValueIds = regionalEkPtkGroupService.Get() .Select(g => g.FieldValueId) .ToList(); 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); 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 для получения expectedUnitIds var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id); if (expectedUnitIds == null || !expectedUnitIds.Any()) { logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов"); return; } #if DEBUG 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("Юнит МОНИТОР-АСОУП-3-ВСИБ найден в expectedUnitIds."); } if (debugCekonUnitId != Guid.Empty && expectedUnitIds.Contains(debugCekonUnitId)) { logger.LogDebug("Юнит ЦЕКОН-ВСИБ найден в expectedUnitIds."); } if (debugVrtUnitId != Guid.Empty && expectedUnitIds.Contains(debugVrtUnitId)) { logger.LogDebug("Юнит ВРТ-ASOUP2-ВСИБ найден в expectedUnitIds."); } #endif // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId if (!jobGroup.GroupingUnitFieldId.HasValue) { logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Отсутствует GroupingUnitFieldId"); return; } var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; var expectedUnitsWithGroupingField = await unitService.Get() .AsNoTracking() .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(); logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); if (!unitIdsWithValidGroupingFieldSet.Any()) { logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId"); return; } // --- Дополнительная фильтрация по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- var unitIdsWithValidWorkGroupFieldSet = await FilterByWorkGroupFieldAsync( expectedUnitsWithGroupingField, unitIdsWithValidGroupingFieldSet, workGroupFieldId, regionalGroupValueIds ); 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 для юнитов, прошедших фильтрацию..."); 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 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(); 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); } } 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); // === DEBUG: СПЕЦИАЛЬНАЯ ПРОВЕРКА ДУБЛИРОВАНИЯ === #if DEBUG await LogGroupingDebugInfo(jobGroup, groupedRelationships); #endif // =============================================== // --- Разрешение конфликта с детерминированной сортировкой --- 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 = 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; // Сортируем по: 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); // === DEBUG: ЛОГИРОВАНИЕ ПОСЛЕ РАЗРЕШЕНИЯ КОНФЛИКТОВ === #if DEBUG await LogGroupingDebugInfo(jobGroup, groupedRelationships, true); #endif // ====================================================== // === Основной цикл обработки === foreach (var kvp in groupedRelationships) { var relationshipUnitId = kvp.Key; var childUnitIds = kvp.Value; if (childUnitIds.Count == 0) continue; #if DEBUG var debugUnitIds = childUnitIds.Concat(new[] { relationshipUnitId }).Distinct().ToList(); var debugUnits = await unitService.Get() .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 (debugWatchUnitIds.Values.Contains(relationshipUnitId) || childUnitNamesForDebug.Contains("ВРТ-ASOUP2-ВСИБ")) { logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юниты: [{ChildUnitNames}]", relationshipUnitId, relationshipUnitName, string.Join(", ", childUnitNamesForDebug)); } #endif logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count); 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(); logger.LogDebug("Связанный юнит {RelationshipUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count); for (int i = 0; i < childUnitGroups.Count; i++) { var subGroup = childUnitGroups[i]; 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(); // === DEBUG: ДЕТАЛЬНОЕ СРАВНЕНИЕ UnitsInTemplate === #if DEBUG if (existingTemplateForSubGroup != null) { 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))) { 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, 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} требует обновления юнитов или их порядка.", 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); } } 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 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 = 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); 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; } } #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); return; } // --- Вспомогательные методы --- private async Task> FilterByWorkGroupFieldAsync( List units, HashSet candidateUnitIds, Guid workGroupFieldId, List regionalGroupValueIds) { return units .Where(u => candidateUnitIds.Contains(u.Id) && u.UnitValues.Any(uv => uv.FieldId == workGroupFieldId && uv.Value != null && regionalGroupValueIds.Contains(uv.Value.Id))) .Select(u => u.Id) .ToHashSet(); } 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) { 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 = template.Index, 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 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 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) ); } }