feat(dal,templateMatcher): уточнена логика UnitFilterService, в TemplateMatcher добавлены методы синхронизации и обновлению существующих шаблонов по группе

This commit is contained in:
Mikhail Kuznetsov
2025-12-30 14:27:59 +10:00
parent 0cb8387349
commit ec53638c58
8 changed files with 427 additions and 148 deletions

View File

@@ -13,7 +13,7 @@ namespace PARR.DAL.DomainServices.Implementations
internal class UnitFilterService : IUnitFilterService
{
#if DEBUG
private readonly Guid targetUnitId = Guid.Parse("d4322a08-246b-4380-8953-8ce4a8446235");
private readonly Guid targetUnitId = Guid.Parse("6c019569-ecba-4d9a-9cdf-cdc1dea93925");
#endif
private readonly ILogger<UnitFilterService> logger;
@@ -123,7 +123,7 @@ namespace PARR.DAL.DomainServices.Implementations
logger.LogDebug("Базовый фильтр по Name '{NameFilter}' дал {Count} юнитов", filter.UnitFilter, initialUnitIds.Count);
#if DEBUG
// Отладка: проверить, есть ли юнит в initialUnitIds
// Отладка: проверить, есть ли юнит в initialUnitIds
if (initialUnitIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в initialUnitIds.", targetUnitId);
@@ -267,7 +267,7 @@ namespace PARR.DAL.DomainServices.Implementations
}
#if DEBUG
// Отладка: проверить, есть ли юнит в candidateUnits после FieldFilter
// Отладка: проверить, есть ли юнит в candidateUnits после FieldFilter
var candidateListAfterFieldFilter = candidateUnits.ToList();
if (candidateListAfterFieldFilter.Any(u => u.Id == targetUnitId))
{
@@ -276,7 +276,6 @@ namespace PARR.DAL.DomainServices.Implementations
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после FieldFilter.", targetUnitId);
//continue; // ❌ юнит отсеялся на этом этапе
}
logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateListAfterFieldFilter.Count);
@@ -288,7 +287,7 @@ namespace PARR.DAL.DomainServices.Implementations
}
#if DEBUG
// Отладка: проверить, есть ли юнит в candidateUnits после RelationshipFilter
// Отладка: проверить, есть ли юнит в candidateUnits после RelationshipFilter
var candidateListAfterRelFilter = candidateUnits.ToList();
if (candidateListAfterRelFilter.Any(u => u.Id == targetUnitId))
{
@@ -297,7 +296,6 @@ namespace PARR.DAL.DomainServices.Implementations
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после RelationshipFilter.", targetUnitId);
//continue; // ❌ юнит отсеялся на этом этапе
}
logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateListAfterRelFilter.Count);
@@ -311,7 +309,7 @@ namespace PARR.DAL.DomainServices.Implementations
logger.LogDebug("После Umbrella-фильтра осталось {Count} юнитов", finalUnits.Count());
#if DEBUG
// Отладка: проверить, есть ли юнит в finalUnits после Umbrella
// Отладка: проверить, есть ли юнит в finalUnits после Umbrella
if (finalUnits.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в finalUnits после Umbrella-фильтра.", targetUnitId);
@@ -331,7 +329,7 @@ namespace PARR.DAL.DomainServices.Implementations
.ToList();
#if DEBUG
// Отладка: проверить, есть ли юнит в newIds
// Отладка: проверить, есть ли юнит в newIds
if (newIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в newIds.", targetUnitId);
@@ -359,7 +357,7 @@ namespace PARR.DAL.DomainServices.Implementations
job.Id, job.UnitFilters.Count, result.Count);
#if DEBUG
// Отладка: проверить, есть ли юнит в result
// Отладка: проверить, есть ли юнит в result
if (result.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в финальном результате.", targetUnitId);
@@ -398,11 +396,11 @@ namespace PARR.DAL.DomainServices.Implementations
logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count());
// Получить все связи для юнита
// Получить все связи для юнита
var parentLinks = await unitInUnitService.GetByChildIdAsync(unitId);
var childLinks = await unitInUnitService.GetByParentIdAsync(unitId);
// Собрать все UnitId, участвующие в связях
// Собрать все UnitId, участвующие в связях
var allRelatedUnitIds = parentLinks
.Select(l => l.ParentUnitId)
.Concat(childLinks.Select(l => l.ChildUnitId))
@@ -411,20 +409,20 @@ namespace PARR.DAL.DomainServices.Implementations
if (!allRelatedUnitIds.Any()) continue;
// Получить значения для всех связанных юнитов
// Получить значения для всех связанных юнитов
var allUnitValues = await unitInValueService.GetByUnitIdsAsync(allRelatedUnitIds);
// Сгруппировать значения по UnitId
// Сгруппировать значения по UnitId
var valuesByUnit = allUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
// Найти UnitId, которые проходят все RelationshipFilters
// Найти UnitId, которые проходят все RelationshipFilters
var matchingUnitIds = new HashSet<Guid>();
foreach (var relatedUnitId in allRelatedUnitIds)
{
// Изменяем на `All` — все фильтры должны подходить
// Изменяем на `All` — все фильтры должны подходить
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
{
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
@@ -435,14 +433,14 @@ namespace PARR.DAL.DomainServices.Implementations
if (!matchingValues.Any()) // Нет значений по полю
{
// Если нет значений, и IsInverse = false → не подходит
// Если нет значений, и IsInverse = true → подходит
// Если нет значений, и IsInverse = false → не подходит
// Если нет значений, и IsInverse = true → подходит
return rf.IsInverse;
}
var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase));
// Учитываем IsInverse
// Учитываем IsInverse
if (rf.IsInverse)
hasMatch = !hasMatch;
@@ -490,7 +488,7 @@ namespace PARR.DAL.DomainServices.Implementations
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
);
// Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр
// Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр
return fieldFilter.IsInverse ? !hasMatch : hasMatch;
}).AsQueryable();
}
@@ -509,13 +507,13 @@ namespace PARR.DAL.DomainServices.Implementations
var isFullMatch = relFilter.IsFullMatch;
var fieldId = relFilter.FieldId;
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
// Используем Trim('%') для всех случаев
var trimmedMask = valueMask.Trim('%');
return query.AsEnumerable().Where(dto =>
{
#if DEBUG
// Отладка: проверить, это нужный юнит
// Отладка: проверить, это нужный юнит
if (dto.Id == targetUnitId)
{
logger.LogDebug("Проверяем юнит {TargetUnitId} с {ParentCount} родителями и {ChildCount} детьми.",
@@ -528,7 +526,7 @@ namespace PARR.DAL.DomainServices.Implementations
if (links == null || !links.Any())
{
#if DEBUG
// Отладка: юнит не имеет связей
// Отладка: юнит не имеет связей
if (dto.Id == targetUnitId)
{
logger.LogDebug("Юнит {TargetUnitId}: нет связей ({Direction}), результат фильтра: {Result}",
@@ -542,7 +540,7 @@ namespace PARR.DAL.DomainServices.Implementations
}
#if DEBUG
// Отладка: проверить, какие связи у юнита
// Отладка: проверить, какие связи у юнита
if (dto.Id == targetUnitId)
{
logger.LogDebug("Юнит {TargetUnitId}: {Count} связей ({Direction}).", targetUnitId, links.Count, isParent ? "Parent" : "Child");
@@ -553,62 +551,72 @@ namespace PARR.DAL.DomainServices.Implementations
}
#endif
var hasMatchingLinks = links.Any(link =>
if (!isFullMatch)
{
var hasMatch = link.Values.Any(v =>
v.FieldId == fieldId &&
v.Value != null &&
(isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) :
isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) :
isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) :
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
);
#if DEBUG
// ✅ Отладка: проверить, какая связь подходит
if (dto.Id == targetUnitId)
// Если IsFullMatch = false — проверяем, есть ли хотя бы одна подходящая связь
var hasMatchingLinks = links.Any(link =>
{
logger.LogDebug(" Проверка связи {LinkUnitId}: hasMatch={HasMatch} (ValueMask={ValueMask}, FieldId={FieldId})", link.UnitId, hasMatch, valueMask, fieldId);
}
#endif
var values = link.Values.Where(v => v.FieldId == fieldId).ToList();
return isInverse ? !hasMatch : hasMatch;
});
if (!values.Any())
{
// Если нет значений по полю — связь не подходит, если IsInverse = false
// Если нет значений по полю — связь подходит, если IsInverse = true
return isInverse;
}
#if DEBUG
// ✅ Отладка: результат hasMatchingLinks
if (dto.Id == targetUnitId)
{
logger.LogDebug(" hasMatchingLinks = {HasMatchingLinks}, isInverse = {IsInverse}", hasMatchingLinks, isInverse);
}
#endif
if (isFullMatch)
{
var allMatch = links.All(link =>
{
var hasMatch = link.Values.Any(v =>
v.FieldId == fieldId &&
var hasMatch = values.Any(v =>
v.Value != null &&
(isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) :
isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) :
isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) :
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
// Проверка с Trim('%')
(valueMask.StartsWith("%") && valueMask.EndsWith("%") ? v.Value.Contains(trimmedMask, StringComparison.OrdinalIgnoreCase) :
valueMask.StartsWith("%") ? v.Value.EndsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) :
valueMask.EndsWith("%") ? v.Value.StartsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) :
string.Equals(v.Value, valueMask, StringComparison.OrdinalIgnoreCase))
);
#if DEBUG
// ✅ Отладка: проверить, все ли связи подходят
if (dto.Id == targetUnitId)
{
logger.LogDebug(" Проверка связи {LinkUnitId} для FullMatch: hasMatch={HasMatch}", link.UnitId, hasMatch);
}
#endif
// Учитываем IsInverse
return isInverse ? !hasMatch : hasMatch;
});
#if DEBUG
// Отладка: результат allMatch
// Отладка: результат hasMatchingLinks
if (dto.Id == targetUnitId)
{
logger.LogDebug(" hasMatchingLinks = {HasMatchingLinks}, isInverse = {IsInverse}", hasMatchingLinks, isInverse);
}
#endif
return hasMatchingLinks;
}
else
{
// Если IsFullMatch = true — все связи должны подходить
var allMatch = links.All(link =>
{
var values = link.Values.Where(v => v.FieldId == fieldId).ToList();
if (!values.Any())
{
// Если нет значений по полю — связь не подходит, если IsInverse = false
// Если нет значений по полю — связь подходит, если IsInverse = true
return isInverse;
}
var hasMatch = values.Any(v =>
v.Value != null &&
// Проверка с Trim('%')
(valueMask.StartsWith("%") && valueMask.EndsWith("%") ? v.Value.Contains(trimmedMask, StringComparison.OrdinalIgnoreCase) :
valueMask.StartsWith("%") ? v.Value.EndsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) :
valueMask.EndsWith("%") ? v.Value.StartsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) :
string.Equals(v.Value, valueMask, StringComparison.OrdinalIgnoreCase))
);
// Учитываем IsInverse
return isInverse ? !hasMatch : hasMatch;
});
#if DEBUG
// Отладка: результат allMatch
if (dto.Id == targetUnitId)
{
logger.LogDebug(" allMatch = {AllMatch}, isFullMatch = {IsFullMatch}", allMatch, isFullMatch);
@@ -617,8 +625,6 @@ namespace PARR.DAL.DomainServices.Implementations
return allMatch;
}
return hasMatchingLinks;
}).AsQueryable();
}
@@ -639,57 +645,171 @@ namespace PARR.DAL.DomainServices.Implementations
logger.LogDebug("RelationshipCountFilter: Min={Min}, Max={Max}, IsParent={IsParent}", min, max, isParentDirection);
// Определяем, есть ли фильтры по полям
var activeFilters = relationshipFilters
.Where(rf => rf.IsParent == isParentDirection && !string.IsNullOrWhiteSpace(rf.ValueMask))
// Разделяем фильтры по направлениям
var parentFilters = relationshipFilters
.Where(rf => rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask))
.ToList();
bool hasFieldFilters = activeFilters.Count > 0;
var childFilters = relationshipFilters
.Where(rf => !rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask))
.ToList();
logger.LogDebug("RelationshipCountFilter: Найдено {Count} фильтров по полям для направления {Direction}", activeFilters.Count, isParentDirection ? "Parent" : "Child");
bool hasParentFieldFilters = parentFilters.Count > 0;
bool hasChildFieldFilters = childFilters.Count > 0;
logger.LogDebug("RelationshipCountFilter: Найдено {ParentCount} фильтров по родителям и {ChildCount} фильтров по детям", parentFilters.Count, childFilters.Count);
return units.Where(dto =>
{
var links = isParentDirection ? dto.Parents : dto.Children;
if (links == null || !links.Any())
{
var result = min == 0;
logger.LogDebug("UnitId {UnitId}: связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result);
return result;
}
logger.LogDebug("UnitId {UnitId}: {Count} связей до фильтрации", dto.Id, links.Count);
int matchingCount = 0;
if (hasFieldFilters)
if (isParentDirection) // Считаем родительские связи
{
// Есть фильтры по полям → считаем только связанные юниты, подходящие под фильтр
foreach (var link in links)
var links = dto.Parents;
if (links == null || !links.Any())
{
// ✅ Изменяем на `All` — все фильтры должны подходить
bool hasMatch = activeFilters.All(f =>
{
var value = link.Values.FirstOrDefault(v => v.FieldId == f.FieldId);
return value != null && value.Value != null && value.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase);
});
var result = min == 0;
logger.LogDebug("UnitId {UnitId}: родительских связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result);
return result;
}
if (hasMatch)
matchingCount++;
logger.LogDebug("UnitId {UnitId}: {Count} родительских связей до фильтрации", dto.Id, links.Count);
if (matchingCount > max)
if (hasParentFieldFilters)
{
// Есть фильтры по родителям → считаем только подходящие
foreach (var link in links)
{
logger.LogDebug("UnitId {UnitId}: matchingCount ({Count}) > max ({Max}) — прерываем подсчёт", dto.Id, matchingCount, max);
break;
// Проверяем, проходит ли связь все фильтры (логика как в ApplyRelationshipFilterToQuery)
bool passesAllFilters = true;
foreach (var f in parentFilters)
{
var values = link.Values.Where(v => v.FieldId == f.FieldId).ToList();
if (!values.Any())
{
// Если нет значений по полю — связь не подходит, если IsInverse = false
// Если нет значений по полю — связь подходит, если IsInverse = true
if (!f.IsInverse)
{
passesAllFilters = false;
break;
}
continue;
}
var hasMatch = values.Any(v =>
v.Value != null &&
// Проверка в зависимости от маски
(f.ValueMask.StartsWith("%") && f.ValueMask.EndsWith("%") ? v.Value.Contains(f.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) :
f.ValueMask.StartsWith("%") ? v.Value.EndsWith(f.ValueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) :
f.ValueMask.EndsWith("%") ? v.Value.StartsWith(f.ValueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) :
string.Equals(v.Value, f.ValueMask, StringComparison.OrdinalIgnoreCase))
);
// Учитываем IsInverse
if (f.IsInverse)
hasMatch = !hasMatch;
if (!hasMatch)
{
passesAllFilters = false;
break;
}
}
if (passesAllFilters)
matchingCount++;
if (matchingCount > max)
{
logger.LogDebug("UnitId {UnitId}: matchingCount ({Count}) > max ({Max}) — прерываем подсчёт", dto.Id, matchingCount, max);
break;
}
}
}
else
{
// Нет фильтров по родителям → считаем общее количество связей (без учёта значений)
matchingCount = links.Count;
logger.LogDebug("UnitId {UnitId}: нет фильтров по родителям — matchingCount = links.Count = {Count}", dto.Id, matchingCount);
}
}
else
else // Считаем дочерние связи
{
// Нет фильтров по полям → считаем общее количество связей (без учёта значений)
matchingCount = links.Count;
logger.LogDebug("UnitId {UnitId}: нет фильтров по полям — matchingCount = links.Count = {Count}", dto.Id, matchingCount);
var links = dto.Children;
if (links == null || !links.Any())
{
var result = min == 0;
logger.LogDebug("UnitId {UnitId}: дочерних связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result);
return result;
}
logger.LogDebug("UnitId {UnitId}: {Count} дочерних связей до фильтрации", dto.Id, links.Count);
if (hasChildFieldFilters)
{
// Есть фильтры по детям → считаем только подходящие
foreach (var link in links)
{
// Проверяем, проходит ли связь все фильтры (логика как в ApplyRelationshipFilterToQuery)
bool passesAllFilters = true;
foreach (var f in childFilters)
{
var values = link.Values.Where(v => v.FieldId == f.FieldId).ToList();
if (!values.Any())
{
// Если нет значений по полю — связь не подходит, если IsInverse = false
// Если нет значений по полю — связь подходит, если IsInverse = true
if (!f.IsInverse)
{
passesAllFilters = false;
break;
}
continue;
}
var hasMatch = values.Any(v =>
v.Value != null &&
// Проверка в зависимости от маски
(f.ValueMask.StartsWith("%") && f.ValueMask.EndsWith("%") ? v.Value.Contains(f.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) :
f.ValueMask.StartsWith("%") ? v.Value.EndsWith(f.ValueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) :
f.ValueMask.EndsWith("%") ? v.Value.StartsWith(f.ValueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) :
string.Equals(v.Value, f.ValueMask, StringComparison.OrdinalIgnoreCase))
);
// Учитываем IsInverse
if (f.IsInverse)
hasMatch = !hasMatch;
if (!hasMatch)
{
passesAllFilters = false;
break;
}
}
if (passesAllFilters)
matchingCount++;
if (matchingCount > max)
{
logger.LogDebug("UnitId {UnitId}: matchingCount ({Count}) > max ({Max}) — прерываем подсчёт", dto.Id, matchingCount, max);
break;
}
}
}
else
{
// Нет фильтров по детям → считаем общее количество связей (без учёта значений)
matchingCount = links.Count;
logger.LogDebug("UnitId {UnitId}: нет фильтров по детям — matchingCount = links.Count = {Count}", dto.Id, matchingCount);
}
}
var finalResult = matchingCount >= min && matchingCount <= max;