feat(dal, templateMatcher): UnitFilterService теперь возвращает модель Dto а не только Id

This commit is contained in:
Mikhail Kuznetsov
2026-01-21 18:25:09 +10:00
parent eb5bed5614
commit 555b946daa
15 changed files with 1105 additions and 968 deletions

View File

@@ -20,6 +20,7 @@ using PARR.DAL.Cache.Services;
using PARR.DAL.Contracts;
using PARR.DAL.DomainModels;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Extensions;
using PARR.DAL.Models.Job;
using PARR.DAL.Services.Interfaces;

View File

@@ -9,7 +9,7 @@ using PARR.API.Contracts.V1.Responses;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Controllers.V1.Base;
using PARR.Constants;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
using Job = PARR.DAL.Models.Job.Job;

View File

@@ -12,7 +12,7 @@ using PARR.API.Services.Interfaces;
using PARR.DAL.Cache.Services.Base;
using PARR.DAL.Contracts;
using PARR.DAL.DomainModels;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Models.Job;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job;

View File

@@ -3,17 +3,14 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PARR.DAL.Context;
using PARR.DAL.DomainServices.Implementations;
using PARR.DAL.DomainServices.UnitFilterService;
// Предположим, что у вас есть реализации IJobService и IUnitService
// Если нет, можно создать моки, но лучше использовать реальные, если они просты
using PARR.DAL.Services.Implementations.Job; // Пример: JobService
using PARR.DAL.Services.Implementations.Unit; // Пример: UnitService
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace PARR.DAL.IntegrationTests.Integration
{

View File

@@ -1,11 +1,8 @@
using Microsoft.Extensions.Logging;
using MockQueryable;
using Moq;
using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;

View File

@@ -1,864 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.DAL.Cache.Services.Base;
using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
namespace PARR.DAL.DomainServices.Implementations
{
internal class UnitFilterService : IUnitFilterService
{
#if DEBUG
private readonly Guid targetUnitId = Guid.Parse("87fc4c36-1ea8-4163-983f-1605fee1de99");
#endif
private readonly ILogger<UnitFilterService> logger;
private readonly IJobService jobService;
private readonly IUnitService unitService;
private readonly IUnitInUnitService unitInUnitService;
private readonly IRedisCacheService cacheService;
private readonly IUnitInValueService unitInValueService;
public UnitFilterService(
ILogger<UnitFilterService> logger,
IJobService jobService,
IUnitService unitService,
IUnitInUnitService unitInUnitService,
IUnitInValueService unitInValueService,
IRedisCacheService cacheService
)
{
this.logger = logger;
this.jobService = jobService;
this.unitService = unitService;
this.unitInUnitService = unitInUnitService;
this.cacheService = cacheService;
this.unitInValueService = unitInValueService;
}
#region вложенные классы
private class UnitInfoDto
{
public Guid Id { get; set; }
public required string Name { get; set; }
}
private class UnitValueDto
{
public Guid FieldId { get; set; }
public string? Value { get; set; }
}
private class RelatedUnitDto
{
public Guid UnitId { get; set; }
public string Name { get; set; } = null!;
public List<UnitValueDto> Values { get; set; } = new();
}
private class UnitDto
{
public Guid Id { get; set; }
public string Name { get; set; } = null!;
public List<UnitValueDto> Values { get; set; } = new();
public List<RelatedUnitDto> Parents { get; set; } = new();
public List<RelatedUnitDto> Children { get; set; } = new();
}
public class CachedUnitIds
{
public List<Guid> UnitIds { get; set; } = new();
public DateTimeOffset Timestamp { get; set; }
public string? Source { get; set; } = "UnitFilterService";
public int Version { get; set; } = 1;
}
#endregion
public async Task<IEnumerable<Guid>?> GetUnitsIdByJobFilterAsync(Guid jobId, int? takeCount = null)
{
var job = await jobService
.Get().AsNoTracking()
.Include(j => j.UnitFilters).ThenInclude(uf => uf.FieldFilters)
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
.FirstOrDefaultAsync(j => j.Id == jobId);
return job == null ? null : await GetUnitsIdByJobFilterAsync(job, takeCount);
}
public async Task<IEnumerable<Guid>?> GetUnitsIdByJobFilterAsync(Job job, int? takeCount = null)
{
if (job.Group == null)
throw new ArgumentNullException(nameof(job.Group), $"Job {job.Id} не содержит Group");
if (job.Group.GroupType == null)
throw new ArgumentNullException(nameof(job.Group.GroupType), $"Job {job.Id} не содержит GroupType");
if (job.UnitFilters == null || !job.UnitFilters.Any())
throw new ArgumentNullException(nameof(job.UnitFilters), $"Job {job.Id} не содержит UnitFilters");
var maxCount = takeCount ?? int.MaxValue;
var collectedIds = new HashSet<Guid>();
var filterNumber = 0;
foreach (var filter in job.UnitFilters)
{
filterNumber++;
if (collectedIds.Count >= maxCount) break;
var remaining = maxCount - collectedIds.Count;
if (remaining <= 0) break;
try
{
logger.LogDebug("Применяем фильтр #{Index} (Id={FilterId})", filterNumber, filter.Id);
// 1 Найти ID юнитов по UnitFilter (Name LIKE)
var initialUnitIds = await GetUnitIdsFromCacheOrDbAsync(filter);
logger.LogDebug("Базовый фильтр по Name '{NameFilter}' дал {Count} юнитов", filter.UnitFilter, initialUnitIds.Count);
#if DEBUG
// Отладка: проверить, есть ли юнит в initialUnitIds
if (initialUnitIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в initialUnitIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в initialUnitIds.", targetUnitId);
//continue; // ❌ юнит отсеялся на этом этапе
}
#endif
if (!initialUnitIds.Any())
{
logger.LogDebug("Фильтр #{Index}: 0 юнитов после UnitFilter. Пропускаем.", filterNumber);
continue;
}
// 2 Загрузить UnitValues для этих юнитов (только FieldId из FieldFilter)
var fieldFilterFieldIds = filter.FieldFilters.Select(f => f.FieldId).ToHashSet();
var unitValues = fieldFilterFieldIds.Any()
? await unitInValueService.GetByUnitIdsAndFieldIdsAsync(initialUnitIds, fieldFilterFieldIds)
: new List<UnitInValue>();
// 3 Загрузить связи
var parentRelFilters = filter.RelationshipFilters.Where(rf => rf.IsParent).ToList();
var childRelFilters = filter.RelationshipFilters.Where(rf => !rf.IsParent).ToList();
var parentLinks = await unitInUnitService.GetParentLinksByChildIdsAsync(initialUnitIds);
var childLinks = await unitInUnitService.GetChildLinksByParentIdsAsync(initialUnitIds);
// 4 ID родителей и детей
var parentUnitIds = parentLinks.Select(l => l.ParentUnitId).ToHashSet();
var childUnitIds = childLinks.Select(l => l.ChildUnitId).ToHashSet();
// 5 Загрузить родительские и дочерние юниты (Id + Name)
var parentUnits = parentUnitIds.Any()
? await unitService.Get().AsNoTracking()
.Where(u => parentUnitIds.Contains(u.Id))
.Select(u => new UnitInfoDto { Id = u.Id, Name = u.Name })
.ToListAsync()
: new List<UnitInfoDto>();
var childUnits = childUnitIds.Any()
? await unitService.Get().AsNoTracking()
.Where(u => childUnitIds.Contains(u.Id))
.Select(u => new UnitInfoDto { Id = u.Id, Name = u.Name })
.ToListAsync()
: new List<UnitInfoDto>();
// 6 UnitValues для родителей и детей (только FieldId из RelationshipFilter)
var relFilterFieldIds = filter.RelationshipFilters.Select(f => f.FieldId).ToHashSet();
var parentUnitValues = relFilterFieldIds.Any() && parentUnitIds.Any()
? await unitInValueService.GetByUnitIdsAndFieldIdsAsync(parentUnitIds, relFilterFieldIds)
: new List<UnitInValue>();
var childUnitValues = relFilterFieldIds.Any() && childUnitIds.Any()
? await unitInValueService.GetByUnitIdsAndFieldIdsAsync(childUnitIds, relFilterFieldIds)
: new List<UnitInValue>();
// 7 Сборка DTO
var unitMap = initialUnitIds.ToDictionary(id => id, id => new UnitDto { Id = id });
// UnitValues → Unit
foreach (var uv in unitValues)
{
if (unitMap.TryGetValue(uv.UnitId, out var dto))
{
dto.Values.Add(new UnitValueDto { FieldId = uv.FieldId, Value = uv.Value?.Value });
}
}
var parentIdMap = parentUnits.ToDictionary(u => u.Id, u => u.Name);
var childIdMap = childUnits.ToDictionary(u => u.Id, u => u.Name);
// ParentLinks → Parent
foreach (var link in parentLinks)
{
if (unitMap.TryGetValue(link.ChildUnitId, out var dto))
{
dto.Parents.Add(new RelatedUnitDto
{
UnitId = link.ParentUnitId,
Name = parentIdMap.GetValueOrDefault(link.ParentUnitId, "")
});
}
}
// ChildLinks → Child
foreach (var link in childLinks)
{
if (unitMap.TryGetValue(link.ParentUnitId, out var dto))
{
dto.Children.Add(new RelatedUnitDto
{
UnitId = link.ChildUnitId,
Name = childIdMap.GetValueOrDefault(link.ChildUnitId, "")
});
}
}
// Привязка значений к родителям
var parentValuesMap = parentUnitValues
.GroupBy(v => v.UnitId)
.ToDictionary(g => g.Key, g => g.Select(v => new UnitValueDto { FieldId = v.FieldId, Value = v.Value?.Value }).ToList());
foreach (var dto in unitMap.Values)
{
foreach (var parent in dto.Parents)
{
if (parentValuesMap.TryGetValue(parent.UnitId, out var values))
{
parent.Values.AddRange(values);
}
}
}
// Привязка значений к детям
var childValuesMap = childUnitValues
.GroupBy(v => v.UnitId)
.ToDictionary(g => g.Key, g => g.Select(v => new UnitValueDto { FieldId = v.FieldId, Value = v.Value?.Value }).ToList());
foreach (var dto in unitMap.Values)
{
foreach (var child in dto.Children)
{
if (childValuesMap.TryGetValue(child.UnitId, out var values))
{
child.Values.AddRange(values);
}
}
}
// 8 Применение фильтров
var candidateUnits = unitMap.Values.AsQueryable();
foreach (var fieldFilter in filter.FieldFilters)
{
candidateUnits = ApplyFieldFilter(candidateUnits, fieldFilter);
}
#if DEBUG
// Отладка: проверить, есть ли юнит в candidateUnits после FieldFilter
var candidateListAfterFieldFilter = candidateUnits.ToList();
if (candidateListAfterFieldFilter.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в candidateUnits после FieldFilter.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после FieldFilter.", targetUnitId);
}
logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateListAfterFieldFilter.Count);
#endif
foreach (var relFilter in filter.RelationshipFilters)
{
candidateUnits = ApplyRelationshipFilterToQuery(candidateUnits, relFilter);
}
#if DEBUG
// Отладка: проверить, есть ли юнит в candidateUnits после RelationshipFilter
var candidateListAfterRelFilter = candidateUnits.ToList();
if (candidateListAfterRelFilter.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в candidateUnits после RelationshipFilter.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после RelationshipFilter.", targetUnitId);
}
logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateListAfterRelFilter.Count);
#endif
// 9 Umbrella-фильтр
var finalUnits = candidateUnits.AsEnumerable();
if (job.Group.GroupType.Code == JobGroupTypesEnum.Umbrella)
{
finalUnits = ApplyRelationshipCountFilter(finalUnits, job, filter.RelationshipFilters);
logger.LogDebug("После Umbrella-фильтра осталось {Count} юнитов", finalUnits.Count());
#if DEBUG
// Отладка: проверить, есть ли юнит в finalUnits после Umbrella
if (finalUnits.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в finalUnits после Umbrella-фильтра.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в finalUnits после Umbrella-фильтра.", targetUnitId);
//continue; // ❌ юнит отсеялся на этом этапе
}
#endif
}
// 10 Взять ID
var newIds = finalUnits
.Select(u => u.Id)
.Take(remaining)
.ToList();
#if DEBUG
// Отладка: проверить, есть ли юнит в newIds
if (newIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в newIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в newIds.", targetUnitId);
// ❌ юнит отсеялся на этапе Take(remaining), если его не было в finalUnits
}
#endif
collectedIds.UnionWith(newIds);
logger.LogDebug("Фильтр #{Index}: найдено {Count} Unit'ов. Всего: {Total}",
filterNumber, newIds.Count, collectedIds.Count);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при обработке фильтра {FilterId} (#{Index}) для Job {JobId}",
filter.Id, filterNumber, job.Id);
}
}
var result = collectedIds.Take(maxCount).ToList();
logger.LogInformation("Job {JobId}: из {FilterCount} фильтров получено {UnitCount} уникальных Unit'ов",
job.Id, job.UnitFilters.Count, result.Count);
#if DEBUG
// Отладка: проверить, есть ли юнит в result
if (result.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в финальном результате.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в финальном результате.", targetUnitId);
}
#endif
return result;
}
public async Task<List<string>> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId)
{
logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId);
var job = await jobService
.Get().AsNoTracking()
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == jobId);
if (job == null)
{
logger.LogWarning("Job с Id {JobId} не найден.", jobId);
throw new ArgumentException($"Job {jobId} не найден.", nameof(jobId));
}
logger.LogDebug("Найден Job: {JobName}. Количество UnitFilters: {FilterCount}", job.Name, job.UnitFilters.Count());
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var filter in job.UnitFilters)
{
if (!filter.RelationshipFilters.Any()) continue;
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, участвующие в связях
var allRelatedUnitIds = parentLinks
.Select(l => l.ParentUnitId)
.Concat(childLinks.Select(l => l.ChildUnitId))
.Distinct()
.ToList();
if (!allRelatedUnitIds.Any()) continue;
// Получить значения для всех связанных юнитов
var allUnitValues = await unitInValueService.GetByUnitIdsAsync(allRelatedUnitIds);
// Сгруппировать значения по UnitId
var valuesByUnit = allUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
// Найти UnitId, которые проходят все RelationshipFilters
var matchingUnitIds = new HashSet<Guid>();
foreach (var relatedUnitId in allRelatedUnitIds)
{
// Изменяем на `All` — все фильтры должны подходить
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
{
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
var matchingValues = values
.Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null)
.ToList();
if (!matchingValues.Any()) // Нет значений по полю
{
// Если нет значений, и IsInverse = false → не подходит
// Если нет значений, и IsInverse = true → подходит
return rf.IsInverse;
}
var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase));
// Учитываем IsInverse
if (rf.IsInverse)
hasMatch = !hasMatch;
return hasMatch;
});
if (passesAllFilters)
matchingUnitIds.Add(relatedUnitId);
}
if (matchingUnitIds.Any())
{
var names = await unitService.Get().AsNoTracking()
.Where(u => matchingUnitIds.Contains(u.Id))
.Select(u => u.Name)
.ToListAsync();
result.UnionWith(names);
}
}
return result.ToList();
}
#region вспомогательные методы
private IQueryable<UnitDto> ApplyFieldFilter(IQueryable<UnitDto> query, JobFieldFilter fieldFilter)
{
var valueMask = fieldFilter.ValueMask?.Trim();
if (string.IsNullOrEmpty(valueMask))
return query;
logger.LogDebug("Применяем FieldFilterDto: FieldId={FieldId}, ValueMask={ValueMask}, IsInverse={IsInverse}", fieldFilter.FieldId, valueMask, fieldFilter.IsInverse);
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
return query.AsEnumerable().Where(dto =>
{
var hasMatch = dto.Values.Any(v =>
v.FieldId == fieldFilter.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))
);
// Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр
return fieldFilter.IsInverse ? !hasMatch : hasMatch;
}).AsQueryable();
}
private IQueryable<UnitDto> ApplyRelationshipFilterToQuery(IQueryable<UnitDto> query, JobRelationshipFilter relFilter)
{
var valueMask = relFilter.ValueMask?.Trim();
if (string.IsNullOrWhiteSpace(valueMask))
return query;
logger.LogDebug("Применяем RelationshipFilterDto: IsParent={IsParent}, IsInverse={IsInverse}, IsFullMatch={IsFullMatch}, FieldId={FieldId}, ValueMask={ValueMask}",
relFilter.IsParent, relFilter.IsInverse, relFilter.IsFullMatch, relFilter.FieldId, valueMask);
var isParent = relFilter.IsParent;
var isInverse = relFilter.IsInverse;
var isFullMatch = relFilter.IsFullMatch;
var fieldId = relFilter.FieldId;
// Используем Trim('%') для всех случаев
var trimmedMask = valueMask.Trim('%');
return query.AsEnumerable().Where(dto =>
{
#if DEBUG
// Отладка: проверить, это нужный юнит
if (dto.Id == targetUnitId)
{
logger.LogDebug("Проверяем юнит {TargetUnitId} с {ParentCount} родителями и {ChildCount} детьми.",
targetUnitId, dto.Parents.Count, dto.Children.Count);
}
#endif
var links = isParent ? dto.Parents : dto.Children;
if (links == null || !links.Any())
{
#if DEBUG
// Отладка: юнит не имеет связей
if (dto.Id == targetUnitId)
{
logger.LogDebug("Юнит {TargetUnitId}: нет связей ({Direction}), результат фильтра: {Result}",
targetUnitId, isParent ? "Parent" : "Child", isFullMatch && isInverse);
}
#endif
// Для IsFullMatch/IsInverse — "все подходят", т.е. true
// Для остальных — false
return isFullMatch && isInverse;
}
#if DEBUG
// Отладка: проверить, какие связи у юнита
if (dto.Id == targetUnitId)
{
logger.LogDebug("Юнит {TargetUnitId}: {Count} связей ({Direction}).", targetUnitId, links.Count, isParent ? "Parent" : "Child");
foreach (var link in links)
{
logger.LogDebug(" Связь: UnitId={LinkUnitId}, Name={LinkName}, Values=[{Values}]", link.UnitId, link.Name, string.Join(", ", link.Values.Select(v => $"{v.FieldId}={v.Value}")));
}
}
#endif
if (!isFullMatch)
{
// Если IsFullMatch = false — проверяем, есть ли хотя бы одна подходящая связь
var hasMatchingLinks = links.Any(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
// Отладка: результат 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);
}
#endif
return allMatch;
}
}).AsQueryable();
}
private IEnumerable<UnitDto> ApplyRelationshipCountFilter(
IEnumerable<UnitDto> units,
Job job,
IEnumerable<JobRelationshipFilter> relationshipFilters)
{
var min = job.MinValueRelationships ?? 0;
var max = job.MaxValueRelationships ?? int.MaxValue;
var isParentDirection = job.IsParentRelationships == true;
if (min == 0 && max == int.MaxValue)
{
logger.LogDebug("RelationshipCountFilter: Min и Max не заданы — пропускаем фильтр.");
return units;
}
logger.LogDebug("RelationshipCountFilter: Min={Min}, Max={Max}, IsParent={IsParent}", min, max, isParentDirection);
// Разделяем фильтры по направлениям
var parentFilters = relationshipFilters
.Where(rf => rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask))
.ToList();
var childFilters = relationshipFilters
.Where(rf => !rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask))
.ToList();
bool hasParentFieldFilters = parentFilters.Count > 0;
bool hasChildFieldFilters = childFilters.Count > 0;
logger.LogDebug("RelationshipCountFilter: Найдено {ParentCount} фильтров по родителям и {ChildCount} фильтров по детям", parentFilters.Count, childFilters.Count);
return units.Where(dto =>
{
int matchingCount = 0;
if (isParentDirection) // Считаем родительские связи
{
var links = dto.Parents;
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 (hasParentFieldFilters)
{
// Есть фильтры по родителям → считаем только подходящие
foreach (var link in links)
{
// Проверяем, проходит ли связь все фильтры (логика как в 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 // Считаем дочерние связи
{
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;
logger.LogDebug("UnitId {UnitId}: matchingCount={Count}, Min={Min}, Max={Max}, результат фильтра: {Result}", dto.Id, matchingCount, min, max, finalResult);
return finalResult;
});
}
private async Task<List<Guid>> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter)
{
var cacheKey = $"uf_ids_{ComputeHash(filter.UnitFilter)}";
var cachedData = await cacheService.GetCachedDataAsync<CachedUnitIds>(cacheKey);
if (cachedData != null)
{
logger.LogDebug("Кэш попал для UnitFilter '{Name}': {Count} юнитов", filter.UnitFilter, cachedData.UnitIds.Count);
return cachedData.UnitIds;
}
logger.LogDebug("Кэш промахнут для UnitFilter '{Name}'. Запрашиваем из БД.", filter.UnitFilter);
var initialUnitIds = await unitService.Get().AsNoTracking()
.Where(unit => EF.Functions.Like(unit.Name, filter.UnitFilter))
.Select(u => u.Id)
.ToListAsync();
logger.LogDebug("Загружено {Count} юнитов из БД для UnitFilter '{Name}'", initialUnitIds.Count, filter.UnitFilter);
var toCache = new CachedUnitIds
{
UnitIds = initialUnitIds,
Timestamp = DateTimeOffset.UtcNow,
Source = GetType().Name,
Version = 1
};
await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1));
return initialUnitIds;
}
private static string ComputeHash(string input)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(hashedBytes).Replace('+', '-').Replace('/', '_').Substring(0, 16);
}
#endregion
}
}

