fix(dal): UnitFilter восставновлена логика по фильтрации свзяанных ЭК, в части работы параметра IsFullMatch

This commit is contained in:
Mikhail Kuznetsov
2026-03-05 16:46:13 +10:00
parent a0369b9ebb
commit b001c45c40

View File

@@ -340,8 +340,7 @@ internal class UnitFilterService : IUnitFilterService
private async Task<List<FilteredUnitContext>> ApplyRelationshipFiltersOnDbAsync(
List<Guid> unitIds,
IEnumerable<JobRelationshipFilter> relationshipFilters,
CancellationToken cancellationToken = default
)
CancellationToken cancellationToken = default)
{
if (!relationshipFilters.Any() || !unitIds.Any())
{
@@ -353,178 +352,87 @@ internal class UnitFilterService : IUnitFilterService
logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}",
unitIds.Count, relationshipFilters.Count());
#if DEBUG
if (unitIds.Contains(targetUnitId))
{
logger.LogDebug("DEBUG: Юнит {TargetUnitId} присутствует во входных данных ApplyRelationshipFiltersOnDbAsync", targetUnitId);
}
#endif
var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id });
var parentRelFilters = relationshipFilters.Where(rf => rf.IsParent).ToList();
var childRelFilters = relationshipFilters.Where(rf => !rf.IsParent).ToList();
var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id });
// Обработка родительских фильтров
if (parentRelFilters.Any())
{
logger.LogDebug(" Родительские фильтры ({Count}):", parentRelFilters.Count);
var allParentLinks = await unitInUnitService.Get()
.AsNoTracking()
.Where(link => unitIds.Contains(link.ChildUnitId))
.Select(link => new { ChildId = link.ChildUnitId, ParentId = link.ParentUnitId })
.ToListAsync(cancellationToken);
var allParentIds = allParentLinks.Select(l => l.ParentId).Distinct().ToList();
logger.LogDebug(" Найдено {ParentCount} уникальных родителей для {LinkCount} связей",
allParentIds.Count, allParentLinks.Count);
var validParentIds = new HashSet<Guid>(allParentIds);
logger.LogDebug(" Начальное количество родителей: {Count}", validParentIds.Count);
foreach (var relFilter in parentRelFilters)
{
var valueMask = relFilter.ValueMask?.Trim();
if (string.IsNullOrWhiteSpace(valueMask))
continue;
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
string dbValueMask;
if (isStartsWith && isEndsWith)
dbValueMask = $"%{valueMask.Trim('%')}%";
else if (isStartsWith)
dbValueMask = $"{valueMask.TrimEnd('%')}%";
else if (isEndsWith)
dbValueMask = $"%{valueMask.TrimStart('%')}";
else
dbValueMask = valueMask;
var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}";
logger.LogDebug(" RelationshipFilter (Parent): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}",
fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);
var matchingParents = await unitService.Get()
.AsNoTracking()
.Where(u => allParentIds.Contains(u.Id))
.Where(u => u.UnitValues.Any(v =>
v.FieldId == relFilter.FieldId &&
EF.Functions.Like(v.Value.Value, dbValueMask)))
.Select(u => u.Id)
.ToListAsync(cancellationToken);
logger.LogDebug(" Найдено {MatchCount} родителей по маске", matchingParents.Count);
if (relFilter.IsInverse)
{
validParentIds.ExceptWith(matchingParents);
logger.LogDebug(" IsInverse=true → исключены {ExcludedCount} родителей, осталось {RemainingCount}",
matchingParents.Count, validParentIds.Count);
}
else
{
validParentIds.IntersectWith(matchingParents);
logger.LogDebug(" IsInverse=false → оставлены {KeptCount} родителей",
validParentIds.Count);
}
}
// Сопоставляем подходящих родителей с их детьми
foreach (var link in allParentLinks)
{
if (validParentIds.Contains(link.ParentId))
{
resultContexts[link.ChildId].ValidParentIds.Add(link.ParentId);
}
}
var totalParentsAdded = resultContexts.Values.Sum(c => c.ValidParentIds.Count);
logger.LogDebug(" Добавлено {TotalParents} родительских связей для {UnitCount} юнитов",
totalParentsAdded, resultContexts.Count);
await ApplyRelationshipFiltersAsync(
unitIds,
resultContexts, // ← Передаём словарь, а не список
parentRelFilters,
isParentDirection: true,
cancellationToken);
}
// Обработка дочерних фильтров
if (childRelFilters.Any())
{
logger.LogDebug(" Дочерние фильтры ({Count}):", childRelFilters.Count);
var allChildLinks = await unitInUnitService.Get()
.AsNoTracking()
.Where(link => unitIds.Contains(link.ParentUnitId))
.Select(link => new { ParentId = link.ParentUnitId, ChildId = link.ChildUnitId })
.ToListAsync(cancellationToken);
var allChildIds = allChildLinks.Select(l => l.ChildId).Distinct().ToList();
logger.LogDebug(" Найдено {ChildCount} уникальных детей для {LinkCount} связей",
allChildIds.Count, allChildLinks.Count);
var validChildIds = new HashSet<Guid>(allChildIds);
logger.LogDebug(" Начальное количество детей: {Count}", validChildIds.Count);
foreach (var relFilter in childRelFilters)
{
var valueMask = relFilter.ValueMask?.Trim();
if (string.IsNullOrWhiteSpace(valueMask))
continue;
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
string dbValueMask;
if (isStartsWith && isEndsWith)
dbValueMask = $"%{valueMask.Trim('%')}%";
else if (isStartsWith)
dbValueMask = $"{valueMask.TrimEnd('%')}%";
else if (isEndsWith)
dbValueMask = $"%{valueMask.TrimStart('%')}";
else
dbValueMask = valueMask;
var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}";
logger.LogDebug(" RelationshipFilter (Child): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}",
fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);
var matchingChildren = await unitService.Get()
.AsNoTracking()
.Where(u => allChildIds.Contains(u.Id))
.Where(u => u.UnitValues.Any(v =>
v.FieldId == relFilter.FieldId &&
EF.Functions.Like(v.Value.Value, dbValueMask)))
.Select(u => u.Id)
.ToListAsync(cancellationToken);
logger.LogDebug(" Найдено {MatchCount} детей по маске", matchingChildren.Count);
if (relFilter.IsInverse)
{
validChildIds.ExceptWith(matchingChildren);
logger.LogDebug(" IsInverse=true → исключены {ExcludedCount} детей, осталось {RemainingCount}",
matchingChildren.Count, validChildIds.Count);
}
else
{
validChildIds.IntersectWith(matchingChildren);
logger.LogDebug(" IsInverse=false → оставлены {KeptCount} детей",
validChildIds.Count);
}
}
// Сопоставляем подходящих детей с их родителями
foreach (var link in allChildLinks)
{
if (validChildIds.Contains(link.ChildId))
{
resultContexts[link.ParentId].ValidChildIds.Add(link.ChildId);
}
}
var totalChildrenAdded = resultContexts.Values.Sum(c => c.ValidChildIds.Count);
logger.LogDebug(" Добавлено {TotalChildren} дочерних связей для {UnitCount} юнитов",
totalChildrenAdded, resultContexts.Count);
await ApplyRelationshipFiltersAsync(
unitIds,
resultContexts, // ← Передаём словарь, а не список
childRelFilters,
isParentDirection: false,
cancellationToken);
}
var result = resultContexts.Values.ToList();
logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: выход {ContextCount} контекстов", result.Count);
// Если фильтры были — оставляем только юниты со связями
// Если фильтров не было — оставляем все юниты
bool hasRelationshipFilters = parentRelFilters.Any() || childRelFilters.Any();
var unitsWithRelationships = hasRelationshipFilters
? resultContexts.Values.Where(c => c.ValidParentIds.Any() || c.ValidChildIds.Any()).ToList()
: resultContexts.Values.ToList();
var result = unitsWithRelationships;
logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: выход {ContextCount} контекстов (из {InitialCount})",
result.Count, resultContexts.Count);
#if DEBUG
foreach (var context in result)
{
var u = await unitService.GetAsync(context.UnitId);
if (u == null)
continue;
logger.LogDebug("Найден ЭК {UnitName}", u.Name);
if (context.ValidParentIds.Any())
{
var parents = context.ValidParentIds;
logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", parents.Count);
foreach (var parent in context.ValidParentIds)
{
var p = await unitService.GetAsync(parent);
logger.LogDebug("\t\t {ParentName};", p?.Name);
}
}
if (context.ValidChildIds.Any())
{
var children = context.ValidChildIds;
logger.LogDebug("\tФильтрам соответствуют {ParentsCount} детей:", children.Count);
foreach (var child in children)
{
var c = await unitService.GetAsync(child);
logger.LogDebug("\t\t {ChildName};", c?.Name);
}
}
}
#endif
return result;
}
@@ -694,7 +602,6 @@ internal class UnitFilterService : IUnitFilterService
}
public async Task<List<string>> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId, CancellationToken cancellationToken = default)
{
logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId);
@@ -785,6 +692,172 @@ internal class UnitFilterService : IUnitFilterService
}
#region вспомогательные методы
/// <summary>
/// Оценивает, проходит ли юнит фильтр связей
/// </summary>
private bool EvaluateFilter(
List<Guid> unitRelations, // Все связи юнита (родители или дети)
List<Guid> matchingRelations, // Связи, которые соответствуют маске
bool isFullMatch, // Полное совпадение (All) или хотя бы одно (Any)
bool isInverse, // Обратный фильтр
bool hasRelations // Есть ли связи у юнита
)
{
int matchingCount = unitRelations.Count(r => matchingRelations.Contains(r));
int totalCount = unitRelations.Count;
// Случай: нет связей в направлении
if (!hasRelations)
{
// Если IsInverse = true → юнит проходит (нет связей, которые нарушают)
// Если IsInverse = false → юнит не проходит (нет связей, которые соответствуют)
return isInverse;
}
// Основная логика
if (isFullMatch)
{
// Все связи должны соответствовать
bool allMatch = matchingCount == totalCount;
if (isInverse)
//Все связи не соответствуют фильтру
return matchingCount == 0;
else
//Все связи соответствуют фильтру
return allMatch;
}
else
{
// Хотя бы одна связь должна соответствовать
return matchingCount > 0;
}
}
/// <summary>
/// Применяет фильтры к связям (родителям или детям)
/// </summary>
private async Task ApplyRelationshipFiltersAsync(
List<Guid> unitIds,
Dictionary<Guid, FilteredUnitContext> resultContexts,
List<JobRelationshipFilter> relFilters,
bool isParentDirection,
CancellationToken cancellationToken)
{
if (!relFilters.Any())
return;
var directionName = isParentDirection ? "Родительские" : "Дочерние";
logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count);
// Получаем все связи в нужном направлении
var allLinks = await unitInUnitService.Get()
.AsNoTracking()
.Where(link => isParentDirection
? unitIds.Contains(link.ChildUnitId)
: unitIds.Contains(link.ParentUnitId))
.Select(link => new
{
SourceId = isParentDirection ? link.ChildUnitId : link.ParentUnitId,
TargetId = isParentDirection ? link.ParentUnitId : link.ChildUnitId
})
.ToListAsync(cancellationToken);
var allTargetIds = allLinks.Select(l => l.TargetId).Distinct().ToList();
logger.LogDebug(" Найдено {TargetCount} уникальных {TargetType} для {LinkCount} связей",
allTargetIds.Count,
isParentDirection ? "родителей" : "детей",
allLinks.Count);
// Группируем связи по исходному юниту для быстрого доступа
var linksBySource = allLinks.GroupBy(l => l.SourceId)
.ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList());
// Обрабатываем каждый фильтр
foreach (var relFilter in relFilters)
{
var valueMask = relFilter.ValueMask?.Trim();
if (string.IsNullOrWhiteSpace(valueMask))
continue;
// Обработка маски LIKE
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
string dbValueMask;
if (isStartsWith && isEndsWith)
dbValueMask = $"%{valueMask.Trim('%')}%";
else if (isStartsWith)
dbValueMask = $"{valueMask.TrimEnd('%')}%";
else if (isEndsWith)
dbValueMask = $"%{valueMask.TrimStart('%')}";
else
dbValueMask = 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 unitService.Get()
.AsNoTracking()
.Where(u => allTargetIds.Contains(u.Id))
.Where(u => u.UnitValues.Any(v =>
v.FieldId == relFilter.FieldId &&
EF.Functions.Like(v.Value.Value, dbValueMask)))
.Select(u => u.Id)
.ToListAsync(cancellationToken);
logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске",
matchingTargetIds.Count,
isParentDirection ? "родителей" : "детей");
// Применяем логику к каждому юниту
foreach (var context in resultContexts.Values)
{
var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List<Guid>());
bool hasTargets = unitTargets.Any();
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);
logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов",
totalAdded,
isParentDirection ? "родительских" : "дочерних",
resultContexts.Count);
}
private async Task<List<Guid>> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter, CancellationToken cancellationToken = default)
{