diff --git a/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs b/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs new file mode 100644 index 00000000..242f9ac3 --- /dev/null +++ b/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Logging; +using MockQueryable; +using Moq; +using PARR.DAL.Contracts; +using PARR.DAL.DomainServices.Implementations; +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.Tests.DomainServices.Implementations +{ + public class ShortcodesServiceTests + { + private readonly Mock> loggerMock = new(); + private readonly Mock settingsMock = new(); + private readonly Mock jobServiceMock = new(); + private readonly Mock unitServiceMock = new(); + private readonly Mock unitInValueServiceMock = new(); + private readonly Mock unitFieldServiceMock = new(); + private readonly Mock unitFilterServiceMock = new(); + + private ShortcodesService CreateService() + => new(loggerMock.Object, settingsMock.Object, jobServiceMock.Object, + unitServiceMock.Object, unitFilterServiceMock.Object, + unitInValueServiceMock.Object, unitFieldServiceMock.Object); + + [Fact] + public async Task ApplyShortcodesAsync_WithEc_ShouldReplaceEc() + { + // Arrange + var unitId = Guid.NewGuid(); + var jobId = Guid.NewGuid(); + + var unit = new Unit { Id = unitId, Name = "Сервер_01" }; + var job = new Job { Id = jobId, Name = "SAN ТО-1 Проверка работы фабрики SAN", TemplateNameMask = "%ЭК%", WorkGroupMask = "", WorkName = "Аудит" }; + + // ✅ БЕЗ .Object — ключевое! + unitServiceMock.Setup(x => x.Get()).Returns(new[] { unit }.BuildMock()); + jobServiceMock.Setup(x => x.Get()).Returns(new[] { job }.BuildMock()); + + var service = CreateService(); + + // Act + var result = await service.ApplyShortcodesAsync("%ЭК%", unitId, jobId); + + // Assert + Assert.Equal("Сервер_01", result); + } + } +} \ No newline at end of file diff --git a/PARR.DAL.Tests/DomainServices/Implementations/UnitFilterServiceTests.cs b/PARR.DAL.Tests/DomainServices/Implementations/UnitFilterServiceTests.cs index a278b40d..903f8c78 100644 --- a/PARR.DAL.Tests/DomainServices/Implementations/UnitFilterServiceTests.cs +++ b/PARR.DAL.Tests/DomainServices/Implementations/UnitFilterServiceTests.cs @@ -4,9 +4,8 @@ using Microsoft.Extensions.Logging; using Moq; using PARR.DAL.Context; using PARR.DAL.DomainServices.Implementations; -using PARR.DAL.Services.Interfaces.Job; -using PARR.DAL.Services.Interfaces.Unit; -using Xunit; +using PARR.DAL.Services.Implementations.Job; +using PARR.DAL.Services.Implementations.Unit; namespace PARR.DAL.Tests { @@ -27,16 +26,26 @@ namespace PARR.DAL.Tests _context.Database.EnsureCreated(); TestDataGenerator.Seed(_context); + // Логгеры var logger = Mock.Of>(); + var jobServiceLogger = Mock.Of>(); + var unitServiceLogger = Mock.Of>(); + var unitInUnitServiceLogger = Mock.Of>(); + var unitInValueServiceLogger = Mock.Of>(); - // Используем реальные сервисы, работающие с _context - var jobServiceMock = new Mock(); - jobServiceMock.Setup(x => x.Get()).Returns(() => _context.Jobs); + // Реальные сервисы с правильными конструкторами + var jobService = new JobService(_context, jobServiceLogger); + var unitService = new UnitService(_context, unitServiceLogger); + var unitInUnitService = new UnitInUnitService(_context, unitInUnitServiceLogger); + var unitInValueService = new UnitInValueService(unitInValueServiceLogger, _context); // ← logger first! - var unitServiceMock = new Mock(); - unitServiceMock.Setup(x => x.Get()).Returns(() => _context.Units); - - _service = new UnitFilterService(logger, jobServiceMock.Object, unitServiceMock.Object); + _service = new UnitFilterService( + logger, + jobService, + unitService, + unitInUnitService, + unitInValueService + ); } public void Dispose() => _context.Dispose(); @@ -75,46 +84,5 @@ namespace PARR.DAL.Tests // Assert result.Should().Contain(new[] { "СХД-КМТ-CISCO-MDS9148-1-ДВС", "СХД-КМТ-CISCO-MDS9148-3-ДВС" }); } - //[Fact] - //public async Task GetRelatedUnitNamesAsync_WithChildRelationshipFilter_ShouldReturnExpectedNames() - //{ - // // Arrange - // var jobId = _context.Jobs.First(j => j.Name == "TestJob_2").Id; - // var unitId = TestDataGenerator.UnitWId; - - // // Проверим связи вручную - // var childLinks = await _context.UnitInUnits - // .Where(u => u.ParentUnitId == unitId) - // .ToListAsync(); - - // var childIds = childLinks.Select(l => l.ChildUnitId).ToList(); - // var childUnits = await _context.Units - // .Where(u => childIds.Contains(u.Id)) - // .ToListAsync(); - - // Console.WriteLine($"Child units of {unitId}:"); - // foreach (var u in childUnits) - // { - // Console.WriteLine($" - {u.Id} ({u.Name})"); - - // var values = await _context.UnitInValues - // .Where(v => v.UnitId == u.Id && v.FieldId == TestDataGenerator.FieldId1) - // .ToListAsync(); - - // foreach (var v in values) - // { - // var val = await _context.UnitFieldValues.FindAsync(v.ValueId); - // Console.WriteLine($" FieldId1 = {val?.Value}"); - // } - // } - - // // Act - // var result = await _service.GetRelatedUnitNamesAsync(jobId, unitId); - - // Console.WriteLine($"Result: [{string.Join(", ", result)}]"); - - // // Assert - // result.Should().Contain(new[] { "СХД-КМТ-CISCO-MDS9148-1-ДВС", "СХД-КМТ-CISCO-MDS9148-3-ДВС" }); - //} } } \ No newline at end of file diff --git a/PARR.DAL.Tests/PARR.DAL.Tests.csproj b/PARR.DAL.Tests/PARR.DAL.Tests.csproj index 1f2d1df6..53184a30 100644 --- a/PARR.DAL.Tests/PARR.DAL.Tests.csproj +++ b/PARR.DAL.Tests/PARR.DAL.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs index d04180dc..a57c0176 100644 --- a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs @@ -31,13 +31,45 @@ namespace PARR.DAL.DomainServices.Implementations 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 Values { get; set; } = new(); + } + + private class UnitDto + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public List Values { get; set; } = new(); + public List Parents { get; set; } = new(); + public List Children { get; set; } = new(); + } + + #endregion + public async Task?> 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) + .Include(j => j.Group).ThenInclude(g => g!.GroupType) .FirstOrDefaultAsync(j => j.Id == jobId); return job == null ? null : await GetUnitsIdByJobFilterAsync(job, takeCount); @@ -45,14 +77,12 @@ namespace PARR.DAL.DomainServices.Implementations public async Task?> GetUnitsIdByJobFilterAsync(Job job, int? takeCount = null) { - #region Проверка обязательных зависимостей 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"); - #endregion var maxCount = takeCount ?? int.MaxValue; var collectedIds = new HashSet(); @@ -69,30 +99,171 @@ namespace PARR.DAL.DomainServices.Implementations { logger.LogDebug("Применяем фильтр #{Index} (Id={FilterId})", filterNumber, filter.Id); - var query = unitService.Get().AsNoTracking() - .Where(unit => EF.Functions.Like(unit.Name, filter.UnitFilter)); + // 1️ Найти ID юнитов по UnitFilter (Name LIKE) + var initialUnitIds = await unitService.Get().AsNoTracking() + .Where(unit => EF.Functions.Like(unit.Name, filter.UnitFilter)) + .Select(u => u.Id) + .ToListAsync(); - logger.LogDebug("Базовый фильтр по Name: {NameFilter}", filter.UnitFilter); + logger.LogDebug("Базовый фильтр по Name '{NameFilter}' дал {Count} юнитов", filter.UnitFilter, initialUnitIds.Count); + + 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(); + + // 3️ Загрузить связи + var parentRelFilters = filter.RelationshipFilters.Where(rf => rf.IsParent).ToList(); + var childRelFilters = filter.RelationshipFilters.Where(rf => !rf.IsParent).ToList(); + + var parentLinks = parentRelFilters.Any() + ? await unitInUnitService.GetParentLinksByChildIdsAsync(initialUnitIds) + : new List(); + + var childLinks = childRelFilters.Any() + ? await unitInUnitService.GetChildLinksByParentIdsAsync(initialUnitIds) + : new List(); + + // 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(); + + 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(); + + // 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(); + + var childUnitValues = relFilterFieldIds.Any() && childUnitIds.Any() + ? await unitInValueService.GetByUnitIdsAndFieldIdsAsync(childUnitIds, relFilterFieldIds) + : new List(); + + // 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) { - query = ApplyFieldFilter(query, fieldFilter); + candidateUnits = ApplyFieldFilter(candidateUnits, fieldFilter); } + logger.LogDebug("После FieldFilter осталось {Count} юнитов", candidateUnits.Count()); + foreach (var relFilter in filter.RelationshipFilters) { - query = ApplyRelationshipFilterToQuery(query, relFilter); + candidateUnits = ApplyRelationshipFilterToQuery(candidateUnits, relFilter); } + logger.LogDebug("После RelationshipFilter осталось {Count} юнитов", candidateUnits.Count()); + + // 9️⃣ Umbrella-фильтр + var finalUnits = candidateUnits.AsEnumerable(); if (job.Group.GroupType.Code == JobGroupTypesEnum.Umbrella) { - query = ApplyRelationshipCountFilter(query, job); + finalUnits = ApplyRelationshipCountFilter(finalUnits, job, filter.RelationshipFilters); + logger.LogDebug("После Umbrella-фильтра осталось {Count} юнитов", finalUnits.Count()); } - var newIds = await query + // 10 Взять ID + var newIds = finalUnits .Select(u => u.Id) .Take(remaining) - .ToListAsync(); + .ToList(); collectedIds.UnionWith(newIds); logger.LogDebug("Фильтр #{Index}: найдено {Count} Unit'ов. Всего: {Total}", @@ -133,11 +304,7 @@ namespace PARR.DAL.DomainServices.Implementations foreach (var filter in job.UnitFilters) { - if (!filter.RelationshipFilters.Any()) - { - logger.LogDebug("UnitFilter.Id {FilterId} не содержит RelationshipFilters. Пропускаем.", filter.Id); - continue; - } + if (!filter.RelationshipFilters.Any()) continue; logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count()); @@ -147,11 +314,7 @@ namespace PARR.DAL.DomainServices.Implementations rf.UnitFilterId, rf.IsParent, rf.FieldId, rf.ValueMask); var valueMask = rf.ValueMask?.Trim(); - if (string.IsNullOrEmpty(valueMask)) - { - logger.LogDebug("ValueMask пуст. Пропускаем фильтр (UnitFilterId={UnitFilterId}).", rf.UnitFilterId); - continue; - } + if (string.IsNullOrEmpty(valueMask)) continue; List relevantLinks; if (rf.IsParent) @@ -159,207 +322,176 @@ namespace PARR.DAL.DomainServices.Implementations else relevantLinks = await unitInUnitService.GetByParentIdAsync(unitId); - logger.LogDebug("Найдено {Count} связей через UnitInUnitService.", relevantLinks.Count); - - // Получаем Id юнитов, которые участвуют в связях (дети или родители) var unitIdsToCheck = rf.IsParent ? relevantLinks.Select(l => l.ParentUnitId).ToList() : relevantLinks.Select(l => l.ChildUnitId).ToList(); - if (!unitIdsToCheck.Any()) - { - logger.LogDebug("Нет юнитов для проверки значений."); - continue; - } + if (!unitIdsToCheck.Any()) continue; - // Получаем значения полей для этих юнитов var unitValues = await unitInValueService.GetByUnitIdsAsync(unitIdsToCheck); - logger.LogDebug("Получено {Count} значений полей через UnitInValueService.", unitValues.Count); - - // Фильтруем юниты, у которых есть нужное значение var matchingUnitIds = unitValues .Where(uv => uv.FieldId == rf.FieldId - && uv.Value != null - && uv.Value.Value != null - && uv.Value.Value.ToString()!.Contains(valueMask, StringComparison.OrdinalIgnoreCase)) + && uv.Value?.Value != null + && uv.Value.Value.Contains(valueMask, StringComparison.OrdinalIgnoreCase)) .Select(uv => uv.UnitId) .Distinct() .ToList(); - logger.LogDebug("Найдено {Count} юнитов с подходящим значением поля.", matchingUnitIds.Count); - - // Получаем имена этих юнитов через IUnitService if (matchingUnitIds.Any()) { - var matchingUnitNames = await unitService.Get().AsNoTracking() + var names = await unitService.Get().AsNoTracking() .Where(u => matchingUnitIds.Contains(u.Id)) .Select(u => u.Name) .ToListAsync(); - - logger.LogDebug("Найдены имена: [{Names}]", string.Join(", ", matchingUnitNames)); - result.UnionWith(matchingUnitNames); + result.UnionWith(names); } } } - logger.LogDebug("Итоговый результат: [{Result}]", string.Join(", ", result)); - return result.ToList(); } - #region Вспомогательные методы фильтрации + #region вспомогательные методы - private IQueryable ApplyFieldFilter(IQueryable query, JobFieldFilter fieldFilter) + private IQueryable ApplyFieldFilter(IQueryable query, JobFieldFilter fieldFilter) { - var fieldId = fieldFilter.FieldId; var valueMask = fieldFilter.ValueMask?.Trim(); - if (string.IsNullOrEmpty(valueMask)) return query; - logger.LogDebug("Фильтр по полю: FieldId={FieldId}, ValueMask={ValueMask}", fieldId, valueMask); + logger.LogDebug("Применяем FieldFilterDto: FieldId={FieldId}, ValueMask={ValueMask}", fieldFilter.FieldId, valueMask); - return query.Where(unit => - unit.UnitValues.Any(v => - v.FieldId == fieldId && + bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); + bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); + + return query.AsEnumerable().Where(dto => + dto.Values.Any(v => + v.FieldId == fieldFilter.FieldId && v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask))); + (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)) + ) + ).AsQueryable(); } - private IQueryable ApplyRelationshipFilterToQuery(IQueryable query, JobRelationshipFilter relFilter) + private IQueryable ApplyRelationshipFilterToQuery(IQueryable query, JobRelationshipFilter relFilter) { - var fieldId = relFilter.FieldId; - var valueMask = relFilter.ValueMask ?? ""; - + var valueMask = relFilter.ValueMask?.Trim(); if (string.IsNullOrWhiteSpace(valueMask)) return query; - logger.LogDebug("Фильтр по связи: IsParent={IsParent}, IsInverse={IsInverse}, IsFullMatch={IsFullMatch}, FieldId={FieldId}, ValueMask={ValueMask}", - relFilter.IsParent, relFilter.IsInverse, relFilter.IsFullMatch, fieldId, valueMask); + logger.LogDebug("Применяем RelationshipFilterDto: IsParent={IsParent}, IsInverse={IsInverse}, IsFullMatch={IsFullMatch}, FieldId={FieldId}, ValueMask={ValueMask}", + relFilter.IsParent, relFilter.IsInverse, relFilter.IsFullMatch, relFilter.FieldId, valueMask); - if (relFilter.IsParent) + var isParent = relFilter.IsParent; + var isInverse = relFilter.IsInverse; + var isFullMatch = relFilter.IsFullMatch; + var fieldId = relFilter.FieldId; + + bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); + bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); + + return query.AsEnumerable().Where(dto => { - if (relFilter.IsInverse) + var links = isParent ? dto.Parents : dto.Children; + + if (links == null || !links.Any()) { - if (relFilter.IsFullMatch) - { - return query.Where(unit => - !unit.ParentUnits.Any() || - unit.ParentUnits.All(link => - !link.ParentUnit!.UnitValues.Any(v => - v.FieldId == fieldId && - v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask)))); - } - else - { - return query.Where(unit => - !unit.ParentUnits.Any() || - unit.ParentUnits.Any(link => - !link.ParentUnit!.UnitValues.Any(v => - v.FieldId == fieldId && - v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask)))); - } + // Для IsFullMatch/IsInverse — "все подходят", т.е. true + // Для остальных — false + return isFullMatch && isInverse; } - else + + var hasMatchingLinks = links.Any(link => { - if (relFilter.IsFullMatch) - { - return query.Where(unit => - !unit.ParentUnits.Any() || - unit.ParentUnits.All(link => - link.ParentUnit!.UnitValues.Any(v => - v.FieldId == fieldId && - v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask)))); - } - else - { - return query.Where(unit => - unit.ParentUnits.Any(link => - link.ParentUnit!.UnitValues.Any(v => - v.FieldId == fieldId && - v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask)))); - } - } - } - else - { - if (relFilter.IsInverse) + var hasMatch = link.Values.Any(v => + v.FieldId == 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)) + ); + + return isInverse ? !hasMatch : hasMatch; + }); + + if (isFullMatch) { - if (relFilter.IsFullMatch) + var allMatch = links.All(link => { - return query.Where(unit => - !unit.ChildUnits.Any() || - unit.ChildUnits.All(link => - !link.ChildUnit!.UnitValues.Any(v => - v.FieldId == fieldId && - v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask)))); - } - else - { - return query.Where(unit => - !unit.ChildUnits.Any() || - unit.ChildUnits.Any(link => - !link.ChildUnit!.UnitValues.Any(v => - v.FieldId == fieldId && - v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask)))); - } + var hasMatch = link.Values.Any(v => + v.FieldId == 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)) + ); + + return isInverse ? !hasMatch : hasMatch; + }); + + return allMatch; } - else - { - if (relFilter.IsFullMatch) - { - return query.Where(unit => - !unit.ChildUnits.Any() || - unit.ChildUnits.All(link => - link.ChildUnit!.UnitValues.Any(v => - v.FieldId == fieldId && - v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask)))); - } - else - { - return query.Where(unit => - unit.ChildUnits.Any(link => - link.ChildUnit!.UnitValues.Any(v => - v.FieldId == fieldId && - v.Value != null && - v.Value.Value != null && - EF.Functions.Like(v.Value.Value, valueMask)))); - } - } - } + + return hasMatchingLinks; + }).AsQueryable(); } - private IQueryable ApplyRelationshipCountFilter(IQueryable query, Job job) + private IEnumerable ApplyRelationshipCountFilter( + IEnumerable units, + Job job, + IEnumerable relationshipFilters) { - int min = job.MinValueRelationships.GetValueOrDefault(0); - int max = job.MaxValueRelationships.GetValueOrDefault(int.MaxValue); + var min = job.MinValueRelationships ?? 0; + var max = job.MaxValueRelationships ?? int.MaxValue; + var isParentDirection = job.IsParentRelationships == true; - logger.LogDebug("Фильтр по количеству связей: Min={Min}, Max={Max}, IsParent={IsParent}", min, max, job.IsParentRelationships); + if (min == 0 && max == int.MaxValue) + return units; - if (job.IsParentRelationships == true) + var activeFilters = relationshipFilters + .Where(rf => rf.IsParent == isParentDirection && !string.IsNullOrWhiteSpace(rf.ValueMask)) + .ToList(); + + if (activeFilters.Count == 0) + return units; + + logger.LogDebug("RelationshipFilter: Min={Min}, Max={Max}, IsParent={IsParent}, Filters={Count}", + min, max, isParentDirection, activeFilters.Count); + + return units.Where(dto => { - return query.Where(unit => unit.ParentUnits.Count >= min && unit.ParentUnits.Count <= max); - } - else - { - return query.Where(unit => unit.ChildUnits.Count >= min && unit.ChildUnits.Count <= max); - } + var links = isParentDirection ? dto.Parents : dto.Children; + + if (links == null || !links.Any()) + return min == 0; + + int matchingCount = 0; + + foreach (var link in links) + { + bool hasMatch = link.Values.Any(v => + activeFilters.Any(f => + v.FieldId == f.FieldId && + v.Value != null && + v.Value.Contains(f.ValueMask, StringComparison.OrdinalIgnoreCase) + ) + ); + + if (hasMatch) + matchingCount++; + + if (matchingCount > max) + break; + } + + return matchingCount >= min && matchingCount <= max; + }); } #endregion diff --git a/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs b/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs index 5cffcc65..cd80eabc 100644 --- a/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs +++ b/PARR.DAL/Services/Implementations/Unit/UnitInUnitService.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using PARR.DAL.Context; using PARR.DAL.Models.Unit; -using PARR.DAL.Services.Abstracts; using PARR.DAL.Services.Interfaces.Unit; namespace PARR.DAL.Services.Implementations.Unit @@ -36,5 +35,24 @@ namespace PARR.DAL.Services.Implementations.Unit .Where(u => u.ChildUnitId == childId) .ToListAsync(); } + + + public async Task> GetParentLinksByChildIdsAsync(IEnumerable childUnitIds) + { + var set = childUnitIds.ToHashSet(); + return await dataContext.UnitInUnits + .AsNoTracking() + .Where(uinu => set.Contains(uinu.ChildUnitId)) + .ToListAsync(); + } + + public async Task> GetChildLinksByParentIdsAsync(IEnumerable parentUnitIds) + { + var set = parentUnitIds.ToHashSet(); + return await dataContext.UnitInUnits + .AsNoTracking() + .Where(uinu => set.Contains(uinu.ParentUnitId)) + .ToListAsync(); + } } } diff --git a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs index fb167c7f..a3426b97 100644 --- a/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs +++ b/PARR.DAL/Services/Implementations/Unit/UnitInValueService.cs @@ -67,5 +67,18 @@ namespace PARR.DAL.Services.Implementations.Unit return keyValuePairs.Select(kvp => (kvp.Key, kvp.Value)).ToList(); } + + + public async Task> GetByUnitIdsAndFieldIdsAsync(IEnumerable unitIds, IEnumerable fieldIds) + { + var unitIdSet = unitIds.ToHashSet(); + var fieldIdSet = fieldIds.ToHashSet(); + + return await dataContext.UnitInValues + .AsNoTracking() + .Include(uv => uv.Value) // UnitFieldValue + .Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId)) + .ToListAsync(); + } } } diff --git a/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs b/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs index ef2d98cc..969d0e74 100644 --- a/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs +++ b/PARR.DAL/Services/Interfaces/Unit/IUnitInUnitService.cs @@ -1,5 +1,4 @@ using PARR.DAL.Models.Unit; -using PARR.DAL.Services.Interfaces.Base; namespace PARR.DAL.Services.Interfaces.Unit { @@ -7,5 +6,15 @@ namespace PARR.DAL.Services.Interfaces.Unit { Task> GetByParentIdAsync(Guid parentId); Task> GetByChildIdAsync(Guid childId); + + /// + /// Получает связи, где ChildUnitId unitIds (для IsParent=True). + /// + Task> GetParentLinksByChildIdsAsync(IEnumerable childUnitIds); + + /// + /// Получает связи, где ParentUnitId unitIds (для IsParent=False). + /// + Task> GetChildLinksByParentIdsAsync(IEnumerable parentUnitIds); } } diff --git a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs index 038308cf..5b56ed49 100644 --- a/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs +++ b/PARR.DAL/Services/Interfaces/Unit/IUnitInValueService.cs @@ -7,5 +7,11 @@ namespace PARR.DAL.Services.Interfaces.Unit Task> GetByUnitIdsAsync(IEnumerable unitIds); Task> GetByUnitIdAsync(Guid unitId); Task> GetFieldValuesAsync(Guid unitId, IReadOnlyCollection aihitNames); + + /// + /// Получает UnitInValue (с Value) для заданных UnitId и FieldId. + /// + Task> GetByUnitIdsAndFieldIdsAsync(IEnumerable unitIds, IEnumerable fieldIds); + } } diff --git a/PARR.TemplateMatcher/TemplateMatcher.cs b/PARR.TemplateMatcher/TemplateMatcher.cs index 0e8d2ac0..6f12e687 100644 --- a/PARR.TemplateMatcher/TemplateMatcher.cs +++ b/PARR.TemplateMatcher/TemplateMatcher.cs @@ -105,7 +105,7 @@ namespace PARR.TemplateMatcher TemplateId = template.Id, JobId = jobId, UnitId = template.UnitId, - Name = template.Name, + Name = GetTemplateNameForUnsed(template.Name), IsActiveTemplate = DefaultUnusedTemplateState, IsActiveSchedule = DefaultUnusedScheduleState, LastRun = template.LastRun, @@ -232,6 +232,7 @@ namespace PARR.TemplateMatcher logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); } + public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); @@ -271,6 +272,7 @@ namespace PARR.TemplateMatcher targetStatus = TemplateStatusTypeEnum.Unused; targetIsActiveTemplate = DefaultUnusedTemplateState; targetIsActiveSchedule = DefaultUnusedScheduleState; + expectedName = GetTemplateNameForUnsed(expectedName); logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId); } @@ -403,6 +405,11 @@ namespace PARR.TemplateMatcher return null; } + private string GetTemplateNameForUnsed(string templateName) + { + return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + private async Task GetJobWithGroupAndAutoControlAsync(Guid jobId) {