View File

@@ -4,8 +4,8 @@ using PARR.Constants;
using PARR.DAL.Cache.Services.Base;
using PARR.DAL.Contracts;
using PARR.DAL.DomainModels;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes.Models;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.Services.Interfaces;

View File

@@ -1,6 +1,7 @@
using PARR.DAL.Models.Job;
using PARR.DAL.DomainServices.UnitFilterService.Models;
using PARR.DAL.Models.Job;
namespace PARR.DAL.DomainServices.Interfaces
namespace PARR.DAL.DomainServices.UnitFilterService
{
public interface IUnitFilterService
{
@@ -21,7 +22,29 @@ namespace PARR.DAL.DomainServices.Interfaces
/// <param name="takeCount">Максимальное количество Unit'ов (опционально)</param>
/// <returns>Список ID Unit'ов</returns>
Task<IEnumerable<Guid>?> GetUnitsIdByJobFilterAsync(
Models.Job.Job job,
Job job,
int? takeCount = null);
/// <summary>
/// Получает отфильтрованные Unit'ы с полной информацией о значениях полей и связях.
/// Возвращает DTO с данными для дальнейшей обработки в синхронизаторах.
/// </summary>
/// <param name="jobId">ID Job</param>
/// <param name="takeCount">Максимальное количество Unit'ов (опционально)</param>
/// <returns>Список UnitFilterResultDto или null, если Job не найден</returns>
Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(
Guid jobId,
int? takeCount = null);
/// <summary>
/// Получает отфильтрованные Unit'ы с полной информацией о значениях полей и связях.
/// Возвращает DTO с данными для дальнейшей обработки в синхронизаторах.
/// </summary>
/// <param name="job">Job с предзагруженными UnitFilters, Group, GroupType</param>
/// <param name="takeCount">Максимальное количество Unit'ов (опционально)</param>
/// <returns>Список UnitFilterResultDto</returns>
Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(
Job job,
int? takeCount = null);
/// <summary>
@@ -30,7 +53,6 @@ namespace PARR.DAL.DomainServices.Interfaces
/// </summary>
/// <param name="jobId">ID Job</param>
/// <param name="unitId">ID Unit, для которого ищутся связи</param>
/// <param name="ct">Токен отмены (опционально)</param>
/// <returns>Список имён связанных Unit'ов (уникальные, без дублей)</returns>
Task<List<string>> GetRelatedUnitNamesAsync(
Guid jobId,

View File

@@ -0,0 +1,23 @@
namespace PARR.DAL.DomainServices.UnitFilterService.Models
{
/// <summary>
/// Связанный юнит с его значениями
/// </summary>
public class RelatedUnitDto
{
/// <summary>
/// ID связанного юнита
/// </summary>
public Guid UnitId { get; set; }
/// <summary>
/// Наименование связанного юнита
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Значения полей связанного юнита
/// </summary>
public List<UnitValueDto> Values { get; set; } = new();
}
}

View File

@@ -0,0 +1,33 @@
namespace PARR.DAL.DomainServices.UnitFilterService.Models
{
/// <summary>
/// Результат фильтрации Unit'ов с полной информацией для синхронизаторов.
/// </summary>
public class UnitFilterResultDto
{
/// <summary>
/// Уникальный идентификатор юнита
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Наименование юнита
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Значения полей юнита
/// </summary>
public List<UnitValueDto> Values { get; set; } = new();
/// <summary>
/// Связанные родительские юниты
/// </summary>
public List<RelatedUnitDto> Parents { get; set; } = new();
/// <summary>
/// Связанные дочерние юниты
/// </summary>
public List<RelatedUnitDto> Children { get; set; } = new();
}
}

View File

@@ -0,0 +1,19 @@
namespace PARR.DAL.DomainServices.UnitFilterService.Models
{
/// <summary>
/// Значение поля юнита
/// </summary>
public class UnitValueDto
{
/// <summary>
/// ID поля
/// </summary>
public Guid FieldId { get; set; }
/// <summary>
/// Значение поля
/// </summary>
public string? Value { get; set; }
}
}

View File

@@ -0,0 +1,901 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.DAL.Cache.Services.Base;
using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.UnitFilterService.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
using System.Collections.Concurrent;
namespace PARR.DAL.DomainServices.UnitFilterService;
internal class UnitFilterService : IUnitFilterService
{
#if DEBUG
private readonly Guid targetUnitId = Guid.Parse("87fc4c36-1ea8-4163-983f-1605fee1de99");
#endif
private readonly ILogger<UnitFilterService> logger;
private readonly IJobService jobService;
private readonly IUnitService unitService;
private readonly IUnitInUnitService unitInUnitService;
private readonly IRedisCacheService cacheService;
private readonly IUnitInValueService unitInValueService;
public UnitFilterService(
ILogger<UnitFilterService> logger,
IJobService jobService,
IUnitService unitService,
IUnitInUnitService unitInUnitService,
IUnitInValueService unitInValueService,
IRedisCacheService cacheService
)
{
this.logger = logger;
this.jobService = jobService;
this.unitService = unitService;
this.unitInUnitService = unitInUnitService;
this.cacheService = cacheService;
this.unitInValueService = unitInValueService;
}
#region DTO классы
private class UnitInfoDto
{
public Guid Id { get; set; }
public required string Name { get; set; }
}
private class UnitDto
{
public Guid Id { get; set; }
public string Name { get; set; } = null!;
public List<UnitValueDto> Values { get; set; } = new();
public List<RelatedUnitDto> Parents { get; set; } = new();
public List<RelatedUnitDto> Children { get; set; } = new();
}
public class CachedUnitIds
{
public List<Guid> UnitIds { get; set; } = new();
public DateTimeOffset Timestamp { get; set; }
public string? Source { get; set; } = "UnitFilterService";
public int Version { get; set; } = 1;
}
#endregion
public async Task<IEnumerable<Guid>?> GetUnitsIdByJobFilterAsync(Guid jobId, int? takeCount = null)
{
var job = await jobService
.Get().AsNoTracking()
.Include(j => j.UnitFilters).ThenInclude(uf => uf.FieldFilters)
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
.FirstOrDefaultAsync(j => j.Id == jobId);
return job == null ? null : await GetUnitsIdByJobFilterAsync(job, takeCount);
}
public async Task<IEnumerable<Guid>?> GetUnitsIdByJobFilterAsync(Job job, int? takeCount = null)
{
var filteredUnits = await GetUnitsByJobFilterAsync(job, takeCount);
return filteredUnits?.Select(u => u.Id).ToList();
}
public async Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(Guid jobId, int? takeCount = null)
{
var job = await jobService
.Get().AsNoTracking()
.Include(j => j.UnitFilters).ThenInclude(uf => uf.FieldFilters)
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
.FirstOrDefaultAsync(j => j.Id == jobId);
return job == null ? null : await GetUnitsByJobFilterAsync(job, takeCount);
}
public async Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(Job job, int? takeCount = null)
{
if (job.Group == null)
throw new ArgumentNullException(nameof(job.Group), $"Job {job.Id} не содержит Group");
if (job.Group.GroupType == null)
throw new ArgumentNullException(nameof(job.Group.GroupType), $"Job {job.Id} не содержит GroupType");
if (job.UnitFilters == null || !job.UnitFilters.Any())
throw new ArgumentNullException(nameof(job.UnitFilters), $"Job {job.Id} не содержит UnitFilters");
// Загружаем имена всех юнитов заранее для оптимизации
var allUnitNames = await LoadAllUnitNamesAsync(job.UnitFilters);
var maxCount = takeCount ?? int.MaxValue;
var collectedUnits = new ConcurrentBag<UnitFilterResultDto>();
var filterNumber = 0;
foreach (var filter in job.UnitFilters)
{
filterNumber++;
if (collectedUnits.Count >= maxCount) break;
var remaining = maxCount - collectedUnits.Count;
if (remaining <= 0) break;
try
{
logger.LogDebug("Применяем фильтр #{Index} (Id={FilterId})", filterNumber, filter.Id);
// 1 Найти ID юнитов по UnitFilter (Name LIKE)
var initialUnitIds = await GetUnitIdsFromCacheOrDbAsync(filter);
logger.LogDebug("Базовый фильтр по Name '{NameFilter}' дал {Count} юнитов", filter.UnitFilter, initialUnitIds.Count);
#if DEBUG
if (initialUnitIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в initialUnitIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в initialUnitIds.", targetUnitId);
}
#endif
if (!initialUnitIds.Any())
{
logger.LogDebug("Фильтр #{Index}: 0 юнитов после UnitFilter. Пропускаем.", filterNumber);
continue;
}
// 2 Загрузить UnitValues для этих юнитов (только FieldId из FieldFilter)
var fieldFilterFieldIds = filter.FieldFilters.Select(f => f.FieldId).ToHashSet();
var unitValues = fieldFilterFieldIds.Any()
? await unitInValueService.GetByUnitIdsAndFieldIdsAsync(initialUnitIds, fieldFilterFieldIds)
: new List<UnitInValue>();
// 3 Загрузить связи
var parentRelFilters = filter.RelationshipFilters.Where(rf => rf.IsParent).ToList();
var childRelFilters = filter.RelationshipFilters.Where(rf => !rf.IsParent).ToList();
var parentLinks = await unitInUnitService.GetParentLinksByChildIdsAsync(initialUnitIds);
var childLinks = await unitInUnitService.GetChildLinksByParentIdsAsync(initialUnitIds);
// 4 ID родителей и детей
var parentUnitIds = parentLinks.Select(l => l.ParentUnitId).ToHashSet();
var childUnitIds = childLinks.Select(l => l.ChildUnitId).ToHashSet();
// 5 Загрузить родительские и дочерние юниты (Id + Name)
var parentUnits = parentUnitIds.Any()
? allUnitNames.Where(u => parentUnitIds.Contains(u.Id)).ToList()
: new List<UnitInfoDto>();
var childUnits = childUnitIds.Any()
? allUnitNames.Where(u => childUnitIds.Contains(u.Id)).ToList()
: new List<UnitInfoDto>();
// 6 UnitValues для родителей и детей (только FieldId из RelationshipFilter)
var relFilterFieldIds = filter.RelationshipFilters.Select(f => f.FieldId).ToHashSet();
var parentUnitValues = relFilterFieldIds.Any() && parentUnitIds.Any()
? await unitInValueService.GetByUnitIdsAndFieldIdsAsync(parentUnitIds, relFilterFieldIds)
: new List<UnitInValue>();
var childUnitValues = relFilterFieldIds.Any() && childUnitIds.Any()
? await unitInValueService.GetByUnitIdsAndFieldIdsAsync(childUnitIds, relFilterFieldIds)
: new List<UnitInValue>();
// 7 Сборка DTO
var unitMap = new Dictionary<Guid, UnitDto>();
var initialUnits = await unitService.Get().AsNoTracking()
.Where(u => initialUnitIds.Contains(u.Id))
.Select(u => new { u.Id, u.Name })
.ToDictionaryAsync(u => u.Id, u => u);
foreach (var id in initialUnitIds)
{
if (initialUnits.TryGetValue(id, out var unitInfo))
{
unitMap[id] = new UnitDto { Id = id, Name = unitInfo.Name };
}
}
// UnitValues → Unit
foreach (var uv in unitValues)
{
if (unitMap.TryGetValue(uv.UnitId, out var dto))
{
dto.Values.Add(new UnitValueDto { FieldId = uv.FieldId, Value = uv.Value?.Value });
}
}
var parentIdMap = parentUnits.ToDictionary(u => u.Id, u => u.Name);
var childIdMap = childUnits.ToDictionary(u => u.Id, u => u.Name);
// ParentLinks → Parent
foreach (var link in parentLinks)
{
if (unitMap.TryGetValue(link.ChildUnitId, out var dto))
{
dto.Parents.Add(new RelatedUnitDto
{
UnitId = link.ParentUnitId,
Name = parentIdMap.GetValueOrDefault(link.ParentUnitId, "")
});
}
}
// ChildLinks → Child
foreach (var link in childLinks)
{
if (unitMap.TryGetValue(link.ParentUnitId, out var dto))
{
dto.Children.Add(new RelatedUnitDto
{
UnitId = link.ChildUnitId,
Name = childIdMap.GetValueOrDefault(link.ChildUnitId, "")
});
}
}
// Привязка значений к родителям
var parentValuesMap = parentUnitValues
.GroupBy(v => v.UnitId)
.ToDictionary(g => g.Key, g => g.Select(v => new UnitValueDto { FieldId = v.FieldId, Value = v.Value?.Value }).ToList());
foreach (var dto in unitMap.Values)
{
foreach (var parent in dto.Parents)
{
if (parentValuesMap.TryGetValue(parent.UnitId, out var values))
{
parent.Values.AddRange(values);
}
}
}
// Привязка значений к детям
var childValuesMap = childUnitValues
.GroupBy(v => v.UnitId)
.ToDictionary(g => g.Key, g => g.Select(v => new UnitValueDto { FieldId = v.FieldId, Value = v.Value?.Value }).ToList());
foreach (var dto in unitMap.Values)
{
foreach (var child in dto.Children)
{
if (childValuesMap.TryGetValue(child.UnitId, out var values))
{
child.Values.AddRange(values);
}
}
}
// 8 Применение фильтров
var candidateUnits = unitMap.Values.AsQueryable();
foreach (var fieldFilter in filter.FieldFilters)
{
candidateUnits = ApplyFieldFilter(candidateUnits, fieldFilter);
}
#if DEBUG
var candidateListAfterFieldFilter = candidateUnits.ToList();
if (candidateListAfterFieldFilter.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в candidateUnits после FieldFilter.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после FieldFilter.", targetUnitId);
}
logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateListAfterFieldFilter.Count);
#endif
foreach (var relFilter in filter.RelationshipFilters)
{
candidateUnits = ApplyRelationshipFilterToQuery(candidateUnits, relFilter);
}
#if DEBUG
var candidateListAfterRelFilter = candidateUnits.ToList();
if (candidateListAfterRelFilter.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в candidateUnits после RelationshipFilter.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в candidateUnits после RelationshipFilter.", targetUnitId);
}
logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateListAfterRelFilter.Count);
#endif
// 9 Umbrella-фильтр
var finalUnits = candidateUnits.AsEnumerable();
if (job.Group.GroupType.Code == JobGroupTypesEnum.Umbrella)
{
finalUnits = ApplyRelationshipCountFilter(finalUnits, job, filter.RelationshipFilters);
logger.LogDebug("После Umbrella-фильтра осталось {Count} юнитов", finalUnits.Count());
#if DEBUG
if (finalUnits.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в finalUnits после Umbrella-фильтра.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в finalUnits после Umbrella-фильтра.", targetUnitId);
}
#endif
}
// 10 Взять DTO и преобразовать в результат
var newUnits = finalUnits
.Select(u => CreateResultDto(u))
.Take(remaining)
.ToList();
#if DEBUG
if (newUnits.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в newUnits.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в newUnits.", targetUnitId);
}
#endif
foreach (var unit in newUnits)
{
collectedUnits.Add(unit);
}
logger.LogDebug("Фильтр #{Index}: найдено {Count} Unit'ов. Всего: {Total}",
filterNumber, newUnits.Count, collectedUnits.Count);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при обработке фильтра {FilterId} (#{Index}) для Job {JobId}",
filter.Id, filterNumber, job.Id);
}
}
var result = collectedUnits.Take(maxCount).ToList();
logger.LogInformation("Job {JobId}: из {FilterCount} фильтров получено {UnitCount} уникальных Unit'ов",
job.Id, job.UnitFilters.Count, result.Count);
#if DEBUG
if (result.Any(u => u.Id == targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в финальном результате.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в финальном результате.", targetUnitId);
}
#endif
return result;
}
private async Task<List<UnitInfoDto>> LoadAllUnitNamesAsync(IEnumerable<JobUnitFilter> filters)
{
var unitIds = new HashSet<Guid>();
foreach (var filter in filters)
{
var ids = await GetUnitIdsFromCacheOrDbAsync(filter);
foreach (var id in ids)
{
unitIds.Add(id);
}
}
return await unitService.Get().AsNoTracking()
.Where(u => unitIds.Contains(u.Id))
.Select(u => new UnitInfoDto { Id = u.Id, Name = u.Name })
.ToListAsync();
}
private UnitFilterResultDto CreateResultDto(UnitDto source)
{
return new UnitFilterResultDto
{
Id = source.Id,
Name = source.Name,
Values = new List<UnitValueDto>(source.Values),
Parents = new List<RelatedUnitDto>(source.Parents),
Children = new List<RelatedUnitDto>(source.Children)
};
}
public async Task<List<string>> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId)
{
logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId);
var job = await jobService
.Get().AsNoTracking()
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == jobId);
if (job == null)
{
logger.LogWarning("Job с Id {JobId} не найден.", jobId);
throw new ArgumentException($"Job {jobId} не найден.", nameof(jobId));
}
logger.LogDebug("Найден Job: {JobName}. Количество UnitFilters: {FilterCount}", job.Name, job.UnitFilters.Count());
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var filter in job.UnitFilters)
{
if (!filter.RelationshipFilters.Any()) continue;
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, участвующие в связях
var allRelatedUnitIds = parentLinks
.Select(l => l.ParentUnitId)
.Concat(childLinks.Select(l => l.ChildUnitId))
.Distinct()
.ToList();
if (!allRelatedUnitIds.Any()) continue;
// Получить значения для всех связанных юнитов
var allUnitValues = await unitInValueService.GetByUnitIdsAsync(allRelatedUnitIds);
// Сгруппировать значения по UnitId
var valuesByUnit = allUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
// Найти UnitId, которые проходят все RelationshipFilters
var matchingUnitIds = new HashSet<Guid>();
foreach (var relatedUnitId in allRelatedUnitIds)
{
// Изменяем на `All` — все фильтры должны подходить
bool passesAllFilters = filter.RelationshipFilters.All(rf =>
{
var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());
var matchingValues = values
.Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null)
.ToList();
if (!matchingValues.Any()) // Нет значений по полю
{
// Если нет значений, и IsInverse = false → не подходит
// Если нет значений, и IsInverse = true → подходит
return rf.IsInverse;
}
var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase));
// Учитываем IsInverse
if (rf.IsInverse)
hasMatch = !hasMatch;
return hasMatch;
});
if (passesAllFilters)
matchingUnitIds.Add(relatedUnitId);
}
if (matchingUnitIds.Any())
{
var names = await unitService.Get().AsNoTracking()
.Where(u => matchingUnitIds.Contains(u.Id))
.Select(u => u.Name)
.ToListAsync();
result.UnionWith(names);
}
}
return result.ToList();
}
#region вспомогательные методы
private IQueryable<UnitDto> ApplyFieldFilter(IQueryable<UnitDto> query, JobFieldFilter fieldFilter)
{
var valueMask = fieldFilter.ValueMask?.Trim();
if (string.IsNullOrEmpty(valueMask))
return query;
logger.LogDebug("Применяем FieldFilterDto: FieldId={FieldId}, ValueMask={ValueMask}, IsInverse={IsInverse}", fieldFilter.FieldId, valueMask, fieldFilter.IsInverse);
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
return query.AsEnumerable().Where(dto =>
{
var hasMatch = dto.Values.Any(v =>
v.FieldId == fieldFilter.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))
);
// Правильная логика: если IsInverse = true, то юнит подходит, если НЕ проходит фильтр
return fieldFilter.IsInverse ? !hasMatch : hasMatch;
}).AsQueryable();
}
private IQueryable<UnitDto> ApplyRelationshipFilterToQuery(IQueryable<UnitDto> query, JobRelationshipFilter relFilter)
{
var valueMask = relFilter.ValueMask?.Trim();
if (string.IsNullOrWhiteSpace(valueMask))
return query;
logger.LogDebug("Применяем RelationshipFilterDto: IsParent={IsParent}, IsInverse={IsInverse}, IsFullMatch={IsFullMatch}, FieldId={FieldId}, ValueMask={ValueMask}",
relFilter.IsParent, relFilter.IsInverse, relFilter.IsFullMatch, relFilter.FieldId, valueMask);
var isParent = relFilter.IsParent;
var isInverse = relFilter.IsInverse;
var isFullMatch = relFilter.IsFullMatch;
var fieldId = relFilter.FieldId;
// Используем Trim('%') для всех случаев
var trimmedMask = valueMask.Trim('%');
return query.AsEnumerable().Where(dto =>
{
#if DEBUG
// Отладка: проверить, это нужный юнит
if (dto.Id == targetUnitId)
{
logger.LogDebug("Проверяем юнит {TargetUnitId} с {ParentCount} родителями и {ChildCount} детьми.",
targetUnitId, dto.Parents.Count, dto.Children.Count);
}
#endif
var links = isParent ? dto.Parents : dto.Children;
if (links == null || !links.Any())
{
#if DEBUG
// Отладка: юнит не имеет связей
if (dto.Id == targetUnitId)
{
logger.LogDebug("Юнит {TargetUnitId}: нет связей ({Direction}), результат фильтра: {Result}",
targetUnitId, isParent ? "Parent" : "Child", isFullMatch && isInverse);
}
#endif
// Для IsFullMatch/IsInverse — "все подходят", т.е. true
// Для остальных — false
return isFullMatch && isInverse;
}
#if DEBUG
// Отладка: проверить, какие связи у юнита
if (dto.Id == targetUnitId)
{
logger.LogDebug("Юнит {TargetUnitId}: {Count} связей ({Direction}).", targetUnitId, links.Count, isParent ? "Parent" : "Child");
foreach (var link in links)
{
logger.LogDebug(" Связь: UnitId={LinkUnitId}, Name={LinkName}, Values=[{Values}]", link.UnitId, link.Name, string.Join(", ", link.Values.Select(v => $"{v.FieldId}={v.Value}")));
}
}
#endif
if (!isFullMatch)
{
// Если IsFullMatch = false — проверяем, есть ли хотя бы одна подходящая связь
var hasMatchingLinks = links.Any(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
// Отладка: результат 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);
}
#endif
return allMatch;
}
}).AsQueryable();
}
private IEnumerable<UnitDto> ApplyRelationshipCountFilter(
IEnumerable<UnitDto> units,
Job job,
IEnumerable<JobRelationshipFilter> relationshipFilters)
{
var min = job.MinValueRelationships ?? 0;
var max = job.MaxValueRelationships ?? int.MaxValue;
var isParentDirection = job.IsParentRelationships == true;
if (min == 0 && max == int.MaxValue)
{
logger.LogDebug("RelationshipCountFilter: Min и Max не заданы — пропускаем фильтр.");
return units;
}
logger.LogDebug("RelationshipCountFilter: Min={Min}, Max={Max}, IsParent={IsParent}", min, max, isParentDirection);
// Разделяем фильтры по направлениям
var parentFilters = relationshipFilters
.Where(rf => rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask))
.ToList();
var childFilters = relationshipFilters
.Where(rf => !rf.IsParent && !string.IsNullOrWhiteSpace(rf.ValueMask))
.ToList();
bool hasParentFieldFilters = parentFilters.Count > 0;
bool hasChildFieldFilters = childFilters.Count > 0;
logger.LogDebug("RelationshipCountFilter: Найдено {ParentCount} фильтров по родителям и {ChildCount} фильтров по детям", parentFilters.Count, childFilters.Count);
return units.Where(dto =>
{
int matchingCount = 0;
if (isParentDirection) // Считаем родительские связи
{
var links = dto.Parents;
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 (hasParentFieldFilters)
{
// Есть фильтры по родителям → считаем только подходящие
foreach (var link in links)
{
// Проверяем, проходит ли связь все фильтры (логика как в 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 // Считаем дочерние связи
{
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;
logger.LogDebug("UnitId {UnitId}: matchingCount={Count}, Min={Min}, Max={Max}, результат фильтра: {Result}", dto.Id, matchingCount, min, max, finalResult);
return finalResult;
});
}
private async Task<List<Guid>> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter)
{
var cacheKey = $"uf_ids_{ComputeHash(filter.UnitFilter)}";
var cachedData = await cacheService.GetCachedDataAsync<CachedUnitIds>(cacheKey);
if (cachedData != null)
{
logger.LogDebug("Кэш попал для UnitFilter '{Name}': {Count} юнитов", filter.UnitFilter, cachedData.UnitIds.Count);
return cachedData.UnitIds;
}
logger.LogDebug("Кэш промахнут для UnitFilter '{Name}'. Запрашиваем из БД.", filter.UnitFilter);
var initialUnitIds = await unitService.Get().AsNoTracking()
.Where(unit => EF.Functions.Like(unit.Name, filter.UnitFilter))
.Select(u => u.Id)
.ToListAsync();
logger.LogDebug("Загружено {Count} юнитов из БД для UnitFilter '{Name}'", initialUnitIds.Count, filter.UnitFilter);
var toCache = new CachedUnitIds
{
UnitIds = initialUnitIds,
Timestamp = DateTimeOffset.UtcNow,
Source = GetType().Name,
Version = 1
};
await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1));
return initialUnitIds;
}
private static string ComputeHash(string input)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(hashedBytes).Replace('+', '-').Replace('/', '_').Substring(0, 16);
}
#endregion
}

