fix(dal): UnitFilter исправлена фильтрация по связям.

This commit is contained in:
Mikhail Kuznetsov
2026-03-12 14:57:33 +10:00
parent b9d1f923b3
commit f93078868b

View File

@@ -55,7 +55,7 @@ internal class UnitFilterService : IUnitFilterService
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
var job = await LoadJobWithFiltersAsync(jobId); var job = await LoadJobWithFiltersAsync(jobId, cancellationToken);
return job == null ? null : await GetUnitsByJobFilterAsync(job, takeCount, cancellationToken); return job == null ? null : await GetUnitsByJobFilterAsync(job, takeCount, cancellationToken);
} }
@@ -248,7 +248,7 @@ internal class UnitFilterService : IUnitFilterService
{ {
return await jobService return await jobService
.Get().AsNoTracking() .Get().AsNoTracking()
.AsSplitQuery() // ← Избегаем предупреждения EF Core о множественных коллекциях .AsSplitQuery()
.Include(j => j.UnitFilters).ThenInclude(uf => uf.FieldFilters).ThenInclude(ff => ff.UnitField) .Include(j => j.UnitFilters).ThenInclude(uf => uf.FieldFilters).ThenInclude(ff => ff.UnitField)
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters).ThenInclude(rf => rf.UnitField) .Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters).ThenInclude(rf => rf.UnitField)
.Include(j => j.Group).ThenInclude(g => g!.GroupType) .Include(j => j.Group).ThenInclude(g => g!.GroupType)
@@ -392,48 +392,89 @@ internal class UnitFilterService : IUnitFilterService
result.Count, resultContexts.Count); result.Count, resultContexts.Count);
#if DEBUG #if DEBUG
foreach (var context in result) foreach (var context in result.Take(10))
{ {
var u = await unitService.GetAsync(context.UnitId); var u = await unitService.GetAsync(context.UnitId);
if (u == null) if (u == null)
continue; continue;
logger.LogDebug("Найден ЭК {UnitName}", u.Name); logger.LogDebug("📦 Найден ЭК {UnitName} (Id={UnitId})", u.Name, u.Id);
// Получаем все FieldId из фильтров для отображения значений
var parentFieldIds = relationshipFilters.Where(rf => rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList();
var childFieldIds = relationshipFilters.Where(rf => !rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList();
// === РОДИТЕЛИ ===
if (context.ValidParentIds.Any()) if (context.ValidParentIds.Any())
{ {
logger.LogDebug("\t📌 Фильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count);
var parents = context.ValidParentIds; foreach (var parentId in context.ValidParentIds)
logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", parents.Count);
foreach (var parent in context.ValidParentIds)
{ {
var p = await unitService.GetAsync(parent); var p = await unitService.GetAsync(parentId);
logger.LogDebug("\t\t👤 {ParentName} (Id={ParentId})", p?.Name, parentId);
logger.LogDebug("\t\t {ParentName};", p?.Name); // Выводим значения только для полей из RelationshipFilters
if (parentFieldIds.Any())
{
var parentValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(
new[] { parentId },
parentFieldIds
);
foreach (var pv in parentValues)
{
var field = pv.Field;
var value = pv.Value?.Value;
logger.LogDebug("\t\t 🔹 {FieldName} = {FieldValue}",
field?.AihitName ?? $"FieldId={pv.FieldId}",
value ?? "null");
}
}
} }
} }
else if (parentRelFilters.Any())
{
logger.LogDebug("\t Родительские фильтры заданы, но подходящих родителей не найдено");
}
// === ДЕТИ ===
if (context.ValidChildIds.Any()) if (context.ValidChildIds.Any())
{ {
logger.LogDebug("\t Фильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count);
var children = context.ValidChildIds; foreach (var childId in context.ValidChildIds)
logger.LogDebug("\tФильтрам соответствуют {ParentsCount} детей:", children.Count);
foreach (var child in children)
{ {
var c = await unitService.GetAsync(child); var c = await unitService.GetAsync(childId);
logger.LogDebug("\t\t {ChildName} (Id={ChildId})", c?.Name, childId);
logger.LogDebug("\t\t {ChildName};", c?.Name); // Выводим значения только для полей из RelationshipFilters
if (childFieldIds.Any())
{
var childValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(
new[] { childId },
childFieldIds
);
foreach (var cv in childValues)
{
var field = cv.Field;
var value = cv.Value?.Value;
logger.LogDebug("\t\t {FieldName} = {FieldValue}",
field?.AihitName ?? $"FieldId={cv.FieldId}",
value ?? "null");
}
}
} }
} }
else if (childRelFilters.Any())
{
logger.LogDebug("\t Дочерние фильтры заданы, но подходящих детей не найдено");
}
} }
#endif #endif
return result; return result;
} }
@@ -696,7 +737,7 @@ internal class UnitFilterService : IUnitFilterService
/// Оценивает, проходит ли юнит фильтр связей /// Оценивает, проходит ли юнит фильтр связей
/// </summary> /// </summary>
private bool EvaluateFilter( private bool EvaluateFilter(
List<Guid> unitRelations, // Все связи юнита (родители или дети) List<Guid> unitRelations, // Все связи юнита
List<Guid> matchingRelations, // Связи, которые соответствуют маске List<Guid> matchingRelations, // Связи, которые соответствуют маске
bool isFullMatch, // Полное совпадение (All) или хотя бы одно (Any) bool isFullMatch, // Полное совпадение (All) или хотя бы одно (Any)
bool isInverse, // Обратный фильтр bool isInverse, // Обратный фильтр
@@ -714,29 +755,33 @@ internal class UnitFilterService : IUnitFilterService
return isInverse; return isInverse;
} }
// Основная логика // Основная логика — только для определения, проходит ли юнит
if (isFullMatch) if (isFullMatch)
{ {
// Все связи должны соответствовать // ВСЕ связи должны соответствовать
bool allMatch = matchingCount == totalCount; bool allMatch = matchingCount == totalCount;
if (isInverse) if (isInverse)
//Все связи не соответствуют фильтру return !allMatch; // НЕ все связи соответствуют
return matchingCount == 0;
else else
//Все связи соответствуют фильтру return allMatch; // ВСЕ связи соответствуют
return allMatch;
} }
else else
{ {
// Хотя бы одна связь должна соответствовать // ХОТЯ БЫ ОДНА связь должна соответствовать
return matchingCount > 0; bool anyMatch = matchingCount > 0;
if (isInverse)
return !anyMatch; // НИ ОДНА связь не соответствует
else
return anyMatch; // ХОТЯ БЫ ОДНА связь соответствует
} }
} }
/// <summary> /// <summary>
/// Применяет фильтры к связям (родителям или детям) /// Применяет фильтры к связям (родителям или детям)
/// Связь должна пройти ВСЕ фильтры направления
/// </summary> /// </summary>
private async Task ApplyRelationshipFiltersAsync( private async Task ApplyRelationshipFiltersAsync(
List<Guid> unitIds, List<Guid> unitIds,
@@ -774,7 +819,10 @@ internal class UnitFilterService : IUnitFilterService
var linksBySource = allLinks.GroupBy(l => l.SourceId) var linksBySource = allLinks.GroupBy(l => l.SourceId)
.ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList()); .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList());
// Обрабатываем каждый фильтр // Находим связи, которые проходят ВСЕ фильтры одновременно
// Для каждого TargetId считаем, сколько фильтров он прошёл
var targetFilterPassCount = new Dictionary<Guid, int>();
foreach (var relFilter in relFilters) foreach (var relFilter in relFilters)
{ {
var valueMask = relFilter.ValueMask?.Trim(); var valueMask = relFilter.ValueMask?.Trim();
@@ -799,7 +847,7 @@ internal class UnitFilterService : IUnitFilterService
isParentDirection ? "Parent" : "Child", isParentDirection ? "Parent" : "Child",
fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);
// Находим целевые юниты, которые соответствуют фильтру // Находим целевые юниты, которые соответствуют текущему фильтру
var matchingTargetIds = await unitService.Get() var matchingTargetIds = await unitService.Get()
.AsNoTracking() .AsNoTracking()
.Where(u => allTargetIds.Contains(u.Id)) .Where(u => allTargetIds.Contains(u.Id))
@@ -813,44 +861,77 @@ internal class UnitFilterService : IUnitFilterService
matchingTargetIds.Count, matchingTargetIds.Count,
isParentDirection ? "родителей" : "детей"); isParentDirection ? "родителей" : "детей");
// Применяем логику к каждому юниту // Считаем, сколько фильтров прошёл каждый TargetId
foreach (var context in resultContexts.Values) foreach (var targetId in matchingTargetIds)
{ {
var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List<Guid>()); if (!targetFilterPassCount.ContainsKey(targetId))
bool hasTargets = unitTargets.Any(); targetFilterPassCount[targetId] = 0;
targetFilterPassCount[targetId]++;
bool passesFilter = EvaluateFilter(
unitTargets,
matchingTargetIds,
relFilter.IsFullMatch,
relFilter.IsInverse,
hasTargets
);
if (passesFilter)
{
var validTargets = relFilter.IsInverse
? unitTargets.Except(matchingTargetIds)
: unitTargets.Intersect(matchingTargetIds);
if (isParentDirection)
context.ValidParentIds.UnionWith(validTargets);
else
context.ValidChildIds.UnionWith(validTargets);
}
else
{
// Юнит не прошёл фильтр — очищаем его связи
if (isParentDirection)
context.ValidParentIds.Clear();
else
context.ValidChildIds.Clear();
}
} }
} }
var totalAdded = resultContexts.Sum(c => // ✅ Оставляем только те связи, которые прошли ВСЕ фильтры
isParentDirection ? c.Value.ValidParentIds.Count : c.Value.ValidChildIds.Count); int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim()));
var validTargetIds = targetFilterPassCount
.Where(kvp => kvp.Value == requiredFiltersCount)
.Select(kvp => kvp.Key)
.ToList();
logger.LogDebug(" Найдено {ValidCount} {TargetType}, прошедших ВСЕ {RequiredCount} фильтров",
validTargetIds.Count,
isParentDirection ? "родителей" : "детей",
requiredFiltersCount);
// Список юнитов, которые не прошли фильтры (для удаления)
var unitsToRemove = new HashSet<Guid>();
// Применяем к каждому юниту
foreach (var context in resultContexts.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 passesFilter = EvaluateFilter(
validTargets, // Только связи, прошедшие ВСЕ фильтры
validTargets, // matchingRelations = validTargets (все валидные)
relFilters.All(rf => rf.IsFullMatch), // IsFullMatch для всех фильтров
relFilters.Any(rf => rf.IsInverse), // IsInverse для всех фильтров
hasTargets
);
if (passesFilter && hasValidTargets)
{
// Сохраняем только валидные связи
if (isParentDirection)
context.ValidParentIds.UnionWith(validTargets);
else
context.ValidChildIds.UnionWith(validTargets);
}
else
{
// Юнит не прошёл фильтр — помечаем на удаление
unitsToRemove.Add(context.UnitId);
}
}
// Удаляем юниты, которые не прошли фильтры
foreach (var unitId in unitsToRemove)
{
resultContexts.Remove(unitId);
}
logger.LogDebug(" Удалено {RemovedCount} юнитов, не прошедших фильтры", unitsToRemove.Count);
var totalAdded = resultContexts.Values.Sum(c =>
isParentDirection ? c.ValidParentIds.Count : c.ValidChildIds.Count);
logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов", logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов",
totalAdded, totalAdded,