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 public interface IUnitInUnitRepository
{ {
Task<List<UnitInUnit>> GetByParentIdAsync(Guid parentId);
Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId);
/// <summary> /// <summary>
/// Получает связи, где ChildUnitId unitIds (для IsParent=True). /// Возвращает все связанные UnitId для заданного юнита в обоих направлениях.
/// Единая точка загрузки связей.
/// </summary> /// </summary>
Task<List<UnitInUnit>> GetParentLinksByChildIdsAsync(IEnumerable<Guid> childUnitIds); Task<List<Guid>> GetRelatedUnitIdsAsync(Guid unitId, CancellationToken ct = default);
/// <summary>
/// Получает связи, где ParentUnitId unitIds (для IsParent=False).
/// </summary>
Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds);
IQueryable<UnitInUnit> Get(); 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.Matchers.Interfaces;
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.JobEntities; using PARR.Domain.Entities.JobEntities;
using PARR.Domain.Entities.Unit;
namespace PARR.Core.Services.UnitFilterService.Matchers; namespace PARR.Core.Services.UnitFilterService.Matchers;
@@ -238,9 +239,9 @@ internal class UnitRelationshipMatcher : IUnitRelationshipMatcher
} }
/// <summary> /// <summary>
/// Обрабатывает маску LIKE для корректной работы с SQL /// Нормализует пользовательскую маску в формат, совместимый с PostgreSQL ILIKE.
/// </summary> /// </summary>
private static string NormalizeLikeMask(string valueMask) internal static string NormalizeLikeMask(string valueMask)
{ {
if (string.IsNullOrWhiteSpace(valueMask)) if (string.IsNullOrWhiteSpace(valueMask))
return valueMask; return valueMask;
@@ -258,4 +259,62 @@ internal class UnitRelationshipMatcher : IUnitRelationshipMatcher
else else
return valueMask; 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.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.JobRepositories; using PARR.Core.Repositories.Interfaces.JobRepositories;
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitFilterService.Matchers;
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces; using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Core.Services.UnitService.Interfaces; using PARR.Core.Services.UnitService.Interfaces;
@@ -224,345 +225,6 @@ internal class UnitFilterService : IUnitFilterService
} }
private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default) private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
{ {
return await jobRepository 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); logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId);
@@ -590,71 +253,47 @@ internal class UnitFilterService : IUnitFilterService
throw new ArgumentException($"Job {jobId} не найден.", nameof(jobId)); 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); var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var filter in job.UnitFilters) foreach (var filter in job.UnitFilters)
{ {
if (!filter.RelationshipFilters.Any()) continue; var relFilters = filter.RelationshipFilters
.Where(rf => !string.IsNullOrWhiteSpace(rf.ValueMask?.Trim()))
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()
.ToList(); .ToList();
if (!allRelatedUnitIds.Any()) continue; if (!relFilters.Any()) continue;
// Получить значения для всех связанных юнитов // Проверяем каждый связанный юнит через единую точку проверки
var allUnitValues = await unitInValueRepository.GetByUnitIdsAsync(allRelatedUnitIds); var passedUnitIds = new HashSet<Guid>();
// Сгруппировать значения по UnitId
var valuesByUnit = allUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
// Найти UnitId, которые проходят все RelationshipFilters
var matchingUnitIds = new HashSet<Guid>();
foreach (var relatedUnitId in allRelatedUnitIds) foreach (var relatedUnitId in allRelatedUnitIds)
{ {
bool passesAllFilters = filter.RelationshipFilters.All(rf => var unitValues = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
{
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
var matchingValues = values // Все фильтры должны пройти (AND между фильтрами в рамках одного UnitFilter)
.Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null) bool passesAll = relFilters.All(rf =>
.ToList(); UnitRelationshipMatcher.TargetPassesFilter(unitValues, rf));
if (!matchingValues.Any()) if (passesAll)
{ passedUnitIds.Add(relatedUnitId);
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 (matchingUnitIds.Any()) if (passedUnitIds.Any())
{ {
// Используем кэширующий сервис вместо прямого запроса к БД var cachedUnits = await unitService.GetWithCachingAsync(passedUnitIds);
var cachedUnits = await unitService.GetWithCachingAsync(matchingUnitIds);
var names = cachedUnits.Values var names = cachedUnits.Values
.Select(u => u.Name) .Select(u => u.Name)
.Where(n => !string.IsNullOrEmpty(n)); .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 return await Get()
.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
.AsNoTracking() .AsNoTracking()
.Where(uinu => set.Contains(uinu.ChildUnitId)) .Where(link => link.ChildUnitId == unitId || link.ParentUnitId == unitId)
.ToListAsync(); .Select(link => link.ChildUnitId == unitId ? link.ParentUnitId : link.ChildUnitId)
} .Distinct()
.ToListAsync(ct);
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();
} }
} }
} }