View File

@@ -8,6 +8,7 @@ using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.Implementations;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.InfluxDbServices;
using PARR.DAL.NextRunServices;
using PARR.DAL.NextRunServices.Subservices;
@@ -22,7 +23,6 @@ using PARR.DAL.Services.Interfaces.Schedule;
using PARR.DAL.Services.Interfaces.Unit;
using PARR.DAL.Settings;
using PARR.DAL.TransformServices;
using System.ComponentModel;
namespace PARR.DAL
{

View File

@@ -6,6 +6,7 @@ using PARR.Common.Domain;
using PARR.Constants;
using PARR.DAL.Cache.Models;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit;
@@ -170,15 +171,18 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
// 3. Использовать фильтры maxJob для получения expectedUnitIds
var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id);
if (expectedUnitIds == null || !expectedUnitIds.Any())
// 3. Использовать фильтры maxJob для получения отфильтрованных юнитов
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
if (filteredUnits == null || !filteredUnits.Any())
{
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов");
return;
}
// Извлекаем ID юнитов для последующих операций
var expectedUnitIds = filteredUnits.Select(u => u.Id).ToList();
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId
if (!jobGroup.GroupingUnitFieldId.HasValue)
{
@@ -188,15 +192,14 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
var unitIdsWithValidGroupingFieldSet = await unitService.Get()
.AsNoTracking()
.Where(u => expectedUnitIds.Contains(u.Id))
.Where(u => u.UnitValues.Any(uv =>
uv.FieldId == groupingFieldId &&
uv.Value != null &&
!string.IsNullOrEmpty(uv.Value.Value)))
// Фильтруем юниты по GroupingUnitFieldId используя данные из DTO
var unitIdsWithValidGroupingFieldSet = filteredUnits
.Where(u => u.Values.Any(v =>
v.FieldId == groupingFieldId &&
!string.IsNullOrEmpty(v.Value)))
.Select(u => u.Id)
.ToListAsync();
.ToList();
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
@@ -208,15 +211,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}
// Аналогично для фильтрации по "РАБОЧАЯ_ГР_ОТВ_ЗАК"
var unitIdsWithValidWorkGroupFieldSet = await unitService.Get()
.AsNoTracking()
.Where(u => unitIdsWithValidGroupingFieldSet.Contains(u.Id))
.Where(u => u.UnitValues.Any(uv =>
uv.FieldId == workGroupFieldId &&
uv.Value != null &&
relationshipGroupValueIds.Contains(uv.Value.Id)))
var unitIdsWithValidWorkGroupFieldSet = filteredUnits
.Where(u => u.Values.Any(v =>
v.FieldId == workGroupFieldId &&
relationshipGroupValueIds.Contains(v.FieldId) &&
!string.IsNullOrEmpty(v.Value)))
.Select(u => u.Id)
.ToListAsync();
.ToList();
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count);
@@ -233,6 +234,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию...");
// Используем unitIdsWithValidGroupingFieldSet для получения связей
var potentialUnitInUnitLinks = await unitInUnitService.Get()
.AsNoTracking()
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
@@ -240,30 +243,26 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count);
// Подготавливаем данные для фильтрации связей
var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet();
var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet();
var parentUnitValues = await unitInValueService.Get()
.AsNoTracking()
.Include(uv => uv.Field)
.Include(uv => uv.Value)
.Where(uv => allParentIds.Contains(uv.UnitId))
.ToListAsync();
// Фильтруем юниты для получения родительских и дочерних значений
var parentUnits = filteredUnits.Where(u => allParentIds.Contains(u.Id)).ToList();
var childUnits = filteredUnits.Where(u => allChildIds.Contains(u.Id)).ToList();
var childUnitValues = await unitInValueService.Get()
.AsNoTracking()
.Include(uv => uv.Field)
.Include(uv => uv.Value)
.Where(uv => allChildIds.Contains(uv.UnitId))
.ToListAsync();
// Создаем словари значений для родителей и детей
var parentValuesMap = parentUnits
.ToDictionary(
u => u.Id,
u => u.Values.ToDictionary(v => v.FieldId, v => v.Value)
);
var parentValuesMap = parentUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
var childValuesMap = childUnitValues
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
var childValuesMap = childUnits
.ToDictionary(
u => u.Id,
u => u.Values.ToDictionary(v => v.FieldId, v => v.Value)
);
logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count);
@@ -273,15 +272,37 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
bool linkMatchesAllFilters = true;
foreach (var rf in relationshipFilters)
{
var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List<UnitInValue>()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List<UnitInValue>());
bool filterMatch = valuesToCheck.Any(uv =>
uv.FieldId == rf.FieldId &&
uv.Value != null &&
uv.Value.Value != null &&
uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase)
);
bool filterMatch;
if (rf.IsParent)
{
// Родительские значения
if (parentValuesMap.TryGetValue(link.ParentUnitId, out var parentValues) &&
parentValues.TryGetValue(rf.FieldId, out var parentValue) &&
parentValue != null)
{
filterMatch = parentValue.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase);
}
else
{
filterMatch = false;
}
}
else
{
// Дочерние значения
if (childValuesMap.TryGetValue(link.ChildUnitId, out var childValues) &&
childValues.TryGetValue(rf.FieldId, out var childValue) &&
childValue != null)
{
filterMatch = childValue.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase);
}
else
{
filterMatch = false;
}
}
// Учитываем инверсию фильтра
if (rf.IsInverse)
filterMatch = !filterMatch;
@@ -350,9 +371,10 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.Distinct()
.ToList();
var unitNamesMap = await unitService.Get()
// Создаем словарь имен для конфликтующих ключей из отфильтрованных юнитов
var unitNamesMap = filteredUnits
.Where(u => conflictKeys.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name ?? string.Empty);
.ToDictionary(u => u.Id, u => u.Name);
foreach (var conflictedUnitEntry in unitToKeys.Where(kvp => kvp.Value.Count > 1))
{
@@ -404,10 +426,10 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count);
var childUnitNameMap = await unitService.Get()
.AsNoTracking()
// Получаем имена юнитов из отфильтрованных данных
var childUnitNameMap = filteredUnits
.Where(u => childUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name);
.ToDictionary(u => u.Id, u => u.Name);
var sortedChildUnitIds = childUnitIds
.OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString()))
@@ -454,10 +476,11 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
// === 2. Сравниваем детерминированно с сортировкой по имени ===
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
var unitNamesForSort = await unitService.Get()
.AsNoTracking()
// Получаем имена юнитов из отфильтрованных данных
var unitNamesForSort = filteredUnits
.Where(u => allUnitIdsForSort.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString());
.ToDictionary(u => u.Id, u => u.Name);
var sortedCurrentUnitIds = currentUnitIds
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
@@ -566,10 +589,12 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
var relationshipUnitId = kvp.Key;
var childUnitIds = kvp.Value;
var childUnitNameMapForDeactivate = await unitService.Get()
.AsNoTracking()
// Получаем имена юнитов из отфильтрованных данных
var childUnitNameMapForDeactivate = filteredUnits
.Where(u => childUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name);
.ToDictionary(u => u.Id, u => u.Name);
var sortedChildUnitIdsForDeactivate = childUnitIds
.OrderBy(id => childUnitNameMapForDeactivate.GetValueOrDefault(id, id.ToString()))
.ToList();

