feat(dal, templateMatcher): UnitFilterService теперь возвращает модель Dto а не только Id
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
901
PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs
Normal file
901
PARR.DAL/DomainServices/UnitFilterService/UnitFilterService.cs
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user