feat(dal,templateMatcher): UnitFilterService переписан, теперь при подсчете количества связей для групп работ зонтик учитываются relationshipFilter. В TemplateMatcher добавлено изменение имени Unused шаблонов в конце имени добавляется DateTimeOffset.UtcNow.ToUnixTimeSeconds() для уникальности неиспользуемых шаблонов. Актуализированы unit-тесты

This commit is contained in:
Mikhail Kuznetsov
2025-12-15 11:50:12 +10:00
parent 03683a13d3
commit 8da4f226a7
9 changed files with 429 additions and 223 deletions

View File

@@ -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<ILogger<ShortcodesService>> loggerMock = new();
private readonly Mock<SettingsFromDb> settingsMock = new();
private readonly Mock<IJobService> jobServiceMock = new();
private readonly Mock<IUnitService> unitServiceMock = new();
private readonly Mock<IUnitInValueService> unitInValueServiceMock = new();
private readonly Mock<IUnitFieldService> unitFieldServiceMock = new();
private readonly Mock<IUnitFilterService> 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);
}
}
}

View File

@@ -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<ILogger<UnitFilterService>>();
var jobServiceLogger = Mock.Of<ILogger<JobService>>();
var unitServiceLogger = Mock.Of<ILogger<UnitService>>();
var unitInUnitServiceLogger = Mock.Of<ILogger<UnitInUnitService>>();
var unitInValueServiceLogger = Mock.Of<ILogger<UnitInValueService>>();
// Используем реальные сервисы, работающие с _context
var jobServiceMock = new Mock<IJobService>();
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<IUnitService>();
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-ДВС" });
//}
}
}

View File

@@ -12,6 +12,7 @@
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.20" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MockQueryable.Moq" Version="7.0.3" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

View File

@@ -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<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();
}
#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)
.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<IEnumerable<Guid>?> 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<Guid>();
@@ -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<UnitInValue>();
// 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<UnitInUnit>();
var childLinks = childRelFilters.Any()
? await unitInUnitService.GetChildLinksByParentIdsAsync(initialUnitIds)
: new List<UnitInUnit>();
// 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)
{
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<UnitInUnit> 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<Unit> ApplyFieldFilter(IQueryable<Unit> query, JobFieldFilter fieldFilter)
private IQueryable<UnitDto> ApplyFieldFilter(IQueryable<UnitDto> 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<Unit> ApplyRelationshipFilterToQuery(IQueryable<Unit> query, JobRelationshipFilter relFilter)
private IQueryable<UnitDto> ApplyRelationshipFilterToQuery(IQueryable<UnitDto> 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<Unit> ApplyRelationshipCountFilter(IQueryable<Unit> query, Job job)
private IEnumerable<UnitDto> ApplyRelationshipCountFilter(
IEnumerable<UnitDto> units,
Job job,
IEnumerable<JobRelationshipFilter> 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

View File

@@ -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<List<UnitInUnit>> GetParentLinksByChildIdsAsync(IEnumerable<Guid> childUnitIds)
{
var set = childUnitIds.ToHashSet();
return await dataContext.UnitInUnits
.AsNoTracking()
.Where(uinu => set.Contains(uinu.ChildUnitId))
.ToListAsync();
}
public async Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds)
{
var set = parentUnitIds.ToHashSet();
return await dataContext.UnitInUnits
.AsNoTracking()
.Where(uinu => set.Contains(uinu.ParentUnitId))
.ToListAsync();
}
}
}

View File

@@ -67,5 +67,18 @@ namespace PARR.DAL.Services.Implementations.Unit
return keyValuePairs.Select(kvp => (kvp.Key, kvp.Value)).ToList();
}
public async Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> 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();
}
}
}

View File

@@ -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<List<UnitInUnit>> GetByParentIdAsync(Guid parentId);
Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId);
/// <summary>
/// Получает связи, где ChildUnitId unitIds (для IsParent=True).
/// </summary>
Task<List<UnitInUnit>> GetParentLinksByChildIdsAsync(IEnumerable<Guid> childUnitIds);
/// <summary>
/// Получает связи, где ParentUnitId unitIds (для IsParent=False).
/// </summary>
Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds);
}
}

View File

@@ -7,5 +7,11 @@ namespace PARR.DAL.Services.Interfaces.Unit
Task<List<UnitInValue>> GetByUnitIdsAsync(IEnumerable<Guid> unitIds);
Task<List<UnitInValue>> GetByUnitIdAsync(Guid unitId);
Task<List<(string FieldName, string? Value)>> GetFieldValuesAsync(Guid unitId, IReadOnlyCollection<string> aihitNames);
/// <summary>
/// Получает UnitInValue (с Value) для заданных UnitId и FieldId.
/// </summary>
Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> fieldIds);
}
}

View File

@@ -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<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
{