From f93078868b0063736d1e8aaefff4e10092afbd56 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Thu, 12 Mar 2026 14:57:33 +1000 Subject: [PATCH 1/2] =?UTF-8?q?fix(dal):=20UnitFilter=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=81=D0=B2=D1=8F=D0=B7=D1=8F=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitFilterService/UnitFilterService.cs | 209 ++++++++++++------ 1 file changed, 145 insertions(+), 64 deletions(-) diff --git a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs index fd64aa4d..84aa80da 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs @@ -55,7 +55,7 @@ internal class UnitFilterService : IUnitFilterService CancellationToken cancellationToken = default ) { - var job = await LoadJobWithFiltersAsync(jobId); + var job = await LoadJobWithFiltersAsync(jobId, cancellationToken); return job == null ? null : await GetUnitsByJobFilterAsync(job, takeCount, cancellationToken); } @@ -248,7 +248,7 @@ internal class UnitFilterService : IUnitFilterService { return await jobService .Get().AsNoTracking() - .AsSplitQuery() // ← Избегаем предупреждения EF Core о множественных коллекциях + .AsSplitQuery() .Include(j => j.UnitFilters).ThenInclude(uf => uf.FieldFilters).ThenInclude(ff => ff.UnitField) .Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters).ThenInclude(rf => rf.UnitField) .Include(j => j.Group).ThenInclude(g => g!.GroupType) @@ -392,48 +392,89 @@ internal class UnitFilterService : IUnitFilterService result.Count, resultContexts.Count); #if DEBUG - foreach (var context in result) + foreach (var context in result.Take(10)) { var u = await unitService.GetAsync(context.UnitId); if (u == null) continue; - logger.LogDebug("Найден ЭК {UnitName}", u.Name); + logger.LogDebug("📦 Найден ЭК {UnitName} (Id={UnitId})", u.Name, u.Id); + // Получаем все FieldId из фильтров для отображения значений + var parentFieldIds = relationshipFilters.Where(rf => rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList(); + var childFieldIds = relationshipFilters.Where(rf => !rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList(); + + // === РОДИТЕЛИ === if (context.ValidParentIds.Any()) { + logger.LogDebug("\t📌 Фильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count); - var parents = context.ValidParentIds; - - logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", parents.Count); - - foreach (var parent in context.ValidParentIds) + foreach (var parentId in context.ValidParentIds) { - var p = await unitService.GetAsync(parent); + var p = await unitService.GetAsync(parentId); + logger.LogDebug("\t\t👤 {ParentName} (Id={ParentId})", p?.Name, parentId); - logger.LogDebug("\t\t {ParentName};", p?.Name); + // Выводим значения только для полей из RelationshipFilters + if (parentFieldIds.Any()) + { + var parentValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync( + new[] { parentId }, + parentFieldIds + ); + + foreach (var pv in parentValues) + { + var field = pv.Field; + var value = pv.Value?.Value; + logger.LogDebug("\t\t 🔹 {FieldName} = {FieldValue}", + field?.AihitName ?? $"FieldId={pv.FieldId}", + value ?? "null"); + } + } } } + else if (parentRelFilters.Any()) + { + logger.LogDebug("\t Родительские фильтры заданы, но подходящих родителей не найдено"); + } + // === ДЕТИ === if (context.ValidChildIds.Any()) { + logger.LogDebug("\t Фильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count); - var children = context.ValidChildIds; - - logger.LogDebug("\tФильтрам соответствуют {ParentsCount} детей:", children.Count); - - foreach (var child in children) + foreach (var childId in context.ValidChildIds) { - var c = await unitService.GetAsync(child); + var c = await unitService.GetAsync(childId); + logger.LogDebug("\t\t {ChildName} (Id={ChildId})", c?.Name, childId); - logger.LogDebug("\t\t {ChildName};", c?.Name); + // Выводим значения только для полей из RelationshipFilters + if (childFieldIds.Any()) + { + var childValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync( + new[] { childId }, + childFieldIds + ); + + foreach (var cv in childValues) + { + var field = cv.Field; + var value = cv.Value?.Value; + logger.LogDebug("\t\t {FieldName} = {FieldValue}", + field?.AihitName ?? $"FieldId={cv.FieldId}", + value ?? "null"); + } + } } } + else if (childRelFilters.Any()) + { + logger.LogDebug("\t Дочерние фильтры заданы, но подходящих детей не найдено"); + } } #endif - return result; } @@ -696,7 +737,7 @@ internal class UnitFilterService : IUnitFilterService /// Оценивает, проходит ли юнит фильтр связей /// private bool EvaluateFilter( - List unitRelations, // Все связи юнита (родители или дети) + List unitRelations, // Все связи юнита List matchingRelations, // Связи, которые соответствуют маске bool isFullMatch, // Полное совпадение (All) или хотя бы одно (Any) bool isInverse, // Обратный фильтр @@ -714,29 +755,33 @@ internal class UnitFilterService : IUnitFilterService return isInverse; } - // Основная логика + // Основная логика — только для определения, проходит ли юнит if (isFullMatch) { - // Все связи должны соответствовать + // ВСЕ связи должны соответствовать bool allMatch = matchingCount == totalCount; if (isInverse) - //Все связи не соответствуют фильтру - return matchingCount == 0; + return !allMatch; // НЕ все связи соответствуют else - //Все связи соответствуют фильтру - return allMatch; + return allMatch; // ВСЕ связи соответствуют } else { - // Хотя бы одна связь должна соответствовать - return matchingCount > 0; + // ХОТЯ БЫ ОДНА связь должна соответствовать + bool anyMatch = matchingCount > 0; + + if (isInverse) + return !anyMatch; // НИ ОДНА связь не соответствует + else + return anyMatch; // ХОТЯ БЫ ОДНА связь соответствует } } /// /// Применяет фильтры к связям (родителям или детям) + /// Связь должна пройти ВСЕ фильтры направления /// private async Task ApplyRelationshipFiltersAsync( List unitIds, @@ -774,7 +819,10 @@ internal class UnitFilterService : IUnitFilterService var linksBySource = allLinks.GroupBy(l => l.SourceId) .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList()); - // Обрабатываем каждый фильтр + // Находим связи, которые проходят ВСЕ фильтры одновременно + // Для каждого TargetId считаем, сколько фильтров он прошёл + var targetFilterPassCount = new Dictionary(); + foreach (var relFilter in relFilters) { var valueMask = relFilter.ValueMask?.Trim(); @@ -799,7 +847,7 @@ internal class UnitFilterService : IUnitFilterService isParentDirection ? "Parent" : "Child", fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); - // Находим целевые юниты, которые соответствуют фильтру + // Находим целевые юниты, которые соответствуют текущему фильтру var matchingTargetIds = await unitService.Get() .AsNoTracking() .Where(u => allTargetIds.Contains(u.Id)) @@ -813,44 +861,77 @@ internal class UnitFilterService : IUnitFilterService matchingTargetIds.Count, isParentDirection ? "родителей" : "детей"); - // Применяем логику к каждому юниту - foreach (var context in resultContexts.Values) + // Считаем, сколько фильтров прошёл каждый TargetId + foreach (var targetId in matchingTargetIds) { - 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(); - } + if (!targetFilterPassCount.ContainsKey(targetId)) + targetFilterPassCount[targetId] = 0; + targetFilterPassCount[targetId]++; } } - var totalAdded = resultContexts.Sum(c => - isParentDirection ? c.Value.ValidParentIds.Count : c.Value.ValidChildIds.Count); + // ✅ Оставляем только те связи, которые прошли ВСЕ фильтры + int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim())); + var validTargetIds = targetFilterPassCount + .Where(kvp => kvp.Value == requiredFiltersCount) + .Select(kvp => kvp.Key) + .ToList(); + + logger.LogDebug(" Найдено {ValidCount} {TargetType}, прошедших ВСЕ {RequiredCount} фильтров", + validTargetIds.Count, + isParentDirection ? "родителей" : "детей", + requiredFiltersCount); + + // Список юнитов, которые не прошли фильтры (для удаления) + var unitsToRemove = new HashSet(); + + // Применяем к каждому юниту + foreach (var context in resultContexts.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 passesFilter = EvaluateFilter( + validTargets, // Только связи, прошедшие ВСЕ фильтры + validTargets, // matchingRelations = validTargets (все валидные) + relFilters.All(rf => rf.IsFullMatch), // IsFullMatch для всех фильтров + relFilters.Any(rf => rf.IsInverse), // IsInverse для всех фильтров + hasTargets + ); + + if (passesFilter && hasValidTargets) + { + // Сохраняем только валидные связи + if (isParentDirection) + context.ValidParentIds.UnionWith(validTargets); + else + context.ValidChildIds.UnionWith(validTargets); + } + else + { + // Юнит не прошёл фильтр — помечаем на удаление + unitsToRemove.Add(context.UnitId); + } + } + + // Удаляем юниты, которые не прошли фильтры + foreach (var unitId in unitsToRemove) + { + resultContexts.Remove(unitId); + } + + logger.LogDebug(" Удалено {RemovedCount} юнитов, не прошедших фильтры", unitsToRemove.Count); + + var totalAdded = resultContexts.Values.Sum(c => + isParentDirection ? c.ValidParentIds.Count : c.ValidChildIds.Count); logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов", totalAdded, From bfc321779d78b1e182be10e56ae5690212b7ea2d Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Thu, 12 Mar 2026 15:09:06 +1000 Subject: [PATCH 2/2] =?UTF-8?q?feat(dal):=20UnitFilter=20=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=8C=D1=88=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=B8=D0=BD=D0=B0=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=B2=D1=8B=D0=B3=D1=80=D1=83=D0=B7=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitFilterService/Models/UnitFilterServiceOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PARR.DAL/DomainServices/UnitFilterService/Models/UnitFilterServiceOptions.cs b/PARR.DAL/DomainServices/UnitFilterService/Models/UnitFilterServiceOptions.cs index 72a2b9a6..23926c1f 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/Models/UnitFilterServiceOptions.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/Models/UnitFilterServiceOptions.cs @@ -2,6 +2,6 @@ { internal class UnitFilterServiceOptions { - public int LoadBatchSize { get; set; } = 200; + public int LoadBatchSize { get; set; } = 100; } }