feat(dal, templateMatcher): UnitFilterService теперь возвращает модель Dto а не только Id
This commit is contained in:
@@ -20,6 +20,7 @@ using PARR.DAL.Cache.Services;
|
|||||||
using PARR.DAL.Contracts;
|
using PARR.DAL.Contracts;
|
||||||
using PARR.DAL.DomainModels;
|
using PARR.DAL.DomainModels;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
using PARR.DAL.DomainServices.Interfaces;
|
||||||
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
using PARR.DAL.Extensions;
|
using PARR.DAL.Extensions;
|
||||||
using PARR.DAL.Models.Job;
|
using PARR.DAL.Models.Job;
|
||||||
using PARR.DAL.Services.Interfaces;
|
using PARR.DAL.Services.Interfaces;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using PARR.API.Contracts.V1.Responses;
|
|||||||
using PARR.API.Contracts.V1.Responses.Base;
|
using PARR.API.Contracts.V1.Responses.Base;
|
||||||
using PARR.API.Controllers.V1.Base;
|
using PARR.API.Controllers.V1.Base;
|
||||||
using PARR.Constants;
|
using PARR.Constants;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
using PARR.DAL.Services.Interfaces.Job;
|
using PARR.DAL.Services.Interfaces.Job;
|
||||||
using PARR.DAL.Services.Interfaces.Unit;
|
using PARR.DAL.Services.Interfaces.Unit;
|
||||||
using Job = PARR.DAL.Models.Job.Job;
|
using Job = PARR.DAL.Models.Job.Job;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using PARR.API.Services.Interfaces;
|
|||||||
using PARR.DAL.Cache.Services.Base;
|
using PARR.DAL.Cache.Services.Base;
|
||||||
using PARR.DAL.Contracts;
|
using PARR.DAL.Contracts;
|
||||||
using PARR.DAL.DomainModels;
|
using PARR.DAL.DomainModels;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
using PARR.DAL.Models.Job;
|
using PARR.DAL.Models.Job;
|
||||||
using PARR.DAL.Services.Interfaces;
|
using PARR.DAL.Services.Interfaces;
|
||||||
using PARR.DAL.Services.Interfaces.Job;
|
using PARR.DAL.Services.Interfaces.Job;
|
||||||
|
|||||||
@@ -3,17 +3,14 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.DAL.Context;
|
using PARR.DAL.Context;
|
||||||
using PARR.DAL.DomainServices.Implementations;
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
|
|
||||||
// Предположим, что у вас есть реализации IJobService и IUnitService
|
// Предположим, что у вас есть реализации IJobService и IUnitService
|
||||||
// Если нет, можно создать моки, но лучше использовать реальные, если они просты
|
// Если нет, можно создать моки, но лучше использовать реальные, если они просты
|
||||||
using PARR.DAL.Services.Implementations.Job; // Пример: JobService
|
using PARR.DAL.Services.Implementations.Job; // Пример: JobService
|
||||||
using PARR.DAL.Services.Implementations.Unit; // Пример: UnitService
|
using PARR.DAL.Services.Implementations.Unit; // Пример: UnitService
|
||||||
using PARR.DAL.Services.Interfaces.Job;
|
using PARR.DAL.Services.Interfaces.Job;
|
||||||
using PARR.DAL.Services.Interfaces.Unit;
|
using PARR.DAL.Services.Interfaces.Unit;
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace PARR.DAL.IntegrationTests.Integration
|
namespace PARR.DAL.IntegrationTests.Integration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MockQueryable;
|
|
||||||
using Moq;
|
using Moq;
|
||||||
using PARR.DAL.Contracts;
|
using PARR.DAL.Contracts;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
|
||||||
using PARR.DAL.DomainServices.Shortcodes;
|
using PARR.DAL.DomainServices.Shortcodes;
|
||||||
using PARR.DAL.Models.Job;
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
using PARR.DAL.Models.Unit;
|
|
||||||
using PARR.DAL.Services.Interfaces.Job;
|
using PARR.DAL.Services.Interfaces.Job;
|
||||||
using PARR.DAL.Services.Interfaces.Unit;
|
using PARR.DAL.Services.Interfaces.Unit;
|
||||||
|
|
||||||
|
|||||||
@@ -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.Cache.Services.Base;
|
||||||
using PARR.DAL.Contracts;
|
using PARR.DAL.Contracts;
|
||||||
using PARR.DAL.DomainModels;
|
using PARR.DAL.DomainModels;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
|
||||||
using PARR.DAL.DomainServices.Shortcodes.Models;
|
using PARR.DAL.DomainServices.Shortcodes.Models;
|
||||||
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
using PARR.DAL.Models;
|
using PARR.DAL.Models;
|
||||||
using PARR.DAL.Models.Job;
|
using PARR.DAL.Models.Job;
|
||||||
using PARR.DAL.Services.Interfaces;
|
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
|
public interface IUnitFilterService
|
||||||
{
|
{
|
||||||
@@ -21,7 +22,29 @@ namespace PARR.DAL.DomainServices.Interfaces
|
|||||||
/// <param name="takeCount">Максимальное количество Unit'ов (опционально)</param>
|
/// <param name="takeCount">Максимальное количество Unit'ов (опционально)</param>
|
||||||
/// <returns>Список ID Unit'ов</returns>
|
/// <returns>Список ID Unit'ов</returns>
|
||||||
Task<IEnumerable<Guid>?> GetUnitsIdByJobFilterAsync(
|
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);
|
int? takeCount = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -30,7 +53,6 @@ namespace PARR.DAL.DomainServices.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="jobId">ID Job</param>
|
/// <param name="jobId">ID Job</param>
|
||||||
/// <param name="unitId">ID Unit, для которого ищутся связи</param>
|
/// <param name="unitId">ID Unit, для которого ищутся связи</param>
|
||||||
/// <param name="ct">Токен отмены (опционально)</param>
|
|
||||||
/// <returns>Список имён связанных Unit'ов (уникальные, без дублей)</returns>
|
/// <returns>Список имён связанных Unit'ов (уникальные, без дублей)</returns>
|
||||||
Task<List<string>> GetRelatedUnitNamesAsync(
|
Task<List<string>> GetRelatedUnitNamesAsync(
|
||||||
Guid jobId,
|
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.Implementations;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
using PARR.DAL.DomainServices.Interfaces;
|
||||||
using PARR.DAL.DomainServices.Shortcodes;
|
using PARR.DAL.DomainServices.Shortcodes;
|
||||||
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
using PARR.DAL.InfluxDbServices;
|
using PARR.DAL.InfluxDbServices;
|
||||||
using PARR.DAL.NextRunServices;
|
using PARR.DAL.NextRunServices;
|
||||||
using PARR.DAL.NextRunServices.Subservices;
|
using PARR.DAL.NextRunServices.Subservices;
|
||||||
@@ -22,7 +23,6 @@ using PARR.DAL.Services.Interfaces.Schedule;
|
|||||||
using PARR.DAL.Services.Interfaces.Unit;
|
using PARR.DAL.Services.Interfaces.Unit;
|
||||||
using PARR.DAL.Settings;
|
using PARR.DAL.Settings;
|
||||||
using PARR.DAL.TransformServices;
|
using PARR.DAL.TransformServices;
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace PARR.DAL
|
namespace PARR.DAL
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using PARR.Common.Domain;
|
|||||||
using PARR.Constants;
|
using PARR.Constants;
|
||||||
using PARR.DAL.Cache.Models;
|
using PARR.DAL.Cache.Models;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
using PARR.DAL.DomainServices.Interfaces;
|
||||||
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
using PARR.DAL.Models;
|
using PARR.DAL.Models;
|
||||||
using PARR.DAL.Models.Job;
|
using PARR.DAL.Models.Job;
|
||||||
using PARR.DAL.Models.Unit;
|
using PARR.DAL.Models.Unit;
|
||||||
@@ -170,15 +171,18 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
|
|
||||||
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
|
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
|
||||||
|
|
||||||
// 3. Использовать фильтры maxJob для получения expectedUnitIds
|
// 3. Использовать фильтры maxJob для получения отфильтрованных юнитов
|
||||||
var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id);
|
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
|
||||||
if (expectedUnitIds == null || !expectedUnitIds.Any())
|
if (filteredUnits == null || !filteredUnits.Any())
|
||||||
{
|
{
|
||||||
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId);
|
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId);
|
||||||
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов");
|
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Извлекаем ID юнитов для последующих операций
|
||||||
|
var expectedUnitIds = filteredUnits.Select(u => u.Id).ToList();
|
||||||
|
|
||||||
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId
|
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId
|
||||||
if (!jobGroup.GroupingUnitFieldId.HasValue)
|
if (!jobGroup.GroupingUnitFieldId.HasValue)
|
||||||
{
|
{
|
||||||
@@ -188,15 +192,14 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
|
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
|
||||||
var unitIdsWithValidGroupingFieldSet = await unitService.Get()
|
|
||||||
.AsNoTracking()
|
// Фильтруем юниты по GroupingUnitFieldId используя данные из DTO
|
||||||
.Where(u => expectedUnitIds.Contains(u.Id))
|
var unitIdsWithValidGroupingFieldSet = filteredUnits
|
||||||
.Where(u => u.UnitValues.Any(uv =>
|
.Where(u => u.Values.Any(v =>
|
||||||
uv.FieldId == groupingFieldId &&
|
v.FieldId == groupingFieldId &&
|
||||||
uv.Value != null &&
|
!string.IsNullOrEmpty(v.Value)))
|
||||||
!string.IsNullOrEmpty(uv.Value.Value)))
|
|
||||||
.Select(u => u.Id)
|
.Select(u => u.Id)
|
||||||
.ToListAsync();
|
.ToList();
|
||||||
|
|
||||||
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
|
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
|
||||||
|
|
||||||
@@ -208,15 +211,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Аналогично для фильтрации по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"
|
// Аналогично для фильтрации по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"
|
||||||
var unitIdsWithValidWorkGroupFieldSet = await unitService.Get()
|
var unitIdsWithValidWorkGroupFieldSet = filteredUnits
|
||||||
.AsNoTracking()
|
.Where(u => u.Values.Any(v =>
|
||||||
.Where(u => unitIdsWithValidGroupingFieldSet.Contains(u.Id))
|
v.FieldId == workGroupFieldId &&
|
||||||
.Where(u => u.UnitValues.Any(uv =>
|
relationshipGroupValueIds.Contains(v.FieldId) &&
|
||||||
uv.FieldId == workGroupFieldId &&
|
!string.IsNullOrEmpty(v.Value)))
|
||||||
uv.Value != null &&
|
|
||||||
relationshipGroupValueIds.Contains(uv.Value.Id)))
|
|
||||||
.Select(u => u.Id)
|
.Select(u => u.Id)
|
||||||
.ToListAsync();
|
.ToList();
|
||||||
|
|
||||||
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count);
|
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>();
|
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
|
||||||
|
|
||||||
logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию...");
|
logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию...");
|
||||||
|
|
||||||
|
// Используем unitIdsWithValidGroupingFieldSet для получения связей
|
||||||
var potentialUnitInUnitLinks = await unitInUnitService.Get()
|
var potentialUnitInUnitLinks = await unitInUnitService.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
|
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
|
||||||
@@ -240,30 +243,26 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
|
|
||||||
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count);
|
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count);
|
||||||
|
|
||||||
|
// Подготавливаем данные для фильтрации связей
|
||||||
var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet();
|
var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet();
|
||||||
var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet();
|
var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet();
|
||||||
|
|
||||||
var parentUnitValues = await unitInValueService.Get()
|
// Фильтруем юниты для получения родительских и дочерних значений
|
||||||
.AsNoTracking()
|
var parentUnits = filteredUnits.Where(u => allParentIds.Contains(u.Id)).ToList();
|
||||||
.Include(uv => uv.Field)
|
var childUnits = filteredUnits.Where(u => allChildIds.Contains(u.Id)).ToList();
|
||||||
.Include(uv => uv.Value)
|
|
||||||
.Where(uv => allParentIds.Contains(uv.UnitId))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var childUnitValues = await unitInValueService.Get()
|
// Создаем словари значений для родителей и детей
|
||||||
.AsNoTracking()
|
var parentValuesMap = parentUnits
|
||||||
.Include(uv => uv.Field)
|
.ToDictionary(
|
||||||
.Include(uv => uv.Value)
|
u => u.Id,
|
||||||
.Where(uv => allChildIds.Contains(uv.UnitId))
|
u => u.Values.ToDictionary(v => v.FieldId, v => v.Value)
|
||||||
.ToListAsync();
|
);
|
||||||
|
|
||||||
var parentValuesMap = parentUnitValues
|
var childValuesMap = childUnits
|
||||||
.GroupBy(uv => uv.UnitId)
|
.ToDictionary(
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
u => u.Id,
|
||||||
|
u => u.Values.ToDictionary(v => v.FieldId, v => v.Value)
|
||||||
var childValuesMap = childUnitValues
|
);
|
||||||
.GroupBy(uv => uv.UnitId)
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
|
||||||
|
|
||||||
logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count);
|
logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count);
|
||||||
|
|
||||||
@@ -273,15 +272,37 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
bool linkMatchesAllFilters = true;
|
bool linkMatchesAllFilters = true;
|
||||||
foreach (var rf in relationshipFilters)
|
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;
|
||||||
|
if (rf.IsParent)
|
||||||
bool filterMatch = valuesToCheck.Any(uv =>
|
{
|
||||||
uv.FieldId == rf.FieldId &&
|
// Родительские значения
|
||||||
uv.Value != null &&
|
if (parentValuesMap.TryGetValue(link.ParentUnitId, out var parentValues) &&
|
||||||
uv.Value.Value != null &&
|
parentValues.TryGetValue(rf.FieldId, out var parentValue) &&
|
||||||
uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase)
|
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)
|
if (rf.IsInverse)
|
||||||
filterMatch = !filterMatch;
|
filterMatch = !filterMatch;
|
||||||
|
|
||||||
@@ -350,9 +371,10 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var unitNamesMap = await unitService.Get()
|
// Создаем словарь имен для конфликтующих ключей из отфильтрованных юнитов
|
||||||
|
var unitNamesMap = filteredUnits
|
||||||
.Where(u => conflictKeys.Contains(u.Id))
|
.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))
|
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);
|
logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count);
|
||||||
|
|
||||||
var childUnitNameMap = await unitService.Get()
|
// Получаем имена юнитов из отфильтрованных данных
|
||||||
.AsNoTracking()
|
var childUnitNameMap = filteredUnits
|
||||||
.Where(u => childUnitIds.Contains(u.Id))
|
.Where(u => childUnitIds.Contains(u.Id))
|
||||||
.ToDictionaryAsync(u => u.Id, u => u.Name);
|
.ToDictionary(u => u.Id, u => u.Name);
|
||||||
|
|
||||||
var sortedChildUnitIds = childUnitIds
|
var sortedChildUnitIds = childUnitIds
|
||||||
.OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString()))
|
.OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString()))
|
||||||
@@ -454,10 +476,11 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
|
|
||||||
// === 2. Сравниваем детерминированно с сортировкой по имени ===
|
// === 2. Сравниваем детерминированно с сортировкой по имени ===
|
||||||
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
|
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
|
||||||
var unitNamesForSort = await unitService.Get()
|
|
||||||
.AsNoTracking()
|
// Получаем имена юнитов из отфильтрованных данных
|
||||||
|
var unitNamesForSort = filteredUnits
|
||||||
.Where(u => allUnitIdsForSort.Contains(u.Id))
|
.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
|
var sortedCurrentUnitIds = currentUnitIds
|
||||||
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
|
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
|
||||||
@@ -566,10 +589,12 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
{
|
{
|
||||||
var relationshipUnitId = kvp.Key;
|
var relationshipUnitId = kvp.Key;
|
||||||
var childUnitIds = kvp.Value;
|
var childUnitIds = kvp.Value;
|
||||||
var childUnitNameMapForDeactivate = await unitService.Get()
|
|
||||||
.AsNoTracking()
|
// Получаем имена юнитов из отфильтрованных данных
|
||||||
|
var childUnitNameMapForDeactivate = filteredUnits
|
||||||
.Where(u => childUnitIds.Contains(u.Id))
|
.Where(u => childUnitIds.Contains(u.Id))
|
||||||
.ToDictionaryAsync(u => u.Id, u => u.Name);
|
.ToDictionary(u => u.Id, u => u.Name);
|
||||||
|
|
||||||
var sortedChildUnitIdsForDeactivate = childUnitIds
|
var sortedChildUnitIdsForDeactivate = childUnitIds
|
||||||
.OrderBy(id => childUnitNameMapForDeactivate.GetValueOrDefault(id, id.ToString()))
|
.OrderBy(id => childUnitNameMapForDeactivate.GetValueOrDefault(id, id.ToString()))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using PARR.DAL.Cache.Models;
|
|||||||
using PARR.DAL.Contracts;
|
using PARR.DAL.Contracts;
|
||||||
using PARR.DAL.DomainServices.Interfaces;
|
using PARR.DAL.DomainServices.Interfaces;
|
||||||
using PARR.DAL.DomainServices.Shortcodes;
|
using PARR.DAL.DomainServices.Shortcodes;
|
||||||
|
using PARR.DAL.DomainServices.UnitFilterService;
|
||||||
using PARR.DAL.Models;
|
using PARR.DAL.Models;
|
||||||
using PARR.DAL.Models.Job;
|
using PARR.DAL.Models.Job;
|
||||||
using PARR.DAL.Services.Interfaces;
|
using PARR.DAL.Services.Interfaces;
|
||||||
@@ -137,8 +138,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
return;
|
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);
|
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
|
||||||
|
|
||||||
@@ -162,6 +164,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Извлекаем ID юнитов для последующих операций
|
||||||
|
var unitIds = filteredUnits.Select(u => u.Id).ToList();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// Отладка: проверить, есть ли юнит в unitIds
|
// Отладка: проверить, есть ли юнит в unitIds
|
||||||
if (unitIds.Contains(targetUnitId))
|
if (unitIds.Contains(targetUnitId))
|
||||||
@@ -198,26 +203,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
.Where(t => !unitIds.Contains(t.UnitId))
|
.Where(t => !unitIds.Contains(t.UnitId))
|
||||||
.ToList();
|
.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)
|
foreach (var unitId in newUnitIds)
|
||||||
{
|
{
|
||||||
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
|
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
|
||||||
@@ -262,8 +247,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
|
logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
|
||||||
await CreateSimpleTemplateAsync(jobId, unitId, initiator);
|
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);
|
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);
|
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
|
||||||
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
|
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;
|
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);
|
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
|
||||||
await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется");
|
await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется");
|
||||||
@@ -381,6 +361,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Извлекаем ID юнитов для последующих операций
|
||||||
|
var unitIds = filteredUnits.Select(u => u.Id).ToList();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// Отладка: проверить, есть ли юнит в unitIds
|
// Отладка: проверить, есть ли юнит в unitIds
|
||||||
if (unitIds.Contains(targetUnitId))
|
if (unitIds.Contains(targetUnitId))
|
||||||
|
|||||||
Reference in New Issue
Block a user