feat(dal,templateMatcher): UnitFilterService переписан, теперь при подсчете количества связей для групп работ зонтик учитываются relationshipFilter. В TemplateMatcher добавлено изменение имени Unused шаблонов в конце имени добавляется DateTimeOffset.UtcNow.ToUnixTimeSeconds() для уникальности неиспользуемых шаблонов. Актуализированы unit-тесты
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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-ДВС" });
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user