feat(dal,templateMatcher): уточнена логика UnitFilterService, в TemplateMatcher добавлены методы синхронизации и обновлению существующих шаблонов по группе
This commit is contained in:
@@ -13,7 +13,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
internal class UnitFilterService : IUnitFilterService
|
internal class UnitFilterService : IUnitFilterService
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#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
|
#endif
|
||||||
|
|
||||||
private readonly ILogger<UnitFilterService> logger;
|
private readonly ILogger<UnitFilterService> logger;
|
||||||
@@ -123,7 +123,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
logger.LogDebug("Базовый фильтр по Name '{NameFilter}' дал {Count} юнитов", filter.UnitFilter, initialUnitIds.Count);
|
logger.LogDebug("Базовый фильтр по Name '{NameFilter}' дал {Count} юнитов", filter.UnitFilter, initialUnitIds.Count);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в initialUnitIds
|
// Отладка: проверить, есть ли юнит в initialUnitIds
|
||||||
if (initialUnitIds.Contains(targetUnitId))
|
if (initialUnitIds.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в initialUnitIds.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} найден в initialUnitIds.", targetUnitId);
|
||||||
@@ -267,7 +267,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в candidateUnits после FieldFilter
|
// Отладка: проверить, есть ли юнит в candidateUnits после FieldFilter
|
||||||
var candidateListAfterFieldFilter = candidateUnits.ToList();
|
var candidateListAfterFieldFilter = candidateUnits.ToList();
|
||||||
if (candidateListAfterFieldFilter.Any(u => u.Id == targetUnitId))
|
if (candidateListAfterFieldFilter.Any(u => u.Id == targetUnitId))
|
||||||
{
|
{
|
||||||
@@ -276,7 +276,6 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после FieldFilter.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после FieldFilter.", targetUnitId);
|
||||||
//continue; // ❌ юнит отсеялся на этом этапе
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateListAfterFieldFilter.Count);
|
logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateListAfterFieldFilter.Count);
|
||||||
@@ -288,7 +287,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в candidateUnits после RelationshipFilter
|
// Отладка: проверить, есть ли юнит в candidateUnits после RelationshipFilter
|
||||||
var candidateListAfterRelFilter = candidateUnits.ToList();
|
var candidateListAfterRelFilter = candidateUnits.ToList();
|
||||||
if (candidateListAfterRelFilter.Any(u => u.Id == targetUnitId))
|
if (candidateListAfterRelFilter.Any(u => u.Id == targetUnitId))
|
||||||
{
|
{
|
||||||
@@ -297,7 +296,6 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после RelationshipFilter.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после RelationshipFilter.", targetUnitId);
|
||||||
//continue; // ❌ юнит отсеялся на этом этапе
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateListAfterRelFilter.Count);
|
logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateListAfterRelFilter.Count);
|
||||||
@@ -311,7 +309,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
logger.LogDebug("После Umbrella-фильтра осталось {Count} юнитов", finalUnits.Count());
|
logger.LogDebug("После Umbrella-фильтра осталось {Count} юнитов", finalUnits.Count());
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в finalUnits после Umbrella
|
// Отладка: проверить, есть ли юнит в finalUnits после Umbrella
|
||||||
if (finalUnits.Any(u => u.Id == targetUnitId))
|
if (finalUnits.Any(u => u.Id == targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в finalUnits после Umbrella-фильтра.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} найден в finalUnits после Umbrella-фильтра.", targetUnitId);
|
||||||
@@ -331,7 +329,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в newIds
|
// Отладка: проверить, есть ли юнит в newIds
|
||||||
if (newIds.Contains(targetUnitId))
|
if (newIds.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в newIds.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} найден в newIds.", targetUnitId);
|
||||||
@@ -359,7 +357,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
job.Id, job.UnitFilters.Count, result.Count);
|
job.Id, job.UnitFilters.Count, result.Count);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в result
|
// Отладка: проверить, есть ли юнит в result
|
||||||
if (result.Contains(targetUnitId))
|
if (result.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в финальном результате.", 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());
|
logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count());
|
||||||
|
|
||||||
// ✅ Получить все связи для юнита
|
// Получить все связи для юнита
|
||||||
var parentLinks = await unitInUnitService.GetByChildIdAsync(unitId);
|
var parentLinks = await unitInUnitService.GetByChildIdAsync(unitId);
|
||||||
var childLinks = await unitInUnitService.GetByParentIdAsync(unitId);
|
var childLinks = await unitInUnitService.GetByParentIdAsync(unitId);
|
||||||
|
|
||||||
// ✅ Собрать все UnitId, участвующие в связях
|
// Собрать все UnitId, участвующие в связях
|
||||||
var allRelatedUnitIds = parentLinks
|
var allRelatedUnitIds = parentLinks
|
||||||
.Select(l => l.ParentUnitId)
|
.Select(l => l.ParentUnitId)
|
||||||
.Concat(childLinks.Select(l => l.ChildUnitId))
|
.Concat(childLinks.Select(l => l.ChildUnitId))
|
||||||
@@ -411,20 +409,20 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
|
|
||||||
if (!allRelatedUnitIds.Any()) continue;
|
if (!allRelatedUnitIds.Any()) continue;
|
||||||
|
|
||||||
// ✅ Получить значения для всех связанных юнитов
|
// Получить значения для всех связанных юнитов
|
||||||
var allUnitValues = await unitInValueService.GetByUnitIdsAsync(allRelatedUnitIds);
|
var allUnitValues = await unitInValueService.GetByUnitIdsAsync(allRelatedUnitIds);
|
||||||
|
|
||||||
// ✅ Сгруппировать значения по UnitId
|
// Сгруппировать значения по UnitId
|
||||||
var valuesByUnit = allUnitValues
|
var valuesByUnit = allUnitValues
|
||||||
.GroupBy(uv => uv.UnitId)
|
.GroupBy(uv => uv.UnitId)
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
// ✅ Найти UnitId, которые проходят все RelationshipFilters
|
// Найти UnitId, которые проходят все RelationshipFilters
|
||||||
var matchingUnitIds = new HashSet<Guid>();
|
var matchingUnitIds = new HashSet<Guid>();
|
||||||
|
|
||||||
foreach (var relatedUnitId in allRelatedUnitIds)
|
foreach (var relatedUnitId in allRelatedUnitIds)
|
||||||
{
|
{
|
||||||
// ✅ Изменяем на `All` — все фильтры должны подходить
|
// Изменяем на `All` — все фильтры должны подходить
|
||||||
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
|
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
|
||||||
{
|
{
|
||||||
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
|
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
|
||||||
@@ -435,14 +433,14 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
|
|
||||||
if (!matchingValues.Any()) // Нет значений по полю
|
if (!matchingValues.Any()) // Нет значений по полю
|
||||||
{
|
{
|
||||||
// ✅ Если нет значений, и IsInverse = false → не подходит
|
// Если нет значений, и IsInverse = false → не подходит
|
||||||
// ✅ Если нет значений, и IsInverse = true → подходит
|
// Если нет значений, и IsInverse = true → подходит
|
||||||
return rf.IsInverse;
|
return rf.IsInverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase));
|
var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
// ✅ Учитываем IsInverse
|
// Учитываем IsInverse
|
||||||
if (rf.IsInverse)
|
if (rf.IsInverse)
|
||||||
hasMatch = !hasMatch;
|
hasMatch = !hasMatch;
|
||||||
|
|
||||||
@@ -490,7 +488,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
|
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр
|
// Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр
|
||||||
return fieldFilter.IsInverse ? !hasMatch : hasMatch;
|
return fieldFilter.IsInverse ? !hasMatch : hasMatch;
|
||||||
}).AsQueryable();
|
}).AsQueryable();
|
||||||
}
|
}
|
||||||
@@ -509,13 +507,13 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
var isFullMatch = relFilter.IsFullMatch;
|
var isFullMatch = relFilter.IsFullMatch;
|
||||||
var fieldId = relFilter.FieldId;
|
var fieldId = relFilter.FieldId;
|
||||||
|
|
||||||
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
|
// Используем Trim('%') для всех случаев
|
||||||
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
|
var trimmedMask = valueMask.Trim('%');
|
||||||
|
|
||||||
return query.AsEnumerable().Where(dto =>
|
return query.AsEnumerable().Where(dto =>
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, это нужный юнит
|
// Отладка: проверить, это нужный юнит
|
||||||
if (dto.Id == targetUnitId)
|
if (dto.Id == targetUnitId)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Проверяем юнит {TargetUnitId} с {ParentCount} родителями и {ChildCount} детьми.",
|
logger.LogDebug("Проверяем юнит {TargetUnitId} с {ParentCount} родителями и {ChildCount} детьми.",
|
||||||
@@ -528,7 +526,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
if (links == null || !links.Any())
|
if (links == null || !links.Any())
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: юнит не имеет связей
|
// Отладка: юнит не имеет связей
|
||||||
if (dto.Id == targetUnitId)
|
if (dto.Id == targetUnitId)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId}: нет связей ({Direction}), результат фильтра: {Result}",
|
logger.LogDebug("Юнит {TargetUnitId}: нет связей ({Direction}), результат фильтра: {Result}",
|
||||||
@@ -542,7 +540,7 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, какие связи у юнита
|
// Отладка: проверить, какие связи у юнита
|
||||||
if (dto.Id == targetUnitId)
|
if (dto.Id == targetUnitId)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId}: {Count} связей ({Direction}).", targetUnitId, links.Count, isParent ? "Parent" : "Child");
|
logger.LogDebug("Юнит {TargetUnitId}: {Count} связей ({Direction}).", targetUnitId, links.Count, isParent ? "Parent" : "Child");
|
||||||
@@ -553,62 +551,72 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (!isFullMatch)
|
||||||
|
{
|
||||||
|
// Если IsFullMatch = false — проверяем, есть ли хотя бы одна подходящая связь
|
||||||
var hasMatchingLinks = links.Any(link =>
|
var hasMatchingLinks = links.Any(link =>
|
||||||
{
|
{
|
||||||
var hasMatch = link.Values.Any(v =>
|
var values = link.Values.Where(v => v.FieldId == fieldId).ToList();
|
||||||
v.FieldId == fieldId &&
|
|
||||||
|
if (!values.Any())
|
||||||
|
{
|
||||||
|
// Если нет значений по полю — связь не подходит, если IsInverse = false
|
||||||
|
// Если нет значений по полю — связь подходит, если IsInverse = true
|
||||||
|
return isInverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasMatch = values.Any(v =>
|
||||||
v.Value != null &&
|
v.Value != null &&
|
||||||
(isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) :
|
// Проверка с Trim('%')
|
||||||
isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) :
|
(valueMask.StartsWith("%") && valueMask.EndsWith("%") ? v.Value.Contains(trimmedMask, StringComparison.OrdinalIgnoreCase) :
|
||||||
isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) :
|
valueMask.StartsWith("%") ? v.Value.EndsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) :
|
||||||
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
|
valueMask.EndsWith("%") ? v.Value.StartsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) :
|
||||||
|
string.Equals(v.Value, valueMask, StringComparison.OrdinalIgnoreCase))
|
||||||
);
|
);
|
||||||
|
|
||||||
#if DEBUG
|
// Учитываем IsInverse
|
||||||
// ✅ Отладка: проверить, какая связь подходит
|
|
||||||
if (dto.Id == targetUnitId)
|
|
||||||
{
|
|
||||||
logger.LogDebug(" Проверка связи {LinkUnitId}: hasMatch={HasMatch} (ValueMask={ValueMask}, FieldId={FieldId})", link.UnitId, hasMatch, valueMask, fieldId);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return isInverse ? !hasMatch : hasMatch;
|
return isInverse ? !hasMatch : hasMatch;
|
||||||
});
|
});
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: результат hasMatchingLinks
|
// Отладка: результат hasMatchingLinks
|
||||||
if (dto.Id == targetUnitId)
|
if (dto.Id == targetUnitId)
|
||||||
{
|
{
|
||||||
logger.LogDebug(" hasMatchingLinks = {HasMatchingLinks}, isInverse = {IsInverse}", hasMatchingLinks, isInverse);
|
logger.LogDebug(" hasMatchingLinks = {HasMatchingLinks}, isInverse = {IsInverse}", hasMatchingLinks, isInverse);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (isFullMatch)
|
return hasMatchingLinks;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
|
// Если IsFullMatch = true — все связи должны подходить
|
||||||
var allMatch = links.All(link =>
|
var allMatch = links.All(link =>
|
||||||
{
|
{
|
||||||
var hasMatch = link.Values.Any(v =>
|
var values = link.Values.Where(v => v.FieldId == fieldId).ToList();
|
||||||
v.FieldId == fieldId &&
|
|
||||||
|
if (!values.Any())
|
||||||
|
{
|
||||||
|
// Если нет значений по полю — связь не подходит, если IsInverse = false
|
||||||
|
// Если нет значений по полю — связь подходит, если IsInverse = true
|
||||||
|
return isInverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasMatch = values.Any(v =>
|
||||||
v.Value != null &&
|
v.Value != null &&
|
||||||
(isStartsWith && isEndsWith ? v.Value.Contains(valueMask.Trim('%'), StringComparison.OrdinalIgnoreCase) :
|
// Проверка с Trim('%')
|
||||||
isStartsWith ? v.Value.StartsWith(valueMask.TrimEnd('%'), StringComparison.OrdinalIgnoreCase) :
|
(valueMask.StartsWith("%") && valueMask.EndsWith("%") ? v.Value.Contains(trimmedMask, StringComparison.OrdinalIgnoreCase) :
|
||||||
isEndsWith ? v.Value.EndsWith(valueMask.TrimStart('%'), StringComparison.OrdinalIgnoreCase) :
|
valueMask.StartsWith("%") ? v.Value.EndsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) :
|
||||||
v.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase))
|
valueMask.EndsWith("%") ? v.Value.StartsWith(trimmedMask, StringComparison.OrdinalIgnoreCase) :
|
||||||
|
string.Equals(v.Value, valueMask, StringComparison.OrdinalIgnoreCase))
|
||||||
);
|
);
|
||||||
|
|
||||||
#if DEBUG
|
// Учитываем IsInverse
|
||||||
// ✅ Отладка: проверить, все ли связи подходят
|
|
||||||
if (dto.Id == targetUnitId)
|
|
||||||
{
|
|
||||||
logger.LogDebug(" Проверка связи {LinkUnitId} для FullMatch: hasMatch={HasMatch}", link.UnitId, hasMatch);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return isInverse ? !hasMatch : hasMatch;
|
return isInverse ? !hasMatch : hasMatch;
|
||||||
});
|
});
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: результат allMatch
|
// Отладка: результат allMatch
|
||||||
if (dto.Id == targetUnitId)
|
if (dto.Id == targetUnitId)
|
||||||
{
|
{
|
||||||
logger.LogDebug(" allMatch = {AllMatch}, isFullMatch = {IsFullMatch}", allMatch, isFullMatch);
|
logger.LogDebug(" allMatch = {AllMatch}, isFullMatch = {IsFullMatch}", allMatch, isFullMatch);
|
||||||
@@ -617,8 +625,6 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
|
|
||||||
return allMatch;
|
return allMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasMatchingLinks;
|
|
||||||
}).AsQueryable();
|
}).AsQueryable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,43 +645,82 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
|
|
||||||
logger.LogDebug("RelationshipCountFilter: Min={Min}, Max={Max}, IsParent={IsParent}", min, max, isParentDirection);
|
logger.LogDebug("RelationshipCountFilter: Min={Min}, Max={Max}, IsParent={IsParent}", min, max, isParentDirection);
|
||||||
|
|
||||||
// Определяем, есть ли фильтры по полям
|
// Разделяем фильтры по направлениям
|
||||||
var activeFilters = relationshipFilters
|
var parentFilters = relationshipFilters
|
||||||
.Where(rf => rf.IsParent == isParentDirection && !string.IsNullOrWhiteSpace(rf.ValueMask))
|
.Where(rf => rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask))
|
||||||
.ToList();
|
.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 =>
|
return units.Where(dto =>
|
||||||
{
|
{
|
||||||
var links = isParentDirection ? dto.Parents : dto.Children;
|
int matchingCount = 0;
|
||||||
|
|
||||||
|
if (isParentDirection) // Считаем родительские связи
|
||||||
|
{
|
||||||
|
var links = dto.Parents;
|
||||||
|
|
||||||
if (links == null || !links.Any())
|
if (links == null || !links.Any())
|
||||||
{
|
{
|
||||||
var result = min == 0;
|
var result = min == 0;
|
||||||
logger.LogDebug("UnitId {UnitId}: связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result);
|
logger.LogDebug("UnitId {UnitId}: родительских связей нет (null или пусто). Min={Min}, результат фильтра: {Result}", dto.Id, min, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("UnitId {UnitId}: {Count} связей до фильтрации", dto.Id, links.Count);
|
logger.LogDebug("UnitId {UnitId}: {Count} родительских связей до фильтрации", dto.Id, links.Count);
|
||||||
|
|
||||||
int matchingCount = 0;
|
if (hasParentFieldFilters)
|
||||||
|
|
||||||
if (hasFieldFilters)
|
|
||||||
{
|
{
|
||||||
// Есть фильтры по полям → считаем только связанные юниты, подходящие под фильтр
|
// Есть фильтры по родителям → считаем только подходящие
|
||||||
foreach (var link in links)
|
foreach (var link in links)
|
||||||
{
|
{
|
||||||
// ✅ Изменяем на `All` — все фильтры должны подходить
|
// Проверяем, проходит ли связь все фильтры (логика как в ApplyRelationshipFilterToQuery)
|
||||||
bool hasMatch = activeFilters.All(f =>
|
bool passesAllFilters = true;
|
||||||
{
|
|
||||||
var value = link.Values.FirstOrDefault(v => v.FieldId == f.FieldId);
|
|
||||||
return value != null && value.Value != null && value.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasMatch)
|
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++;
|
matchingCount++;
|
||||||
|
|
||||||
if (matchingCount > max)
|
if (matchingCount > max)
|
||||||
@@ -687,9 +732,84 @@ namespace PARR.DAL.DomainServices.Implementations
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Нет фильтров по полям → считаем общее количество связей (без учёта значений)
|
// Нет фильтров по родителям → считаем общее количество связей (без учёта значений)
|
||||||
matchingCount = links.Count;
|
matchingCount = links.Count;
|
||||||
logger.LogDebug("UnitId {UnitId}: нет фильтров по полям — matchingCount = links.Count = {Count}", dto.Id, matchingCount);
|
logger.LogDebug("UnitId {UnitId}: нет фильтров по родителям — matchingCount = links.Count = {Count}", dto.Id, matchingCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // Считаем дочерние связи
|
||||||
|
{
|
||||||
|
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;
|
var finalResult = matchingCount >= min && matchingCount <= max;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ namespace PARR.EsppSync
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Построим TemplateForShortcodes из уже загруженного template
|
// Построим TemplateForShortcodes из уже загруженного template
|
||||||
var templateForShortcodes = new TemplateForShortcodes
|
var templateForShortcodes = new TemplateForShortcodes
|
||||||
{
|
{
|
||||||
Id = template.Id,
|
Id = template.Id,
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ namespace PARR.TemplateMatcher
|
|||||||
Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator);
|
Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator);
|
||||||
Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator);
|
Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator);
|
||||||
Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
|
Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
|
||||||
|
Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,14 +93,14 @@ namespace PARR.TemplateMatcher
|
|||||||
switch (query.Action)
|
switch (query.Action)
|
||||||
{
|
{
|
||||||
case TemplateMatcherActionEnum.Sync:
|
case TemplateMatcherActionEnum.Sync:
|
||||||
// Создать недостающие шаблоны, привязать к нужному Job, включить/выключить по фильтрам
|
|
||||||
await templateMatcherService.SyncTemplatesForJobGroup(query.Id, query.Initiator);
|
await templateMatcherService.SyncTemplatesForJobGroup(query.Id, query.Initiator);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TemplateMatcherActionEnum.Update:
|
case TemplateMatcherActionEnum.Update:
|
||||||
// Обновить существующие шаблоны: имя, привязка к Job, вкл/выкл по фильтрам
|
|
||||||
//await templateMatcherService.UpdateTemplatesForJob(query.Id, query.Initiator);
|
//await templateMatcherService.UpdateTemplatesForJob(query.Id, query.Initiator);
|
||||||
logger.LogWarning("Обработка EntityType JobGroup и TemplateMatcherActionEnum.Update не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
|
await templateMatcherService.UpdateTemplatesForJobGroup(query.Id, query.Initiator);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.LogWarning("Неизвестное действие для {EntityType}: {Action}", query.EntityType, query.Action);
|
logger.LogWarning("Неизвестное действие для {EntityType}: {Action}", query.EntityType, query.Action);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using PARR.BLL.Domain.Mq;
|
|||||||
using PARR.BLL.Services.Interfaces;
|
using PARR.BLL.Services.Interfaces;
|
||||||
using PARR.Common.Domain;
|
using PARR.Common.Domain;
|
||||||
using PARR.Constants;
|
using PARR.Constants;
|
||||||
|
using PARR.DAL.Contracts;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
using PARR.DAL.DomainServices.Interfaces;
|
||||||
using PARR.DAL.Models;
|
using PARR.DAL.Models;
|
||||||
using PARR.DAL.Models.Job;
|
using PARR.DAL.Models.Job;
|
||||||
@@ -14,6 +15,7 @@ using PARR.DAL.Services.Interfaces.Unit;
|
|||||||
using PARR.DAL.TransformServices;
|
using PARR.DAL.TransformServices;
|
||||||
using PARR.TemplateMatcher.Services.Interfaces;
|
using PARR.TemplateMatcher.Services.Interfaces;
|
||||||
using PARR.TemplateMatcher.Settings;
|
using PARR.TemplateMatcher.Settings;
|
||||||
|
using System.Configuration;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||||
@@ -60,7 +62,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
IUnitFieldService unitFieldService,
|
IUnitFieldService unitFieldService,
|
||||||
ITemplateDeactivator templateDeactivator,
|
ITemplateDeactivator templateDeactivator,
|
||||||
ITemplateNameNormalizer templateNameNormalizer,
|
ITemplateNameNormalizer templateNameNormalizer,
|
||||||
ITemplateUpdaterMqSender templateUpdaterMqSender
|
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
||||||
|
SettingsFromDb settingsFromDb
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
@@ -155,7 +158,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в expectedUnitIds
|
// Отладка: проверить, есть ли юнит в expectedUnitIds
|
||||||
if (expectedUnitIds.Contains(targetUnitId))
|
if (expectedUnitIds.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId);
|
||||||
@@ -191,7 +194,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в unitIdsWithValidGroupingFieldSet
|
// Отладка: проверить, есть ли юнит в unitIdsWithValidGroupingFieldSet
|
||||||
if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId))
|
if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
|
||||||
@@ -223,7 +226,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
.ToHashSet(); // И снова в HashSet
|
.ToHashSet(); // И снова в HashSet
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet
|
// Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet
|
||||||
if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId))
|
if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
|
||||||
@@ -257,7 +260,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, юнит участвует в potentialUnitInUnitLinks
|
// Отладка: проверить, юнит участвует в potentialUnitInUnitLinks
|
||||||
var potentialLinksContainingTarget = potentialUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList();
|
var potentialLinksContainingTarget = potentialUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList();
|
||||||
if (potentialLinksContainingTarget.Any())
|
if (potentialLinksContainingTarget.Any())
|
||||||
{
|
{
|
||||||
@@ -334,7 +337,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, юнит участвует в filteredUnitInUnitLinks
|
// Отладка: проверить, юнит участвует в filteredUnitInUnitLinks
|
||||||
var filteredLinksContainingTarget = filteredUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList();
|
var filteredLinksContainingTarget = filteredUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList();
|
||||||
if (filteredLinksContainingTarget.Any())
|
if (filteredLinksContainingTarget.Any())
|
||||||
{
|
{
|
||||||
@@ -377,7 +380,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count);
|
logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, юнит есть в groupedRelationships.Values
|
// Отладка: проверить, юнит есть в groupedRelationships.Values
|
||||||
var allUnitsInGroups = groupedRelationships.Values.SelectMany(x => x).ToList();
|
var allUnitsInGroups = groupedRelationships.Values.SelectMany(x => x).ToList();
|
||||||
if (allUnitsInGroups.Contains(targetUnitId))
|
if (allUnitsInGroups.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
@@ -410,7 +413,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, юнит есть в unitToKeys до поиска конфликтов
|
// Отладка: проверить, юнит есть в unitToKeys до поиска конфликтов
|
||||||
if (unitToKeys.ContainsKey(targetUnitId))
|
if (unitToKeys.ContainsKey(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в unitToKeys до поиска конфликтов, находится в {Count} группах: [{Groups}]", targetUnitId, unitToKeys[targetUnitId].Count, string.Join(", ", unitToKeys[targetUnitId]));
|
logger.LogDebug("Юнит {TargetUnitId} найден в unitToKeys до поиска конфликтов, находится в {Count} группах: [{Groups}]", targetUnitId, unitToKeys[targetUnitId].Count, string.Join(", ", unitToKeys[targetUnitId]));
|
||||||
@@ -425,7 +428,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList();
|
var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, юнит в conflictedUnits
|
// Отладка: проверить, юнит в conflictedUnits
|
||||||
var targetConflictedEntry = conflictedUnits.FirstOrDefault(c => c.Key == targetUnitId);
|
var targetConflictedEntry = conflictedUnits.FirstOrDefault(c => c.Key == targetUnitId);
|
||||||
if (targetConflictedEntry.Key != default)
|
if (targetConflictedEntry.Key != default)
|
||||||
{
|
{
|
||||||
@@ -468,7 +471,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, юнит есть в groupedRelationships.Values после разрешения конфликтов
|
// Отладка: проверить, юнит есть в groupedRelationships.Values после разрешения конфликтов
|
||||||
var allUnitsInGroupsAfterConflictResolution = groupedRelationships.Values.SelectMany(x => x).ToList();
|
var allUnitsInGroupsAfterConflictResolution = groupedRelationships.Values.SelectMany(x => x).ToList();
|
||||||
if (allUnitsInGroupsAfterConflictResolution.Contains(targetUnitId))
|
if (allUnitsInGroupsAfterConflictResolution.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
@@ -503,7 +506,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
if (childUnitIds.Count == 0)
|
if (childUnitIds.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли ВРТ-AOS-05-ДВС в childUnitIds
|
// Отладка: проверить, есть ли ВРТ-AOS-05-ДВС в childUnitIds
|
||||||
var childUnitNames = childUnitIds.Select(id => unitService.Get().AsNoTracking().Where(u => u.Id == id).Select(u => u.Name).FirstOrDefaultAsync().GetAwaiter().GetResult() ?? id.ToString()).ToList();
|
var childUnitNames = childUnitIds.Select(id => unitService.Get().AsNoTracking().Where(u => u.Id == id).Select(u => u.Name).FirstOrDefaultAsync().GetAwaiter().GetResult() ?? id.ToString()).ToList();
|
||||||
if (childUnitNames.Contains("ВРТ-AOS-05-ДВС"))
|
if (childUnitNames.Contains("ВРТ-AOS-05-ДВС"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,11 +95,26 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
if (unitIds == null || !unitIds.Any())
|
if (unitIds == null || !unitIds.Any())
|
||||||
{
|
{
|
||||||
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
|
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
|
||||||
|
|
||||||
|
// Деактивировать все существующие шаблоны для этого Job
|
||||||
|
var existingTemplatesForDeactivation = await templateService.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.UnitsInTemplate)
|
||||||
|
.Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var unusedTemplate in existingTemplatesForDeactivation)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
|
||||||
|
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в unitIds
|
// Отладка: проверить, есть ли юнит в unitIds
|
||||||
if (unitIds.Contains(targetUnitId))
|
if (unitIds.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
|
||||||
@@ -107,12 +122,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
|
||||||
return; // ❌ юнит отсеялся на этом этапе
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var existingTemplates = await templateService.Get()
|
var existingTemplates = await templateService.Get()
|
||||||
.AsNoTracking()
|
|
||||||
.Include(t => t.UnitsInTemplate)
|
.Include(t => t.UnitsInTemplate)
|
||||||
.Where(t => t.JobId == jobId)
|
.Where(t => t.JobId == jobId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -203,7 +216,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// ✅ Отладка: проверить, есть ли юнит в unitIds
|
// Отладка: проверить, есть ли юнит в unitIds
|
||||||
if (unitIds.Contains(targetUnitId))
|
if (unitIds.Contains(targetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
|
||||||
@@ -211,7 +224,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
|
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
|
||||||
return; // ❌ юнит отсеялся на этом этапе
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.BLL.Domain.Mq;
|
using PARR.BLL.Domain.Mq;
|
||||||
using PARR.BLL.Services.Interfaces;
|
|
||||||
using PARR.Common.Domain;
|
using PARR.Common.Domain;
|
||||||
using PARR.Constants;
|
using PARR.Constants;
|
||||||
|
using PARR.DAL.Contracts;
|
||||||
using PARR.DAL.Models;
|
using PARR.DAL.Models;
|
||||||
using PARR.DAL.Models.Job;
|
|
||||||
using PARR.DAL.Services.Interfaces;
|
using PARR.DAL.Services.Interfaces;
|
||||||
using PARR.DAL.Services.Interfaces.Job;
|
using PARR.DAL.Services.Interfaces.Job;
|
||||||
using PARR.TemplateMatcher.Services.Interfaces;
|
using PARR.TemplateMatcher.Services.Interfaces;
|
||||||
using PARR.TemplateMatcher.Settings;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||||
|
|
||||||
@@ -24,20 +21,23 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
|||||||
private readonly IJobService jobService;
|
private readonly IJobService jobService;
|
||||||
private readonly ITemplateNameNormalizer namenormalizer;
|
private readonly ITemplateNameNormalizer namenormalizer;
|
||||||
private readonly ITemplateUpdaterMqSender sender;
|
private readonly ITemplateUpdaterMqSender sender;
|
||||||
private readonly Guid unusedJobId = Guid.Parse("8f85a91c-a223-4686-bb69-1f0ee73624f2");
|
private readonly SettingsFromDb settingsFromDb;
|
||||||
|
|
||||||
public TemplateDeactivator(
|
public TemplateDeactivator(
|
||||||
ILogger<TemplateDeactivator> logger,
|
ILogger<TemplateDeactivator> logger,
|
||||||
ITemplateService templateService,
|
ITemplateService templateService,
|
||||||
IJobService jobService,
|
IJobService jobService,
|
||||||
ITemplateNameNormalizer namenormalizer,
|
ITemplateNameNormalizer namenormalizer,
|
||||||
ITemplateUpdaterMqSender sender)
|
ITemplateUpdaterMqSender sender,
|
||||||
|
SettingsFromDb settingsFromDb
|
||||||
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.templateService = templateService;
|
this.templateService = templateService;
|
||||||
this.jobService = jobService;
|
this.jobService = jobService;
|
||||||
this.namenormalizer = namenormalizer;
|
this.namenormalizer = namenormalizer;
|
||||||
this.sender = sender;
|
this.sender = sender;
|
||||||
|
this.settingsFromDb = settingsFromDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator)
|
public async Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator)
|
||||||
@@ -68,7 +68,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
|||||||
.Include(j => j.Tnk)
|
.Include(j => j.Tnk)
|
||||||
.Include(j => j.Group)
|
.Include(j => j.Group)
|
||||||
.ThenInclude(g => g!.GroupType)
|
.ThenInclude(g => g!.GroupType)
|
||||||
.FirstOrDefaultAsync(j => j.Id == unusedJobId);
|
.FirstOrDefaultAsync(j => j.Id == settingsFromDb.JobIdForUnusedTemplates);
|
||||||
|
|
||||||
if (unusedJob == null)
|
if (unusedJob == null)
|
||||||
{
|
{
|
||||||
@@ -84,7 +84,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
|||||||
TemplateId = template.Id,
|
TemplateId = template.Id,
|
||||||
JobId = unusedJob.Id,
|
JobId = unusedJob.Id,
|
||||||
UnitId = template.UnitId,
|
UnitId = template.UnitId,
|
||||||
Name = expectedName,
|
Name = expectedName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
IsActiveTemplate = DefaultUnusedTemplateState,
|
IsActiveTemplate = DefaultUnusedTemplateState,
|
||||||
IsActiveSchedule = DefaultUnusedScheduleState,
|
IsActiveSchedule = DefaultUnusedScheduleState,
|
||||||
LastRun = template.LastRun,
|
LastRun = template.LastRun,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using PARR.DAL.Contracts;
|
|||||||
using PARR.DAL.Models.Job;
|
using PARR.DAL.Models.Job;
|
||||||
using PARR.DAL.Services.Interfaces.Job;
|
using PARR.DAL.Services.Interfaces.Job;
|
||||||
using PARR.TemplateMatcher.Services.Implementations;
|
using PARR.TemplateMatcher.Services.Implementations;
|
||||||
using PARR.TemplateMatcher.Services.Implemetaions;
|
|
||||||
using PARR.TemplateMatcher.Services.Interfaces;
|
using PARR.TemplateMatcher.Services.Interfaces;
|
||||||
|
|
||||||
namespace PARR.TemplateMatcher
|
namespace PARR.TemplateMatcher
|
||||||
@@ -14,17 +13,19 @@ namespace PARR.TemplateMatcher
|
|||||||
{
|
{
|
||||||
private readonly ILogger<TemplateMatcher> logger;
|
private readonly ILogger<TemplateMatcher> logger;
|
||||||
private readonly IJobService jobService;
|
private readonly IJobService jobService;
|
||||||
private readonly IEnumerable<ITemplateSynchronizer> synchronizers; // Получаем все стратегии
|
private readonly IJobGroupService jobGroupService;
|
||||||
|
private readonly IEnumerable<ITemplateSynchronizer> synchronizers;
|
||||||
|
|
||||||
public TemplateMatcher(
|
public TemplateMatcher(
|
||||||
ILogger<TemplateMatcher> logger,
|
ILogger<TemplateMatcher> logger,
|
||||||
IJobService jobService,
|
IJobService jobService,
|
||||||
|
IJobGroupService jobGroupService,
|
||||||
IEnumerable<ITemplateSynchronizer> synchronizers
|
IEnumerable<ITemplateSynchronizer> synchronizers
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.jobService = jobService;
|
this.jobService = jobService;
|
||||||
|
this.jobGroupService = jobGroupService;
|
||||||
this.synchronizers = synchronizers;
|
this.synchronizers = synchronizers;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ namespace PARR.TemplateMatcher
|
|||||||
{
|
{
|
||||||
logger.LogError("GroupedTemplateSynchronizer не найден.");
|
logger.LogError("GroupedTemplateSynchronizer не найден.");
|
||||||
}
|
}
|
||||||
return; // Завершаем выполнение для группового Job
|
return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -72,13 +73,33 @@ namespace PARR.TemplateMatcher
|
|||||||
{
|
{
|
||||||
logger.LogError("SimpleTemplateSynchronizer не найден.");
|
logger.LogError("SimpleTemplateSynchronizer не найден.");
|
||||||
}
|
}
|
||||||
return; // Завершаем выполнение для обычного Job
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
|
public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
|
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
|
||||||
|
|
||||||
|
var jobGroup = await jobGroupService.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(jg => jg.GroupType)
|
||||||
|
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
|
||||||
|
|
||||||
|
if (jobGroup == null || jobGroup.GroupType == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем стратегию по типу группы
|
||||||
|
switch (jobGroup.GroupType.Code)
|
||||||
|
{
|
||||||
|
case JobGroupTypesEnum.Group:
|
||||||
|
// Проверяем, есть ли GroupingUnitFieldId — это признак "настоящей" группировки
|
||||||
|
if (jobGroup.GroupingUnitFieldId.HasValue)
|
||||||
|
{
|
||||||
|
logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId);
|
||||||
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
|
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
|
||||||
if (synchronizer != null)
|
if (synchronizer != null)
|
||||||
{
|
{
|
||||||
@@ -86,9 +107,71 @@ namespace PARR.TemplateMatcher
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogError("GroupedTemplateSynchronizer не найден.");
|
logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId);
|
||||||
|
//await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobGroupTypesEnum.Umbrella:
|
||||||
|
logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId);
|
||||||
|
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobGroupTypesEnum.Simple:
|
||||||
|
default:
|
||||||
|
logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId);
|
||||||
|
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Начало обновления шаблонов для JobGroup {JobGroupId}", jobGroupId);
|
||||||
|
|
||||||
|
var jobGroup = await jobGroupService.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(jg => jg.GroupType)
|
||||||
|
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
|
||||||
|
|
||||||
|
if (jobGroup == null || jobGroup.GroupType == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (jobGroup.GroupType.Code)
|
||||||
|
{
|
||||||
|
case JobGroupTypesEnum.Group:
|
||||||
|
if (jobGroup.GroupingUnitFieldId.HasValue)
|
||||||
|
{
|
||||||
|
logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId);
|
||||||
|
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobGroupTypesEnum.Umbrella:
|
||||||
|
logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId);
|
||||||
|
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobGroupTypesEnum.Simple:
|
||||||
|
default:
|
||||||
|
logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId);
|
||||||
|
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator)
|
public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator)
|
||||||
{
|
{
|
||||||
@@ -113,14 +196,12 @@ namespace PARR.TemplateMatcher
|
|||||||
{
|
{
|
||||||
// Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение)
|
// Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение)
|
||||||
await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator);
|
await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator);
|
||||||
// Или, возможно, имеет смысл вызвать полную синхронизацию для группы:
|
|
||||||
// await synchronizer.SyncTemplatesForJobGroupAsync(job.GroupId, initiator);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogError("GroupedTemplateSynchronizer не найден.");
|
logger.LogError("GroupedTemplateSynchronizer не найден.");
|
||||||
}
|
}
|
||||||
return; // Завершаем выполнение
|
return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -135,15 +216,77 @@ namespace PARR.TemplateMatcher
|
|||||||
{
|
{
|
||||||
logger.LogError("SimpleTemplateSynchronizer не найден.");
|
logger.LogError("SimpleTemplateSynchronizer не найден.");
|
||||||
}
|
}
|
||||||
return; // Завершаем выполнение
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Вспомогательные методы ---
|
// --- Вспомогательные методы ---
|
||||||
|
private async Task SyncJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Синхронизация JobGroup {JobGroupId} как Collection (по каждому Job'у отдельно)", jobGroupId);
|
||||||
|
|
||||||
|
var jobIds = await jobService.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(j => j.GroupId == jobGroupId)
|
||||||
|
.Select(j => j.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!jobIds.Any())
|
||||||
|
{
|
||||||
|
logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Найдено {Count} Job'ов в JobGroup {JobGroupId}", jobIds.Count, jobGroupId);
|
||||||
|
|
||||||
|
var simpleSynchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
|
||||||
|
|
||||||
|
if (simpleSynchronizer == null)
|
||||||
|
{
|
||||||
|
logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var jobId in jobIds)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Синхронизация Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId);
|
||||||
|
await simpleSynchronizer.SyncTemplatesForJobAsync(jobId, initiator);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task UpdateJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Обновление JobGroup {JobGroupId} как Collection (по каждому Job'у)", jobGroupId);
|
||||||
|
|
||||||
|
var jobIds = await jobService.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(j => j.GroupId == jobGroupId)
|
||||||
|
.Select(j => j.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!jobIds.Any())
|
||||||
|
{
|
||||||
|
logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var jobId in jobIds)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Обновление шаблонов для Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId);
|
||||||
|
await UpdateTemplatesForJob(jobId, initiator);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
|
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
|
||||||
{
|
{
|
||||||
return await jobService.Get()
|
return await jobService.Get()
|
||||||
.AsNoTracking() // Добавлено
|
.AsNoTracking()
|
||||||
.Include(j => j.Group)
|
.Include(j => j.Group)
|
||||||
.ThenInclude(j => j!.GroupType)
|
.ThenInclude(j => j!.GroupType)
|
||||||
.Include(j => j.AutoControl)
|
.Include(j => j.AutoControl)
|
||||||
|
|||||||
Reference in New Issue
Block a user