diff --git a/PARR.Core/Services/UnitFilterService/Models/FilteredUnitContext.cs b/PARR.Core/Services/UnitFilterService/Models/UnitFilterMatchResult.cs similarity index 85% rename from PARR.Core/Services/UnitFilterService/Models/FilteredUnitContext.cs rename to PARR.Core/Services/UnitFilterService/Models/UnitFilterMatchResult.cs index 0466fa3e..4ad0f42c 100644 --- a/PARR.Core/Services/UnitFilterService/Models/FilteredUnitContext.cs +++ b/PARR.Core/Services/UnitFilterService/Models/UnitFilterMatchResult.cs @@ -1,6 +1,6 @@ namespace PARR.Core.Services.UnitFilterService.Models { - internal class FilteredUnitContext + internal class UnitFilterMatchResult { public Guid UnitId { get; set; } public HashSet ValidParentIds { get; set; } = new(); diff --git a/PARR.Core/Services/UnitFilterService/Models/UnitFilterServiceOptions.cs b/PARR.Core/Services/UnitFilterService/Models/UnitFilterServiceOptions.cs index 25a38c32..fe798dc8 100644 --- a/PARR.Core/Services/UnitFilterService/Models/UnitFilterServiceOptions.cs +++ b/PARR.Core/Services/UnitFilterService/Models/UnitFilterServiceOptions.cs @@ -2,6 +2,6 @@ { internal class UnitFilterServiceOptions { - public int LoadBatchSize { get; set; } = 50; + public int LoadBatchSize { get; set; } = 100; } } diff --git a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs index 41d7d23f..9896fb07 100644 --- a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs +++ b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs @@ -16,7 +16,7 @@ namespace PARR.Core.Services.UnitFilterService; internal class UnitFilterService : IUnitFilterService { #if DEBUG - private readonly Guid targetUnitId = Guid.Parse("9d88fff2-a861-487f-b73d-bce1f0218e9f"); + private readonly Guid debugTargetUnitId = Guid.Parse("f2017292-193c-48e9-b333-3a00e737c6fc"); #endif private const int DebugMaxUnitsToLog = 10; @@ -94,7 +94,7 @@ internal class UnitFilterService : IUnitFilterService logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count); // Собираем все контексты юнитов, прошедших фильтрацию - var allFilteredContexts = new List(); + var allFilteredContexts = new List(); // Преобразуем в список для индексации var unitFiltersList = job.UnitFilters.ToList(); @@ -118,9 +118,9 @@ internal class UnitFilterService : IUnitFilterService } #if DEBUG - if (initialUnitIds.Contains(targetUnitId)) + if (initialUnitIds.Contains(debugTargetUnitId)) { - logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", targetUnitId, i + 1); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", debugTargetUnitId, i + 1); } #endif @@ -136,13 +136,13 @@ internal class UnitFilterService : IUnitFilterService } #if DEBUG - if (initialUnitIds.Contains(targetUnitId) && !fieldFilteredIds.Contains(targetUnitId)) + if (initialUnitIds.Contains(debugTargetUnitId) && !fieldFilteredIds.Contains(debugTargetUnitId)) { - logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", targetUnitId, i + 1); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", debugTargetUnitId, i + 1); } #endif - // 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ FilteredUnitContext + // 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ UnitFilterMatchResult var relStopwatch = Stopwatch.StartNew(); var relationshipFilteredContexts = await ProcessRelationshipFiltersAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken); relStopwatch.Stop(); @@ -154,11 +154,11 @@ internal class UnitFilterService : IUnitFilterService } #if DEBUG - var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == targetUnitId); + var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId); if (targetContext != null) { logger.LogDebug("DEBUG: Юнит {TargetUnitId} прошёл фильтр #{Index}. Родителей: {ParentCount}, Детей: {ChildCount}", - targetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count); + debugTargetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count); } #endif @@ -186,7 +186,7 @@ internal class UnitFilterService : IUnitFilterService // Убираем дубликаты по UnitId, объединяя связи var mergedContexts = allFilteredContexts .GroupBy(c => c.UnitId) - .Select(g => new FilteredUnitContext + .Select(g => new UnitFilterMatchResult { UnitId = g.Key, ValidParentIds = new HashSet(g.SelectMany(c => c.ValidParentIds)), @@ -204,7 +204,7 @@ internal class UnitFilterService : IUnitFilterService job.MinValueRelationships, job.MaxValueRelationships, job.IsParentRelationships); #if DEBUG - var targetBeforeUmbrella = mergedContexts.FirstOrDefault(c => c.UnitId == targetUnitId); + var targetBeforeUmbrella = mergedContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId); if (targetBeforeUmbrella != null) { var count = job.IsParentRelationships == true @@ -213,7 +213,7 @@ internal class UnitFilterService : IUnitFilterService var passes = count >= (job.MinValueRelationships ?? 0) && count <= (job.MaxValueRelationships ?? int.MaxValue); logger.LogDebug("DEBUG: Юнит {TargetUnitId} перед Umbrella: Count={Count}, Min={Min}, Max={Max}, Passes={Passes}", - targetUnitId, count, job.MinValueRelationships, job.MaxValueRelationships, passes); + debugTargetUnitId, count, job.MinValueRelationships, job.MaxValueRelationships, passes); } #endif @@ -222,10 +222,10 @@ internal class UnitFilterService : IUnitFilterService logger.LogInformation("Этап фильтрации по числу связей завершён: после Umbrella-фильтра осталось {UnitCount} юнитов", umbrellaFilteredContexts.Count); #if DEBUG - var targetAfterUmbrella = umbrellaFilteredContexts.FirstOrDefault(c => c.UnitId == targetUnitId); + var targetAfterUmbrella = umbrellaFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId); if (targetBeforeUmbrella != null && targetAfterUmbrella == null) { - logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", targetUnitId); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", debugTargetUnitId); } #endif } @@ -249,7 +249,6 @@ internal class UnitFilterService : IUnitFilterService return result; } - private async Task LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default) { return await jobService @@ -294,39 +293,26 @@ internal class UnitFilterService : IUnitFilterService logger.LogDebug(" FieldFilter #{Index}: Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}", filterIndex, fieldName, dbValueMask, fieldFilter.IsInverse); - //if (fieldFilter.IsInverse) - //{ - // query = query.Where(u => !u.UnitValues.Any(v => - // v.FieldId == fieldFilter.FieldId && - // EF.Functions.ILike(v.Value.Value, dbValueMask))); - //} - //else - //{ - // query = query.Where(u => u.UnitValues.Any(v => - // v.FieldId == fieldFilter.FieldId && - // EF.Functions.ILike(v.Value.Value, dbValueMask))); - //} - query = unitService.GetUnitByFieldAndValue(query, fieldFilter.FieldId, fieldFilter.ValueMask!, fieldFilter.IsInverse); #if DEBUG - if (unitIds.Contains(targetUnitId)) + if (unitIds.Contains(debugTargetUnitId)) { var unitHasField = await unitInValueService.Get() .AsNoTracking() - .AnyAsync(uiv => uiv.UnitId == targetUnitId && uiv.FieldId == fieldFilter.FieldId, cancellationToken); + .AnyAsync(uiv => uiv.UnitId == debugTargetUnitId && uiv.FieldId == fieldFilter.FieldId, cancellationToken); var unitValue = await unitInValueService.Get() .AsNoTracking() - .Where(uiv => uiv.UnitId == targetUnitId && uiv.FieldId == fieldFilter.FieldId) + .Where(uiv => uiv.UnitId == debugTargetUnitId && uiv.FieldId == fieldFilter.FieldId) .Select(uiv => uiv.Value.Value) .FirstOrDefaultAsync(cancellationToken); var intermediateResult = await query.Select(u => u.Id).ToListAsync(cancellationToken); - var passes = intermediateResult.Contains(targetUnitId); + var passes = intermediateResult.Contains(debugTargetUnitId); logger.LogDebug(" DEBUG: Юнит {TargetUnitId}: Поле={FieldName}, Значение={Value}, Маска={Mask}, HasField={HasField}, Проходит={Passes}", - targetUnitId, fieldName, unitValue ?? "null", dbValueMask, unitHasField, passes); + debugTargetUnitId, fieldName, unitValue ?? "null", dbValueMask, unitHasField, passes); query = unitService.Get().AsNoTracking() .Where(u => intermediateResult.Contains(u.Id)); @@ -344,7 +330,7 @@ internal class UnitFilterService : IUnitFilterService /// /// Применяем фильтры аттрибутов у связанных ЭК /// - private async Task> ProcessRelationshipFiltersAsync( + private async Task> ProcessRelationshipFiltersAsync( List unitIds, IEnumerable relationshipFilters, CancellationToken cancellationToken = default) @@ -353,29 +339,29 @@ internal class UnitFilterService : IUnitFilterService { logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений", unitIds.Count); - return unitIds.Select(id => new FilteredUnitContext { UnitId = id }).ToList(); + return unitIds.Select(id => new UnitFilterMatchResult { UnitId = id }).ToList(); } logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}", unitIds.Count, relationshipFilters.Count()); #if DEBUG - if (unitIds.Contains(targetUnitId)) + if (unitIds.Contains(debugTargetUnitId)) { - logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден во входных данных ProcessRelationshipFiltersAsync", targetUnitId); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден во входных данных ProcessRelationshipFiltersAsync", debugTargetUnitId); } #endif - var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id }); + var preFilteredUnitsById = unitIds.ToDictionary(id => id, id => new UnitFilterMatchResult { UnitId = id }); var parentRelFilters = relationshipFilters.Where(rf => rf.IsParent).ToList(); var childRelFilters = relationshipFilters.Where(rf => !rf.IsParent).ToList(); if (parentRelFilters.Any()) { - await ApplyRelationshipFiltersOnDbAsync( + await ApplyRelationshipFiltersToUnitsAsync( unitIds, - resultContexts, + preFilteredUnitsById, parentRelFilters, isParentDirection: true, cancellationToken); @@ -383,48 +369,48 @@ internal class UnitFilterService : IUnitFilterService if (childRelFilters.Any()) { - await ApplyRelationshipFiltersOnDbAsync( + await ApplyRelationshipFiltersToUnitsAsync( unitIds, - resultContexts, + preFilteredUnitsById, childRelFilters, isParentDirection: false, cancellationToken); } #if DEBUG - var unitsWithAnyConnections = resultContexts.Values.Count(c => c.ValidParentIds.Any() || c.ValidChildIds.Any()); - var unitsWithoutConnections = resultContexts.Values.Count(c => !c.ValidParentIds.Any() && !c.ValidChildIds.Any()); - logger.LogDebug("DEBUG: После ApplyRelationshipFiltersOnDbAsync: {WithConnections} юнитов со связями, {WithoutConnections} без связей", + var unitsWithAnyConnections = preFilteredUnitsById.Values.Count(c => c.ValidParentIds.Any() || c.ValidChildIds.Any()); + var unitsWithoutConnections = preFilteredUnitsById.Values.Count(c => !c.ValidParentIds.Any() && !c.ValidChildIds.Any()); + logger.LogDebug("DEBUG: После ApplyRelationshipFiltersToUnitsAsync: {WithConnections} юнитов со связями, {WithoutConnections} без связей", unitsWithAnyConnections, unitsWithoutConnections); - var targetContext = resultContexts.Values.FirstOrDefault(c => c.UnitId == targetUnitId); + var targetContext = preFilteredUnitsById.Values.FirstOrDefault(c => c.UnitId == debugTargetUnitId); if (targetContext != null) { logger.LogDebug("DEBUG: Юнит {TargetUnitId}: Родителей={ParentCount}, Детей={ChildCount}, PassesFilter={Passes}", - targetUnitId, + debugTargetUnitId, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count, targetContext.ValidParentIds.Any() || targetContext.ValidChildIds.Any()); } #endif - var result = resultContexts.Values.ToList(); + var result = preFilteredUnitsById.Values.ToList(); logger.LogDebug("ProcessRelationshipFiltersAsync: выход {ContextCount} контекстов (из {InitialCount})", - result.Count, resultContexts.Count); + result.Count, preFilteredUnitsById.Count); #if DEBUG - if (unitIds.Contains(targetUnitId)) + if (unitIds.Contains(debugTargetUnitId)) { - var targetContextInResult = result.FirstOrDefault(c => c.UnitId == targetUnitId); + var targetContextInResult = result.FirstOrDefault(c => c.UnitId == debugTargetUnitId); if (targetContextInResult != null) { logger.LogDebug("DEBUG: Юнит {TargetUnitId} в результатах ProcessRelationshipFiltersAsync. Родителей: {ParentCount}, Детей: {ChildCount}", - targetUnitId, targetContextInResult.ValidParentIds.Count, targetContextInResult.ValidChildIds.Count); + debugTargetUnitId, targetContextInResult.ValidParentIds.Count, targetContextInResult.ValidChildIds.Count); } else { - logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ в результатах ProcessRelationshipFiltersAsync (удалён)", targetUnitId); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ в результатах ProcessRelationshipFiltersAsync (удалён)", debugTargetUnitId); } } @@ -536,7 +522,7 @@ internal class UnitFilterService : IUnitFilterService /// /// /// - private List ApplyUmbrellaFilterInMemory(List contexts, Job job) + private List ApplyUmbrellaFilterInMemory(List contexts, Job job) { var min = job.MinValueRelationships ?? 0; var max = job.MaxValueRelationships ?? int.MaxValue; @@ -545,17 +531,17 @@ internal class UnitFilterService : IUnitFilterService if (min == 0 && max == int.MaxValue) return contexts; - var result = new List(); + var result = new List(); foreach (var context in contexts) { int count = isParentDirection ? context.ValidParentIds.Count : context.ValidChildIds.Count; bool passes = count >= min && count <= max; #if DEBUG - if (context.UnitId == targetUnitId) + if (context.UnitId == debugTargetUnitId) { logger.LogDebug("DEBUG Umbrella: Юнит {TargetUnitId}, Count={Count}, Min={Min}, Max={Max}, Passes={Passes}", - targetUnitId, count, min, max, passes); + debugTargetUnitId, count, min, max, passes); } #endif @@ -574,20 +560,20 @@ internal class UnitFilterService : IUnitFilterService /// /// /// - private async Task> LoadFinalResultAsync(List contexts, CancellationToken cancellationToken = default) + private async Task> LoadFinalResultAsync(List contexts, CancellationToken cancellationToken = default) { var allResults = new List(); #if DEBUG - var targetInContexts = contexts.FirstOrDefault(c => c.UnitId == targetUnitId); + var targetInContexts = contexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId); if (targetInContexts != null) { logger.LogDebug("DEBUG: Юнит {TargetUnitId} передан в LoadFinalResultAsync. Родителей: {ParentCount}, Детей: {ChildCount}", - targetUnitId, targetInContexts.ValidParentIds.Count, targetInContexts.ValidChildIds.Count); + debugTargetUnitId, targetInContexts.ValidParentIds.Count, targetInContexts.ValidChildIds.Count); } else { - logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", targetUnitId); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", debugTargetUnitId); } #endif @@ -607,7 +593,7 @@ internal class UnitFilterService : IUnitFilterService /// /// /// - private async Task> LoadBatchAsync(List batch, CancellationToken cancellationToken = default) + private async Task> LoadBatchAsync(List batch, CancellationToken cancellationToken = default) { var unitIds = batch.Select(c => c.UnitId).ToList(); var allParentIds = batch.SelectMany(c => c.ValidParentIds).Distinct().ToList(); @@ -786,12 +772,14 @@ internal class UnitFilterService : IUnitFilterService #region вспомогательные методы + + /// /// Применяет фильтры к связям (родителям или детям) /// - private async Task ApplyRelationshipFiltersOnDbAsync( + private async Task ApplyRelationshipFiltersToUnitsAsync( List unitIds, - Dictionary resultContexts, + Dictionary preFilteredUnitsById, List relFilters, bool isParentDirection, CancellationToken cancellationToken) @@ -802,6 +790,7 @@ internal class UnitFilterService : IUnitFilterService var directionName = isParentDirection ? "Родительские" : "Дочерние"; logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count); + // 1. Получить все связи (unitInUnit) для юнитов из unitIds var allLinks = await unitInUnitService.Get() .AsNoTracking() .Where(link => isParentDirection @@ -814,16 +803,18 @@ internal class UnitFilterService : IUnitFilterService }) .ToListAsync(cancellationToken); - var allTargetIds = allLinks.Select(l => l.TargetId).Distinct().ToList(); + var allRelatedUnitIds = allLinks.Select(l => l.TargetId).Distinct().ToList(); logger.LogDebug(" Найдено {TargetCount} уникальных {TargetType} для {LinkCount} связей", - allTargetIds.Count, + allRelatedUnitIds.Count, isParentDirection ? "родителей" : "детей", allLinks.Count); - var linksBySource = allLinks.GroupBy(l => l.SourceId) + // 2. Сгруппировать связи по SourceId (ID юнита из unitIds) + var relationshipsByUnitId = allLinks.GroupBy(l => l.SourceId) .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList()); - var targetFilterPassCount = new Dictionary(); + // 3. Для каждого фильтра в группе найдём TargetId, которые ему соответствуют (с учётом IsInverse) + var matchedTargetIdsByFilter = new Dictionary>(); foreach (var relFilter in relFilters) { @@ -832,137 +823,156 @@ internal class UnitFilterService : IUnitFilterService continue; string dbValueMask = NormalizeLikeMask(valueMask); - var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}"; + logger.LogDebug(" RelationshipFilter ({Direction}): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}", isParentDirection ? "Parent" : "Child", fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); - //var matchingTargetIds = await unitInValueService.Get() - // .AsNoTracking() - // .Where(uiv => uiv.FieldId == relFilter.FieldId) - // .Where(uiv => EF.Functions.ILike(uiv.Value.Value, dbValueMask)) - // .Select(uiv => uiv.UnitId) - // .Distinct() - // .ToListAsync(cancellationToken); - var matchingTargetIds = await unitInValueService.GetMatchingTargetIds(relFilter.FieldId, dbValueMask) - .ToListAsync(cancellationToken); + .ToListAsync(cancellationToken); + // Ограничиваем найденные ID только теми, которые действительно связаны с юнитами из unitIds var filteredMatchingTargetIds = matchingTargetIds - .Intersect(allTargetIds) + .Intersect(allRelatedUnitIds) .ToList(); logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске", filteredMatchingTargetIds.Count, isParentDirection ? "родителей" : "детей"); + HashSet targetIdsThatPassThisFilter; + if (relFilter.IsInverse) { - foreach (var targetId in allTargetIds) - { - if (!filteredMatchingTargetIds.Contains(targetId)) - { - if (!targetFilterPassCount.ContainsKey(targetId)) - targetFilterPassCount[targetId] = 0; - targetFilterPassCount[targetId]++; - } - } - - logger.LogDebug(" Для IsInverse: {NonMatchCount} {TargetType} НЕ соответствуют маске", - allTargetIds.Count - filteredMatchingTargetIds.Count, - isParentDirection ? "родителей" : "детей"); + // Юнит проходит фильтр, если его значение НЕ соответствует маске + // Это означает, что юниты, НЕ входящие в filteredMatchingTargetIds, проходят фильтр + targetIdsThatPassThisFilter = new HashSet(allRelatedUnitIds.Except(filteredMatchingTargetIds)); + logger.LogDebug(" (IsInverse) Юниты, прошедшие фильтр: {Count}", targetIdsThatPassThisFilter.Count); } else { - foreach (var targetId in filteredMatchingTargetIds) - { - if (!targetFilterPassCount.ContainsKey(targetId)) - targetFilterPassCount[targetId] = 0; - targetFilterPassCount[targetId]++; - } + // Юнит проходит фильтр, если его значение соответствует маске + targetIdsThatPassThisFilter = new HashSet(filteredMatchingTargetIds); + logger.LogDebug(" (Direct) Юниты, прошедшие фильтр: {Count}", targetIdsThatPassThisFilter.Count); } + + // Сохраняем результат для этого конкретного фильтра + matchedTargetIdsByFilter[relFilter] = targetIdsThatPassThisFilter; } - int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim())); - var validTargetIds = targetFilterPassCount - .Where(kvp => kvp.Value == requiredFiltersCount) - .Select(kvp => kvp.Key) - .ToList(); + // 4. Определим, какие юниты (context.UnitId) проходят ВСЕ фильтры с учётом IsFullMatch + var unitIdsToRemove = new HashSet(); - logger.LogDebug(" Найдено {ValidCount} {TargetType}, прошедших ВСЕ {RequiredCount} фильтров", - validTargetIds.Count, - isParentDirection ? "родителей" : "детей", - requiredFiltersCount); - - var unitsToRemove = new HashSet(); - - foreach (var context in resultContexts.Values) + foreach (var context in preFilteredUnitsById.Values) { - if (unitsToRemove.Contains(context.UnitId)) - continue; - - var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List()); - bool hasTargets = unitTargets.Any(); - - var validTargets = unitTargets.Intersect(validTargetIds).ToList(); - bool hasValidTargets = validTargets.Any(); - - bool isFullMatch = relFilters.All(rf => rf.IsFullMatch); - bool isInverse = relFilters.Any(rf => rf.IsInverse); - - bool passesFilter; + // Получаем связанные юниты для конкретного context.UnitId + var relatedUnitIds = relationshipsByUnitId.GetValueOrDefault(context.UnitId, new List()).ToHashSet(); + bool hasTargets = relatedUnitIds.Count > 0; if (!hasTargets) { - passesFilter = isInverse; + // Нет связей - проверяем, есть ли фильтры, которые требуют наличия связей + // Если есть хотя бы один фильтр с маской, и связей нет - юнит не проходит. + var hasFiltersWithMask = relFilters.Any(rf => !string.IsNullOrWhiteSpace(rf.ValueMask?.Trim())); + if (hasFiltersWithMask) + { + unitIdsToRemove.Add(context.UnitId); + continue; // Переходим к следующему юниту + } + // Если фильтров с маской нет, юнит остаётся. } - else if (isFullMatch) + + bool unitPassesCurrentFilterGroup = true; + + // Проверяем, проходит ли юнит все фильтры в группе + foreach (var relFilter in relFilters) { - passesFilter = validTargets.Count == unitTargets.Count; + var valueMask = relFilter.ValueMask?.Trim(); + if (string.IsNullOrWhiteSpace(valueMask)) + { + continue; // Пустой фильтр пропускаем + } + + // Получаем юниты, прошедшие конкретный фильтр relFilter + var targetIdsThatPassThisFilter = matchedTargetIdsByFilter[relFilter]; + + if (relFilter.IsFullMatch) + { + // ВСЕ relatedUnitIds должны пройти этот конкретный фильтр relFilter + // Это означает, что каждый relatedUnitId должен быть в targetIdsThatPassThisFilter + bool allTargetsPassThisFilter = relatedUnitIds.All(targetId => targetIdsThatPassThisFilter.Contains(targetId)); + if (!allTargetsPassThisFilter) + { + unitPassesCurrentFilterGroup = false; + break; // Не нужно проверять остальные фильтры, юнит заведомо не проходит + } + } + else // IsFullMatch = false + { + // Хоть один relatedUnitId должен пройти этот конкретный фильтр relFilter + // Это означает, что хотя бы один relatedUnitId должен быть в targetIdsThatPassThisFilter + bool anyTargetPassesThisFilter = relatedUnitIds.Any(targetId => targetIdsThatPassThisFilter.Contains(targetId)); + if (!anyTargetPassesThisFilter) + { + unitPassesCurrentFilterGroup = false; + break; // Не нужно проверять остальные фильтры, юнит заведимо не проходит + } + } + } + + if (!unitPassesCurrentFilterGroup) + { + unitIdsToRemove.Add(context.UnitId); } else { - passesFilter = hasValidTargets; - } + // Юнит прошёл все фильтры. + // Теперь определим, какие из его связей (relatedUnitIds) идут в ValidParentIds/ValidChildIds. + // Это должны быть связи, прошедшие *все* фильтры (т.е. быть в пересечении результатов для каждого фильтра). + // Соберём пересечение всех targetId, прошедших каждый фильтр для этого конкретного юнита. + // Начинаем с множества всех его связей. + var validRelatedIds = new HashSet(relatedUnitIds); - if (passesFilter) - { - if (isFullMatch) + foreach (var relFilter in relFilters) { - if (isParentDirection) - context.ValidParentIds.UnionWith(unitTargets); - else - context.ValidChildIds.UnionWith(unitTargets); + var valueMask = relFilter.ValueMask?.Trim(); + if (string.IsNullOrWhiteSpace(valueMask)) + continue; + + var targetIdsThatPassThisFilter = matchedTargetIdsByFilter[relFilter]; + // Пересекаем текущий список validRelatedIds с результатами для этого фильтра + validRelatedIds.IntersectWith(targetIdsThatPassThisFilter); + } + + // Добавляем полученные валидные связи в контекст юнита + if (isParentDirection) + { + context.ValidParentIds.UnionWith(validRelatedIds); } else { - if (isParentDirection) - context.ValidParentIds.UnionWith(validTargets); - else - context.ValidChildIds.UnionWith(validTargets); + context.ValidChildIds.UnionWith(validRelatedIds); } } - else - { - unitsToRemove.Add(context.UnitId); - } } - foreach (var unitId in unitsToRemove) + // Удаляем юниты, которые не прошли проверку + foreach (var unitId in unitIdsToRemove) { - resultContexts.Remove(unitId); + preFilteredUnitsById.Remove(unitId); } - logger.LogDebug(" Удалено {RemovedCount} юнитов, не прошедших фильтры", unitsToRemove.Count); + logger.LogDebug(" Удалено {RemovedCount} юнитов, не прошедших фильтры (новая логика с IsFullMatch/IsInverse для каждого фильтра)", + unitIdsToRemove.Count); - var totalAdded = resultContexts.Values.Sum(c => + var totalAdded = preFilteredUnitsById.Values.Sum(c => isParentDirection ? c.ValidParentIds.Count : c.ValidChildIds.Count); - logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов", + logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов (новая логика)", totalAdded, isParentDirection ? "родительских" : "дочерних", - resultContexts.Count); + preFilteredUnitsById.Count); } @@ -979,11 +989,6 @@ internal class UnitFilterService : IUnitFilterService var dbValueMask = NormalizeLikeMask(filter.UnitFilter); - //var initialUnitIds = await unitService.Get().AsNoTracking() - // .Where(unit => EF.Functions.ILike(unit.Name, dbValueMask)) - // .Select(u => u.Id) - // .ToListAsync(cancellationToken); - var initialUnitIds = await unitService.GetInitialUnitIds(dbValueMask).ToListAsync(cancellationToken); var toCache = new UnitFilterIds diff --git a/PARR.Domain/Entities/Job/JobRelationshipFilter.cs b/PARR.Domain/Entities/Job/JobRelationshipFilter.cs index 6a9e75bf..5a7e9d67 100644 --- a/PARR.Domain/Entities/Job/JobRelationshipFilter.cs +++ b/PARR.Domain/Entities/Job/JobRelationshipFilter.cs @@ -8,15 +8,8 @@ namespace PARR.Domain.Entities.Job [Table("RelationshipFilters", Schema = DatabaseSchemas.Job)] [Comment("Таблица фильтров связей ЭК")] [PrimaryKey(nameof(UnitFilterId), nameof(FieldId))] - public class JobRelationshipFilter //: IBase + public class JobRelationshipFilter { - //[Key] - //public Guid Id { get; set; } - - //public DateTimeOffset DateCreated { get; set; } - - //public DateTimeOffset? DateModified { get; set; } - public Guid UnitFilterId { get; set; } /// /// Родительская связь - true, diff --git a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs index 48333313..faae576b 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs @@ -6,6 +6,7 @@ using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Services.MatchingStatusService; using PARR.Core.Services.UnitFilterService; +using PARR.Core.Services.UnitFilterService.Models; using PARR.Domain.Cache.Models; using PARR.Domain.Common.Rabbit.Messages; using PARR.Domain.Entities; @@ -78,6 +79,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); } + public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); @@ -154,6 +156,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями"); + await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); + logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); return; } @@ -231,37 +235,16 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer return; } - // --- ПОСТРОЕНИЕ ОБРАТНОГО ОТОБРАЖЕНИЯ (после фильтрации) --- logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации)."); - var reverseMapping = new Dictionary>(); - foreach (var dto in finalFilteredUnitFilterResults) + var reverseMapping = await BuildReverseMappingAsync(finalFilteredUnitFilterResults, maxJob); + + logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count); + + if (!reverseMapping.Any()) { - List relatedUnitIds; - if (maxJob.IsParentRelationships == true) - { - // dto.Id - это ParentUnitId, связанные - ChildUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate - // relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона - relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); - } - else - { - // dto.Id - это ChildUnitId, связанные - ParentUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate - // relatedUnitIds - это ParentUnitIds, которые станут UnitId шаблона - relatedUnitIds = dto.Parents.Select(p => p.UnitId).ToList(); - } - - // dto.Id - это юнит, который прошел фильтры, он будет в UnitsInTemplate - var unitInTemplateId = dto.Id; - - foreach (var relatedUnitId in relatedUnitIds) - { - // relatedUnitId уже прошел все фильтры, т.к. dto.Id (его связанный юнит) прошел фильтры - if (!reverseMapping.ContainsKey(relatedUnitId)) - { - reverseMapping[relatedUnitId] = new List(); - } - reverseMapping[relatedUnitId].Add(unitInTemplateId); - } + logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); + await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения"); + return; } logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count); @@ -288,7 +271,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer // Создадим маппинг UnitId (из UnitsInTemplate) -> значение поля для внутренней группировки var unitInTemplateToInnerGroupingValueMap = innerGroupingValues .Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value)) - .ToDictionary(uv => uv.UnitId, uv => uv.Value.Value); + .ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value); // 7. Основной цикл обработки: итерируемся по potentialUnitIds (UnitId шаблонов) var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); @@ -304,20 +287,23 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer // Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId .GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных")) + .OrderBy(g => g.Key, StringComparer.Ordinal) // <-- Сортировка по имени внутренней группы .ToList(); - logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate.", potentialUnitId, innerGroupedUnitsInTemplate.Count); + logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate (отсортировано).", potentialUnitId, innerGroupedUnitsInTemplate.Count); - // 8. Цикл по внутренним группам UnitsInTemplate - foreach (var innerGroup in innerGroupedUnitsInTemplate) + // --- СОБЕРЕМ ВСЕ ИТОГОВЫЕ ПОДГРУППЫ ДЛЯ ЭТОГО potentialUnitId --- + var allFinalSubGroups = new List<(List UnitsInTemplateSubGroup, string InnerGroupName, int SubGroupSizeWithinInnerGroup)>(); // (UnitsInTemplate, Имя_внутренней_группы, размер_подгруппы_внутри_внутренней_группы) + + foreach (var innerGroup in innerGroupedUnitsInTemplate) // Теперь проходит в отсортированном порядке по groupingValueName { var groupingValueName = innerGroup.Key; - var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Список юнитов (UnitId), связанных с potentialUnitId и имеющих одно и то же значение поля logger.LogDebug("Обработка внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId} с {Count} юнитами.", groupingValueName, potentialUnitId, unitsInTemplateInInnerGroup.Count); // Разбиваем юниты из *этой* внутренней группы на подгруппы по maxJob.MaxValueRelationships + // Это нужно делать для каждой внутренней группы отдельно int maxValueForSplitting = maxJob.MaxValueRelationships!.Value; var unitsInTemplateSubGroups = unitsInTemplateInInnerGroup .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) @@ -327,149 +313,154 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer logger.LogDebug("Внутренняя группа '{GroupingValue}' для UnitId {PotentialUnitId}: разбит на {GroupCount} подгрупп UnitsInTemplate.", groupingValueName, potentialUnitId, unitsInTemplateSubGroups.Count); - // 9. Цикл по подгруппам UnitsInTemplate для создания/обновления шаблонов - for (int i = 1; i <= unitsInTemplateSubGroups.Count; i++) // Индекс начинается с 1 + // Добавим каждую *итоговую* подгруппу в общий список + foreach (var subGroup in unitsInTemplateSubGroups) + allFinalSubGroups.Add((subGroup, groupingValueName!, subGroup.Count)); + } + + int globalIndexForThisUnitId = 1; + foreach (var finalSubGroupData in allFinalSubGroups) + { + var unitsInTemplateSubGroup = finalSubGroupData.UnitsInTemplateSubGroup; + var subGroupSize = finalSubGroupData.SubGroupSizeWithinInnerGroup; + var originatingInnerGroupName = finalSubGroupData.InnerGroupName; + + logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", globalIndexForThisUnitId, originatingInnerGroupName, potentialUnitId, subGroupSize); + + Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); + + // --- Добавляем УНИКАЛЬНЫЙ ключ в список ожидаемых --- + expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, globalIndexForThisUnitId)); + + var existingTemplatesForRelationship = await templateService.Get() + .AsNoTracking() + .Include(t => t.Unit) + .Include(t => t.Job) + .ThenInclude(t => t!.Tnk) + .Include(t => t.Job) + .ThenInclude(t => t!.Group) + .ThenInclude(t => t!.GroupType) + .Include(t => t.UnitsInTemplate) + .ThenInclude(uit => uit.Unit) + .Where(t => t.JobId == targetJob.Id && t.UnitId == potentialUnitId && t.Index == globalIndexForThisUnitId && t.StatusTypeId == TemplateStatusTypeEnum.Used) + .ToListAsync(); + + var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault(); + + if (existingTemplateForSubGroup != null) { - var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции - var subGroupSize = unitsInTemplateSubGroup.Count; - logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", i, groupingValueName, potentialUnitId, subGroupSize); + // === 1. Получаем текущие и новые ID юнитов === + var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); + var proposedUnitIds = unitsInTemplateSubGroup.ToList(); - Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); - - // --- Добавляем ключ в список ожидаемых прямо здесь --- - expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, i)); // potentialUnitId - это UnitId шаблона - - var existingTemplatesForRelationship = await templateService.Get() + // === 2. Сравниваем детерминированно с сортировкой по имени === + var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList(); + var unitNamesForSort = await unitService.Get() .AsNoTracking() - .Include(t => t.Unit) - .Include(t => t.Job) - .ThenInclude(t => t!.Tnk) - .Include(t => t.Job) - .ThenInclude(t => t!.Group) - .ThenInclude(t => t!.GroupType) - .Include(t => t.UnitsInTemplate) - .ThenInclude(uit => uit.Unit) - .Where(t => t.JobId == targetJob.Id && t.UnitId == potentialUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) - .ToListAsync(); + .Where(u => allUnitIdsForSort.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString()); - var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault(); + var sortedCurrentUnitIds = currentUnitIds + .OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString())) + .ToList(); - if (existingTemplateForSubGroup != null) + var sortedProposedUnitIds = proposedUnitIds + .OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString())) + .ToList(); + + bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds); + + if (unitsAreEqual) { - // === 1. Получаем текущие и новые ID юнитов === - var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); - var proposedUnitIds = unitsInTemplateSubGroup.ToList(); + logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id); + existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList(); + var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup); - // === 2. Сравниваем детерминированно с сортировкой по имени === - var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList(); - var unitNamesForSort = await unitService.Get() - .AsNoTracking() - .Where(u => allUnitIdsForSort.Contains(u.Id)) - .ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString()); - - var sortedCurrentUnitIds = currentUnitIds - .OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString())) - .ToList(); - - var sortedProposedUnitIds = proposedUnitIds - .OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString())) - .ToList(); - - bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds); - - if (unitsAreEqual) + if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { - logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id); - existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.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 updateRequest = new TemplateUpdaterMq - { - TemplateId = existingTemplateForSubGroup.Id, - JobId = targetJob.Id, - UnitId = potentialUnitId, - Name = expectedName, - IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate, - IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule, - IsNew = false, - Index = i, - StatusTypeId = TemplateStatusTypeEnum.Used, - Initiator = initiator, - UnitsInTemplate = sortedProposedUnitIds - }; - await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); - } - else - { - logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id); - } - } - else - { - logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов или их порядка (после сортировки).", existingTemplateForSubGroup.Id); - var newTargetJob = SelectTargetJob(jobsInGroup, unitsInTemplateSubGroup.Count, maxJob); // Размер - из подмножества - if (newTargetJob.Id != existingTemplateForSubGroup.JobId) - { - logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id); - } - await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, sortedProposedUnitIds, newTargetJob, initiator, i); - } - } - else - { - var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator); - if (reusableTemplate != null) - { - logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, potentialUnitId, i); - - var tempTemplateForName = new Template - { - Id = reusableTemplate.Id, - Name = reusableTemplate.Name, - JobId = targetJob.Id, - UnitId = potentialUnitId, - Index = i, - Job = targetJob, - Unit = reusableTemplate.Unit, - UnitsInTemplate = unitsInTemplateSubGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList() - }; - - var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); - + logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id); var updateRequest = new TemplateUpdaterMq { - TemplateId = reusableTemplate.Id, + TemplateId = existingTemplateForSubGroup.Id, JobId = targetJob.Id, UnitId = potentialUnitId, Name = expectedName, - IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, - IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, + IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate, + IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule, + IsNew = false, + Index = globalIndexForThisUnitId, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, - IsNew = true, - Index = i, - UnitsInTemplate = unitsInTemplateSubGroup + UnitsInTemplate = sortedProposedUnitIds }; - await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } else { - logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}, с {Count} юнитами.", targetJob.Id, potentialUnitId, i, unitsInTemplateSubGroup.Count); - await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, i, initiator); + logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id); } } + else + { + logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов или их порядка (после сортировки).", existingTemplateForSubGroup.Id); + var newTargetJob = SelectTargetJob(jobsInGroup, unitsInTemplateSubGroup.Count, maxJob); // Размер - из подмножества + if (newTargetJob.Id != existingTemplateForSubGroup.JobId) + { + logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id); + } + await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, sortedProposedUnitIds, newTargetJob, initiator, globalIndexForThisUnitId); + } } + else + { + var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator); + if (reusableTemplate != null) + { + logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, potentialUnitId, globalIndexForThisUnitId); + + var tempTemplateForName = new Template + { + Id = reusableTemplate.Id, + Name = reusableTemplate.Name, + JobId = targetJob.Id, + UnitId = potentialUnitId, + Index = globalIndexForThisUnitId, + Job = targetJob, + Unit = reusableTemplate.Unit, + UnitsInTemplate = unitsInTemplateSubGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList() + }; + + var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = reusableTemplate.Id, + JobId = targetJob.Id, + UnitId = potentialUnitId, + Name = expectedName, + IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, + IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator, + IsNew = true, + Index = globalIndexForThisUnitId, + UnitsInTemplate = unitsInTemplateSubGroup + }; + + await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); + } + else + { + logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}, с {Count} юнитами.", targetJob.Id, potentialUnitId, globalIndexForThisUnitId, unitsInTemplateSubGroup.Count); + await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, globalIndexForThisUnitId, initiator); + } + } + + globalIndexForThisUnitId++; // Увеличиваем индекс для следующей итоговой подгруппы } } // === Деактивация === - // Теперь expectedTemplateKeys уже собран - - // Получаем ВСЕ шаблоны для JobGroup (не только для текущих potentialUnitIds) var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); var allExistingTemplatesInGroup = await templateService.Get() .AsNoTracking() @@ -560,7 +551,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); - //var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false); + var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, @@ -569,8 +560,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer Name = expectedName, IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, - //LastRun = template.LastRun, - //NextRun = nextRun, IsNew = false, Index = newIndex, StatusTypeId = TemplateStatusTypeEnum.Used, @@ -618,6 +607,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer ); } + #region Вспомогательные методы private async Task GetFieldIdByAihitNameAsync(string fieldName) { @@ -629,5 +619,149 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } return field.Id; } + + + /// + /// Строит обратное отображение: UnitId шаблона -> [UnitsInTemplate] + /// Решает конфликты, когда юнит из UnitsInTemplate может быть связан с несколькими UnitId шаблона. + /// + /// Результаты фильтрации, содержащие связи. + /// Job, используемый для определения направления связей (IsParentRelationships). + /// Словарь, где ключ - это UnitId шаблона, а значение - список юнитов, входящих в него (UnitsInTemplate). + private async Task>> BuildReverseMappingAsync(IEnumerable unitFilterResults, Job maxJob) + { + logger.LogDebug("Разрешение конфликта: определение, для какого UnitId выбрать каждый юнит из UnitsInTemplate."); + + // 1. Собираем все потенциальные пары (relatedUnitId, unitInTemplateId) + var potentialAssignments = new Dictionary>(); // relatedUnitId -> [unitInTemplateId, ...] + var allPotentialRelatedUnitIds = new HashSet(); + var allUnitInTemplateIds = new HashSet(); + + foreach (var dto in unitFilterResults) + { + List relatedUnitIds; + if (maxJob.IsParentRelationships == true) + { + relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); // relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона + } + else + { + relatedUnitIds = dto.Parents.Select(p => p.UnitId).ToList(); // relatedUnitIds - это ParentUnitIds, которые станут UnitId шаблона + } + + var unitInTemplateId = dto.Id; // dto.Id - это юнит, который прошел фильтры, он будет в UnitsInTemplate + + allUnitInTemplateIds.Add(unitInTemplateId); + allPotentialRelatedUnitIds.UnionWith(relatedUnitIds); // Собираем все unique relatedUnitId + + foreach (var relatedUnitId in relatedUnitIds) + { + if (!potentialAssignments.ContainsKey(relatedUnitId)) + { + potentialAssignments[relatedUnitId] = new List(); + } + potentialAssignments[relatedUnitId].Add(unitInTemplateId); + } + } + + // 2. Загрузим имена всех potentialRelatedUnitIds для сортировки по алфавиту при равенстве связей + var relatedUnitNames = await unitService.Get() + .AsNoTracking() + .Where(u => allPotentialRelatedUnitIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString()); + + // 3. Для каждого unitInTemplateId, найти лучший relatedUnitId + var unitInTemplateToBestRelatedUnit = new Dictionary(); // unitInTemplateId -> bestRelatedUnitId + + foreach (var unitInTemplateId in allUnitInTemplateIds) + { + var candidates = potentialAssignments + .Where(kvp => kvp.Value.Contains(unitInTemplateId)) + .Select(kvp => kvp.Key) + .ToList(); + + if (candidates.Count == 1) + { + // Только один кандидат, просто назначаем + unitInTemplateToBestRelatedUnit[unitInTemplateId] = candidates[0]; + } + else if (candidates.Count > 1) + { + // Несколько кандидатов, применяем правила: 1. Больше связей -> лучше. 2. По алфавиту. + Guid bestCandidate = candidates[0]; // Инициализируем первым кандидатом + + // Загрузим количество связей для каждого кандидата + // Количество связей - это общее число юнитов (в Parents или Children) в UnitFilterResultDto, связанном с *этим* relatedUnitId + var candidateRelationshipCounts = new Dictionary(); + foreach (var candidateId in candidates) + { + // Найдем все dto, которые привели к этому candidateId + // Это dto.Id, у которых candidateId был в Parents (если IsParentRelationships) или в Children (если !IsParentRelationships) + var relevantDtos = unitFilterResults.Where(dto => + { + if (maxJob.IsParentRelationships == true) + { + return dto.Children.Any(c => c.UnitId == candidateId); + } + else + { + return dto.Parents.Any(p => p.UnitId == candidateId); + } + }).ToList(); + + // Общее количество связей для этого candidateId - это сумма связей (Parents.Count или Children.Count) из *всех* relevantDtos + int totalRelationships = 0; + foreach (var relevantDto in relevantDtos) + { + if (maxJob.IsParentRelationships == true) + { + totalRelationships += relevantDto.Children.Count; + } + else + { + totalRelationships += relevantDto.Parents.Count; + } + } + + candidateRelationshipCounts[candidateId] = totalRelationships; + } + + // Применяем правило 1: больше связей -> лучше + int bestCount = candidateRelationshipCounts[bestCandidate]; + foreach (var candidateId in candidates.Skip(1)) + { + int candidateCount = candidateRelationshipCounts[candidateId]; + if (candidateCount > bestCount || + (candidateCount == bestCount && string.Compare(relatedUnitNames.GetValueOrDefault(candidateId, candidateId.ToString()), relatedUnitNames.GetValueOrDefault(bestCandidate, bestCandidate.ToString()), StringComparison.OrdinalIgnoreCase) < 0)) + { + bestCandidate = candidateId; + bestCount = candidateCount; + } + } + + unitInTemplateToBestRelatedUnit[unitInTemplateId] = bestCandidate; + } + // else: если candidates.Count == 0 (что маловероятно, если dto.Id гарантированно связан), то unitInTemplateId не будет в unitInTemplateToBestRelatedUnit + } + + logger.LogDebug("Разрешение конфликта завершено. Найдено {Count} однозначных назначений.", unitInTemplateToBestRelatedUnit.Count); + + // 4. Построение reverseMapping на основе решённых конфликтов + var reverseMapping = new Dictionary>(); + foreach (var assignmentKvp in unitInTemplateToBestRelatedUnit) + { + var unitInTemplateId = assignmentKvp.Key; + var bestRelatedUnitId = assignmentKvp.Value; + + if (!reverseMapping.ContainsKey(bestRelatedUnitId)) + { + reverseMapping[bestRelatedUnitId] = new List(); + } + reverseMapping[bestRelatedUnitId].Add(unitInTemplateId); + } + + return reverseMapping; + } + #endregion } \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Implemetaions/TemplateDeactivator.cs b/PARR.TemplateMatcher/Services/Implemetaions/TemplateDeactivator.cs index 4b382273..481c1c64 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/TemplateDeactivator.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/TemplateDeactivator.cs @@ -48,7 +48,7 @@ internal class TemplateDeactivator : ITemplateDeactivator public async Task DeactivateTemplateAsync(Template template, HistoryInitiator initiator) { if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) - return true; // уже в обработке + return true; if (template.StatusTypeId == TemplateStatusTypeEnum.Unused) {