fix(dal): исправлен UnitFilterService, фильтровались юниты у которых нет связей, но проверку фильтров они проходили.

This commit is contained in:
Mikhail Kuznetsov
2026-03-18 17:51:54 +10:00
parent 3704a131b4
commit f75dca0a3b
2 changed files with 180 additions and 127 deletions

View File

@@ -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<UnitFilterService> logger;
@@ -262,9 +265,6 @@ internal class UnitFilterService : IUnitFilterService
/// <summary>
/// Применяем фильтры аттрибутов
/// </summary>
/// <param name="unitIds"></param>
/// <param name="fieldFilters"></param>
/// <returns></returns>
private async Task<List<Guid>> ApplyFieldFiltersOnDbAsync(
List<Guid> unitIds,
IEnumerable<JobFieldFilter> 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
/// <summary>
/// Применяем фильтры аттрибутов у связанных ЭК
/// </summary>
/// <param name="unitIds"></param>
/// <param name="relationshipFilters"></param>
/// <returns></returns>
private async Task<List<FilteredUnitContext>> ProcessRelationshipFiltersAsync(
List<Guid> unitIds,
IEnumerable<JobRelationshipFilter> 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 вспомогательные методы
/// <summary>
/// Применяет фильтры к связям (родителям или детям)
/// Связь должна пройти ВСЕ фильтры направления
/// </summary>
private async Task ApplyRelationshipFiltersOnDbAsync(
List<Guid> 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<List<Guid>> 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;
}
/// <summary>
/// Обрабатывает маску LIKE для корректной работы с SQL
/// </summary>
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
}

View File

@@ -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;