From 5eb0ab81b9b9a97c80edeb30221debce142567a7 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Fri, 16 Jan 2026 19:45:38 +1000 Subject: [PATCH] =?UTF-8?q?feat(tempateMatcher):=20=D0=B2=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=82=D1=87=D0=B5=D1=80=D1=8B=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D0=B5=20=D1=81=20Mat?= =?UTF-8?q?chingStatusService,=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=81=D0=B0=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GroupedTemplateSynchronizer.cs | 1203 +++++++++-------- .../SimpleTemplateSynchronizer.cs | 514 ++++--- 2 files changed, 936 insertions(+), 781 deletions(-) diff --git a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs index e018ca97..d3f73c12 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs @@ -4,6 +4,7 @@ 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; @@ -43,6 +44,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer private readonly ITemplateDeactivator templateDeactivator; private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; + private readonly IMatchingStatusService matchingStatusService; public GroupedTemplateSynchronizer( ILogger logger, @@ -60,7 +62,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer IUnitFieldService unitFieldService, ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, - ITemplateUpdaterMqSender templateUpdaterMqSender + ITemplateUpdaterMqSender templateUpdaterMqSender, + IMatchingStatusService matchingStatusService ) { this.logger = logger; @@ -79,6 +82,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer this.templateDeactivator = templateDeactivator; this.templateNameNormalizer = templateNameNormalizer; this.templateUpdaterMqSender = templateUpdaterMqSender; + this.matchingStatusService = matchingStatusService; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) @@ -90,640 +94,650 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { 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()) + // === Проверка: уже запущена? === + var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); + if (existingStatus.DetailsJobGroups?.Any() == true) { - logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); + logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId); return; } - var jobsInGroup = jobGroup.Jobs.ToList(); - - // --- Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- - var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"); - if (workGroupField == null) + // === Устанавливаем статус "в процессе" === + var initialStatus = new MatchingStatusItemDto { - 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); - } -#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() - .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); - } -#endif - - logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); - - if (!unitIdsWithValidGroupingFieldSet.Any()) - { - logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); - return; - } - - // --- Дополнительная фильтрация по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- - var unitIdsWithValidWorkGroupFieldSet = await FilterByWorkGroupFieldAsync( - expectedUnitsWithGroupingField, - unitIdsWithValidGroupingFieldSet, - workGroupFieldId, - regionalGroupValueIds + 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) ); -#if DEBUG - // Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet - if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId)) + try { - logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId); - } - else - { - logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId); - } -#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, связанные с relationshipUnitId - if (childUnitIds.Count == 0) - continue; - -#if DEBUG - // === Безопасная асинхронная загрузка имён для отладки === - var debugUnitIds = childUnitIds.Concat(new[] { relationshipUnitId }).Distinct().ToList(); - var debugUnits = await unitService.Get() + // 1. Получить JobGroup и связанные Job'ы + var jobGroup = await jobGroupService.Get() .AsNoTracking() - .Where(u => debugUnitIds.Contains(u.Id)) - .ToDictionaryAsync(u => u.Id, u => u.Name); + .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); - var childUnitNamesForDebug = childUnitIds.Select(id => debugUnits.GetValueOrDefault(id, id.ToString())).ToList(); - var relationshipUnitName = debugUnits.GetValueOrDefault(relationshipUnitId, relationshipUnitId.ToString()); - - if (childUnitNamesForDebug.Contains("ВРТ-AOS-05-ДВС")) + if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) { - logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юнит 'ВРТ-AOS-05-ДВС' в childUnitIds: [{ChildUnitNames}]", relationshipUnitId, relationshipUnitName, string.Join(", ", childUnitNamesForDebug)); - } -#endif - - logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count); - - // --- СОРТИРОВКА childUnitIds ПО ИМЕНИ --- - 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(); - - // Применяем ограничение MaxValueRelationships maxJob - if (!maxJob.MaxValueRelationships.HasValue) - { - logger.LogWarning("Job {JobId} не заполнено MaxValueRelationships.", maxJob.Id); + logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); + await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст"); return; } - 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()) + + 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("Связанный юнит {RelationshipUnitId}: разбит на {GroupCount} подгрупп (после сортировки по имени).", relationshipUnitId, childUnitGroups.Count); + logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", regionalGroupValueIds.Count); - // Для каждой подгруппы: - for (int i = 0; i < childUnitGroups.Count; i++) + // 2. Найти Job с максимальным MaxValueRelationships + var maxJob = jobsInGroup + .Where(j => j.MaxValueRelationships.HasValue) + .OrderByDescending(j => j.MaxValueRelationships) + .FirstOrDefault(); + + if (maxJob == null) { - var subGroup = childUnitGroups[i]; - var subGroupSize = subGroup.Count; + logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); + await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships"); + return; + } - logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RelationshipUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize); + if (maxJob.UnitFilters == null) + { + logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id); + } - // Выбираем targetJob для подгруппы - Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); + logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships); - // 9. Загрузить существующие шаблоны для targetJob, связанные с relationshipUnitId - var existingTemplatesForRelationship = await templateService.Get() - .Include(t => t.Unit) - .Include(t => t.Job) - .ThenInclude(t => t!.Tnk) - .Include(t => t.Job) - .ThenInclude(t => t.Group) - .ThenInclude(t => t.GroupType) - .AsNoTracking() - .Include(t => t.UnitsInTemplate) - .Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) - .ToListAsync(); + // 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; + } - var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault(); +#if DEBUG + if (expectedUnitIds.Contains(targetUnitId)) + { + logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId); + } + else + { + logger.LogDebug("Юнит {TargetUnitId} НЕ найден в expectedUnitIds.", targetUnitId); + } +#endif - if (existingTemplateForSubGroup != null) + // 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(); + +#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()) + { + logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); + await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId"); + return; + } + + // --- Дополнительная фильтрация по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- + var unitIdsWithValidWorkGroupFieldSet = await FilterByWorkGroupFieldAsync( + expectedUnitsWithGroupingField, + unitIdsWithValidGroupingFieldSet, + workGroupFieldId, + 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()) + { + 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 existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet(); - var newUnitIds = subGroup.ToHashSet(); + var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List()); - if (existingUnitIds.SetEquals(newUnitIds)) + 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) { - logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Relationship {RelationshipId}, Index {Index}) актуален по юнитам.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i); + linkMatchesAllFilters = false; + break; + } + } - // Проверить, изменилось ли имя шаблона - var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup); - if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase)) + if (linkMatchesAllFilters) + { + filteredUnitInUnitLinks.Add(link); + } + } + + logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count); + + // --- Сгруппировать юниты --- + var groupedRelationships = new Dictionary>(); + foreach (var link in filteredUnitInUnitLinks) + { + var parentUnitId = link.ParentUnitId; + var childUnitId = link.ChildUnitId; + + if (unitIdsWithValidGroupingFieldSet.Contains(parentUnitId)) + { + if (!groupedRelationships.ContainsKey(childUnitId)) + { + groupedRelationships[childUnitId] = new List(); + } + groupedRelationships[childUnitId].Add(parentUnitId); + } + else if (unitIdsWithValidGroupingFieldSet.Contains(childUnitId)) + { + if (!groupedRelationships.ContainsKey(parentUnitId)) + { + groupedRelationships[parentUnitId] = new List(); + } + groupedRelationships[parentUnitId].Add(childUnitId); + } + } + + logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count); + + // --- Разрешение конфликта --- + var unitToKeys = new Dictionary>(); + foreach (var kvp in groupedRelationships) + { + var key = kvp.Key; + var units = kvp.Value; + foreach (var unitId in units) + { + if (!unitToKeys.ContainsKey(unitId)) + { + unitToKeys[unitId] = new List(); + } + unitToKeys[unitId].Add(key); + } + } + + var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList(); + 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; + } + } + + foreach (var key in keysForUnit) + { + if (key != bestKey) + { + groupedRelationships[key].Remove(unitId); + } + } + } + + var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList(); + foreach (var key in keysToRemove) + { + 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 { - logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.Name, expectedName); + // Обновляем 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 nextRun = await GetNextRunAsync(targetJob, existingTemplateForSubGroup.NextRun); + // Подсчёт деактивации + 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}"); + + // === Основной цикл обработки === + int created = 0, updated = 0, deactivated = 0; + 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 (childUnitNamesForDebug.Contains("ВРТ-AOS-05-ДВС")) + { + logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юнит 'ВРТ-AOS-05-ДВС' в childUnitIds: [{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) + .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} актуален по юнитам.", 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); + updated++; + await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); + } + 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); + 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); + + 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 = existingTemplateForSubGroup.Id, + TemplateId = reusableTemplate.Id, JobId = targetJob.Id, UnitId = relationshipUnitId, Name = expectedName, - IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate, - IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule, - LastRun = existingTemplateForSubGroup.LastRun, - NextRun = nextRun, - Index = i, + 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); + created++; + await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } else { - logger.LogDebug("Шаблон {TemplateId} полностью актуален (юниты и имя).", existingTemplateForSubGroup.Id); + 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}"); } } - else - { - logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Relationship {RelationshipId}, 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 + } + + // === Деактивация === + foreach (var existingTemplate in allExistingTemplatesInGroup) + { + var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); + if (!expectedTemplateKeys.Contains(key)) { - 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); - - // === Создаём временный Template для нормализации имени === - 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); - } + 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}"); } } - } - // 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе - var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); - foreach (var kvp in groupedRelationships) + // === Успешное завершение === + await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно"); + await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); + logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); + } + catch (Exception ex) { - var relationshipUnitId = kvp.Key; - var childUnitIds = kvp.Value; - - // --- СОРТИРОВКА ПЕРЕД ФОРМИРОВАНИЕМ expectedTemplateKeys --- - 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)); - } + logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId); + await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}"); + throw; } - - 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)) - { - logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {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) @@ -734,9 +748,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer // --- Вспомогательные методы --- - /// - /// Фильтрует юниты по полю "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" - /// private async Task> FilterByWorkGroupFieldAsync( List units, HashSet candidateUnitIds, @@ -753,9 +764,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .ToHashSet(); } - /// - /// Выбирает Job, соответствующий размеру подгруппы - /// private Job SelectTargetJob(List jobsInGroup, int subGroupSize, Job maxJob) { Job? targetJob = jobsInGroup @@ -794,7 +802,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer return; } - // === Создаём временный Template для нормализации имени === var tempTemplateForName = new Template { Id = template.Id, @@ -854,4 +861,20 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer 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) + ); + } } \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs index 1898fe20..d8ab7bdd 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs @@ -4,6 +4,7 @@ using PARR.BLL.Domain.Mq; using PARR.BLL.Services.Interfaces; using PARR.Common.Domain; using PARR.Constants; +using PARR.DAL.Cache.Models; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.Models; @@ -38,6 +39,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer private readonly ITemplateDeactivator templateDeactivator; private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; + private readonly IMatchingStatusService matchingStatusService; public SimpleTemplateSynchronizer( ILogger logger, @@ -56,7 +58,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer IUnitFieldService unitFieldService, ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, - ITemplateUpdaterMqSender templateUpdaterMqSender + ITemplateUpdaterMqSender templateUpdaterMqSender, + IMatchingStatusService matchingStatusService ) { this.logger = logger; @@ -70,174 +73,237 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer this.templateDeactivator = templateDeactivator; this.templateNameNormalizer = templateNameNormalizer; this.templateUpdaterMqSender = templateUpdaterMqSender; + this.matchingStatusService = matchingStatusService; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для Job {JobId}", jobId); - var job = await jobService.Get() - .AsNoTracking() - .Include(j => j.AutoControl) - .Include(j => j.Tnk) - .Include(j => j.Group) - .ThenInclude(g => g!.GroupType) - .Include(j => j.UnitFilters) - .ThenInclude(uf => uf.RelationshipFilters) - .FirstOrDefaultAsync(j => j.Id == jobId); - - if (job == null) + // === Проверка: уже запущена? === + var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); + if (existingStatus.DetailsJobs?.Any() == true) { - logger.LogWarning("Job {JobId} не найден.", jobId); + logger.LogWarning("Синхронизация для Job {JobId} уже запущена. Пропускаем.", jobId); return; } - var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); - if (unitIds == null || !unitIds.Any()) + // === Устанавливаем статус "в процессе" === + var initialStatus = new MatchingStatusItemDto { - logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); + DateStart = DateTimeOffset.UtcNow, + Action = TemplateMatcherActionEnum.Sync, + Comment = "Начало синхронизации" + }; + await matchingStatusService.SetMatchingStatusAsync( + jobId, + SyncTaskEntityTypeEnum.Job, + new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, + TimeSpan.FromMinutes(35) + ); - // Деактивировать все существующие шаблоны для этого Job - var existingTemplatesForDeactivation = await templateService.Get() + try + { + var job = await jobService.Get() .AsNoTracking() - .Include(t => t.UnitsInTemplate) - .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) - .ToListAsync(); + .Include(j => j.AutoControl) + .Include(j => j.Tnk) + .Include(j => j.Group) + .ThenInclude(g => g!.GroupType) + .Include(j => j.UnitFilters) + .ThenInclude(uf => uf.RelationshipFilters) + .FirstOrDefaultAsync(j => j.Id == jobId); - foreach (var unusedTemplate in existingTemplatesForDeactivation) + if (job == null) { - logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); - await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); + logger.LogWarning("Job {JobId} не найден.", jobId); + await UpdateMatchingStatusAsync(jobId, "Job не найден"); + return; } - - logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); - return; - } - -#if DEBUG - // Отладка: проверить, есть ли юнит в unitIds - if (unitIds.Contains(targetUnitId)) - { - logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); - } - else - { - logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId); - } -#endif - - var existingTemplates = await templateService.Get() - .Include(t => t.UnitsInTemplate) - .Include(t => t.Job) - .ThenInclude(t => t!.Group) - .ThenInclude(t => t.GroupType) - .Include(t => t.Job) - .ThenInclude(t => t!.Tnk) - .Include(t => t.Unit) - .Where(t => t.JobId == jobId) - .ToListAsync(); - - var existingUsedTemplates = existingTemplates - .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used) - .ToList(); - - var existingUnitIds = existingUsedTemplates - .Select(t => t.UnitId) - .ToHashSet(); - - var newUnitIds = unitIds.Except(existingUnitIds).ToList(); - var unusedTemplates = existingUsedTemplates - .Where(t => !unitIds.Contains(t.UnitId)) - .ToList(); - - // Обработка новых юнитов - foreach (var unitId in newUnitIds) - { - var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); - if (reusableTemplate != null) + + var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); { - logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId); - - // === Создаём временный Template для нормализации имени === - var tempTemplateForName = new Template + if (unitIds == null || !unitIds.Any()) { - Id = reusableTemplate.Id, - Name = reusableTemplate.Name, - JobId = jobId, - UnitId = unitId, - Index = reusableTemplate.Index, - Job = job, // загруженный job - Unit = reusableTemplate.Unit, // может быть null — нормально - UnitsInTemplate = new List() // для простого шаблона - }; + logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); - var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); - var nextRun = await GetNextRunAsync(job); + var existingTemplatesForDeactivation = await templateService.Get() + .AsNoTracking() + .Include(t => t.UnitsInTemplate) + .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) + .ToListAsync(); - var updateRequest = new TemplateUpdaterMq - { - TemplateId = reusableTemplate.Id, - JobId = jobId, - UnitId = unitId, - Name = expectedName, - IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, - IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, - StatusTypeId = TemplateStatusTypeEnum.Used, - Initiator = initiator, - NextRun = nextRun, - UnitsInTemplate = new List() // для простого шаблона - }; + await UpdateMatchingStatusAsync(jobId, $"Нет Unit'ов. Деактивация {existingTemplatesForDeactivation.Count} шаблонов..."); - await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); + foreach (var unusedTemplate in existingTemplatesForDeactivation) + { + logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); + await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); + } + + await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена: нет Unit'ов"); + await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); + logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); + return; + } + } +#if DEBUG + if (unitIds.Contains(targetUnitId)) + { + logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); } else { - logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId); - await CreateSimpleTemplateAsync(jobId, unitId, initiator); + logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId); } - } +#endif - // Обработка существующих шаблонов (проверка имени) - foreach (var template in existingUsedTemplates) - { - if (unitIds.Contains(template.UnitId)) + var existingTemplates = await templateService.Get() + .Include(t => t.UnitsInTemplate) + .Include(t => t.Job) + .ThenInclude(t => t!.Group) + .ThenInclude(t => t.GroupType) + .Include(t => t.Job) + .ThenInclude(t => t!.Tnk) + .Include(t => t.Unit) + .Where(t => t.JobId == jobId) + .ToListAsync(); + + var existingUsedTemplates = existingTemplates + .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used) + .ToList(); + + var existingUnitIds = existingUsedTemplates + .Select(t => t.UnitId) + .ToHashSet(); + + var newUnitIds = unitIds.Except(existingUnitIds).ToList(); + var unusedTemplates = existingUsedTemplates + .Where(t => !unitIds.Contains(t.UnitId)) + .ToList(); + + // === Подсчёт операций === + int toCreate = newUnitIds.Count; + int toUpdate = 0; + int toDeactivate = unusedTemplates.Count; + + // Подсчёт обновлений по имени + foreach (var template in existingUsedTemplates) { - var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); - if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) + if (unitIds.Contains(template.UnitId)) { - logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); + var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); + if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) + toUpdate++; + } + } - var nextRun = await GetNextRunAsync(job, template.NextRun); + await UpdateMatchingStatusAsync(jobId, $"Осталось: создать={toCreate}, обновить={toUpdate}, деактивировать={toDeactivate}"); + + // === Обработка новых юнитов === + int created = 0, updated = 0, deactivated = 0; + foreach (var unitId in newUnitIds) + { + var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); + if (reusableTemplate != null) + { + logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId); + + var tempTemplateForName = new Template + { + Id = reusableTemplate.Id, + Name = reusableTemplate.Name, + JobId = jobId, + UnitId = unitId, + Index = reusableTemplate.Index, + Job = job, + Unit = reusableTemplate.Unit, + UnitsInTemplate = new List() + }; + + var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); + var nextRun = await GetNextRunAsync(job); var updateRequest = new TemplateUpdaterMq { - TemplateId = template.Id, + TemplateId = reusableTemplate.Id, JobId = jobId, - UnitId = template.UnitId, + UnitId = unitId, Name = expectedName, - IsActiveTemplate = template.IsActiveTemplate, - IsActiveSchedule = template.IsActiveSchedule, - LastRun = template.LastRun, - NextRun = nextRun, - Index = template.Index, + IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, + IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, - UnitsInTemplate = new List() // для простого шаблона + NextRun = nextRun, + UnitsInTemplate = new List() }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } + else + { + logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId); + await CreateSimpleTemplateAsync(jobId, unitId, initiator); + } + created++; + await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } - } - // Деактивация лишних шаблонов - foreach (var unusedTemplate in unusedTemplates) + // === Обработка существующих шаблонов (проверка имени) === + foreach (var template in existingUsedTemplates) + { + if (unitIds.Contains(template.UnitId)) + { + var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); + if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", template.Id); + + var nextRun = await GetNextRunAsync(job, template.NextRun); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = jobId, + 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 = new List() + }; + + await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); + updated++; + await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); + } + } + } + + // === Деактивация лишних шаблонов === + foreach (var unusedTemplate in unusedTemplates) + { + logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); + await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); + deactivated++; + await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); + } + + // === Успешное завершение === + await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно"); + await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); + logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); + } + catch (Exception ex) { - logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); - await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); + logger.LogError(ex, "Ошибка при синхронизации Job {JobId}", jobId); + await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}"); + throw; } - - logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); } public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) @@ -249,86 +315,136 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer { logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId); - var job = await jobService.Get() - .AsNoTracking() - .Include(j => j.AutoControl) - .Include(j => j.Tnk) - .Include(j => j.Group) - .ThenInclude(g => g!.GroupType) - .Include(j => j.UnitFilters) - .ThenInclude(uf => uf.RelationshipFilters) - .FirstOrDefaultAsync(j => j.Id == jobId); - - if (job == null) + // === Проверка: уже запущена? === + var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); + if (existingStatus.DetailsJobs?.Any() == true) { - logger.LogWarning("Job {JobId} не найден.", jobId); + logger.LogWarning("Обновление для Job {JobId} уже запущено. Пропускаем.", jobId); return; } - var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); - if (unitIds == null || !unitIds.Any()) + var initialStatus = new MatchingStatusItemDto { - logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); - return; - } + DateStart = DateTimeOffset.UtcNow, + Action = TemplateMatcherActionEnum.Update, + Comment = "Начало обновления имён" + }; + await matchingStatusService.SetMatchingStatusAsync( + jobId, + SyncTaskEntityTypeEnum.Job, + new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, + TimeSpan.FromMinutes(30) + ); + + try + { + var job = await jobService.Get() + .AsNoTracking() + .Include(j => j.AutoControl) + .Include(j => j.Tnk) + .Include(j => j.Group) + .ThenInclude(g => g!.GroupType) + .Include(j => j.UnitFilters) + .ThenInclude(uf => uf.RelationshipFilters) + .FirstOrDefaultAsync(j => j.Id == jobId); + + if (job == null) + { + logger.LogWarning("Job {JobId} не найден.", jobId); + await UpdateMatchingStatusAsync(jobId, "Job не найден"); + return; + } + + var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); + if (unitIds == null || !unitIds.Any()) + { + logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); + await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется"); + await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); + return; + } #if DEBUG - // Отладка: проверить, есть ли юнит в unitIds - if (unitIds.Contains(targetUnitId)) - { - logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); - } - else - { - logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId); - } + if (unitIds.Contains(targetUnitId)) + { + logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); + } + else + { + logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId); + } #endif - var existingTemplates = await templateService.Get() - .AsNoTracking() - .Include(t => t.Unit) - .Include(t => t.UnitsInTemplate) - .Include(t => t.Job) - .ThenInclude(t => t!.Group) - .ThenInclude(t => t!.GroupType) - .Include(t => t.Job) - .ThenInclude(t => t!.Tnk) - .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) - .ToListAsync(); + var existingTemplates = await templateService.Get() + .AsNoTracking() + .Include(t => t.Unit) + .Include(t => t.UnitsInTemplate) + .Include(t => t.Job) + .ThenInclude(t => t!.Group) + .ThenInclude(t => t!.GroupType) + .Include(t => t.Job) + .ThenInclude(t => t!.Tnk) + .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) + .ToListAsync(); - foreach (var template in existingTemplates) - { - if (unitIds.Contains(template.UnitId)) + int toUpdate = 0; + foreach (var template in existingTemplates) { - var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); - if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) + if (unitIds.Contains(template.UnitId)) { - logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); - - var nextRun = await GetNextRunAsync(job, template.NextRun); - - var updateRequest = new TemplateUpdaterMq - { - TemplateId = template.Id, - JobId = jobId, - 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 = new List() // для простого шаблона - }; - - await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); + var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); + if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) + toUpdate++; } } - } - logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId); + await UpdateMatchingStatusAsync(jobId, $"Обновление имён: {toUpdate} шаблонов"); + + int updated = 0; + foreach (var template in existingTemplates) + { + if (unitIds.Contains(template.UnitId)) + { + var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); + if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", template.Id); + + var nextRun = await GetNextRunAsync(job, template.NextRun); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = jobId, + 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 = new List() + }; + + await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); + updated++; + await UpdateMatchingStatusAsync(jobId, $"Обновлено: {updated}/{toUpdate}"); + } + } + } + + await UpdateMatchingStatusAsync(jobId, "Обновление завершено"); + await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); + logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при обновлении Job {JobId}", jobId); + await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}"); + throw; + } } private async Task CreateSimpleTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator) @@ -339,7 +455,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer { JobId = jobId, UnitId = unitId, - UnitsInTemplate = new List(), // для простого шаблона + UnitsInTemplate = new List(), HistoryInitiator = initiator }; @@ -355,4 +471,20 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer var referenceDate = job.Group?.ReferenceDate ?? DateTimeOffset.UtcNow; return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); } + + private async Task UpdateMatchingStatusAsync(Guid jobId, string comment) + { + var status = new MatchingStatusItemDto + { + DateStart = DateTimeOffset.UtcNow, + Action = TemplateMatcherActionEnum.Sync, + Comment = comment + }; + await matchingStatusService.SetMatchingStatusAsync( + jobId, + SyncTaskEntityTypeEnum.Job, + new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, + TimeSpan.FromMinutes(30) + ); + } } \ No newline at end of file