diff --git a/PARR.Core/Repositories/Interfaces/Unit/IUnitInUnitRepository.cs b/PARR.Core/Repositories/Interfaces/Unit/IUnitInUnitRepository.cs index 1ce029c5..498ad908 100644 --- a/PARR.Core/Repositories/Interfaces/Unit/IUnitInUnitRepository.cs +++ b/PARR.Core/Repositories/Interfaces/Unit/IUnitInUnitRepository.cs @@ -4,19 +4,11 @@ namespace PARR.Core.Repositories.Interfaces.Unit { public interface IUnitInUnitRepository { - Task> GetByParentIdAsync(Guid parentId); - Task> GetByChildIdAsync(Guid childId); - /// - /// Получает связи, где ChildUnitId unitIds (для IsParent=True). + /// Возвращает все связанные UnitId для заданного юнита в обоих направлениях. + /// Единая точка загрузки связей. /// - Task> GetParentLinksByChildIdsAsync(IEnumerable childUnitIds); - - /// - /// Получает связи, где ParentUnitId unitIds (для IsParent=False). - /// - Task> GetChildLinksByParentIdsAsync(IEnumerable parentUnitIds); - + Task> GetRelatedUnitIdsAsync(Guid unitId, CancellationToken ct = default); IQueryable Get(); } diff --git a/PARR.Core/Services/UnitFilterService/Matchers/Implemetations/UnitRelationshipMatcher.cs b/PARR.Core/Services/UnitFilterService/Matchers/Implemetations/UnitRelationshipMatcher.cs index 08d02ecf..ae666179 100644 --- a/PARR.Core/Services/UnitFilterService/Matchers/Implemetations/UnitRelationshipMatcher.cs +++ b/PARR.Core/Services/UnitFilterService/Matchers/Implemetations/UnitRelationshipMatcher.cs @@ -4,6 +4,7 @@ using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Services.UnitFilterService.Matchers.Interfaces; using PARR.Core.Services.UnitFilterService.Models; using PARR.Domain.Entities.JobEntities; +using PARR.Domain.Entities.Unit; namespace PARR.Core.Services.UnitFilterService.Matchers; @@ -238,9 +239,9 @@ internal class UnitRelationshipMatcher : IUnitRelationshipMatcher } /// - /// Обрабатывает маску LIKE для корректной работы с SQL + /// Нормализует пользовательскую маску в формат, совместимый с PostgreSQL ILIKE. /// - private static string NormalizeLikeMask(string valueMask) + internal static string NormalizeLikeMask(string valueMask) { if (string.IsNullOrWhiteSpace(valueMask)) return valueMask; @@ -258,4 +259,62 @@ internal class UnitRelationshipMatcher : IUnitRelationshipMatcher else return valueMask; } + + /// + /// Проверяет соответствие значения маске в формате ILIKE. + /// Эмулирует поведение PostgreSQL ILIKE для использования в C#-коде. + /// Регистронезависима. + /// + internal static bool MatchesLikeMask(string value, string mask) + { + if (string.IsNullOrEmpty(mask)) + return true; + + bool startsWithWildcard = mask.StartsWith('%'); + bool endsWithWildcard = mask.EndsWith('%'); + var core = mask.Trim('%'); + + if (startsWithWildcard && endsWithWildcard) + return value.Contains(core, StringComparison.OrdinalIgnoreCase); + + if (endsWithWildcard) + return value.StartsWith(core, StringComparison.OrdinalIgnoreCase); + + if (startsWithWildcard) + return value.EndsWith(core, StringComparison.OrdinalIgnoreCase); + + return value.Equals(core, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Проверяет, проходит ли один target-юнит один RelationshipFilter. + /// Единая точка истины для UnitRelationshipMatcher и GetRelatedUnitNamesAsync. + /// Учитывает IsInverse. Не учитывает IsFullMatch (это ответственность вызывающего кода). + /// + internal static bool TargetPassesFilter( + IReadOnlyList unitValues, + JobRelationshipFilter rf) + { + var normalizedMask = NormalizeLikeMask(rf.ValueMask?.Trim() ?? string.Empty); + if (string.IsNullOrEmpty(normalizedMask)) + return true; + + var matchingValues = unitValues + .Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null) + .ToList(); + + bool hasMatch; + if (!matchingValues.Any()) + { + hasMatch = rf.IsInverse; + } + else + { + hasMatch = matchingValues.Any(uv => MatchesLikeMask(uv.Value!.Value!, normalizedMask)); + if (rf.IsInverse) + hasMatch = !hasMatch; + } + + return hasMatch; + } } \ No newline at end of file diff --git a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs index f52bf540..f409d486 100644 --- a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs +++ b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using PARR.Core.Common.Interfaces; using PARR.Core.Repositories.Interfaces.JobRepositories; using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.UnitFilterService.Matchers; using PARR.Core.Services.UnitFilterService.Matchers.Interfaces; using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitService.Interfaces; @@ -224,345 +225,6 @@ internal class UnitFilterService : IUnitFilterService } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - private async Task LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default) { return await jobRepository @@ -575,7 +237,8 @@ internal class UnitFilterService : IUnitFilterService } - public async Task> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId, CancellationToken cancellationToken = default) + public async Task> GetRelatedUnitNamesAsync( + Guid jobId, Guid unitId, CancellationToken cancellationToken = default) { logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId); @@ -590,71 +253,47 @@ internal class UnitFilterService : IUnitFilterService throw new ArgumentException($"Job {jobId} не найден.", nameof(jobId)); } - logger.LogDebug("Найден Job: {JobName}. Количество UnitFilters: {FilterCount}", job.Name, job.UnitFilters.Count()); + // Единая точка загрузки связей + var allRelatedUnitIds = await unitInUnitRepository + .GetRelatedUnitIdsAsync(unitId, cancellationToken); + + if (!allRelatedUnitIds.Any()) + return new List(); + + // Загружаем значения всех связанных юнитов одним запросом + var allUnitValues = await unitInValueRepository.GetByUnitIdsAsync(allRelatedUnitIds); + var valuesByUnit = allUnitValues + .GroupBy(uv => uv.UnitId) + .ToDictionary(g => g.Key, g => g.ToList()); var result = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var filter in job.UnitFilters) { - if (!filter.RelationshipFilters.Any()) continue; - - logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count()); - - // Получить все связи для юнита - var parentLinks = await unitInUnitRepository.GetByChildIdAsync(unitId); - var childLinks = await unitInUnitRepository.GetByParentIdAsync(unitId); - - // Собрать все UnitId, участвующие в связях - var allRelatedUnitIds = parentLinks - .Select(l => l.ParentUnitId) - .Concat(childLinks.Select(l => l.ChildUnitId)) - .Distinct() + var relFilters = filter.RelationshipFilters + .Where(rf => !string.IsNullOrWhiteSpace(rf.ValueMask?.Trim())) .ToList(); - if (!allRelatedUnitIds.Any()) continue; + if (!relFilters.Any()) continue; - // Получить значения для всех связанных юнитов - var allUnitValues = await unitInValueRepository.GetByUnitIdsAsync(allRelatedUnitIds); - - // Сгруппировать значения по UnitId - var valuesByUnit = allUnitValues - .GroupBy(uv => uv.UnitId) - .ToDictionary(g => g.Key, g => g.ToList()); - - // Найти UnitId, которые проходят все RelationshipFilters - var matchingUnitIds = new HashSet(); + // Проверяем каждый связанный юнит через единую точку проверки + var passedUnitIds = new HashSet(); foreach (var relatedUnitId in allRelatedUnitIds) { - bool passesAllFilters = filter.RelationshipFilters.All(rf => - { - var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List()); + var unitValues = valuesByUnit.GetValueOrDefault(relatedUnitId, new List()); - var matchingValues = values - .Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null) - .ToList(); + // Все фильтры должны пройти (AND между фильтрами в рамках одного UnitFilter) + bool passesAll = relFilters.All(rf => + UnitRelationshipMatcher.TargetPassesFilter(unitValues, rf)); - if (!matchingValues.Any()) - { - return rf.IsInverse; - } - - var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase)); - - if (rf.IsInverse) - hasMatch = !hasMatch; - - return hasMatch; - }); - - if (passesAllFilters) - matchingUnitIds.Add(relatedUnitId); + if (passesAll) + passedUnitIds.Add(relatedUnitId); } - if (matchingUnitIds.Any()) + if (passedUnitIds.Any()) { - // Используем кэширующий сервис вместо прямого запроса к БД - var cachedUnits = await unitService.GetWithCachingAsync(matchingUnitIds); + var cachedUnits = await unitService.GetWithCachingAsync(passedUnitIds); var names = cachedUnits.Values .Select(u => u.Name) .Where(n => !string.IsNullOrEmpty(n)); diff --git a/PARR.DAL/Repositories/Unit/UnitInUnitRepository.cs b/PARR.DAL/Repositories/Unit/UnitInUnitRepository.cs index b3e63e9e..be200460 100644 --- a/PARR.DAL/Repositories/Unit/UnitInUnitRepository.cs +++ b/PARR.DAL/Repositories/Unit/UnitInUnitRepository.cs @@ -27,38 +27,14 @@ namespace PARR.DAL.Repositories.Unit } - public Task> GetByParentIdAsync(Guid parentId) + public async Task> GetRelatedUnitIdsAsync(Guid unitId, CancellationToken ct = default) { - return dataContext.UnitInUnits - .Where(u => u.ParentUnitId == parentId) - .ToListAsync(); - } - - - public Task> GetByChildIdAsync(Guid childId) - { - return dataContext.UnitInUnits - .Where(u => u.ChildUnitId == childId) - .ToListAsync(); - } - - - public async Task> GetParentLinksByChildIdsAsync(IEnumerable childUnitIds) - { - var set = childUnitIds.ToHashSet(); - return await dataContext.UnitInUnits + return await Get() .AsNoTracking() - .Where(uinu => set.Contains(uinu.ChildUnitId)) - .ToListAsync(); - } - - public async Task> GetChildLinksByParentIdsAsync(IEnumerable parentUnitIds) - { - var set = parentUnitIds.ToHashSet(); - return await dataContext.UnitInUnits - .AsNoTracking() - .Where(uinu => set.Contains(uinu.ParentUnitId)) - .ToListAsync(); + .Where(link => link.ChildUnitId == unitId || link.ParentUnitId == unitId) + .Select(link => link.ChildUnitId == unitId ? link.ParentUnitId : link.ChildUnitId) + .Distinct() + .ToListAsync(ct); } } }