refactor(unitFilterService): логика фильтрации связанных ЭК перенесена в класс, который за это отвечает.

This commit is contained in:
Mikhail Kuznetsov
2026-07-31 10:46:48 +10:00
parent 94bea5c46d
commit fb20395e53
4 changed files with 98 additions and 432 deletions

View File

@@ -4,19 +4,11 @@ namespace PARR.Core.Repositories.Interfaces.Unit
{
public interface IUnitInUnitRepository
{
Task<List<UnitInUnit>> GetByParentIdAsync(Guid parentId);
Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId);
/// <summary>
/// Получает связи, где ChildUnitId unitIds (для IsParent=True).
/// Возвращает все связанные UnitId для заданного юнита в обоих направлениях.
/// Единая точка загрузки связей.
/// </summary>
Task<List<UnitInUnit>> GetParentLinksByChildIdsAsync(IEnumerable<Guid> childUnitIds);
/// <summary>
/// Получает связи, где ParentUnitId unitIds (для IsParent=False).
/// </summary>
Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds);
Task<List<Guid>> GetRelatedUnitIdsAsync(Guid unitId, CancellationToken ct = default);
IQueryable<UnitInUnit> Get();
}

View File

@@ -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
}
/// <summary>
/// Обрабатывает маску LIKE для корректной работы с SQL
/// Нормализует пользовательскую маску в формат, совместимый с PostgreSQL ILIKE.
/// </summary>
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;
}
/// <summary>
/// Проверяет соответствие значения маске в формате ILIKE.
/// Эмулирует поведение PostgreSQL ILIKE для использования в C#-коде.
/// Регистронезависима.
/// </summary>
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);
}
/// <summary>
/// Проверяет, проходит ли один target-юнит один RelationshipFilter.
/// Единая точка истины для UnitRelationshipMatcher и GetRelatedUnitNamesAsync.
/// Учитывает IsInverse. Не учитывает IsFullMatch (это ответственность вызывающего кода).
/// </summary>
internal static bool TargetPassesFilter(
IReadOnlyList<UnitInValue> 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;
}
}

View File

@@ -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<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
{
return await jobRepository
@@ -575,7 +237,8 @@ internal class UnitFilterService : IUnitFilterService
}
public async Task<List<string>> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId, CancellationToken cancellationToken = default)
public async Task<List<string>> 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<string>();
// Загружаем значения всех связанных юнитов одним запросом
var allUnitValues = await unitInValueRepository.GetByUnitIdsAsync(allRelatedUnitIds);
var valuesByUnit = allUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
var result = new HashSet<string>(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<Guid>();
// Проверяем каждый связанный юнит через единую точку проверки
var passedUnitIds = new HashSet<Guid>();
foreach (var relatedUnitId in allRelatedUnitIds)
{
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
{
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
var unitValues = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
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));

View File

@@ -27,38 +27,14 @@ namespace PARR.DAL.Repositories.Unit
}
public Task<List<UnitInUnit>> GetByParentIdAsync(Guid parentId)
public async Task<List<Guid>> GetRelatedUnitIdsAsync(Guid unitId, CancellationToken ct = default)
{
return dataContext.UnitInUnits
.Where(u => u.ParentUnitId == parentId)
.ToListAsync();
}
public Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId)
{
return dataContext.UnitInUnits
.Where(u => u.ChildUnitId == childId)
.ToListAsync();
}
public async Task<List<UnitInUnit>> GetParentLinksByChildIdsAsync(IEnumerable<Guid> childUnitIds)
{
var set = childUnitIds.ToHashSet();
return await dataContext.UnitInUnits
return await Get()
.AsNoTracking()
.Where(uinu => set.Contains(uinu.ChildUnitId))
.ToListAsync();
}
public async Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> 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);
}
}
}