refactor(unitFilterService): логика фильтрации связанных ЭК перенесена в класс, который за это отвечает.
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user