diff --git a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs index 13f59b85..fd64aa4d 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs @@ -340,8 +340,7 @@ internal class UnitFilterService : IUnitFilterService private async Task> ApplyRelationshipFiltersOnDbAsync( List unitIds, IEnumerable relationshipFilters, - CancellationToken cancellationToken = default - ) + CancellationToken cancellationToken = default) { if (!relationshipFilters.Any() || !unitIds.Any()) { @@ -353,178 +352,87 @@ internal class UnitFilterService : IUnitFilterService logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}", unitIds.Count, relationshipFilters.Count()); -#if DEBUG - if (unitIds.Contains(targetUnitId)) - { - logger.LogDebug("DEBUG: Юнит {TargetUnitId} присутствует во входных данных ApplyRelationshipFiltersOnDbAsync", targetUnitId); - } -#endif + var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id }); var parentRelFilters = relationshipFilters.Where(rf => rf.IsParent).ToList(); var childRelFilters = relationshipFilters.Where(rf => !rf.IsParent).ToList(); - var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id }); - // Обработка родительских фильтров if (parentRelFilters.Any()) { - logger.LogDebug(" Родительские фильтры ({Count}):", parentRelFilters.Count); - - var allParentLinks = await unitInUnitService.Get() - .AsNoTracking() - .Where(link => unitIds.Contains(link.ChildUnitId)) - .Select(link => new { ChildId = link.ChildUnitId, ParentId = link.ParentUnitId }) - .ToListAsync(cancellationToken); - - var allParentIds = allParentLinks.Select(l => l.ParentId).Distinct().ToList(); - logger.LogDebug(" Найдено {ParentCount} уникальных родителей для {LinkCount} связей", - allParentIds.Count, allParentLinks.Count); - - var validParentIds = new HashSet(allParentIds); - logger.LogDebug(" Начальное количество родителей: {Count}", validParentIds.Count); - - foreach (var relFilter in parentRelFilters) - { - var valueMask = relFilter.ValueMask?.Trim(); - if (string.IsNullOrWhiteSpace(valueMask)) - continue; - - bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); - bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); - string dbValueMask; - if (isStartsWith && isEndsWith) - dbValueMask = $"%{valueMask.Trim('%')}%"; - else if (isStartsWith) - dbValueMask = $"{valueMask.TrimEnd('%')}%"; - else if (isEndsWith) - dbValueMask = $"%{valueMask.TrimStart('%')}"; - else - dbValueMask = valueMask; - - var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}"; - logger.LogDebug(" RelationshipFilter (Parent): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}", - fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); - - var matchingParents = await unitService.Get() - .AsNoTracking() - .Where(u => allParentIds.Contains(u.Id)) - .Where(u => u.UnitValues.Any(v => - v.FieldId == relFilter.FieldId && - EF.Functions.Like(v.Value.Value, dbValueMask))) - .Select(u => u.Id) - .ToListAsync(cancellationToken); - - logger.LogDebug(" Найдено {MatchCount} родителей по маске", matchingParents.Count); - - if (relFilter.IsInverse) - { - validParentIds.ExceptWith(matchingParents); - logger.LogDebug(" IsInverse=true → исключены {ExcludedCount} родителей, осталось {RemainingCount}", - matchingParents.Count, validParentIds.Count); - } - else - { - validParentIds.IntersectWith(matchingParents); - logger.LogDebug(" IsInverse=false → оставлены {KeptCount} родителей", - validParentIds.Count); - } - } - - // Сопоставляем подходящих родителей с их детьми - foreach (var link in allParentLinks) - { - if (validParentIds.Contains(link.ParentId)) - { - resultContexts[link.ChildId].ValidParentIds.Add(link.ParentId); - } - } - - var totalParentsAdded = resultContexts.Values.Sum(c => c.ValidParentIds.Count); - logger.LogDebug(" Добавлено {TotalParents} родительских связей для {UnitCount} юнитов", - totalParentsAdded, resultContexts.Count); + await ApplyRelationshipFiltersAsync( + unitIds, + resultContexts, // ← Передаём словарь, а не список + parentRelFilters, + isParentDirection: true, + cancellationToken); } // Обработка дочерних фильтров if (childRelFilters.Any()) { - logger.LogDebug(" Дочерние фильтры ({Count}):", childRelFilters.Count); - - var allChildLinks = await unitInUnitService.Get() - .AsNoTracking() - .Where(link => unitIds.Contains(link.ParentUnitId)) - .Select(link => new { ParentId = link.ParentUnitId, ChildId = link.ChildUnitId }) - .ToListAsync(cancellationToken); - - var allChildIds = allChildLinks.Select(l => l.ChildId).Distinct().ToList(); - logger.LogDebug(" Найдено {ChildCount} уникальных детей для {LinkCount} связей", - allChildIds.Count, allChildLinks.Count); - - var validChildIds = new HashSet(allChildIds); - logger.LogDebug(" Начальное количество детей: {Count}", validChildIds.Count); - - foreach (var relFilter in childRelFilters) - { - var valueMask = relFilter.ValueMask?.Trim(); - if (string.IsNullOrWhiteSpace(valueMask)) - continue; - - bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); - bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); - string dbValueMask; - if (isStartsWith && isEndsWith) - dbValueMask = $"%{valueMask.Trim('%')}%"; - else if (isStartsWith) - dbValueMask = $"{valueMask.TrimEnd('%')}%"; - else if (isEndsWith) - dbValueMask = $"%{valueMask.TrimStart('%')}"; - else - dbValueMask = valueMask; - - var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}"; - logger.LogDebug(" RelationshipFilter (Child): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}", - fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); - - var matchingChildren = await unitService.Get() - .AsNoTracking() - .Where(u => allChildIds.Contains(u.Id)) - .Where(u => u.UnitValues.Any(v => - v.FieldId == relFilter.FieldId && - EF.Functions.Like(v.Value.Value, dbValueMask))) - .Select(u => u.Id) - .ToListAsync(cancellationToken); - - logger.LogDebug(" Найдено {MatchCount} детей по маске", matchingChildren.Count); - - if (relFilter.IsInverse) - { - validChildIds.ExceptWith(matchingChildren); - logger.LogDebug(" IsInverse=true → исключены {ExcludedCount} детей, осталось {RemainingCount}", - matchingChildren.Count, validChildIds.Count); - } - else - { - validChildIds.IntersectWith(matchingChildren); - logger.LogDebug(" IsInverse=false → оставлены {KeptCount} детей", - validChildIds.Count); - } - } - - // Сопоставляем подходящих детей с их родителями - foreach (var link in allChildLinks) - { - if (validChildIds.Contains(link.ChildId)) - { - resultContexts[link.ParentId].ValidChildIds.Add(link.ChildId); - } - } - - var totalChildrenAdded = resultContexts.Values.Sum(c => c.ValidChildIds.Count); - logger.LogDebug(" Добавлено {TotalChildren} дочерних связей для {UnitCount} юнитов", - totalChildrenAdded, resultContexts.Count); + await ApplyRelationshipFiltersAsync( + unitIds, + resultContexts, // ← Передаём словарь, а не список + childRelFilters, + isParentDirection: false, + cancellationToken); } - var result = resultContexts.Values.ToList(); - logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: выход {ContextCount} контекстов", result.Count); + // Если фильтры были — оставляем только юниты со связями + // Если фильтров не было — оставляем все юниты + bool hasRelationshipFilters = parentRelFilters.Any() || childRelFilters.Any(); + + var unitsWithRelationships = hasRelationshipFilters + ? resultContexts.Values.Where(c => c.ValidParentIds.Any() || c.ValidChildIds.Any()).ToList() + : resultContexts.Values.ToList(); + + var result = unitsWithRelationships; + logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: выход {ContextCount} контекстов (из {InitialCount})", + result.Count, resultContexts.Count); + +#if DEBUG + foreach (var context in result) + { + var u = await unitService.GetAsync(context.UnitId); + + if (u == null) + continue; + + logger.LogDebug("Найден ЭК {UnitName}", u.Name); + + if (context.ValidParentIds.Any()) + { + + var parents = context.ValidParentIds; + + logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", parents.Count); + + foreach (var parent in context.ValidParentIds) + { + var p = await unitService.GetAsync(parent); + + logger.LogDebug("\t\t {ParentName};", p?.Name); + } + } + + if (context.ValidChildIds.Any()) + { + + var children = context.ValidChildIds; + + logger.LogDebug("\tФильтрам соответствуют {ParentsCount} детей:", children.Count); + + foreach (var child in children) + { + var c = await unitService.GetAsync(child); + + logger.LogDebug("\t\t {ChildName};", c?.Name); + } + } + } +#endif + return result; } @@ -694,7 +602,6 @@ internal class UnitFilterService : IUnitFilterService } - public async Task> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId, CancellationToken cancellationToken = default) { logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId); @@ -785,6 +692,172 @@ internal class UnitFilterService : IUnitFilterService } #region вспомогательные методы + /// + /// Оценивает, проходит ли юнит фильтр связей + /// + private bool EvaluateFilter( + List unitRelations, // Все связи юнита (родители или дети) + List matchingRelations, // Связи, которые соответствуют маске + bool isFullMatch, // Полное совпадение (All) или хотя бы одно (Any) + bool isInverse, // Обратный фильтр + bool hasRelations // Есть ли связи у юнита + ) + { + int matchingCount = unitRelations.Count(r => matchingRelations.Contains(r)); + int totalCount = unitRelations.Count; + + // Случай: нет связей в направлении + if (!hasRelations) + { + // Если IsInverse = true → юнит проходит (нет связей, которые нарушают) + // Если IsInverse = false → юнит не проходит (нет связей, которые соответствуют) + return isInverse; + } + + // Основная логика + if (isFullMatch) + { + // Все связи должны соответствовать + bool allMatch = matchingCount == totalCount; + + if (isInverse) + //Все связи не соответствуют фильтру + return matchingCount == 0; + else + //Все связи соответствуют фильтру + return allMatch; + } + else + { + // Хотя бы одна связь должна соответствовать + return matchingCount > 0; + } + } + + + /// + /// Применяет фильтры к связям (родителям или детям) + /// + private async Task ApplyRelationshipFiltersAsync( + List unitIds, + Dictionary resultContexts, + List relFilters, + bool isParentDirection, + CancellationToken cancellationToken) + { + if (!relFilters.Any()) + return; + + var directionName = isParentDirection ? "Родительские" : "Дочерние"; + logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count); + + // Получаем все связи в нужном направлении + var allLinks = await unitInUnitService.Get() + .AsNoTracking() + .Where(link => isParentDirection + ? unitIds.Contains(link.ChildUnitId) + : unitIds.Contains(link.ParentUnitId)) + .Select(link => new + { + SourceId = isParentDirection ? link.ChildUnitId : link.ParentUnitId, + TargetId = isParentDirection ? link.ParentUnitId : link.ChildUnitId + }) + .ToListAsync(cancellationToken); + + var allTargetIds = allLinks.Select(l => l.TargetId).Distinct().ToList(); + logger.LogDebug(" Найдено {TargetCount} уникальных {TargetType} для {LinkCount} связей", + allTargetIds.Count, + isParentDirection ? "родителей" : "детей", + allLinks.Count); + + // Группируем связи по исходному юниту для быстрого доступа + var linksBySource = allLinks.GroupBy(l => l.SourceId) + .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList()); + + // Обрабатываем каждый фильтр + foreach (var relFilter in relFilters) + { + var valueMask = relFilter.ValueMask?.Trim(); + if (string.IsNullOrWhiteSpace(valueMask)) + continue; + + // Обработка маски LIKE + bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); + bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); + string dbValueMask; + if (isStartsWith && isEndsWith) + dbValueMask = $"%{valueMask.Trim('%')}%"; + else if (isStartsWith) + dbValueMask = $"{valueMask.TrimEnd('%')}%"; + else if (isEndsWith) + dbValueMask = $"%{valueMask.TrimStart('%')}"; + else + dbValueMask = 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 unitService.Get() + .AsNoTracking() + .Where(u => allTargetIds.Contains(u.Id)) + .Where(u => u.UnitValues.Any(v => + v.FieldId == relFilter.FieldId && + EF.Functions.Like(v.Value.Value, dbValueMask))) + .Select(u => u.Id) + .ToListAsync(cancellationToken); + + logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске", + matchingTargetIds.Count, + isParentDirection ? "родителей" : "детей"); + + // Применяем логику к каждому юниту + foreach (var context in resultContexts.Values) + { + var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List()); + bool hasTargets = unitTargets.Any(); + + bool passesFilter = EvaluateFilter( + unitTargets, + matchingTargetIds, + relFilter.IsFullMatch, + relFilter.IsInverse, + hasTargets + ); + + if (passesFilter) + { + var validTargets = relFilter.IsInverse + ? unitTargets.Except(matchingTargetIds) + : unitTargets.Intersect(matchingTargetIds); + + if (isParentDirection) + context.ValidParentIds.UnionWith(validTargets); + else + context.ValidChildIds.UnionWith(validTargets); + } + else + { + // Юнит не прошёл фильтр — очищаем его связи + if (isParentDirection) + context.ValidParentIds.Clear(); + else + context.ValidChildIds.Clear(); + } + } + } + + var totalAdded = resultContexts.Sum(c => + isParentDirection ? c.Value.ValidParentIds.Count : c.Value.ValidChildIds.Count); + + logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов", + totalAdded, + isParentDirection ? "родительских" : "дочерних", + resultContexts.Count); + } + private async Task> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter, CancellationToken cancellationToken = default) {