diff --git a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs index ccb61375..ea19feed 100644 --- a/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs @@ -16,9 +16,12 @@ namespace PARR.DAL.DomainServices.UnitFilterService; internal class UnitFilterService : IUnitFilterService { #if DEBUG - private readonly Guid targetUnitId = Guid.Parse("0e84812c-66ee-46fc-bb72-2afbecf354cb"); + private readonly Guid targetUnitId = Guid.Parse("9d88fff2-a861-487f-b73d-bce1f0218e9f"); #endif + private const int DebugMaxUnitsToLog = 10; + private const int DebugMaxRelationsToLog = 5; + private readonly int batchSize; private readonly ILogger logger; @@ -262,9 +265,6 @@ internal class UnitFilterService : IUnitFilterService /// /// Применяем фильтры аттрибутов /// - /// - /// - /// private async Task> ApplyFieldFiltersOnDbAsync( List unitIds, IEnumerable fieldFilters, @@ -288,18 +288,7 @@ internal class UnitFilterService : IUnitFilterService if (string.IsNullOrEmpty(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; + string dbValueMask = NormalizeLikeMask(valueMask); var fieldName = fieldFilter.UnitField?.AihitName ?? $"FieldId={fieldFilter.FieldId}"; logger.LogDebug(" FieldFilter #{Index}: Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}", @@ -317,6 +306,30 @@ internal class UnitFilterService : IUnitFilterService v.FieldId == fieldFilter.FieldId && EF.Functions.ILike(v.Value.Value, dbValueMask))); } + +#if DEBUG + if (unitIds.Contains(targetUnitId)) + { + var unitHasField = await unitInValueService.Get() + .AsNoTracking() + .AnyAsync(uiv => uiv.UnitId == targetUnitId && uiv.FieldId == fieldFilter.FieldId, cancellationToken); + + var unitValue = await unitInValueService.Get() + .AsNoTracking() + .Where(uiv => uiv.UnitId == targetUnitId && uiv.FieldId == fieldFilter.FieldId) + .Select(uiv => uiv.Value.Value) + .FirstOrDefaultAsync(cancellationToken); + + var intermediateResult = await query.Select(u => u.Id).ToListAsync(cancellationToken); + var passes = intermediateResult.Contains(targetUnitId); + + logger.LogDebug(" DEBUG: Юнит {TargetUnitId}: Поле={FieldName}, Значение={Value}, Маска={Mask}, HasField={HasField}, Проходит={Passes}", + targetUnitId, fieldName, unitValue ?? "null", dbValueMask, unitHasField, passes); + + query = unitService.Get().AsNoTracking() + .Where(u => intermediateResult.Contains(u.Id)); + } +#endif } var result = await query.Select(u => u.Id).ToListAsync(cancellationToken); @@ -329,9 +342,6 @@ internal class UnitFilterService : IUnitFilterService /// /// Применяем фильтры аттрибутов у связанных ЭК /// - /// - /// - /// private async Task> ProcessRelationshipFiltersAsync( List unitIds, IEnumerable relationshipFilters, @@ -347,12 +357,18 @@ internal class UnitFilterService : IUnitFilterService logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}", unitIds.Count, relationshipFilters.Count()); +#if DEBUG + if (unitIds.Contains(targetUnitId)) + { + logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден во входных данных ProcessRelationshipFiltersAsync", 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(); - // Обработка родительских фильтров if (parentRelFilters.Any()) { await ApplyRelationshipFiltersOnDbAsync( @@ -363,7 +379,6 @@ internal class UnitFilterService : IUnitFilterService cancellationToken); } - // Обработка дочерних фильтров if (childRelFilters.Any()) { await ApplyRelationshipFiltersOnDbAsync( @@ -374,120 +389,137 @@ internal class UnitFilterService : IUnitFilterService cancellationToken); } - // Если фильтры были — оставляем только юниты со связями - // Если фильтров не было — оставляем все юниты - bool hasRelationshipFilters = parentRelFilters.Any() || childRelFilters.Any(); +#if DEBUG + var unitsWithAnyConnections = resultContexts.Values.Count(c => c.ValidParentIds.Any() || c.ValidChildIds.Any()); + var unitsWithoutConnections = resultContexts.Values.Count(c => !c.ValidParentIds.Any() && !c.ValidChildIds.Any()); + logger.LogDebug("DEBUG: После ApplyRelationshipFiltersOnDbAsync: {WithConnections} юнитов со связями, {WithoutConnections} без связей", + unitsWithAnyConnections, unitsWithoutConnections); - var unitsWithRelationships = hasRelationshipFilters - ? resultContexts.Values.Where(c => c.ValidParentIds.Any() || c.ValidChildIds.Any()).ToList() - : resultContexts.Values.ToList(); + var targetContext = resultContexts.Values.FirstOrDefault(c => c.UnitId == targetUnitId); + if (targetContext != null) + { + logger.LogDebug("DEBUG: Юнит {TargetUnitId}: Родителей={ParentCount}, Детей={ChildCount}, PassesFilter={Passes}", + targetUnitId, + targetContext.ValidParentIds.Count, + targetContext.ValidChildIds.Count, + targetContext.ValidParentIds.Any() || targetContext.ValidChildIds.Any()); + } +#endif + + var result = resultContexts.Values.ToList(); - var result = unitsWithRelationships; logger.LogDebug("ProcessRelationshipFiltersAsync: выход {ContextCount} контекстов (из {InitialCount})", result.Count, resultContexts.Count); #if DEBUG - // Собираем все UnitId для пакетной загрузки (основные + родители + дети) - var allUnitIds = result.Select(c => c.UnitId) - .Concat(result.SelectMany(c => c.ValidParentIds)) - .Concat(result.SelectMany(c => c.ValidChildIds)) - .Distinct() - .ToList(); // ← Убрали .Take(100) - - // Пакетная загрузка всех юнитов - var unitsMap = await unitService.Get().AsNoTracking() - .Where(u => allUnitIds.Contains(u.Id)) - .ToDictionaryAsync(u => u.Id, cancellationToken); - - // Получаем все 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(); - var allFieldIds = parentFieldIds.Concat(childFieldIds).Distinct().ToList(); - - // ПАКЕТНАЯ загрузка значений для ВСЕХ юнитов сразу - var allValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitIds, allFieldIds); - var valuesByUnit = allValues.GroupBy(v => v.UnitId) - .ToDictionary(g => g.Key, g => g.ToList()); - - // ПАКЕТНАЯ загрузка имён полей - var fieldsMap = await unitFieldService.Get() - .AsNoTracking() - .Where(f => allFieldIds.Contains(f.Id)) - .ToDictionaryAsync(f => f.Id, f => f.AihitName, cancellationToken); - - // Логируем только первые 10 юнитов (чтобы не засорять лог) - foreach (var context in result.Take(10)) + if (unitIds.Contains(targetUnitId)) { - var u = unitsMap.GetValueOrDefault(context.UnitId); - - if (u == null) - continue; - - logger.LogDebug("Найден ЭК {UnitName} (Id={UnitId})", u.Name, u.Id); - - // === РОДИТЕЛИ === - if (context.ValidParentIds.Any()) + var targetContextInResult = result.FirstOrDefault(c => c.UnitId == targetUnitId); + if (targetContextInResult != null) { - logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count); + logger.LogDebug("DEBUG: Юнит {TargetUnitId} в результатах ProcessRelationshipFiltersAsync. Родителей: {ParentCount}, Детей: {ChildCount}", + targetUnitId, targetContextInResult.ValidParentIds.Count, targetContextInResult.ValidChildIds.Count); + } + else + { + logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ в результатах ProcessRelationshipFiltersAsync (удалён)", targetUnitId); + } + } - // Логируем только первые 5 родителей (чтобы не засорять лог) - foreach (var parentId in context.ValidParentIds.Take(5)) + if (logger.IsEnabled(LogLevel.Debug)) + { + var allUnitIds = result.Select(c => c.UnitId) + .Concat(result.SelectMany(c => c.ValidParentIds)) + .Concat(result.SelectMany(c => c.ValidChildIds)) + .Distinct() + .ToList(); + + var unitsMap = await unitService.Get().AsNoTracking() + .Where(u => allUnitIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, cancellationToken); + + 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(); + var allFieldIds = parentFieldIds.Concat(childFieldIds).Distinct().ToList(); + + var allValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitIds, allFieldIds); + var valuesByUnit = allValues.GroupBy(v => v.UnitId) + .ToDictionary(g => g.Key, g => g.ToList()); + + var fieldsMap = await unitFieldService.Get() + .AsNoTracking() + .Where(f => allFieldIds.Contains(f.Id)) + .ToDictionaryAsync(f => f.Id, f => f.AihitName, cancellationToken); + + foreach (var context in result.Take(DebugMaxUnitsToLog)) + { + var u = unitsMap.GetValueOrDefault(context.UnitId); + if (u == null) + continue; + + logger.LogDebug("Найден ЭК {UnitName} (Id={UnitId})", u.Name, u.Id); + + if (context.ValidParentIds.Any()) { - var p = unitsMap.GetValueOrDefault(parentId); - logger.LogDebug("\t\t{ParentName} (Id={ParentId})", p?.Name ?? "null", parentId); + logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count); - if (parentFieldIds.Any() && valuesByUnit.TryGetValue(parentId, out var parentValues)) + foreach (var parentId in context.ValidParentIds.Take(DebugMaxRelationsToLog)) { - foreach (var pv in parentValues.Where(v => parentFieldIds.Contains(v.FieldId))) + var p = unitsMap.GetValueOrDefault(parentId); + logger.LogDebug("\t\t{ParentName} (Id={ParentId})", p?.Name ?? "null", parentId); + + if (parentFieldIds.Any() && valuesByUnit.TryGetValue(parentId, out var parentValues)) { - var fieldName = fieldsMap.GetValueOrDefault(pv.FieldId) ?? $"FieldId={pv.FieldId}"; - logger.LogDebug("\t\t {FieldName} = {FieldValue}", - fieldName, - pv.Value?.Value ?? "null"); + foreach (var pv in parentValues.Where(v => parentFieldIds.Contains(v.FieldId))) + { + var fieldName = fieldsMap.GetValueOrDefault(pv.FieldId) ?? $"FieldId={pv.FieldId}"; + logger.LogDebug("\t\t {FieldName} = {FieldValue}", + fieldName, + pv.Value?.Value ?? "null"); + } } } - } - if (context.ValidParentIds.Count > 5) - { - logger.LogDebug("\t\t... и ещё {Count} родителей", context.ValidParentIds.Count - 5); - } - } - else if (parentRelFilters.Any()) - { - logger.LogDebug("\tРодительские фильтры заданы, но подходящих родителей не найдено"); - } - - // === ДЕТИ === - if (context.ValidChildIds.Any()) - { - logger.LogDebug("\tФильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count); - - foreach (var childId in context.ValidChildIds.Take(5)) - { - var c = unitsMap.GetValueOrDefault(childId); - logger.LogDebug("\t\t{ChildName} (Id={ChildId})", c?.Name ?? "null", childId); - - if (childFieldIds.Any() && valuesByUnit.TryGetValue(childId, out var childValues)) + if (context.ValidParentIds.Count > DebugMaxRelationsToLog) { - foreach (var cv in childValues.Where(v => childFieldIds.Contains(v.FieldId))) - { - var fieldName = fieldsMap.GetValueOrDefault(cv.FieldId) ?? $"FieldId={cv.FieldId}"; - logger.LogDebug("\t\t {FieldName} = {FieldValue}", - fieldName, - cv.Value?.Value ?? "null"); - } + logger.LogDebug("\t\t... и ещё {Count} родителей", context.ValidParentIds.Count - DebugMaxRelationsToLog); } } - - if (context.ValidChildIds.Count > 5) + else if (parentRelFilters.Any()) { - logger.LogDebug("\t\t... и ещё {Count} детей", context.ValidChildIds.Count - 5); + logger.LogDebug("\tРодительские фильтры заданы, но подходящих родителей не найдено"); + } + + if (context.ValidChildIds.Any()) + { + logger.LogDebug("\tФильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count); + + foreach (var childId in context.ValidChildIds.Take(DebugMaxRelationsToLog)) + { + var c = unitsMap.GetValueOrDefault(childId); + logger.LogDebug("\t\t{ChildName} (Id={ChildId})", c?.Name ?? "null", childId); + + if (childFieldIds.Any() && valuesByUnit.TryGetValue(childId, out var childValues)) + { + foreach (var cv in childValues.Where(v => childFieldIds.Contains(v.FieldId))) + { + var fieldName = fieldsMap.GetValueOrDefault(cv.FieldId) ?? $"FieldId={cv.FieldId}"; + logger.LogDebug("\t\t {FieldName} = {FieldValue}", + fieldName, + cv.Value?.Value ?? "null"); + } + } + } + + if (context.ValidChildIds.Count > DebugMaxRelationsToLog) + { + logger.LogDebug("\t\t... и ещё {Count} детей", context.ValidChildIds.Count - DebugMaxRelationsToLog); + } + } + else if (childRelFilters.Any()) + { + logger.LogDebug("\tДочерние фильтры заданы, но подходящих детей не найдено"); } - } - else if (childRelFilters.Any()) - { - logger.LogDebug("\tДочерние фильтры заданы, но подходящих детей не найдено"); } } #endif @@ -752,7 +784,6 @@ internal class UnitFilterService : IUnitFilterService #region вспомогательные методы /// /// Применяет фильтры к связям (родителям или детям) - /// Связь должна пройти ВСЕ фильтры направления /// private async Task ApplyRelationshipFiltersOnDbAsync( List unitIds, @@ -796,17 +827,7 @@ internal class UnitFilterService : IUnitFilterService 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; + string dbValueMask = NormalizeLikeMask(valueMask); var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}"; logger.LogDebug(" RelationshipFilter ({Direction}): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}", @@ -938,6 +959,7 @@ internal class UnitFilterService : IUnitFilterService } + private async Task> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter, CancellationToken cancellationToken = default) { var cacheKey = cacheService.GetKey(new[] { "uf_ids", filter.UnitFilter }, isUseHash: true); @@ -948,8 +970,10 @@ internal class UnitFilterService : IUnitFilterService return cachedData.Data.UnitIds; } + var dbValueMask = NormalizeLikeMask(filter.UnitFilter); + var initialUnitIds = await unitService.Get().AsNoTracking() - .Where(unit => EF.Functions.ILike(unit.Name, filter.UnitFilter)) + .Where(unit => EF.Functions.ILike(unit.Name, dbValueMask)) .Select(u => u.Id) .ToListAsync(cancellationToken); @@ -965,5 +989,29 @@ internal class UnitFilterService : IUnitFilterService return initialUnitIds; } + + + /// + /// Обрабатывает маску LIKE для корректной работы с SQL + /// + private static string NormalizeLikeMask(string valueMask) + { + if (string.IsNullOrWhiteSpace(valueMask)) + return valueMask; + + valueMask = valueMask.Trim(); + bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); + bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); + + if (isStartsWith && isEndsWith) + return $"%{valueMask.Trim('%')}%"; + else if (isStartsWith) + return $"{valueMask.TrimEnd('%')}%"; + else if (isEndsWith) + return $"%{valueMask.TrimStart('%')}"; + else + return valueMask; + } + #endregion } \ No newline at end of file diff --git a/PARR.DAL/Services/Implementations/RobotConfigurationService.cs b/PARR.DAL/Services/Implementations/RobotConfigurationService.cs index b8d14cd0..bbf72a2b 100644 --- a/PARR.DAL/Services/Implementations/RobotConfigurationService.cs +++ b/PARR.DAL/Services/Implementations/RobotConfigurationService.cs @@ -53,6 +53,11 @@ namespace PARR.DAL.Services.Implementations // ставить статус Updating можно только если текущий статус == Ok if (configuration.TaskStatusCode != (int)TaskStatusEnum.Ok) { + int taskStatusValue = configuration.TaskStatusCode; + string taskStatusName = Enum.IsDefined(typeof(TaskStatusEnum), taskStatusValue) + ? ((TaskStatusEnum)taskStatusValue).ToString() + : $"Unknown ({taskStatusValue})"; + logger.LogInformation("Нельзя установить статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}, так как текущий статус {currentStatus}", updatingStatus, configuration.Id, configuration.TemplateId, configuration.TaskStatusCode); return false;