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