fix(core, templateMatcher): Исправлен метод ApplyRelationshipFiltersToUnitsAsync - неверно фильтровались ЭК при одновременно включенном IsFullMatch и IsInverse, теперь учитываются все связи; В GroupedTemplateSynchronizer изменен порядок индексации шаблонов.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
namespace PARR.Core.Services.UnitFilterService.Models
|
||||
{
|
||||
internal class FilteredUnitContext
|
||||
internal class UnitFilterMatchResult
|
||||
{
|
||||
public Guid UnitId { get; set; }
|
||||
public HashSet<Guid> ValidParentIds { get; set; } = new();
|
||||
@@ -2,6 +2,6 @@
|
||||
{
|
||||
internal class UnitFilterServiceOptions
|
||||
{
|
||||
public int LoadBatchSize { get; set; } = 50;
|
||||
public int LoadBatchSize { get; set; } = 100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace PARR.Core.Services.UnitFilterService;
|
||||
internal class UnitFilterService : IUnitFilterService
|
||||
{
|
||||
#if DEBUG
|
||||
private readonly Guid targetUnitId = Guid.Parse("9d88fff2-a861-487f-b73d-bce1f0218e9f");
|
||||
private readonly Guid debugTargetUnitId = Guid.Parse("f2017292-193c-48e9-b333-3a00e737c6fc");
|
||||
#endif
|
||||
|
||||
private const int DebugMaxUnitsToLog = 10;
|
||||
@@ -94,7 +94,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count);
|
||||
|
||||
// Собираем все контексты юнитов, прошедших фильтрацию
|
||||
var allFilteredContexts = new List<FilteredUnitContext>();
|
||||
var allFilteredContexts = new List<UnitFilterMatchResult>();
|
||||
|
||||
// Преобразуем в список для индексации
|
||||
var unitFiltersList = job.UnitFilters.ToList();
|
||||
@@ -118,9 +118,9 @@ internal class UnitFilterService : IUnitFilterService
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (initialUnitIds.Contains(targetUnitId))
|
||||
if (initialUnitIds.Contains(debugTargetUnitId))
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", targetUnitId, i + 1);
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", debugTargetUnitId, i + 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -136,13 +136,13 @@ internal class UnitFilterService : IUnitFilterService
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (initialUnitIds.Contains(targetUnitId) && !fieldFilteredIds.Contains(targetUnitId))
|
||||
if (initialUnitIds.Contains(debugTargetUnitId) && !fieldFilteredIds.Contains(debugTargetUnitId))
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", targetUnitId, i + 1);
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", debugTargetUnitId, i + 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
// 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ FilteredUnitContext
|
||||
// 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ UnitFilterMatchResult
|
||||
var relStopwatch = Stopwatch.StartNew();
|
||||
var relationshipFilteredContexts = await ProcessRelationshipFiltersAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken);
|
||||
relStopwatch.Stop();
|
||||
@@ -154,11 +154,11 @@ internal class UnitFilterService : IUnitFilterService
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == targetUnitId);
|
||||
var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||
if (targetContext != null)
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} прошёл фильтр #{Index}. Родителей: {ParentCount}, Детей: {ChildCount}",
|
||||
targetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count);
|
||||
debugTargetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -186,7 +186,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
// Убираем дубликаты по UnitId, объединяя связи
|
||||
var mergedContexts = allFilteredContexts
|
||||
.GroupBy(c => c.UnitId)
|
||||
.Select(g => new FilteredUnitContext
|
||||
.Select(g => new UnitFilterMatchResult
|
||||
{
|
||||
UnitId = g.Key,
|
||||
ValidParentIds = new HashSet<Guid>(g.SelectMany(c => c.ValidParentIds)),
|
||||
@@ -204,7 +204,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
job.MinValueRelationships, job.MaxValueRelationships, job.IsParentRelationships);
|
||||
|
||||
#if DEBUG
|
||||
var targetBeforeUmbrella = mergedContexts.FirstOrDefault(c => c.UnitId == targetUnitId);
|
||||
var targetBeforeUmbrella = mergedContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||
if (targetBeforeUmbrella != null)
|
||||
{
|
||||
var count = job.IsParentRelationships == true
|
||||
@@ -213,7 +213,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
var passes = count >= (job.MinValueRelationships ?? 0) && count <= (job.MaxValueRelationships ?? int.MaxValue);
|
||||
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} перед Umbrella: Count={Count}, Min={Min}, Max={Max}, Passes={Passes}",
|
||||
targetUnitId, count, job.MinValueRelationships, job.MaxValueRelationships, passes);
|
||||
debugTargetUnitId, count, job.MinValueRelationships, job.MaxValueRelationships, passes);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -222,10 +222,10 @@ internal class UnitFilterService : IUnitFilterService
|
||||
logger.LogInformation("Этап фильтрации по числу связей завершён: после Umbrella-фильтра осталось {UnitCount} юнитов", umbrellaFilteredContexts.Count);
|
||||
|
||||
#if DEBUG
|
||||
var targetAfterUmbrella = umbrellaFilteredContexts.FirstOrDefault(c => c.UnitId == targetUnitId);
|
||||
var targetAfterUmbrella = umbrellaFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||
if (targetBeforeUmbrella != null && targetAfterUmbrella == null)
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", targetUnitId);
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", debugTargetUnitId);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -249,7 +249,6 @@ internal class UnitFilterService : IUnitFilterService
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await jobService
|
||||
@@ -294,39 +293,26 @@ internal class UnitFilterService : IUnitFilterService
|
||||
logger.LogDebug(" FieldFilter #{Index}: Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}",
|
||||
filterIndex, fieldName, dbValueMask, fieldFilter.IsInverse);
|
||||
|
||||
//if (fieldFilter.IsInverse)
|
||||
//{
|
||||
// query = query.Where(u => !u.UnitValues.Any(v =>
|
||||
// v.FieldId == fieldFilter.FieldId &&
|
||||
// EF.Functions.ILike(v.Value.Value, dbValueMask)));
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// query = query.Where(u => u.UnitValues.Any(v =>
|
||||
// v.FieldId == fieldFilter.FieldId &&
|
||||
// EF.Functions.ILike(v.Value.Value, dbValueMask)));
|
||||
//}
|
||||
|
||||
query = unitService.GetUnitByFieldAndValue(query, fieldFilter.FieldId, fieldFilter.ValueMask!, fieldFilter.IsInverse);
|
||||
|
||||
#if DEBUG
|
||||
if (unitIds.Contains(targetUnitId))
|
||||
if (unitIds.Contains(debugTargetUnitId))
|
||||
{
|
||||
var unitHasField = await unitInValueService.Get()
|
||||
.AsNoTracking()
|
||||
.AnyAsync(uiv => uiv.UnitId == targetUnitId && uiv.FieldId == fieldFilter.FieldId, cancellationToken);
|
||||
.AnyAsync(uiv => uiv.UnitId == debugTargetUnitId && uiv.FieldId == fieldFilter.FieldId, cancellationToken);
|
||||
|
||||
var unitValue = await unitInValueService.Get()
|
||||
.AsNoTracking()
|
||||
.Where(uiv => uiv.UnitId == targetUnitId && uiv.FieldId == fieldFilter.FieldId)
|
||||
.Where(uiv => uiv.UnitId == debugTargetUnitId && uiv.FieldId == fieldFilter.FieldId)
|
||||
.Select(uiv => uiv.Value.Value)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
var intermediateResult = await query.Select(u => u.Id).ToListAsync(cancellationToken);
|
||||
var passes = intermediateResult.Contains(targetUnitId);
|
||||
var passes = intermediateResult.Contains(debugTargetUnitId);
|
||||
|
||||
logger.LogDebug(" DEBUG: Юнит {TargetUnitId}: Поле={FieldName}, Значение={Value}, Маска={Mask}, HasField={HasField}, Проходит={Passes}",
|
||||
targetUnitId, fieldName, unitValue ?? "null", dbValueMask, unitHasField, passes);
|
||||
debugTargetUnitId, fieldName, unitValue ?? "null", dbValueMask, unitHasField, passes);
|
||||
|
||||
query = unitService.Get().AsNoTracking()
|
||||
.Where(u => intermediateResult.Contains(u.Id));
|
||||
@@ -344,7 +330,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
/// <summary>
|
||||
/// Применяем фильтры аттрибутов у связанных ЭК
|
||||
/// </summary>
|
||||
private async Task<List<FilteredUnitContext>> ProcessRelationshipFiltersAsync(
|
||||
private async Task<List<UnitFilterMatchResult>> ProcessRelationshipFiltersAsync(
|
||||
List<Guid> unitIds,
|
||||
IEnumerable<JobRelationshipFilter> relationshipFilters,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -353,29 +339,29 @@ internal class UnitFilterService : IUnitFilterService
|
||||
{
|
||||
logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений",
|
||||
unitIds.Count);
|
||||
return unitIds.Select(id => new FilteredUnitContext { UnitId = id }).ToList();
|
||||
return unitIds.Select(id => new UnitFilterMatchResult { UnitId = id }).ToList();
|
||||
}
|
||||
|
||||
logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}",
|
||||
unitIds.Count, relationshipFilters.Count());
|
||||
|
||||
#if DEBUG
|
||||
if (unitIds.Contains(targetUnitId))
|
||||
if (unitIds.Contains(debugTargetUnitId))
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден во входных данных ProcessRelationshipFiltersAsync", targetUnitId);
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден во входных данных ProcessRelationshipFiltersAsync", debugTargetUnitId);
|
||||
}
|
||||
#endif
|
||||
|
||||
var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id });
|
||||
var preFilteredUnitsById = unitIds.ToDictionary(id => id, id => new UnitFilterMatchResult { UnitId = id });
|
||||
|
||||
var parentRelFilters = relationshipFilters.Where(rf => rf.IsParent).ToList();
|
||||
var childRelFilters = relationshipFilters.Where(rf => !rf.IsParent).ToList();
|
||||
|
||||
if (parentRelFilters.Any())
|
||||
{
|
||||
await ApplyRelationshipFiltersOnDbAsync(
|
||||
await ApplyRelationshipFiltersToUnitsAsync(
|
||||
unitIds,
|
||||
resultContexts,
|
||||
preFilteredUnitsById,
|
||||
parentRelFilters,
|
||||
isParentDirection: true,
|
||||
cancellationToken);
|
||||
@@ -383,48 +369,48 @@ internal class UnitFilterService : IUnitFilterService
|
||||
|
||||
if (childRelFilters.Any())
|
||||
{
|
||||
await ApplyRelationshipFiltersOnDbAsync(
|
||||
await ApplyRelationshipFiltersToUnitsAsync(
|
||||
unitIds,
|
||||
resultContexts,
|
||||
preFilteredUnitsById,
|
||||
childRelFilters,
|
||||
isParentDirection: false,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
var unitsWithAnyConnections = resultContexts.Values.Count(c => c.ValidParentIds.Any() || c.ValidChildIds.Any());
|
||||
var unitsWithoutConnections = resultContexts.Values.Count(c => !c.ValidParentIds.Any() && !c.ValidChildIds.Any());
|
||||
logger.LogDebug("DEBUG: После ApplyRelationshipFiltersOnDbAsync: {WithConnections} юнитов со связями, {WithoutConnections} без связей",
|
||||
var unitsWithAnyConnections = preFilteredUnitsById.Values.Count(c => c.ValidParentIds.Any() || c.ValidChildIds.Any());
|
||||
var unitsWithoutConnections = preFilteredUnitsById.Values.Count(c => !c.ValidParentIds.Any() && !c.ValidChildIds.Any());
|
||||
logger.LogDebug("DEBUG: После ApplyRelationshipFiltersToUnitsAsync: {WithConnections} юнитов со связями, {WithoutConnections} без связей",
|
||||
unitsWithAnyConnections, unitsWithoutConnections);
|
||||
|
||||
var targetContext = resultContexts.Values.FirstOrDefault(c => c.UnitId == targetUnitId);
|
||||
var targetContext = preFilteredUnitsById.Values.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||
if (targetContext != null)
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId}: Родителей={ParentCount}, Детей={ChildCount}, PassesFilter={Passes}",
|
||||
targetUnitId,
|
||||
debugTargetUnitId,
|
||||
targetContext.ValidParentIds.Count,
|
||||
targetContext.ValidChildIds.Count,
|
||||
targetContext.ValidParentIds.Any() || targetContext.ValidChildIds.Any());
|
||||
}
|
||||
#endif
|
||||
|
||||
var result = resultContexts.Values.ToList();
|
||||
var result = preFilteredUnitsById.Values.ToList();
|
||||
|
||||
logger.LogDebug("ProcessRelationshipFiltersAsync: выход {ContextCount} контекстов (из {InitialCount})",
|
||||
result.Count, resultContexts.Count);
|
||||
result.Count, preFilteredUnitsById.Count);
|
||||
|
||||
#if DEBUG
|
||||
if (unitIds.Contains(targetUnitId))
|
||||
if (unitIds.Contains(debugTargetUnitId))
|
||||
{
|
||||
var targetContextInResult = result.FirstOrDefault(c => c.UnitId == targetUnitId);
|
||||
var targetContextInResult = result.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||
if (targetContextInResult != null)
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} в результатах ProcessRelationshipFiltersAsync. Родителей: {ParentCount}, Детей: {ChildCount}",
|
||||
targetUnitId, targetContextInResult.ValidParentIds.Count, targetContextInResult.ValidChildIds.Count);
|
||||
debugTargetUnitId, targetContextInResult.ValidParentIds.Count, targetContextInResult.ValidChildIds.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ в результатах ProcessRelationshipFiltersAsync (удалён)", targetUnitId);
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ в результатах ProcessRelationshipFiltersAsync (удалён)", debugTargetUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,7 +522,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
/// <param name="contexts"></param>
|
||||
/// <param name="job"></param>
|
||||
/// <returns></returns>
|
||||
private List<FilteredUnitContext> ApplyUmbrellaFilterInMemory(List<FilteredUnitContext> contexts, Job job)
|
||||
private List<UnitFilterMatchResult> ApplyUmbrellaFilterInMemory(List<UnitFilterMatchResult> contexts, Job job)
|
||||
{
|
||||
var min = job.MinValueRelationships ?? 0;
|
||||
var max = job.MaxValueRelationships ?? int.MaxValue;
|
||||
@@ -545,17 +531,17 @@ internal class UnitFilterService : IUnitFilterService
|
||||
if (min == 0 && max == int.MaxValue)
|
||||
return contexts;
|
||||
|
||||
var result = new List<FilteredUnitContext>();
|
||||
var result = new List<UnitFilterMatchResult>();
|
||||
foreach (var context in contexts)
|
||||
{
|
||||
int count = isParentDirection ? context.ValidParentIds.Count : context.ValidChildIds.Count;
|
||||
bool passes = count >= min && count <= max;
|
||||
|
||||
#if DEBUG
|
||||
if (context.UnitId == targetUnitId)
|
||||
if (context.UnitId == debugTargetUnitId)
|
||||
{
|
||||
logger.LogDebug("DEBUG Umbrella: Юнит {TargetUnitId}, Count={Count}, Min={Min}, Max={Max}, Passes={Passes}",
|
||||
targetUnitId, count, min, max, passes);
|
||||
debugTargetUnitId, count, min, max, passes);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -574,20 +560,20 @@ internal class UnitFilterService : IUnitFilterService
|
||||
/// </summary>
|
||||
/// <param name="contexts"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<List<UnitFilterResultDto>> LoadFinalResultAsync(List<FilteredUnitContext> contexts, CancellationToken cancellationToken = default)
|
||||
private async Task<List<UnitFilterResultDto>> LoadFinalResultAsync(List<UnitFilterMatchResult> contexts, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var allResults = new List<UnitFilterResultDto>();
|
||||
|
||||
#if DEBUG
|
||||
var targetInContexts = contexts.FirstOrDefault(c => c.UnitId == targetUnitId);
|
||||
var targetInContexts = contexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||
if (targetInContexts != null)
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} передан в LoadFinalResultAsync. Родителей: {ParentCount}, Детей: {ChildCount}",
|
||||
targetUnitId, targetInContexts.ValidParentIds.Count, targetInContexts.ValidChildIds.Count);
|
||||
debugTargetUnitId, targetInContexts.ValidParentIds.Count, targetInContexts.ValidChildIds.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", targetUnitId);
|
||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", debugTargetUnitId);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -607,7 +593,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
/// </summary>
|
||||
/// <param name="batch"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<List<UnitFilterResultDto>> LoadBatchAsync(List<FilteredUnitContext> batch, CancellationToken cancellationToken = default)
|
||||
private async Task<List<UnitFilterResultDto>> LoadBatchAsync(List<UnitFilterMatchResult> batch, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var unitIds = batch.Select(c => c.UnitId).ToList();
|
||||
var allParentIds = batch.SelectMany(c => c.ValidParentIds).Distinct().ToList();
|
||||
@@ -786,12 +772,14 @@ internal class UnitFilterService : IUnitFilterService
|
||||
|
||||
|
||||
#region вспомогательные методы
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Применяет фильтры к связям (родителям или детям)
|
||||
/// </summary>
|
||||
private async Task ApplyRelationshipFiltersOnDbAsync(
|
||||
private async Task ApplyRelationshipFiltersToUnitsAsync(
|
||||
List<Guid> unitIds,
|
||||
Dictionary<Guid, FilteredUnitContext> resultContexts,
|
||||
Dictionary<Guid, UnitFilterMatchResult> preFilteredUnitsById,
|
||||
List<JobRelationshipFilter> relFilters,
|
||||
bool isParentDirection,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -802,6 +790,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
var directionName = isParentDirection ? "Родительские" : "Дочерние";
|
||||
logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count);
|
||||
|
||||
// 1. Получить все связи (unitInUnit) для юнитов из unitIds
|
||||
var allLinks = await unitInUnitService.Get()
|
||||
.AsNoTracking()
|
||||
.Where(link => isParentDirection
|
||||
@@ -814,16 +803,18 @@ internal class UnitFilterService : IUnitFilterService
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var allTargetIds = allLinks.Select(l => l.TargetId).Distinct().ToList();
|
||||
var allRelatedUnitIds = allLinks.Select(l => l.TargetId).Distinct().ToList();
|
||||
logger.LogDebug(" Найдено {TargetCount} уникальных {TargetType} для {LinkCount} связей",
|
||||
allTargetIds.Count,
|
||||
allRelatedUnitIds.Count,
|
||||
isParentDirection ? "родителей" : "детей",
|
||||
allLinks.Count);
|
||||
|
||||
var linksBySource = allLinks.GroupBy(l => l.SourceId)
|
||||
// 2. Сгруппировать связи по SourceId (ID юнита из unitIds)
|
||||
var relationshipsByUnitId = allLinks.GroupBy(l => l.SourceId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList());
|
||||
|
||||
var targetFilterPassCount = new Dictionary<Guid, int>();
|
||||
// 3. Для каждого фильтра в группе найдём TargetId, которые ему соответствуют (с учётом IsInverse)
|
||||
var matchedTargetIdsByFilter = new Dictionary<JobRelationshipFilter, HashSet<Guid>>();
|
||||
|
||||
foreach (var relFilter in relFilters)
|
||||
{
|
||||
@@ -832,137 +823,156 @@ internal class UnitFilterService : IUnitFilterService
|
||||
continue;
|
||||
|
||||
string dbValueMask = NormalizeLikeMask(valueMask);
|
||||
|
||||
var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}";
|
||||
|
||||
logger.LogDebug(" RelationshipFilter ({Direction}): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}",
|
||||
isParentDirection ? "Parent" : "Child",
|
||||
fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);
|
||||
|
||||
//var matchingTargetIds = await unitInValueService.Get()
|
||||
// .AsNoTracking()
|
||||
// .Where(uiv => uiv.FieldId == relFilter.FieldId)
|
||||
// .Where(uiv => EF.Functions.ILike(uiv.Value.Value, dbValueMask))
|
||||
// .Select(uiv => uiv.UnitId)
|
||||
// .Distinct()
|
||||
// .ToListAsync(cancellationToken);
|
||||
|
||||
var matchingTargetIds = await unitInValueService.GetMatchingTargetIds(relFilter.FieldId, dbValueMask)
|
||||
.ToListAsync(cancellationToken);
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Ограничиваем найденные ID только теми, которые действительно связаны с юнитами из unitIds
|
||||
var filteredMatchingTargetIds = matchingTargetIds
|
||||
.Intersect(allTargetIds)
|
||||
.Intersect(allRelatedUnitIds)
|
||||
.ToList();
|
||||
|
||||
logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске",
|
||||
filteredMatchingTargetIds.Count,
|
||||
isParentDirection ? "родителей" : "детей");
|
||||
|
||||
HashSet<Guid> targetIdsThatPassThisFilter;
|
||||
|
||||
if (relFilter.IsInverse)
|
||||
{
|
||||
foreach (var targetId in allTargetIds)
|
||||
{
|
||||
if (!filteredMatchingTargetIds.Contains(targetId))
|
||||
{
|
||||
if (!targetFilterPassCount.ContainsKey(targetId))
|
||||
targetFilterPassCount[targetId] = 0;
|
||||
targetFilterPassCount[targetId]++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug(" Для IsInverse: {NonMatchCount} {TargetType} НЕ соответствуют маске",
|
||||
allTargetIds.Count - filteredMatchingTargetIds.Count,
|
||||
isParentDirection ? "родителей" : "детей");
|
||||
// Юнит проходит фильтр, если его значение НЕ соответствует маске
|
||||
// Это означает, что юниты, НЕ входящие в filteredMatchingTargetIds, проходят фильтр
|
||||
targetIdsThatPassThisFilter = new HashSet<Guid>(allRelatedUnitIds.Except(filteredMatchingTargetIds));
|
||||
logger.LogDebug(" (IsInverse) Юниты, прошедшие фильтр: {Count}", targetIdsThatPassThisFilter.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var targetId in filteredMatchingTargetIds)
|
||||
{
|
||||
if (!targetFilterPassCount.ContainsKey(targetId))
|
||||
targetFilterPassCount[targetId] = 0;
|
||||
targetFilterPassCount[targetId]++;
|
||||
}
|
||||
// Юнит проходит фильтр, если его значение соответствует маске
|
||||
targetIdsThatPassThisFilter = new HashSet<Guid>(filteredMatchingTargetIds);
|
||||
logger.LogDebug(" (Direct) Юниты, прошедшие фильтр: {Count}", targetIdsThatPassThisFilter.Count);
|
||||
}
|
||||
|
||||
// Сохраняем результат для этого конкретного фильтра
|
||||
matchedTargetIdsByFilter[relFilter] = targetIdsThatPassThisFilter;
|
||||
}
|
||||
|
||||
int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim()));
|
||||
var validTargetIds = targetFilterPassCount
|
||||
.Where(kvp => kvp.Value == requiredFiltersCount)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
// 4. Определим, какие юниты (context.UnitId) проходят ВСЕ фильтры с учётом IsFullMatch
|
||||
var unitIdsToRemove = new HashSet<Guid>();
|
||||
|
||||
logger.LogDebug(" Найдено {ValidCount} {TargetType}, прошедших ВСЕ {RequiredCount} фильтров",
|
||||
validTargetIds.Count,
|
||||
isParentDirection ? "родителей" : "детей",
|
||||
requiredFiltersCount);
|
||||
|
||||
var unitsToRemove = new HashSet<Guid>();
|
||||
|
||||
foreach (var context in resultContexts.Values)
|
||||
foreach (var context in preFilteredUnitsById.Values)
|
||||
{
|
||||
if (unitsToRemove.Contains(context.UnitId))
|
||||
continue;
|
||||
|
||||
var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List<Guid>());
|
||||
bool hasTargets = unitTargets.Any();
|
||||
|
||||
var validTargets = unitTargets.Intersect(validTargetIds).ToList();
|
||||
bool hasValidTargets = validTargets.Any();
|
||||
|
||||
bool isFullMatch = relFilters.All(rf => rf.IsFullMatch);
|
||||
bool isInverse = relFilters.Any(rf => rf.IsInverse);
|
||||
|
||||
bool passesFilter;
|
||||
// Получаем связанные юниты для конкретного context.UnitId
|
||||
var relatedUnitIds = relationshipsByUnitId.GetValueOrDefault(context.UnitId, new List<Guid>()).ToHashSet();
|
||||
bool hasTargets = relatedUnitIds.Count > 0;
|
||||
|
||||
if (!hasTargets)
|
||||
{
|
||||
passesFilter = isInverse;
|
||||
// Нет связей - проверяем, есть ли фильтры, которые требуют наличия связей
|
||||
// Если есть хотя бы один фильтр с маской, и связей нет - юнит не проходит.
|
||||
var hasFiltersWithMask = relFilters.Any(rf => !string.IsNullOrWhiteSpace(rf.ValueMask?.Trim()));
|
||||
if (hasFiltersWithMask)
|
||||
{
|
||||
unitIdsToRemove.Add(context.UnitId);
|
||||
continue; // Переходим к следующему юниту
|
||||
}
|
||||
// Если фильтров с маской нет, юнит остаётся.
|
||||
}
|
||||
else if (isFullMatch)
|
||||
|
||||
bool unitPassesCurrentFilterGroup = true;
|
||||
|
||||
// Проверяем, проходит ли юнит все фильтры в группе
|
||||
foreach (var relFilter in relFilters)
|
||||
{
|
||||
passesFilter = validTargets.Count == unitTargets.Count;
|
||||
var valueMask = relFilter.ValueMask?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(valueMask))
|
||||
{
|
||||
continue; // Пустой фильтр пропускаем
|
||||
}
|
||||
|
||||
// Получаем юниты, прошедшие конкретный фильтр relFilter
|
||||
var targetIdsThatPassThisFilter = matchedTargetIdsByFilter[relFilter];
|
||||
|
||||
if (relFilter.IsFullMatch)
|
||||
{
|
||||
// ВСЕ relatedUnitIds должны пройти этот конкретный фильтр relFilter
|
||||
// Это означает, что каждый relatedUnitId должен быть в targetIdsThatPassThisFilter
|
||||
bool allTargetsPassThisFilter = relatedUnitIds.All(targetId => targetIdsThatPassThisFilter.Contains(targetId));
|
||||
if (!allTargetsPassThisFilter)
|
||||
{
|
||||
unitPassesCurrentFilterGroup = false;
|
||||
break; // Не нужно проверять остальные фильтры, юнит заведомо не проходит
|
||||
}
|
||||
}
|
||||
else // IsFullMatch = false
|
||||
{
|
||||
// Хоть один relatedUnitId должен пройти этот конкретный фильтр relFilter
|
||||
// Это означает, что хотя бы один relatedUnitId должен быть в targetIdsThatPassThisFilter
|
||||
bool anyTargetPassesThisFilter = relatedUnitIds.Any(targetId => targetIdsThatPassThisFilter.Contains(targetId));
|
||||
if (!anyTargetPassesThisFilter)
|
||||
{
|
||||
unitPassesCurrentFilterGroup = false;
|
||||
break; // Не нужно проверять остальные фильтры, юнит заведимо не проходит
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!unitPassesCurrentFilterGroup)
|
||||
{
|
||||
unitIdsToRemove.Add(context.UnitId);
|
||||
}
|
||||
else
|
||||
{
|
||||
passesFilter = hasValidTargets;
|
||||
}
|
||||
// Юнит прошёл все фильтры.
|
||||
// Теперь определим, какие из его связей (relatedUnitIds) идут в ValidParentIds/ValidChildIds.
|
||||
// Это должны быть связи, прошедшие *все* фильтры (т.е. быть в пересечении результатов для каждого фильтра).
|
||||
// Соберём пересечение всех targetId, прошедших каждый фильтр для этого конкретного юнита.
|
||||
// Начинаем с множества всех его связей.
|
||||
var validRelatedIds = new HashSet<Guid>(relatedUnitIds);
|
||||
|
||||
if (passesFilter)
|
||||
{
|
||||
if (isFullMatch)
|
||||
foreach (var relFilter in relFilters)
|
||||
{
|
||||
if (isParentDirection)
|
||||
context.ValidParentIds.UnionWith(unitTargets);
|
||||
else
|
||||
context.ValidChildIds.UnionWith(unitTargets);
|
||||
var valueMask = relFilter.ValueMask?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(valueMask))
|
||||
continue;
|
||||
|
||||
var targetIdsThatPassThisFilter = matchedTargetIdsByFilter[relFilter];
|
||||
// Пересекаем текущий список validRelatedIds с результатами для этого фильтра
|
||||
validRelatedIds.IntersectWith(targetIdsThatPassThisFilter);
|
||||
}
|
||||
|
||||
// Добавляем полученные валидные связи в контекст юнита
|
||||
if (isParentDirection)
|
||||
{
|
||||
context.ValidParentIds.UnionWith(validRelatedIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isParentDirection)
|
||||
context.ValidParentIds.UnionWith(validTargets);
|
||||
else
|
||||
context.ValidChildIds.UnionWith(validTargets);
|
||||
context.ValidChildIds.UnionWith(validRelatedIds);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
unitsToRemove.Add(context.UnitId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var unitId in unitsToRemove)
|
||||
// Удаляем юниты, которые не прошли проверку
|
||||
foreach (var unitId in unitIdsToRemove)
|
||||
{
|
||||
resultContexts.Remove(unitId);
|
||||
preFilteredUnitsById.Remove(unitId);
|
||||
}
|
||||
|
||||
logger.LogDebug(" Удалено {RemovedCount} юнитов, не прошедших фильтры", unitsToRemove.Count);
|
||||
logger.LogDebug(" Удалено {RemovedCount} юнитов, не прошедших фильтры (новая логика с IsFullMatch/IsInverse для каждого фильтра)",
|
||||
unitIdsToRemove.Count);
|
||||
|
||||
var totalAdded = resultContexts.Values.Sum(c =>
|
||||
var totalAdded = preFilteredUnitsById.Values.Sum(c =>
|
||||
isParentDirection ? c.ValidParentIds.Count : c.ValidChildIds.Count);
|
||||
|
||||
logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов",
|
||||
logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов (новая логика)",
|
||||
totalAdded,
|
||||
isParentDirection ? "родительских" : "дочерних",
|
||||
resultContexts.Count);
|
||||
preFilteredUnitsById.Count);
|
||||
}
|
||||
|
||||
|
||||
@@ -979,11 +989,6 @@ internal class UnitFilterService : IUnitFilterService
|
||||
|
||||
var dbValueMask = NormalizeLikeMask(filter.UnitFilter);
|
||||
|
||||
//var initialUnitIds = await unitService.Get().AsNoTracking()
|
||||
// .Where(unit => EF.Functions.ILike(unit.Name, dbValueMask))
|
||||
// .Select(u => u.Id)
|
||||
// .ToListAsync(cancellationToken);
|
||||
|
||||
var initialUnitIds = await unitService.GetInitialUnitIds(dbValueMask).ToListAsync(cancellationToken);
|
||||
|
||||
var toCache = new UnitFilterIds
|
||||
|
||||
Reference in New Issue
Block a user