diff --git a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs index 10fcfa1d..128546e2 100644 --- a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs @@ -13,7 +13,7 @@ namespace PARR.DAL.DomainServices.Implementations internal class UnitFilterService : IUnitFilterService { #if DEBUG - private readonly Guid targetUnitId = Guid.Parse("d4322a08-246b-4380-8953-8ce4a8446235"); + private readonly Guid targetUnitId = Guid.Parse("6c019569-ecba-4d9a-9cdf-cdc1dea93925"); #endif private readonly ILogger logger; @@ -123,7 +123,7 @@ namespace PARR.DAL.DomainServices.Implementations logger.LogDebug("Базовый фильтр по Name '{NameFilter}' дал {Count} юнитов", filter.UnitFilter, initialUnitIds.Count); #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в initialUnitIds + // Отладка: проверить, есть ли юнит в initialUnitIds if (initialUnitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в initialUnitIds.", targetUnitId); @@ -267,7 +267,7 @@ namespace PARR.DAL.DomainServices.Implementations } #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в candidateUnits после FieldFilter + // Отладка: проверить, есть ли юнит в candidateUnits после FieldFilter var candidateListAfterFieldFilter = candidateUnits.ToList(); if (candidateListAfterFieldFilter.Any(u => u.Id == targetUnitId)) { @@ -276,7 +276,6 @@ namespace PARR.DAL.DomainServices.Implementations else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после FieldFilter.", targetUnitId); - //continue; // ❌ юнит отсеялся на этом этапе } logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateListAfterFieldFilter.Count); @@ -288,7 +287,7 @@ namespace PARR.DAL.DomainServices.Implementations } #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в candidateUnits после RelationshipFilter + // Отладка: проверить, есть ли юнит в candidateUnits после RelationshipFilter var candidateListAfterRelFilter = candidateUnits.ToList(); if (candidateListAfterRelFilter.Any(u => u.Id == targetUnitId)) { @@ -297,7 +296,6 @@ namespace PARR.DAL.DomainServices.Implementations else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после RelationshipFilter.", targetUnitId); - //continue; // ❌ юнит отсеялся на этом этапе } logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateListAfterRelFilter.Count); @@ -311,7 +309,7 @@ namespace PARR.DAL.DomainServices.Implementations logger.LogDebug("После Umbrella-фильтра осталось {Count} юнитов", finalUnits.Count()); #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в finalUnits после Umbrella + // Отладка: проверить, есть ли юнит в finalUnits после Umbrella if (finalUnits.Any(u => u.Id == targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в finalUnits после Umbrella-фильтра.", targetUnitId); @@ -331,7 +329,7 @@ namespace PARR.DAL.DomainServices.Implementations .ToList(); #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в newIds + // Отладка: проверить, есть ли юнит в newIds if (newIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в newIds.", targetUnitId); @@ -359,7 +357,7 @@ namespace PARR.DAL.DomainServices.Implementations job.Id, job.UnitFilters.Count, result.Count); #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в result + // Отладка: проверить, есть ли юнит в result if (result.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в финальном результате.", targetUnitId); @@ -398,11 +396,11 @@ namespace PARR.DAL.DomainServices.Implementations logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count()); - // ✅ Получить все связи для юнита + // Получить все связи для юнита var parentLinks = await unitInUnitService.GetByChildIdAsync(unitId); var childLinks = await unitInUnitService.GetByParentIdAsync(unitId); - // ✅ Собрать все UnitId, участвующие в связях + // Собрать все UnitId, участвующие в связях var allRelatedUnitIds = parentLinks .Select(l => l.ParentUnitId) .Concat(childLinks.Select(l => l.ChildUnitId)) @@ -411,20 +409,20 @@ namespace PARR.DAL.DomainServices.Implementations if (!allRelatedUnitIds.Any()) continue; - // ✅ Получить значения для всех связанных юнитов + // Получить значения для всех связанных юнитов var allUnitValues = await unitInValueService.GetByUnitIdsAsync(allRelatedUnitIds); - // ✅ Сгруппировать значения по UnitId + // Сгруппировать значения по UnitId var valuesByUnit = allUnitValues .GroupBy(uv => uv.UnitId) .ToDictionary(g => g.Key, g => g.ToList()); - // ✅ Найти UnitId, которые проходят все RelationshipFilters + // Найти UnitId, которые проходят все RelationshipFilters var matchingUnitIds = new HashSet(); foreach (var relatedUnitId in allRelatedUnitIds) { - // ✅ Изменяем на `All` — все фильтры должны подходить + // Изменяем на `All` — все фильтры должны подходить bool passesAllFilters = filter.RelationshipFilters.All(rf => { var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List()); @@ -435,14 +433,14 @@ namespace PARR.DAL.DomainServices.Implementations if (!matchingValues.Any()) // Нет значений по полю { - // ✅ Если нет значений, и IsInverse = false → не подходит - // ✅ Если нет значений, и IsInverse = true → подходит + // Если нет значений, и IsInverse = false → не подходит + // Если нет значений, и IsInverse = true → подходит return rf.IsInverse; } var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase)); - // ✅ Учитываем IsInverse + // Учитываем IsInverse if (rf.IsInverse) hasMatch = !hasMatch; @@ -490,7 +488,7 @@ namespace PARR.DAL.DomainServices.Implementations v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase)) ); - // ✅ Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр + // Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр return fieldFilter.IsInverse ? !hasMatch : hasMatch; }).AsQueryable(); } @@ -509,13 +507,13 @@ namespace PARR.DAL.DomainServices.Implementations var isFullMatch = relFilter.IsFullMatch; var fieldId = relFilter.FieldId; - bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); - bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); + // Используем Trim('%') для всех случаев + var trimmedMask = valueMask.Trim('%'); return query.AsEnumerable().Where(dto => { #if DEBUG - // ✅ Отладка: проверить, это нужный юнит + // Отладка: проверить, это нужный юнит if (dto.Id == targetUnitId) { logger.LogDebug("Проверяем юнит {TargetUnitId} с {ParentCount} родителями и {ChildCount} детьми.", @@ -528,7 +526,7 @@ namespace PARR.DAL.DomainServices.Implementations if (links == null || !links.Any()) { #if DEBUG - // ✅ Отладка: юнит не имеет связей + // Отладка: юнит не имеет связей if (dto.Id == targetUnitId) { logger.LogDebug("Юнит {TargetUnitId}: нет связей ({Direction}), результат фильтра: {Result}", @@ -542,7 +540,7 @@ namespace PARR.DAL.DomainServices.Implementations } #if DEBUG - // ✅ Отладка: проверить, какие связи у юнита + // Отладка: проверить, какие связи у юнита if (dto.Id == targetUnitId) { logger.LogDebug("Юнит {TargetUnitId}: {Count} связей ({Direction}).", targetUnitId, links.Count, isParent ? "Parent" : "Child"); @@ -553,62 +551,72 @@ namespace PARR.DAL.DomainServices.Implementations } #endif - var hasMatchingLinks = links.Any(link => + if (!isFullMatch) { - var hasMatch = link.Values.Any(v => - v.FieldId == fieldId && - v.Value != null && - (isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) : - isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) : - isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) : - v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase)) - ); - -#if DEBUG - // ✅ Отладка: проверить, какая связь подходит - if (dto.Id == targetUnitId) + // Если IsFullMatch = false — проверяем, есть ли хотя бы одна подходящая связь + var hasMatchingLinks = links.Any(link => { - logger.LogDebug(" Проверка связи {LinkUnitId}: hasMatch={HasMatch} (ValueMask={ValueMask}, FieldId={FieldId})", link.UnitId, hasMatch, valueMask, fieldId); - } -#endif + var values = link.Values.Where(v => v.FieldId == fieldId).ToList(); - return isInverse ? !hasMatch : hasMatch; - }); + if (!values.Any()) + { + // Если нет значений по полю — связь не подходит, если IsInverse = false + // Если нет значений по полю — связь подходит, если IsInverse = true + return isInverse; + } -#if DEBUG - // ✅ Отладка: результат hasMatchingLinks - if (dto.Id == targetUnitId) - { - logger.LogDebug(" hasMatchingLinks = {HasMatchingLinks}, isInverse = {IsInverse}", hasMatchingLinks, isInverse); - } -#endif - - if (isFullMatch) - { - var allMatch = links.All(link => - { - var hasMatch = link.Values.Any(v => - v.FieldId == fieldId && + var hasMatch = values.Any(v => v.Value != null && - (isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) : - isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) : - isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) : - v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase)) + // Проверка с Trim('%') + (valueMask.StartsWith("%") && valueMask.EndsWith("%") ? v.Value.Contains(trimmedMask, StringComparison.OrdinalIgnoreCase) : + valueMask.StartsWith("%") ? v.Value.EndsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) : + valueMask.EndsWith("%") ? v.Value.StartsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) : + string.Equals(v.Value, valueMask, StringComparison.OrdinalIgnoreCase)) ); -#if DEBUG - // ✅ Отладка: проверить, все ли связи подходят - if (dto.Id == targetUnitId) - { - logger.LogDebug(" Проверка связи {LinkUnitId} для FullMatch: hasMatch={HasMatch}", link.UnitId, hasMatch); - } -#endif - + // Учитываем IsInverse return isInverse ? !hasMatch : hasMatch; }); #if DEBUG - // ✅ Отладка: результат allMatch + // Отладка: результат hasMatchingLinks + if (dto.Id == targetUnitId) + { + logger.LogDebug(" hasMatchingLinks = {HasMatchingLinks}, isInverse = {IsInverse}", hasMatchingLinks, isInverse); + } +#endif + + return hasMatchingLinks; + } + else + { + // Если IsFullMatch = true — все связи должны подходить + var allMatch = links.All(link => + { + var values = link.Values.Where(v => v.FieldId == fieldId).ToList(); + + if (!values.Any()) + { + // Если нет значений по полю — связь не подходит, если IsInverse = false + // Если нет значений по полю — связь подходит, если IsInverse = true + return isInverse; + } + + var hasMatch = values.Any(v => + v.Value != null && + // Проверка с Trim('%') + (valueMask.StartsWith("%") && valueMask.EndsWith("%") ? v.Value.Contains(trimmedMask, StringComparison.OrdinalIgnoreCase) : + valueMask.StartsWith("%") ? v.Value.EndsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) : + valueMask.EndsWith("%") ? v.Value.StartsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) : + string.Equals(v.Value, valueMask, StringComparison.OrdinalIgnoreCase)) + ); + + // Учитываем IsInverse + return isInverse ? !hasMatch : hasMatch; + }); + +#if DEBUG + // Отладка: результат allMatch if (dto.Id == targetUnitId) { logger.LogDebug(" allMatch = {AllMatch}, isFullMatch = {IsFullMatch}", allMatch, isFullMatch); @@ -617,8 +625,6 @@ namespace PARR.DAL.DomainServices.Implementations return allMatch; } - - return hasMatchingLinks; }).AsQueryable(); } @@ -639,57 +645,171 @@ namespace PARR.DAL.DomainServices.Implementations logger.LogDebug("RelationshipCountFilter: Min={Min}, Max={Max}, IsParent={IsParent}", min, max, isParentDirection); - // Определяем, есть ли фильтры по полям - var activeFilters = relationshipFilters - .Where(rf => rf.IsParent == isParentDirection && !string.IsNullOrWhiteSpace(rf.ValueMask)) + // Разделяем фильтры по направлениям + var parentFilters = relationshipFilters + .Where(rf => rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask)) .ToList(); - bool hasFieldFilters = activeFilters.Count > 0; + var childFilters = relationshipFilters + .Where(rf => !rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask)) + .ToList(); - logger.LogDebug("RelationshipCountFilter: Найдено {Count} фильтров по полям для направления {Direction}", activeFilters.Count, isParentDirection ? "Parent" : "Child"); + bool hasParentFieldFilters = parentFilters.Count > 0; + bool hasChildFieldFilters = childFilters.Count > 0; + + logger.LogDebug("RelationshipCountFilter: Найдено {ParentCount} фильтров по родителям и {ChildCount} фильтров по детям", parentFilters.Count, childFilters.Count); return units.Where(dto => { - var links = isParentDirection ? dto.Parents : dto.Children; - - if (links == null || !links.Any()) - { - var result = min == 0; - logger.LogDebug("UnitId {UnitId}: связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result); - return result; - } - - logger.LogDebug("UnitId {UnitId}: {Count} связей до фильтрации", dto.Id, links.Count); - int matchingCount = 0; - if (hasFieldFilters) + if (isParentDirection) // Считаем родительские связи { - // Есть фильтры по полям → считаем только связанные юниты, подходящие под фильтр - foreach (var link in links) + var links = dto.Parents; + + if (links == null || !links.Any()) { - // ✅ Изменяем на `All` — все фильтры должны подходить - bool hasMatch = activeFilters.All(f => - { - var value = link.Values.FirstOrDefault(v => v.FieldId == f.FieldId); - return value != null && value.Value != null && value.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase); - }); + var result = min == 0; + logger.LogDebug("UnitId {UnitId}: родительских связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result); + return result; + } - if (hasMatch) - matchingCount++; + logger.LogDebug("UnitId {UnitId}: {Count} родительских связей до фильтрации", dto.Id, links.Count); - if (matchingCount > max) + if (hasParentFieldFilters) + { + // Есть фильтры по родителям → считаем только подходящие + foreach (var link in links) { - logger.LogDebug("UnitId {UnitId}: matchingCount ({Count}) > max ({Max}) — прерываем подсчёт", dto.Id, matchingCount, max); - break; + // Проверяем, проходит ли связь все фильтры (логика как в ApplyRelationshipFilterToQuery) + bool passesAllFilters = true; + + foreach (var f in parentFilters) + { + var values = link.Values.Where(v => v.FieldId == f.FieldId).ToList(); + + if (!values.Any()) + { + // Если нет значений по полю — связь не подходит, если IsInverse = false + // Если нет значений по полю — связь подходит, если IsInverse = true + if (!f.IsInverse) + { + passesAllFilters = false; + break; + } + continue; + } + + var hasMatch = values.Any(v => + v.Value != null && + // Проверка в зависимости от маски + (f.ValueMask.StartsWith("%") && f.ValueMask.EndsWith("%") ? v.Value.Contains(f.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) : + f.ValueMask.StartsWith("%") ? v.Value.EndsWith(f.ValueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) : + f.ValueMask.EndsWith("%") ? v.Value.StartsWith(f.ValueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) : + string.Equals(v.Value, f.ValueMask, StringComparison.OrdinalIgnoreCase)) + ); + + // Учитываем IsInverse + if (f.IsInverse) + hasMatch = !hasMatch; + + if (!hasMatch) + { + passesAllFilters = false; + break; + } + } + + if (passesAllFilters) + matchingCount++; + + if (matchingCount > max) + { + logger.LogDebug("UnitId {UnitId}: matchingCount ({Count}) > max ({Max}) — прерываем подсчёт", dto.Id, matchingCount, max); + break; + } } } + else + { + // Нет фильтров по родителям → считаем общее количество связей (без учёта значений) + matchingCount = links.Count; + logger.LogDebug("UnitId {UnitId}: нет фильтров по родителям — matchingCount = links.Count = {Count}", dto.Id, matchingCount); + } } - else + else // Считаем дочерние связи { - // Нет фильтров по полям → считаем общее количество связей (без учёта значений) - matchingCount = links.Count; - logger.LogDebug("UnitId {UnitId}: нет фильтров по полям — matchingCount = links.Count = {Count}", dto.Id, matchingCount); + var links = dto.Children; + + if (links == null || !links.Any()) + { + var result = min == 0; + logger.LogDebug("UnitId {UnitId}: дочерних связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result); + return result; + } + + logger.LogDebug("UnitId {UnitId}: {Count} дочерних связей до фильтрации", dto.Id, links.Count); + + if (hasChildFieldFilters) + { + // Есть фильтры по детям → считаем только подходящие + foreach (var link in links) + { + // Проверяем, проходит ли связь все фильтры (логика как в ApplyRelationshipFilterToQuery) + bool passesAllFilters = true; + + foreach (var f in childFilters) + { + var values = link.Values.Where(v => v.FieldId == f.FieldId).ToList(); + + if (!values.Any()) + { + // Если нет значений по полю — связь не подходит, если IsInverse = false + // Если нет значений по полю — связь подходит, если IsInverse = true + if (!f.IsInverse) + { + passesAllFilters = false; + break; + } + continue; + } + + var hasMatch = values.Any(v => + v.Value != null && + // Проверка в зависимости от маски + (f.ValueMask.StartsWith("%") && f.ValueMask.EndsWith("%") ? v.Value.Contains(f.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) : + f.ValueMask.StartsWith("%") ? v.Value.EndsWith(f.ValueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) : + f.ValueMask.EndsWith("%") ? v.Value.StartsWith(f.ValueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) : + string.Equals(v.Value, f.ValueMask, StringComparison.OrdinalIgnoreCase)) + ); + + // Учитываем IsInverse + if (f.IsInverse) + hasMatch = !hasMatch; + + if (!hasMatch) + { + passesAllFilters = false; + break; + } + } + + if (passesAllFilters) + matchingCount++; + + if (matchingCount > max) + { + logger.LogDebug("UnitId {UnitId}: matchingCount ({Count}) > max ({Max}) — прерываем подсчёт", dto.Id, matchingCount, max); + break; + } + } + } + else + { + // Нет фильтров по детям → считаем общее количество связей (без учёта значений) + matchingCount = links.Count; + logger.LogDebug("UnitId {UnitId}: нет фильтров по детям — matchingCount = links.Count = {Count}", dto.Id, matchingCount); + } } var finalResult = matchingCount >= min && matchingCount <= max; diff --git a/PARR.EsppSync/SyncService.cs b/PARR.EsppSync/SyncService.cs index 2a03240e..f5f65a20 100644 --- a/PARR.EsppSync/SyncService.cs +++ b/PARR.EsppSync/SyncService.cs @@ -90,7 +90,7 @@ namespace PARR.EsppSync return; } - // ✅ Построим TemplateForShortcodes из уже загруженного template + // Построим TemplateForShortcodes из уже загруженного template var templateForShortcodes = new TemplateForShortcodes { Id = template.Id, diff --git a/PARR.TemplateMatcher/ITemplateMatcher.cs b/PARR.TemplateMatcher/ITemplateMatcher.cs index c923797e..28b6e200 100644 --- a/PARR.TemplateMatcher/ITemplateMatcher.cs +++ b/PARR.TemplateMatcher/ITemplateMatcher.cs @@ -7,5 +7,6 @@ namespace PARR.TemplateMatcher Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator); Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator); Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator); + Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator); } } diff --git a/PARR.TemplateMatcher/MqTemplateMatcher.cs b/PARR.TemplateMatcher/MqTemplateMatcher.cs index 8361a7d8..1f123fe7 100644 --- a/PARR.TemplateMatcher/MqTemplateMatcher.cs +++ b/PARR.TemplateMatcher/MqTemplateMatcher.cs @@ -93,14 +93,14 @@ namespace PARR.TemplateMatcher switch (query.Action) { case TemplateMatcherActionEnum.Sync: - // Создать недостающие шаблоны, привязать к нужному Job, включить/выключить по фильтрам await templateMatcherService.SyncTemplatesForJobGroup(query.Id, query.Initiator); break; + case TemplateMatcherActionEnum.Update: - // Обновить существующие шаблоны: имя, привязка к Job, вкл/выкл по фильтрам //await templateMatcherService.UpdateTemplatesForJob(query.Id, query.Initiator); - logger.LogWarning("Обработка EntityType JobGroup и TemplateMatcherActionEnum.Update не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action); + await templateMatcherService.UpdateTemplatesForJobGroup(query.Id, query.Initiator); break; + default: logger.LogWarning("Неизвестное действие для {EntityType}: {Action}", query.EntityType, query.Action); break; diff --git a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs index 7e24b340..1dbdb404 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.Contracts; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.Models; using PARR.DAL.Models.Job; @@ -14,6 +15,7 @@ using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.TransformServices; using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Settings; +using System.Configuration; using System.Text.Json; namespace PARR.TemplateMatcher.Services.Implementations; @@ -60,7 +62,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer IUnitFieldService unitFieldService, ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, - ITemplateUpdaterMqSender templateUpdaterMqSender + ITemplateUpdaterMqSender templateUpdaterMqSender, + SettingsFromDb settingsFromDb ) { this.logger = logger; @@ -155,7 +158,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в expectedUnitIds + // Отладка: проверить, есть ли юнит в expectedUnitIds if (expectedUnitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId); @@ -191,7 +194,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .ToHashSet(); #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в unitIdsWithValidGroupingFieldSet + // Отладка: проверить, есть ли юнит в unitIdsWithValidGroupingFieldSet if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId); @@ -223,7 +226,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .ToHashSet(); // И снова в HashSet #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet + // Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId); @@ -257,7 +260,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer .ToListAsync(); #if DEBUG - // ✅ Отладка: проверить, юнит участвует в potentialUnitInUnitLinks + // Отладка: проверить, юнит участвует в potentialUnitInUnitLinks var potentialLinksContainingTarget = potentialUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList(); if (potentialLinksContainingTarget.Any()) { @@ -334,7 +337,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } #if DEBUG - // ✅ Отладка: проверить, юнит участвует в filteredUnitInUnitLinks + // Отладка: проверить, юнит участвует в filteredUnitInUnitLinks var filteredLinksContainingTarget = filteredUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList(); if (filteredLinksContainingTarget.Any()) { @@ -377,7 +380,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count); #if DEBUG - // ✅ Отладка: проверить, юнит есть в groupedRelationships.Values + // Отладка: проверить, юнит есть в groupedRelationships.Values var allUnitsInGroups = groupedRelationships.Values.SelectMany(x => x).ToList(); if (allUnitsInGroups.Contains(targetUnitId)) { @@ -410,7 +413,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } #if DEBUG - // ✅ Отладка: проверить, юнит есть в unitToKeys до поиска конфликтов + // Отладка: проверить, юнит есть в unitToKeys до поиска конфликтов if (unitToKeys.ContainsKey(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitToKeys до поиска конфликтов, находится в {Count} группах: [{Groups}]", targetUnitId, unitToKeys[targetUnitId].Count, string.Join(", ", unitToKeys[targetUnitId])); @@ -425,7 +428,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList(); #if DEBUG - // ✅ Отладка: проверить, юнит в conflictedUnits + // Отладка: проверить, юнит в conflictedUnits var targetConflictedEntry = conflictedUnits.FirstOrDefault(c => c.Key == targetUnitId); if (targetConflictedEntry.Key != default) { @@ -468,7 +471,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } #if DEBUG - // ✅ Отладка: проверить, юнит есть в groupedRelationships.Values после разрешения конфликтов + // Отладка: проверить, юнит есть в groupedRelationships.Values после разрешения конфликтов var allUnitsInGroupsAfterConflictResolution = groupedRelationships.Values.SelectMany(x => x).ToList(); if (allUnitsInGroupsAfterConflictResolution.Contains(targetUnitId)) { @@ -503,7 +506,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer if (childUnitIds.Count == 0) continue; #if DEBUG - // ✅ Отладка: проверить, есть ли ВРТ-AOS-05-ДВС в childUnitIds + // Отладка: проверить, есть ли ВРТ-AOS-05-ДВС в childUnitIds var childUnitNames = childUnitIds.Select(id => unitService.Get().AsNoTracking().Where(u => u.Id == id).Select(u => u.Name).FirstOrDefaultAsync().GetAwaiter().GetResult() ?? id.ToString()).ToList(); if (childUnitNames.Contains("ВРТ-AOS-05-ДВС")) { diff --git a/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs index 8e8f3bca..ee0c43e1 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs @@ -95,11 +95,26 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer if (unitIds == null || !unitIds.Any()) { logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); + + // Деактивировать все существующие шаблоны для этого Job + var existingTemplatesForDeactivation = await templateService.Get() + .AsNoTracking() + .Include(t => t.UnitsInTemplate) + .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) + .ToListAsync(); + + foreach (var unusedTemplate in existingTemplatesForDeactivation) + { + logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); + await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); + } + + logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); return; } #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в unitIds + // Отладка: проверить, есть ли юнит в unitIds if (unitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); @@ -107,12 +122,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId); - return; // ❌ юнит отсеялся на этом этапе } #endif var existingTemplates = await templateService.Get() - .AsNoTracking() .Include(t => t.UnitsInTemplate) .Where(t => t.JobId == jobId) .ToListAsync(); @@ -203,7 +216,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer } #if DEBUG - // ✅ Отладка: проверить, есть ли юнит в unitIds + // Отладка: проверить, есть ли юнит в unitIds if (unitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); @@ -211,7 +224,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId); - return; // ❌ юнит отсеялся на этом этапе } #endif diff --git a/PARR.TemplateMatcher/Services/Implemetaions/TemplateDeactivator.cs b/PARR.TemplateMatcher/Services/Implemetaions/TemplateDeactivator.cs index 3ae6e151..315525ac 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/TemplateDeactivator.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/TemplateDeactivator.cs @@ -1,16 +1,13 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.BLL.Domain.Mq; -using PARR.BLL.Services.Interfaces; using PARR.Common.Domain; using PARR.Constants; +using PARR.DAL.Contracts; using PARR.DAL.Models; -using PARR.DAL.Models.Job; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.TemplateMatcher.Services.Interfaces; -using PARR.TemplateMatcher.Settings; -using System.Text.Json; namespace PARR.TemplateMatcher.Services.Implementations; @@ -24,20 +21,23 @@ internal class TemplateDeactivator : ITemplateDeactivator private readonly IJobService jobService; private readonly ITemplateNameNormalizer namenormalizer; private readonly ITemplateUpdaterMqSender sender; - private readonly Guid unusedJobId = Guid.Parse("8f85a91c-a223-4686-bb69-1f0ee73624f2"); + private readonly SettingsFromDb settingsFromDb; public TemplateDeactivator( ILogger logger, ITemplateService templateService, IJobService jobService, ITemplateNameNormalizer namenormalizer, - ITemplateUpdaterMqSender sender) + ITemplateUpdaterMqSender sender, + SettingsFromDb settingsFromDb + ) { this.logger = logger; this.templateService = templateService; this.jobService = jobService; this.namenormalizer = namenormalizer; this.sender = sender; + this.settingsFromDb = settingsFromDb; } public async Task DeactivateTemplateAsync(Template template, HistoryInitiator initiator) @@ -68,7 +68,7 @@ internal class TemplateDeactivator : ITemplateDeactivator .Include(j => j.Tnk) .Include(j => j.Group) .ThenInclude(g => g!.GroupType) - .FirstOrDefaultAsync(j => j.Id == unusedJobId); + .FirstOrDefaultAsync(j => j.Id == settingsFromDb.JobIdForUnusedTemplates); if (unusedJob == null) { @@ -84,7 +84,7 @@ internal class TemplateDeactivator : ITemplateDeactivator TemplateId = template.Id, JobId = unusedJob.Id, UnitId = template.UnitId, - Name = expectedName, + Name = expectedName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds(), IsActiveTemplate = DefaultUnusedTemplateState, IsActiveSchedule = DefaultUnusedScheduleState, LastRun = template.LastRun, diff --git a/PARR.TemplateMatcher/TemplateMatcher.cs b/PARR.TemplateMatcher/TemplateMatcher.cs index 0c5de48a..66611aee 100644 --- a/PARR.TemplateMatcher/TemplateMatcher.cs +++ b/PARR.TemplateMatcher/TemplateMatcher.cs @@ -5,7 +5,6 @@ using PARR.DAL.Contracts; using PARR.DAL.Models.Job; using PARR.DAL.Services.Interfaces.Job; using PARR.TemplateMatcher.Services.Implementations; -using PARR.TemplateMatcher.Services.Implemetaions; using PARR.TemplateMatcher.Services.Interfaces; namespace PARR.TemplateMatcher @@ -14,17 +13,19 @@ namespace PARR.TemplateMatcher { private readonly ILogger logger; private readonly IJobService jobService; - private readonly IEnumerable synchronizers; // Получаем все стратегии - + private readonly IJobGroupService jobGroupService; + private readonly IEnumerable synchronizers; public TemplateMatcher( ILogger logger, IJobService jobService, + IJobGroupService jobGroupService, IEnumerable synchronizers ) { this.logger = logger; this.jobService = jobService; + this.jobGroupService = jobGroupService; this.synchronizers = synchronizers; } @@ -57,7 +58,7 @@ namespace PARR.TemplateMatcher { logger.LogError("GroupedTemplateSynchronizer не найден."); } - return; // Завершаем выполнение для группового Job + return; } else { @@ -72,24 +73,106 @@ namespace PARR.TemplateMatcher { logger.LogError("SimpleTemplateSynchronizer не найден."); } - return; // Завершаем выполнение для обычного Job + return; } } public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); - var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); - if (synchronizer != null) + + var jobGroup = await jobGroupService.Get() + .AsNoTracking() + .Include(jg => jg.GroupType) + .FirstOrDefaultAsync(jg => jg.Id == jobGroupId); + + if (jobGroup == null || jobGroup.GroupType == null) { - await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator); + logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId); + return; } - else + + // Определяем стратегию по типу группы + switch (jobGroup.GroupType.Code) { - logger.LogError("GroupedTemplateSynchronizer не найден."); + case JobGroupTypesEnum.Group: + // Проверяем, есть ли GroupingUnitFieldId — это признак "настоящей" группировки + if (jobGroup.GroupingUnitFieldId.HasValue) + { + logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId); + var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); + if (synchronizer != null) + { + await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator); + } + else + { + logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId); + } + } + else + { + logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId); + //await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); + } + break; + + case JobGroupTypesEnum.Umbrella: + logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId); + await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); + break; + + case JobGroupTypesEnum.Simple: + default: + logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId); + await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); + break; } } + public async Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) + { + logger.LogDebug("Начало обновления шаблонов для JobGroup {JobGroupId}", jobGroupId); + + var jobGroup = await jobGroupService.Get() + .AsNoTracking() + .Include(jg => jg.GroupType) + .FirstOrDefaultAsync(jg => jg.Id == jobGroupId); + + if (jobGroup == null || jobGroup.GroupType == null) + { + logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId); + return; + } + + switch (jobGroup.GroupType.Code) + { + case JobGroupTypesEnum.Group: + if (jobGroup.GroupingUnitFieldId.HasValue) + { + logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId); + } + else + { + logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId); + await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); + } + break; + + case JobGroupTypesEnum.Umbrella: + logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId); + await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); + break; + + case JobGroupTypesEnum.Simple: + default: + logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId); + await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); + break; + } + } + + public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); @@ -113,14 +196,12 @@ namespace PARR.TemplateMatcher { // Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение) await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator); - // Или, возможно, имеет смысл вызвать полную синхронизацию для группы: - // await synchronizer.SyncTemplatesForJobGroupAsync(job.GroupId, initiator); } else { logger.LogError("GroupedTemplateSynchronizer не найден."); } - return; // Завершаем выполнение + return; } else { @@ -135,15 +216,77 @@ namespace PARR.TemplateMatcher { logger.LogError("SimpleTemplateSynchronizer не найден."); } - return; // Завершаем выполнение + return; } } // --- Вспомогательные методы --- + private async Task SyncJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator) + { + logger.LogDebug("Синхронизация JobGroup {JobGroupId} как Collection (по каждому Job'у отдельно)", jobGroupId); + + var jobIds = await jobService.Get() + .AsNoTracking() + .Where(j => j.GroupId == jobGroupId) + .Select(j => j.Id) + .ToListAsync(); + + if (!jobIds.Any()) + { + logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId); + return; + } + + logger.LogDebug("Найдено {Count} Job'ов в JobGroup {JobGroupId}", jobIds.Count, jobGroupId); + + var simpleSynchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); + + if (simpleSynchronizer == null) + { + logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId); + return; + } + + foreach (var jobId in jobIds) + { + logger.LogDebug("Синхронизация Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId); + await simpleSynchronizer.SyncTemplatesForJobAsync(jobId, initiator); + } + + logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId); + } + + + private async Task UpdateJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator) + { + logger.LogDebug("Обновление JobGroup {JobGroupId} как Collection (по каждому Job'у)", jobGroupId); + + var jobIds = await jobService.Get() + .AsNoTracking() + .Where(j => j.GroupId == jobGroupId) + .Select(j => j.Id) + .ToListAsync(); + + if (!jobIds.Any()) + { + logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId); + return; + } + + foreach (var jobId in jobIds) + { + logger.LogDebug("Обновление шаблонов для Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId); + await UpdateTemplatesForJob(jobId, initiator); + } + + logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId); + } + + private async Task GetJobWithGroupAndAutoControlAsync(Guid jobId) { return await jobService.Get() - .AsNoTracking() // Добавлено + .AsNoTracking() .Include(j => j.Group) .ThenInclude(j => j!.GroupType) .Include(j => j.AutoControl)