View File

@@ -8,6 +8,7 @@ using PARR.DAL.Cache.Models;
using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.Services.Interfaces;
@@ -137,8 +138,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return;
}
var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId);
if (unitIds == null || !unitIds.Any())
// === Получение отфильтрованных юнитов с полной информацией ===
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
if (filteredUnits == null || !filteredUnits.Any())
{
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
@@ -162,6 +164,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return;
}
// Извлекаем ID юнитов для последующих операций
var unitIds = filteredUnits.Select(u => u.Id).ToList();
#if DEBUG
// Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(targetUnitId))
@@ -198,26 +203,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
.Where(t => !unitIds.Contains(t.UnitId))
.ToList();
// === Подсчёт операций ===
int toCreate = newUnitIds.Count;
int toUpdate = 0;
int toDeactivate = unusedTemplates.Count;
// Подсчёт обновлений по имени
foreach (var template in existingUsedTemplates)
{
if (unitIds.Contains(template.UnitId))
{
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
toUpdate++;
}
}
await UpdateMatchingStatusAsync(jobId, $"Осталось: создать={toCreate}, обновить={toUpdate}, деактивировать={toDeactivate}");
// === Обработка новых юнитов ===
int created = 0, updated = 0, deactivated = 0;
foreach (var unitId in newUnitIds)
{
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
@@ -262,8 +247,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
await CreateSimpleTemplateAsync(jobId, unitId, initiator);
}
created++;
await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
}
// === Обработка существующих шаблонов (проверка имени) ===
@@ -295,8 +278,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
updated++;
await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
}
}
}
@@ -306,8 +287,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
deactivated++;
await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
}
// === Успешное завершение ===
@@ -372,8 +351,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return;
}
var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId);
if (unitIds == null || !unitIds.Any())
// === Получение отфильтрованных юнитов с полной информацией ===
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
if (filteredUnits == null || !filteredUnits.Any())
{
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется");
@@ -381,6 +361,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return;
}
// Извлекаем ID юнитов для последующих операций
var unitIds = filteredUnits.Select(u => u.Id).ToList();
#if DEBUG
// Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(targetUnitId))