fix(templateMatcher): Исправление ошибок в именах шаблонов и рефакторинг синхронизации
- Устранена ошибка, когда при переиспользовании шаблона для шорткодов передавался ЭК "КОМПЛЕКСЫ-[ЗО]" вместо целевого ЭК шаблона. - Проведен небольшой рефакторинг общих процессов синхронизации различных типов групп работ.
This commit is contained in:
@@ -82,14 +82,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateMatcher", "PAR
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateMatcherWorker", "PARR.TemplateMatcherWorker\PARR.TemplateMatcherWorker.csproj", "{DA6B2A17-6E64-4E2C-8D4C-E278BEE94E03}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.DAL.Tests", "PARR.DAL.Tests\PARR.DAL.Tests.csproj", "{D61A7AB6-D785-477A-95D1-FBED049D6CE2}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.Core.Tests", "PARR.DAL.Tests\PARR.Core.Tests.csproj", "{D61A7AB6-D785-477A-95D1-FBED049D6CE2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateUpdaterWorker", "PARR.TemplateUpdaterWorker\PARR.TemplateUpdaterWorker.csproj", "{27263750-9C80-4F9D-A550-B9D9C473B386}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateUpdater", "PARR.TemplateUpdater\PARR.TemplateUpdater.csproj", "{10905C04-A6E5-42BC-9804-8671D22C5D6E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.DAL.IntegrationTests", "PARR.DAL.IntegrationTests\PARR.DAL.IntegrationTests.csproj", "{D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.Core", "PARR.Core\PARR.Core.csproj", "{E93DFC30-9886-486A-9F67-F782ABE0D1E0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.Domain", "PARR.Domain\PARR.Domain.csproj", "{3740461A-7235-4C26-81F3-3977529D646D}"
|
||||
@@ -256,10 +254,6 @@ Global
|
||||
{10905C04-A6E5-42BC-9804-8671D22C5D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{10905C04-A6E5-42BC-9804-8671D22C5D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{10905C04-A6E5-42BC-9804-8671D22C5D6E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E93DFC30-9886-486A-9F67-F782ABE0D1E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E93DFC30-9886-486A-9F67-F782ABE0D1E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E93DFC30-9886-486A-9F67-F782ABE0D1E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.5.2" />
|
||||
<PackageReference Include="InfluxDB.Client" Version="4.17.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.16" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.16" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.16" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.5.2" />
|
||||
<PackageReference Include="InfluxDB.Client" Version="4.17.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.16" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.16" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.16" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PARR.Domain\PARR.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PARR.Domain\PARR.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Разрешаем тестовой сборке видеть internal классы PARR.Core -->
|
||||
<InternalsVisibleTo Include="PARR.Core.Tests" />
|
||||
<!-- Разрешаем Castle DynamicProxy (Moq / NSubstitute) видеть internal классы PARR.Core -->
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("PARR.DAL.Tests")]
|
||||
[assembly: InternalsVisibleTo("PARR.DAL.IntegrationTests")]
|
||||
@@ -2,6 +2,6 @@
|
||||
{
|
||||
internal class UnitFilterServiceOptions
|
||||
{
|
||||
public int LoadBatchSize { get; set; } = 100;
|
||||
public int LoadBatchSize { get; set; } = 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace PARR.Core.Services.UnitFilterService;
|
||||
internal class UnitFilterService : IUnitFilterService
|
||||
{
|
||||
#if DEBUG
|
||||
private readonly Guid debugTargetUnitId = Guid.Parse("3494fbec-adb9-4167-8619-b2a8b6b6e36a");
|
||||
private readonly Guid debugTargetUnitId = Guid.Parse("a3b5f3e2-928e-48b7-b481-df4556e1ed32");
|
||||
#endif
|
||||
|
||||
private const int DebugMaxUnitsToLog = 10;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Core.Services.Shortcodes;
|
||||
using PARR.Core.Services.UnitFilterService;
|
||||
using PARR.Domain.Settings;
|
||||
|
||||
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<IJobRepository> jobServiceMock = new();
|
||||
private readonly Mock<IUnitRepository> unitServiceMock = new();
|
||||
private readonly Mock<IUnitInValueRepository> unitInValueServiceMock = new();
|
||||
private readonly Mock<IUnitFieldRepository> unitFieldServiceMock = new();
|
||||
private readonly Mock<IUnitFilterService> unitFilterServiceMock = new();
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyShortcodesAsync_WithEc_ShouldReplaceEc()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
namespace PARR.DAL.Tests
|
||||
{
|
||||
public class UnitFilterServiceTests //: IDisposable
|
||||
{
|
||||
// private readonly DbContextOptions<DataContext> _options;
|
||||
// private readonly DataContext _context;
|
||||
// private readonly UnitFilterService _service;
|
||||
|
||||
// public UnitFilterServiceTests()
|
||||
// {
|
||||
// _options = new DbContextOptionsBuilder<DataContext>()
|
||||
// .UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")
|
||||
// .EnableSensitiveDataLogging()
|
||||
// .Options;
|
||||
|
||||
// _context = new DataContext(_options);
|
||||
// _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>>();
|
||||
|
||||
// // Реальные сервисы с правильными конструкторами
|
||||
// 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!
|
||||
|
||||
// _service = new UnitFilterService(
|
||||
// logger,
|
||||
// jobService,
|
||||
// unitService,
|
||||
// unitInUnitService,
|
||||
// unitInValueService
|
||||
// );
|
||||
// }
|
||||
|
||||
// public void Dispose() => _context.Dispose();
|
||||
|
||||
// [Fact]
|
||||
// public async Task GetUnitsIdByJobFilterAsync_WithExistingJobId_ShouldReturnExpectedUnits()
|
||||
// {
|
||||
// // Arrange
|
||||
// var jobId = _context.Jobs.First(j => j.Name == "TestJob_1").Id;
|
||||
// var expectedUnitIds = new[] { TestDataGenerator.UnitAId, TestDataGenerator.UnitBId };
|
||||
|
||||
// // Act
|
||||
// var result = await _service.GetUnitsIdByJobFilterAsync(jobId, takeCount: 10);
|
||||
|
||||
// // Assert
|
||||
// result.Should().NotBeNull()
|
||||
// .And.HaveCount(2)
|
||||
// .And.Contain(expectedUnitIds);
|
||||
// }
|
||||
|
||||
// [Fact]
|
||||
// public async Task GetRelatedUnitNamesAsync_WithChildRelationshipFilter_ShouldReturnExpectedNames()
|
||||
// {
|
||||
// // Arrange
|
||||
// var jobId = _context.Jobs.First(j => j.Name == "TestJob_2").Id;
|
||||
// var unitId = TestDataGenerator.UnitWId; // Родитель
|
||||
|
||||
// // rf.IsParent = false
|
||||
// // Ищем юниты, у которых ParentUnitId = UnitWId (т.е. это дети UnitWId)
|
||||
// // и у этих юнитов (детей) есть FieldId1 = "коммутатор"
|
||||
// // Это UnitXId и UnitYId -> имена: "СХД-КМТ-CISCO-MDS9148-1-ДВС", "СХД-КМТ-CISCO-MDS9148-3-ДВС"
|
||||
|
||||
// // Act
|
||||
// var result = await _service.GetRelatedUnitNamesAsync(jobId, unitId);
|
||||
|
||||
// // Assert
|
||||
// result.Should().Contain(new[] { "СХД-КМТ-CISCO-MDS9148-1-ДВС", "СХД-КМТ-CISCO-MDS9148-3-ДВС" });
|
||||
// }
|
||||
}
|
||||
}
|
||||
33
PARR.DAL.Tests/PARR.Core.Tests.csproj
Normal file
33
PARR.DAL.Tests/PARR.Core.Tests.csproj
Normal file
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.16" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="MockQueryable.Moq" Version="9.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PARR.Core\PARR.Core.csproj" />
|
||||
<ProjectReference Include="..\PARR.DAL\PARR.DAL.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,616 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using PARR.Core.Common.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Cache.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.Core.Tests.Services.UnitFilterService;
|
||||
|
||||
public class UnitFilterServiceTests
|
||||
{
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт тестовую сущность Unit с заданным значением поля.
|
||||
/// Используется для подготовки данных в тестах фильтрации.
|
||||
/// </summary>
|
||||
/// <param name="id">Уникальный идентификатор юнита</param>
|
||||
/// <param name="name">Имя юнита для отладки</param>
|
||||
/// <param name="fieldId">Идентификатор поля, к которому привязывается значение</param>
|
||||
/// <param name="fieldValue">Значение поля</param>
|
||||
private static Unit BuildTestUnit(Guid id, string name, Guid fieldId, string fieldValue)
|
||||
{
|
||||
var fieldVal = new UnitFieldValue { Id = Guid.NewGuid(), Value = fieldValue };
|
||||
return new Unit
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
UnitValues = new List<UnitInValue>
|
||||
{
|
||||
new UnitInValue
|
||||
{
|
||||
UnitId = id,
|
||||
FieldId = fieldId,
|
||||
ValueId = fieldVal.Id,
|
||||
Value = fieldVal
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт тестовый объект JobGroupType с кодом Umbrella.
|
||||
/// Используется для тестирования логики групповых работ типа "зонтик".
|
||||
/// </summary>
|
||||
private static JobGroupType BuildTestJobGroupType() => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = JobGroupTypesEnum.Group,
|
||||
Name = "Grouped Group",
|
||||
Description = "Test"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт тестовый объект JobGroup с заполненными обязательными полями.
|
||||
/// Используется для подготовки навигационных свойств в тестах.
|
||||
/// </summary>
|
||||
private static JobGroup BuildTestJobGroup() => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GroupName = "Test Group",
|
||||
ShortDescription = "Short",
|
||||
FullDescription = "Full",
|
||||
Solution = "Sol",
|
||||
TemplateDuration = "0:00:00",
|
||||
ReferenceDate = DateTimeOffset.UtcNow,
|
||||
GroupTypeId = Guid.NewGuid(),
|
||||
ScheduleExcludeTypeId = Guid.NewGuid(),
|
||||
GroupType = BuildTestJobGroupType()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт тестовый объект JobAutoControl с отключённым автоконтролем.
|
||||
/// Используется для заполнения обязательного свойства в сущности Job.
|
||||
/// </summary>
|
||||
private static JobAutoControl BuildTestAutoControl(Guid jobId) => new()
|
||||
{
|
||||
JobId = jobId,
|
||||
IsEnable = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Контейнер для моков репозиториев.
|
||||
/// Упрощает передачу зависимостей в метод создания сервиса.
|
||||
/// </summary>
|
||||
private class RepositoryMocks
|
||||
{
|
||||
public Mock<IUnitRepository> Unit { get; set; }
|
||||
public Mock<IUnitInUnitRepository> UnitInUnit { get; set; }
|
||||
public Mock<IUnitInValueRepository> UnitInValue { get; set; }
|
||||
public Mock<IUnitFieldRepository> UnitField { get; set; }
|
||||
public Mock<IJobRepository> Job { get; set; }
|
||||
public Mock<IRedisCacheService> Cache { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Настраивает базовые моки репозиториев с использованием InMemory-контекста.
|
||||
/// Возвращает объект с подготовленными моками для повторного использования в тестах.
|
||||
/// </summary>
|
||||
/// <param name="context">Экземпляр DataContext с тестовыми данными</param>
|
||||
private static RepositoryMocks ArrangeRepositoryMocks(DataContext context)
|
||||
{
|
||||
var unitRepoMock = new Mock<IUnitRepository>();
|
||||
unitRepoMock.Setup(r => r.Get()).Returns(context.Units.AsQueryable());
|
||||
unitRepoMock.Setup(r => r.GetInitialUnitIds(It.IsAny<string>()))
|
||||
.Returns(context.Units.Select(u => u.Id).AsQueryable());
|
||||
|
||||
var unitInUnitRepoMock = new Mock<IUnitInUnitRepository>();
|
||||
unitInUnitRepoMock.Setup(r => r.Get()).Returns(context.UnitInUnits.AsQueryable());
|
||||
|
||||
var unitInValueRepoMock = new Mock<IUnitInValueRepository>();
|
||||
// Единый подход: используем контекст для поддержки IAsyncQueryProvider
|
||||
// Это гарантирует корректную работу .ToListAsync() внутри сервиса
|
||||
unitInValueRepoMock.Setup(r => r.GetMatchingTargetIds(It.IsAny<Guid>(), It.IsAny<string>()))
|
||||
.Returns((Guid fId, string mask) =>
|
||||
context.UnitInValues
|
||||
.AsNoTracking()
|
||||
.Where(uv => uv.FieldId == fId && uv.Value != null)
|
||||
.Select(uv => uv.UnitId)
|
||||
.Distinct()
|
||||
.AsQueryable());
|
||||
|
||||
var unitFieldRepoMock = new Mock<IUnitFieldRepository>();
|
||||
unitFieldRepoMock.Setup(r => r.Get()).Returns(new List<UnitField>().AsQueryable());
|
||||
|
||||
var jobRepoMock = new Mock<IJobRepository>();
|
||||
jobRepoMock.Setup(r => r.Get()).Returns(context.Jobs.AsQueryable());
|
||||
|
||||
var cacheMock = new Mock<IRedisCacheService>();
|
||||
cacheMock.Setup(r => r.GetCachedDataAsync<UnitFilterIds>(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.ReturnsAsync((UnitFilterIds)null!);
|
||||
|
||||
return new RepositoryMocks
|
||||
{
|
||||
Unit = unitRepoMock,
|
||||
UnitInUnit = unitInUnitRepoMock,
|
||||
UnitInValue = unitInValueRepoMock,
|
||||
UnitField = unitFieldRepoMock,
|
||||
Job = jobRepoMock,
|
||||
Cache = cacheMock
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт экземпляр тестируемого сервиса (SUT — System Under Test).
|
||||
/// Инкапсулирует логику конструктора для упрощения тестов.
|
||||
/// </summary>
|
||||
/// <param name="mocks">Подготовленные моки репозиториев</param>
|
||||
/// <param name="logger">Экземпляр логгера для сервиса</param>
|
||||
private static PARR.Core.Services.UnitFilterService.UnitFilterService CreateSut(
|
||||
RepositoryMocks mocks,
|
||||
ILogger<PARR.Core.Services.UnitFilterService.UnitFilterService> logger)
|
||||
{
|
||||
var serviceOptions = Options.Create(new PARR.Core.Services.UnitFilterService.Models.UnitFilterServiceOptions { LoadBatchSize = 100 });
|
||||
|
||||
return new PARR.Core.Services.UnitFilterService.UnitFilterService(
|
||||
logger,
|
||||
mocks.Job.Object,
|
||||
mocks.Unit.Object,
|
||||
mocks.UnitInUnit.Object,
|
||||
mocks.UnitInValue.Object,
|
||||
mocks.Cache.Object,
|
||||
serviceOptions,
|
||||
mocks.UnitField.Object
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, что юниты, не прошедшие фильтрацию по полю (FieldFilter)
|
||||
/// или по связям (RelationshipFilter), корректно исключаются из результата.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetUnitsByJobFilterAsync_UnitsWithInvalidFieldOrRelationshipValue_AreExcluded()
|
||||
{
|
||||
// === Arrange ===
|
||||
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
using var context = new DataContext(dbOptions);
|
||||
|
||||
var jobId = Guid.NewGuid();
|
||||
var fieldId = Guid.NewGuid();
|
||||
var validParentId = Guid.NewGuid();
|
||||
var wrongParentId = Guid.NewGuid();
|
||||
|
||||
var unitPassesAll = Guid.NewGuid();
|
||||
var unitFailsFieldFilter = Guid.NewGuid();
|
||||
var unitFailsRelationshipFilter = Guid.NewGuid();
|
||||
|
||||
// Подготовка тестовых данных: юниты
|
||||
context.Units.AddRange(
|
||||
BuildTestUnit(unitPassesAll, "Unit_PassesAll", fieldId, "Accepted Value"),
|
||||
BuildTestUnit(unitFailsFieldFilter, "Unit_FailsField", fieldId, "Rejected Value"),
|
||||
BuildTestUnit(unitFailsRelationshipFilter, "Unit_FailsRel", fieldId, "Accepted Value"),
|
||||
BuildTestUnit(validParentId, "Parent_Valid", fieldId, "Valid Parent"),
|
||||
BuildTestUnit(wrongParentId, "Parent_Wrong", fieldId, "Wrong Parent")
|
||||
);
|
||||
|
||||
// Подготовка тестовых данных: связи между юнитами
|
||||
context.UnitInUnits.AddRange(
|
||||
new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitPassesAll, DateCreated = DateTimeOffset.UtcNow },
|
||||
new UnitInUnit { ParentUnitId = wrongParentId, ChildUnitId = unitFailsRelationshipFilter, DateCreated = DateTimeOffset.UtcNow }
|
||||
);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Формируем тестовый объект Job с фильтрами
|
||||
var job = new Job
|
||||
{
|
||||
Id = jobId,
|
||||
Name = "Test Job",
|
||||
WorkName = "Test Work",
|
||||
TemplateNameMask = "TestMask",
|
||||
WorkGroupMask = "TestGroup",
|
||||
ResponseAreaMask = "TestArea",
|
||||
GroupId = Guid.NewGuid(),
|
||||
TnkId = Guid.NewGuid(),
|
||||
MinValueRelationships = 1,
|
||||
MaxValueRelationships = 10,
|
||||
IsParentRelationships = true,
|
||||
AutoControl = BuildTestAutoControl(jobId),
|
||||
Group = BuildTestJobGroup(),
|
||||
UnitFilters = new List<JobUnitFilter>
|
||||
{
|
||||
new JobUnitFilter
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
JobId = jobId,
|
||||
UnitFilter = "%",
|
||||
FieldFilters = new List<JobFieldFilter>
|
||||
{
|
||||
new JobFieldFilter { Id = Guid.NewGuid(), FieldId = fieldId, ValueMask = "Accepted%", IsInverse = false }
|
||||
},
|
||||
RelationshipFilters = new List<JobRelationshipFilter>
|
||||
{
|
||||
new JobRelationshipFilter { UnitFilterId = Guid.NewGuid(), FieldId = fieldId, ValueMask = "%", IsParent = true, IsInverse = false, IsFullMatch = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Настройка моков репозиториев
|
||||
var mocks = ArrangeRepositoryMocks(context);
|
||||
|
||||
// Переопределяем мок фильтрации по полю: в реальном коде используется EF.Functions.ILike,
|
||||
// в тесте заменяем на прямое сравнение строк для предсказуемости
|
||||
mocks.Unit.Setup(r => r.GetUnitByFieldAndValue(It.IsAny<IQueryable<Unit>>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.Returns((IQueryable<Unit> q, Guid fId, string mask, bool inv) =>
|
||||
q.Where(u => u.UnitValues != null && u.UnitValues.Any(v => v.FieldId == fId && v.Value != null && v.Value.Value == "Accepted Value")));
|
||||
|
||||
// ИСПРАВЛЕНО: используем запрос к контексту вместо массива.
|
||||
// Массивный AsQueryable() не поддерживает IAsyncQueryProvider, что вызывает крах при вызове .ToListAsync() внутри сервиса.
|
||||
// Запрос к InMemory DbSet гарантирует корректную асинхронную материализацию.
|
||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(fieldId, It.IsAny<string>()))
|
||||
.Returns(context.Units.Where(u => u.Id == validParentId).Select(u => u.Id).AsQueryable());
|
||||
|
||||
var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
|
||||
var service = CreateSut(mocks, logger);
|
||||
|
||||
// === Act ===
|
||||
var result = await service.GetUnitsByJobFilterAsync(job);
|
||||
|
||||
// === Assert ===
|
||||
var resultIds = result.Select(u => u.Id).ToList();
|
||||
|
||||
Assert.Contains(unitPassesAll, resultIds);
|
||||
Assert.DoesNotContain(unitFailsFieldFilter, resultIds);
|
||||
Assert.DoesNotContain(unitFailsRelationshipFilter, resultIds);
|
||||
|
||||
Assert.Single(result);
|
||||
var dto = result.First();
|
||||
Assert.Equal("Accepted Value", dto.Values.FirstOrDefault()?.Value);
|
||||
Assert.Single(dto.Parents);
|
||||
Assert.Equal(validParentId, dto.Parents.First().UnitId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, что юниты исключаются, если их единственный родительский юнит
|
||||
/// содержит запрещённое значение тега (сценарий с IsInverse = true).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetUnitsByJobFilterAsync_ParentWithForbiddenTag_UnitIsExcluded()
|
||||
{
|
||||
// === Arrange ===
|
||||
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
using var context = new DataContext(dbOptions);
|
||||
|
||||
var jobId = Guid.NewGuid();
|
||||
var tagFieldId = Guid.NewGuid();
|
||||
var validParentId = Guid.NewGuid();
|
||||
var invalidParentId = Guid.NewGuid();
|
||||
var unitWithValidParent = Guid.NewGuid();
|
||||
var unitWithInvalidParent = Guid.NewGuid();
|
||||
|
||||
// Подготовка тестовых данных: юниты
|
||||
context.Units.AddRange(
|
||||
BuildTestUnit(validParentId, "Parent_Valid", tagFieldId, "ОТВ.ЭК"),
|
||||
BuildTestUnit(invalidParentId, "Parent_Invalid", tagFieldId, "ПАРР-РРПТК-ОТВ.ЭК"),
|
||||
BuildTestUnit(unitWithValidParent, "Unit_Valid", Guid.NewGuid(), "Val"),
|
||||
BuildTestUnit(unitWithInvalidParent, "Unit_Invalid", Guid.NewGuid(), "Val")
|
||||
);
|
||||
|
||||
// Подготовка тестовых данных: связи между юнитами
|
||||
context.UnitInUnits.AddRange(
|
||||
new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitWithValidParent, DateCreated = DateTimeOffset.UtcNow },
|
||||
new UnitInUnit { ParentUnitId = invalidParentId, ChildUnitId = unitWithInvalidParent, DateCreated = DateTimeOffset.UtcNow }
|
||||
);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Формируем тестовый объект Job с настройкой IsInverse = true
|
||||
// IsInverse = true означает: исключить родителей, которые СОВПАДАЮТ с маской
|
||||
var job = new Job
|
||||
{
|
||||
Id = jobId,
|
||||
Name = "Test Job Inverse",
|
||||
WorkName = "Test Work",
|
||||
TemplateNameMask = "TestMask",
|
||||
WorkGroupMask = "TestGroup",
|
||||
ResponseAreaMask = "TestArea",
|
||||
GroupId = Guid.NewGuid(),
|
||||
TnkId = Guid.NewGuid(),
|
||||
MinValueRelationships = 1,
|
||||
MaxValueRelationships = 10,
|
||||
IsParentRelationships = true,
|
||||
AutoControl = BuildTestAutoControl(jobId),
|
||||
Group = BuildTestJobGroup(),
|
||||
UnitFilters = new List<JobUnitFilter>
|
||||
{
|
||||
new JobUnitFilter
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
JobId = jobId,
|
||||
UnitFilter = "%",
|
||||
FieldFilters = new List<JobFieldFilter>(),
|
||||
RelationshipFilters = new List<JobRelationshipFilter>
|
||||
{
|
||||
new JobRelationshipFilter
|
||||
{
|
||||
UnitFilterId = Guid.NewGuid(),
|
||||
FieldId = tagFieldId,
|
||||
ValueMask = "%ПАРР-РРПТК-ОТВ.ЭК%",
|
||||
IsParent = true,
|
||||
IsInverse = true,
|
||||
IsFullMatch = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Настройка моков репозиториев
|
||||
var mocks = ArrangeRepositoryMocks(context);
|
||||
|
||||
// Переопределяем мок: возвращаем только родителей с запрещённым тегом.
|
||||
// В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
|
||||
// IsInverse = true в сервисе инвертирует результат, поэтому эти родители будут исключены.
|
||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny<string>()))
|
||||
.Returns((Guid fId, string mask) =>
|
||||
context.UnitInValues
|
||||
.AsNoTracking()
|
||||
.Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("ПАРР-РРПТК-ОТВ.ЭК", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(uv => uv.UnitId)
|
||||
.Distinct()
|
||||
.AsQueryable());
|
||||
|
||||
var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
|
||||
var service = CreateSut(mocks, logger);
|
||||
|
||||
// === Act ===
|
||||
var result = await service.GetUnitsByJobFilterAsync(job);
|
||||
|
||||
// === Assert ===
|
||||
var resultIds = result.Select(u => u.Id).ToList();
|
||||
|
||||
Assert.Contains(unitWithValidParent, resultIds);
|
||||
Assert.DoesNotContain(unitWithInvalidParent, resultIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет логику IsFullMatch = true: юнит исключается,
|
||||
/// если хотя бы один из его родительских юнитов не соответствует фильтру.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetUnitsByJobFilterAsync_IsFullMatch_UnitWithAnyNonMatchingParent_IsExcluded()
|
||||
{
|
||||
// === Arrange ===
|
||||
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
using var context = new DataContext(dbOptions);
|
||||
|
||||
var jobId = Guid.NewGuid();
|
||||
var checkFieldId = Guid.NewGuid();
|
||||
var parentGoodId = Guid.NewGuid();
|
||||
var parentBadId = Guid.NewGuid();
|
||||
var unitMixedId = Guid.NewGuid();
|
||||
var unitCleanId = Guid.NewGuid();
|
||||
|
||||
// Подготовка тестовых данных: юниты
|
||||
context.Units.AddRange(
|
||||
BuildTestUnit(parentGoodId, "Parent_Good", checkFieldId, "Good Parent"),
|
||||
BuildTestUnit(parentBadId, "Parent_Bad", checkFieldId, "Bad Parent"),
|
||||
BuildTestUnit(unitMixedId, "Unit_Mixed", Guid.NewGuid(), "Mixed"),
|
||||
BuildTestUnit(unitCleanId, "Unit_Clean", Guid.NewGuid(), "Clean")
|
||||
);
|
||||
|
||||
// Подготовка тестовых данных: связи между юнитами
|
||||
context.UnitInUnits.AddRange(
|
||||
new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
||||
new UnitInUnit { ParentUnitId = parentBadId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
||||
new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitCleanId, DateCreated = DateTimeOffset.UtcNow }
|
||||
);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Формируем тестовый объект Job с настройкой IsFullMatch = true
|
||||
// IsFullMatch = true означает: ВСЕ родители должны соответствовать фильтру
|
||||
var job = new Job
|
||||
{
|
||||
Id = jobId,
|
||||
Name = "Test Job FullMatch",
|
||||
WorkName = "Test Work",
|
||||
TemplateNameMask = "TestMask",
|
||||
WorkGroupMask = "TestGroup",
|
||||
ResponseAreaMask = "TestArea",
|
||||
GroupId = Guid.NewGuid(),
|
||||
TnkId = Guid.NewGuid(),
|
||||
MinValueRelationships = 1,
|
||||
MaxValueRelationships = 10,
|
||||
IsParentRelationships = true,
|
||||
AutoControl = BuildTestAutoControl(jobId),
|
||||
Group = BuildTestJobGroup(),
|
||||
UnitFilters = new List<JobUnitFilter>
|
||||
{
|
||||
new JobUnitFilter
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
JobId = jobId,
|
||||
UnitFilter = "%",
|
||||
FieldFilters = new List<JobFieldFilter>(),
|
||||
RelationshipFilters = new List<JobRelationshipFilter>
|
||||
{
|
||||
new JobRelationshipFilter
|
||||
{
|
||||
UnitFilterId = Guid.NewGuid(),
|
||||
FieldId = checkFieldId,
|
||||
ValueMask = "%Good%",
|
||||
IsParent = true,
|
||||
IsInverse = false,
|
||||
IsFullMatch = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Настройка моков репозиториев
|
||||
var mocks = ArrangeRepositoryMocks(context);
|
||||
|
||||
// Переопределяем мок: возвращаем только родителей, соответствующих маске "Good".
|
||||
// В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
|
||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(checkFieldId, It.IsAny<string>()))
|
||||
.Returns((Guid fId, string mask) =>
|
||||
context.UnitInValues
|
||||
.AsNoTracking()
|
||||
.Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("Good", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(uv => uv.UnitId)
|
||||
.Distinct()
|
||||
.AsQueryable());
|
||||
|
||||
var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
|
||||
var service = CreateSut(mocks, logger);
|
||||
|
||||
// === Act ===
|
||||
var result = await service.GetUnitsByJobFilterAsync(job);
|
||||
|
||||
// === Assert ===
|
||||
var resultIds = result.Select(u => u.Id).ToList();
|
||||
|
||||
Assert.Contains(unitCleanId, resultIds);
|
||||
Assert.DoesNotContain(unitMixedId, resultIds);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет комбинацию флагов: IsParent=true, IsInverse=true, IsFullMatch=true.
|
||||
/// Логика: юнит проходит, только если НИ ОДИН из его родителей не содержит запрещённое значение.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetUnitsByJobFilterAsync_IsParentAndInverseAndFullMatch_UnitWithAnyForbiddenParent_IsExcluded()
|
||||
{
|
||||
// === Arrange ===
|
||||
var dbOptions = new DbContextOptionsBuilder<DataContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
using var context = new DataContext(dbOptions);
|
||||
|
||||
var jobId = Guid.NewGuid();
|
||||
var tagFieldId = Guid.NewGuid();
|
||||
var parentForbiddenId = Guid.NewGuid(); // Родитель с запрещённым тегом
|
||||
var parentCleanId = Guid.NewGuid(); // Родитель без запрещённого тега
|
||||
var unitMixedId = Guid.NewGuid(); // Юнит с одним "плохим" и одним "хорошим" родителем
|
||||
var unitAllCleanId = Guid.NewGuid(); // Юнит только с "хорошими" родителями
|
||||
|
||||
// Подготовка тестовых данных: юниты
|
||||
context.Units.AddRange(
|
||||
BuildTestUnit(parentForbiddenId, "Parent_Forbidden", tagFieldId, "FORBIDDEN-TAG"),
|
||||
BuildTestUnit(parentCleanId, "Parent_Clean", tagFieldId, "CLEAN-TAG"),
|
||||
BuildTestUnit(unitMixedId, "Unit_Mixed", Guid.NewGuid(), "Mixed"),
|
||||
BuildTestUnit(unitAllCleanId, "Unit_AllClean", Guid.NewGuid(), "AllClean")
|
||||
);
|
||||
|
||||
// Подготовка тестовых данных: связи
|
||||
// unitMixed имеет обоих родителей — один с запрещённым тегом, один без
|
||||
// unitAllClean имеет только "чистого" родителя
|
||||
context.UnitInUnits.AddRange(
|
||||
new UnitInUnit { ParentUnitId = parentForbiddenId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
||||
new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
|
||||
new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitAllCleanId, DateCreated = DateTimeOffset.UtcNow }
|
||||
);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Формируем тестовый объект Job с комбинацией флагов:
|
||||
// IsParent = true: проверяем родительские связи
|
||||
// IsInverse = true: исключаем родителей, которые СОВПАДАЮТ с маской
|
||||
// IsFullMatch = true: ВСЕ родители должны пройти фильтр (ни один не должен совпасть с маской)
|
||||
var job = new Job
|
||||
{
|
||||
Id = jobId,
|
||||
Name = "Test Job Combined Flags",
|
||||
WorkName = "Test Work",
|
||||
TemplateNameMask = "TestMask",
|
||||
WorkGroupMask = "TestGroup",
|
||||
ResponseAreaMask = "TestArea",
|
||||
GroupId = Guid.NewGuid(),
|
||||
TnkId = Guid.NewGuid(),
|
||||
MinValueRelationships = 1,
|
||||
MaxValueRelationships = 10,
|
||||
IsParentRelationships = true,
|
||||
AutoControl = BuildTestAutoControl(jobId),
|
||||
Group = BuildTestJobGroup(),
|
||||
UnitFilters = new List<JobUnitFilter>
|
||||
{
|
||||
new JobUnitFilter
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
JobId = jobId,
|
||||
UnitFilter = "%",
|
||||
FieldFilters = new List<JobFieldFilter>(),
|
||||
RelationshipFilters = new List<JobRelationshipFilter>
|
||||
{
|
||||
new JobRelationshipFilter
|
||||
{
|
||||
UnitFilterId = Guid.NewGuid(),
|
||||
FieldId = tagFieldId,
|
||||
ValueMask = "%FORBIDDEN%",
|
||||
IsParent = true,
|
||||
IsInverse = true,
|
||||
IsFullMatch = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Настройка моков репозиториев
|
||||
var mocks = ArrangeRepositoryMocks(context);
|
||||
|
||||
// Переопределяем мок: возвращаем только родителей, содержащих "FORBIDDEN".
|
||||
// В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
|
||||
// Из-за IsInverse=true эти родители будут исключены из "валидных".
|
||||
// Из-за IsFullMatch=true юнит пройдёт, только если ВСЕ его родители валидны.
|
||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny<string>()))
|
||||
.Returns((Guid fId, string mask) =>
|
||||
context.UnitInValues
|
||||
.AsNoTracking()
|
||||
.Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("FORBIDDEN", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(uv => uv.UnitId)
|
||||
.Distinct()
|
||||
.AsQueryable());
|
||||
|
||||
var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
|
||||
var service = CreateSut(mocks, logger);
|
||||
|
||||
// === Act ===
|
||||
var result = await service.GetUnitsByJobFilterAsync(job);
|
||||
|
||||
// === Assert ===
|
||||
var resultIds = result.Select(u => u.Id).ToList();
|
||||
|
||||
// unitAllCleanId должен остаться: у него один родитель, и он не содержит запрещённый тег
|
||||
Assert.Contains(unitAllCleanId, resultIds);
|
||||
|
||||
// unitMixedId должен быть исключён: у него есть родитель с запрещённым тегом.
|
||||
// Логика:
|
||||
// 1. GetMatchingTargetIds возвращает {parentForbiddenId}
|
||||
// 2. IsInverse=true → валидные родители = все.Кроме({parentForbiddenId}) = {parentCleanId}
|
||||
// 3. IsFullMatch=true → проверяем: все родители {parentForbiddenId, parentCleanId} входят в {parentCleanId}? Нет.
|
||||
// 4. Юнит исключается.
|
||||
Assert.DoesNotContain(unitMixedId, resultIds);
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.DAL.Tests
|
||||
{
|
||||
public static class TestDataGenerator
|
||||
{
|
||||
// --- Unit IDs ---
|
||||
public static readonly Guid UnitAId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
public static readonly Guid UnitBId = Guid.Parse("00000000-0000-1111-1111-111111111111");
|
||||
public static readonly Guid UnitCId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||
public static readonly Guid UnitDId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
||||
public static readonly Guid UnitWId = Guid.Parse("44444444-4444-4444-4444-444444444444");
|
||||
public static readonly Guid UnitXId = Guid.Parse("55555555-5555-5555-5555-555555555555");
|
||||
public static readonly Guid UnitYId = Guid.Parse("66666666-6666-6666-6666-666666666666");
|
||||
public static readonly Guid UnitZId = Guid.Parse("77777777-7777-7777-7777-777777777777");
|
||||
|
||||
// --- Field IDs (для обхода составного ключа UnitFilterId+FieldId) ---
|
||||
public static readonly Guid FieldId1 = Guid.Parse("08b42860-09a7-4a1f-aa4f-0290e86df883");//ТИП_ЭК
|
||||
public static readonly Guid FieldId2 = Guid.Parse("5b2d3f5b-a9f3-4a6b-9142-70ef4357ac37"); //неуникальный
|
||||
public static readonly Guid FieldId3 = Guid.Parse("5f3c7b8b-583d-411b-b9bd-1e045ddc5a8e");//ПОДКАТЕГОРИЯ_ЭК
|
||||
public static readonly Guid FieldId4 = Guid.Parse("3e9ac5aa-531b-4ffc-8f93-694d00aa92a0");//активный
|
||||
|
||||
// --- Field Value IDs ---
|
||||
public static readonly Guid FieldValueId1 = Guid.Parse("fe18edcd-ab6a-4f3e-b7bc-e54aad9ecc0d"); // система хранения данных (схд) field3
|
||||
public static readonly Guid FieldValueId2 = Guid.Parse("f385b22c-42c3-4db9-bd41-b829603cc2d9"); // 1 field2+field4
|
||||
public static readonly Guid FieldValueId3 = Guid.Parse("2badc139-69cb-4d52-830a-1b3c6a9faf9d"); // 0 field2+field4
|
||||
public static readonly Guid FieldValueId4 = Guid.Parse("fde68ef7-a07e-4c10-872c-cd98c3d1c30b"); // коммутатор
|
||||
public static readonly Guid FieldValueId5 = Guid.Parse("9f169862-3f78-49f2-b967-13b5b2f56231"); // серверное оборудование
|
||||
public static readonly Guid FieldValueId6 = Guid.Parse("075b6fad-d24f-40f4-aa0e-83a4cfc46bbf"); // стойка цвк
|
||||
public static readonly Guid FieldValueId7 = Guid.Parse("9679afd8-8004-4ce2-bd6f-9005b037ef96"); // схд
|
||||
|
||||
internal static void Seed(DataContext context)
|
||||
{
|
||||
if (context.Units.Any()) return;
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
// 1. Значения полей (остаются те же)
|
||||
var fieldValues = new[]
|
||||
{
|
||||
new UnitFieldValue { Id = FieldValueId1, Value = "система хранения данных (схд)", DateCreated = now },
|
||||
new UnitFieldValue { Id = FieldValueId2, Value = "1", DateCreated = now },
|
||||
new UnitFieldValue { Id = FieldValueId3, Value = "0", DateCreated = now },
|
||||
new UnitFieldValue { Id = FieldValueId4, Value = "коммутатор", DateCreated = now },
|
||||
};
|
||||
context.UnitFieldValues.AddRange(fieldValues);
|
||||
|
||||
// 2. Unit'ы
|
||||
var units = new[]
|
||||
{
|
||||
new Unit { Id = UnitAId, Name = "СХД-КМТ-DVGD-MDS9148-1-ДВС", DateCreated = now },
|
||||
new Unit { Id = UnitBId, Name = "СХД-КМТ-DVGD-MDS9148-2-ДВС", DateCreated = now },
|
||||
new Unit { Id = UnitCId, Name = "СРВ-СТОЙКА-СТОЙКА-ХИВЦ-4.0-ДВС", DateCreated = now },
|
||||
new Unit { Id = UnitDId, Name = "СХД-VSP-E590-01-ДВС", DateCreated = now },
|
||||
new Unit { Id = UnitWId, Name = "СХД-КМТ-SAN1_FAB_1-ДВС", DateCreated = now },
|
||||
new Unit { Id = UnitXId, Name = "СХД-КМТ-CISCO-MDS9148-1-ДВС", DateCreated = now },
|
||||
new Unit { Id = UnitYId, Name = "СХД-КМТ-CISCO-MDS9148-3-ДВС", DateCreated = now },
|
||||
};
|
||||
context.Units.AddRange(units);
|
||||
|
||||
// 3. UnitInUnit (связи)
|
||||
var unitInUnits = new[]
|
||||
{
|
||||
new UnitInUnit { ParentUnitId = UnitAId, ChildUnitId = UnitCId, DateCreated = now },
|
||||
new UnitInUnit { ParentUnitId = UnitBId, ChildUnitId = UnitCId, DateCreated = now },
|
||||
new UnitInUnit { ParentUnitId = UnitDId, ChildUnitId = UnitAId, DateCreated = now },
|
||||
new UnitInUnit { ParentUnitId = UnitDId, ChildUnitId = UnitBId, DateCreated = now },
|
||||
new UnitInUnit { ParentUnitId = UnitWId, ChildUnitId = UnitXId, DateCreated = now },
|
||||
new UnitInUnit { ParentUnitId = UnitWId, ChildUnitId = UnitYId, DateCreated = now }
|
||||
};
|
||||
context.UnitInUnits.AddRange(unitInUnits);
|
||||
|
||||
// 4. UnitInValue
|
||||
var unitInValues = new List<UnitInValue>
|
||||
{
|
||||
// Основные (FieldId)
|
||||
new UnitInValue { UnitId = UnitAId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitAId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitAId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitAId, FieldId = FieldId2, ValueId = FieldValueId3, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitBId, FieldId = FieldId2, ValueId = FieldValueId3, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitBId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitBId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitBId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitCId, FieldId = FieldId3, ValueId = FieldValueId5, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitCId, FieldId = FieldId1, ValueId = FieldValueId6, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitCId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitDId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitDId, FieldId = FieldId1, ValueId = FieldValueId7, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitDId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitWId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitWId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitWId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitWId, FieldId = FieldId2, ValueId = FieldValueId2, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitXId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitXId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitXId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitYId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitYId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
|
||||
new UnitInValue { UnitId = UnitYId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
|
||||
|
||||
};
|
||||
context.UnitInValues.AddRange(unitInValues);
|
||||
|
||||
// 5. JobGroupType и Group
|
||||
var groupType = context.JobGroupTypes
|
||||
.FirstOrDefault(t => t.Code == JobGroupTypesEnum.Simple)
|
||||
?? throw new InvalidOperationException("JobGroupType 'Simple' не найден в контексте.");
|
||||
|
||||
var group = new JobGroup
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GroupTypeId = groupType.Id,
|
||||
GroupType = groupType,
|
||||
GroupName = "Группа",
|
||||
ShortDescription = "%ТНК-КРАТКО% %ЭК%",
|
||||
FullDescription = "Подробно:\n%СВЯЗИ%",
|
||||
Solution = "Решение",
|
||||
TemplateDuration = "540 00:00:00",
|
||||
ReferenceDate = now,
|
||||
IsAutoDistributionEnabled = false,
|
||||
IsAgent = false,
|
||||
DateCreated = now
|
||||
};
|
||||
context.JobGroups.Add(group);
|
||||
|
||||
// 6. Jobs
|
||||
var jobs = new List<Job>();
|
||||
|
||||
// --- TestJob ---
|
||||
var job1 = new Job
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "TestJob_1",
|
||||
WorkName = "Работа",
|
||||
TemplateNameMask = "%ЭК%",
|
||||
WorkGroupMask = "%ГРУППА_РАБОТ%",
|
||||
GroupId = group.Id,
|
||||
TnkId = Guid.NewGuid(),
|
||||
Group = group,
|
||||
DateCreated = now,
|
||||
ResponseAreaMask = "%ЗО_РГ%"
|
||||
};
|
||||
var unitFilter1Id = Guid.NewGuid();
|
||||
job1.UnitFilters = new List<JobUnitFilter>
|
||||
{
|
||||
new JobUnitFilter
|
||||
{
|
||||
Id = unitFilter1Id,
|
||||
UnitFilter = "СХД-%",
|
||||
JobId = job1.Id,
|
||||
Job = job1,
|
||||
DateCreated = now,
|
||||
FieldFilters = new List<JobFieldFilter>
|
||||
{
|
||||
new JobFieldFilter
|
||||
{
|
||||
UnitFilterId = unitFilter1Id,
|
||||
FieldId = FieldId1,
|
||||
ValueMask="коммутатор"
|
||||
},
|
||||
new JobFieldFilter
|
||||
{
|
||||
UnitFilterId = unitFilter1Id,
|
||||
FieldId = FieldId4,
|
||||
ValueMask="1"
|
||||
},
|
||||
new JobFieldFilter
|
||||
{
|
||||
UnitFilterId = unitFilter1Id,
|
||||
FieldId = FieldId2,
|
||||
ValueMask="0"
|
||||
},
|
||||
},
|
||||
RelationshipFilters = new List<JobRelationshipFilter>
|
||||
{
|
||||
new JobRelationshipFilter
|
||||
{
|
||||
IsParent = true,
|
||||
IsInverse = true,
|
||||
IsFullMatch = true,
|
||||
FieldId = FieldId1,
|
||||
ValueMask = "коммутатор",
|
||||
UnitFilterId = unitFilter1Id
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
jobs.Add(job1);
|
||||
|
||||
// --- TestJob_2 (с IsParent = false для дочерних связей) ---
|
||||
var job2 = new Job
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "TestJob_2",
|
||||
WorkName = "Работа2",
|
||||
TemplateNameMask = "%ЭК%",
|
||||
WorkGroupMask = "%ГРУППА_РАБОТ%",
|
||||
GroupId = group.Id,
|
||||
TnkId = Guid.NewGuid(),
|
||||
Group = group,
|
||||
DateCreated = now,
|
||||
ResponseAreaMask = "%ЗО_РГ%"
|
||||
};
|
||||
var unitFilter2Id = Guid.NewGuid();
|
||||
job2.UnitFilters = new List<JobUnitFilter>
|
||||
{
|
||||
new JobUnitFilter
|
||||
{
|
||||
Id = unitFilter2Id,
|
||||
UnitFilter = "СХД-%",
|
||||
JobId = job2.Id,
|
||||
Job = job2,
|
||||
DateCreated = now,
|
||||
FieldFilters = new List<JobFieldFilter>
|
||||
{
|
||||
new JobFieldFilter
|
||||
{
|
||||
UnitFilterId = unitFilter2Id,
|
||||
FieldId = FieldId1,
|
||||
ValueMask="коммутатор"
|
||||
},
|
||||
new JobFieldFilter
|
||||
{
|
||||
UnitFilterId = unitFilter2Id,
|
||||
FieldId = FieldId4,
|
||||
ValueMask="1"
|
||||
},
|
||||
},
|
||||
RelationshipFilters = new List<JobRelationshipFilter>
|
||||
{
|
||||
new JobRelationshipFilter
|
||||
{
|
||||
IsParent = false, // <-- Новый фильтр: ищем дочерние юниты
|
||||
IsInverse = false, // <-- Простой фильтр
|
||||
IsFullMatch = false, // <-- Простой фильтр
|
||||
FieldId = FieldId1,
|
||||
ValueMask = "коммутатор",
|
||||
UnitFilterId = unitFilter2Id
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
jobs.Add(job2);
|
||||
|
||||
context.Jobs.AddRange(jobs);
|
||||
context.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,11 @@
|
||||
<ProjectReference Include="..\PARR.Core\PARR.Core.csproj" />
|
||||
<ProjectReference Include="..\PARR.Domain\PARR.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Разрешаем тестовой сборке видеть internal классы PARR.Core -->
|
||||
<InternalsVisibleTo Include="PARR.Core.Tests" />
|
||||
<!-- Разрешаем Castle DynamicProxy (Moq / NSubstitute) видеть internal классы PARR.Core -->
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("PARR.DAL.Tests")]
|
||||
[assembly: InternalsVisibleTo("PARR.DAL.IntegrationTests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
17
PARR.TemplateMatcher/Models/TemplateAllocationRequest.cs
Normal file
17
PARR.TemplateMatcher/Models/TemplateAllocationRequest.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
|
||||
namespace PARR.TemplateMatcher.Models
|
||||
{
|
||||
public record TemplateAllocationRequest(
|
||||
Job TargetJob,
|
||||
Guid TargetUnitId,
|
||||
Unit? TargetUnit,
|
||||
int? Index,
|
||||
List<UnitInTemplateMessage> UnitsInTemplate,
|
||||
bool IsActiveTemplate,
|
||||
bool IsActiveSchedule,
|
||||
HistoryInitiator Initiator);
|
||||
}
|
||||
@@ -19,30 +19,26 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
private readonly ILogger<GroupedTemplateProcessor> logger;
|
||||
private readonly ITemplateRepository templateRepository;
|
||||
private readonly IUnitRepository unitRepository;
|
||||
private readonly ITemplateReuser templateReuser;
|
||||
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
||||
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
|
||||
private readonly MqSettings mqSettings;
|
||||
private readonly IRabbitService mqService;
|
||||
private readonly ITemplateAllocationService templateAllocationService;
|
||||
private readonly ITemplateMqPublisher templateMqPublisher;
|
||||
|
||||
public GroupedTemplateProcessor(
|
||||
ILogger<GroupedTemplateProcessor> logger,
|
||||
ITemplateRepository templateRepository,
|
||||
IUnitRepository unitRepository,
|
||||
ITemplateReuser templateReuser,
|
||||
ITemplateNameNormalizer templateNameNormalizer,
|
||||
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
||||
ITemplateAllocationService templateAllocationService,
|
||||
ITemplateMqPublisher templateMqPublisher,
|
||||
MqSettings mqSettings,
|
||||
IRabbitService mqService)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.templateRepository = templateRepository;
|
||||
this.unitRepository = unitRepository;
|
||||
this.templateReuser = templateReuser;
|
||||
this.templateNameNormalizer = templateNameNormalizer;
|
||||
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
||||
this.mqSettings = mqSettings;
|
||||
this.mqService = mqService;
|
||||
this.templateAllocationService = templateAllocationService;
|
||||
this.templateMqPublisher = templateMqPublisher;
|
||||
}
|
||||
|
||||
public async Task<HashSet<(Guid JobId, Guid UnitId, int Index)>> ProcessAsync(
|
||||
@@ -89,7 +85,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator);
|
||||
await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +155,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
UnitFieldValueId = t.UnitFieldValueId
|
||||
}).ToList()
|
||||
};
|
||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -174,50 +170,33 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplateSubGroup,
|
||||
Job targetJob,
|
||||
int globalIndex,
|
||||
HistoryInitiator initiator)
|
||||
HistoryInitiator initiator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator);
|
||||
// Вычисляем флаги активности
|
||||
var isActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false;
|
||||
var isActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false;
|
||||
|
||||
if (reusableTemplate != null)
|
||||
{
|
||||
logger.LogInformation("Переиспользован шаблон {TemplateId}.", reusableTemplate.Id);
|
||||
|
||||
var tempTemplateForName = new Template
|
||||
// Маппим кортежи в сообщения
|
||||
var unitsInTemplateMsg = unitsInTemplateSubGroup
|
||||
.Select(e => new UnitInTemplateMessage
|
||||
{
|
||||
Id = reusableTemplate.Id,
|
||||
Name = reusableTemplate.Name,
|
||||
JobId = targetJob.Id,
|
||||
UnitId = potentialUnitId,
|
||||
Index = globalIndex,
|
||||
Job = targetJob,
|
||||
Unit = reusableTemplate.Unit,
|
||||
UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitsInTemplate { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
|
||||
};
|
||||
UnitId = e.UnitId,
|
||||
UnitFieldValueId = e.UnitFieldValueId
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||||
var request = new TemplateAllocationRequest(
|
||||
TargetJob: targetJob,
|
||||
TargetUnitId: potentialUnitId,
|
||||
TargetUnit: null,
|
||||
Index: globalIndex,
|
||||
UnitsInTemplate: unitsInTemplateMsg,
|
||||
IsActiveTemplate: isActiveTemplate,
|
||||
IsActiveSchedule: isActiveSchedule,
|
||||
Initiator: initiator);
|
||||
|
||||
var updateRequest = new TemplateUpdaterMessage
|
||||
{
|
||||
TemplateId = reusableTemplate.Id,
|
||||
JobId = targetJob.Id,
|
||||
UnitId = potentialUnitId,
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false,
|
||||
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
Initiator = initiator,
|
||||
IsNew = true,
|
||||
Index = globalIndex,
|
||||
UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
|
||||
};
|
||||
|
||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Создание нового шаблона.");
|
||||
await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, globalIndex, initiator);
|
||||
}
|
||||
await templateAllocationService.AllocateAsync(request, ct);
|
||||
}
|
||||
|
||||
private async Task UpdateTemplateUnitsAsync(
|
||||
@@ -270,32 +249,9 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
UnitsInTemplate = newUnitEntries.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
|
||||
};
|
||||
|
||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
|
||||
private async Task CreateGroupedTemplateAsync(
|
||||
Guid jobId,
|
||||
Guid relationshipUnitId,
|
||||
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplate,
|
||||
int index,
|
||||
HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogInformation("Создание нового группового шаблона.");
|
||||
|
||||
var mqRequest = new TemplateGeneratorMessage
|
||||
{
|
||||
JobId = jobId,
|
||||
UnitId = relationshipUnitId,
|
||||
UnitsInTemplate = unitsInTemplate.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList(),
|
||||
Index = index,
|
||||
HistoryInitiator = initiator
|
||||
};
|
||||
|
||||
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
|
||||
|
||||
if (!result.IsSuccess)
|
||||
logger.LogError("Ошибка отправки команды создания шаблона.");
|
||||
}
|
||||
|
||||
private static Job SelectTargetJob(List<Job> jobsInGroup, int subGroupSize, Job maxJob)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.Domain.Settings;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
|
||||
@@ -40,10 +41,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
private readonly IRabbitService mqService;
|
||||
private readonly ITemplateRepository templateService;
|
||||
private readonly IJobRepository jobService;
|
||||
private readonly ITemplateReuser templateReuser;
|
||||
private readonly ITemplateDeactivator templateDeactivator;
|
||||
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
||||
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
|
||||
private readonly ITemplateAllocationService templateAllocationService;
|
||||
private readonly ITemplateMqPublisher templateMqPublisher;
|
||||
private readonly IMatchingStatusService matchingStatusService;
|
||||
private readonly SettingsFromDb settingsFromDb;
|
||||
private readonly IOptions<TemplateSettings> templateSettings;
|
||||
@@ -58,10 +59,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
IRabbitService mqService,
|
||||
ITemplateRepository templateService,
|
||||
IJobRepository jobService,
|
||||
ITemplateReuser templateReuser,
|
||||
ITemplateDeactivator templateDeactivator,
|
||||
ITemplateNameNormalizer templateNameNormalizer,
|
||||
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
||||
ITemplateAllocationService templateAllocationService,
|
||||
ITemplateMqPublisher templateMqPublisher,
|
||||
IMatchingStatusService matchingStatusService,
|
||||
SettingsFromDb settingsFromDb,
|
||||
IOptions<TemplateSettings> templateSettings,
|
||||
@@ -76,10 +77,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
this.mqService = mqService;
|
||||
this.templateService = templateService;
|
||||
this.jobService = jobService;
|
||||
this.templateReuser = templateReuser;
|
||||
this.templateDeactivator = templateDeactivator;
|
||||
this.templateNameNormalizer = templateNameNormalizer;
|
||||
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
||||
this.templateAllocationService = templateAllocationService;
|
||||
this.templateMqPublisher = templateMqPublisher;
|
||||
this.matchingStatusService = matchingStatusService;
|
||||
this.settingsFromDb = settingsFromDb;
|
||||
this.templateSettings = templateSettings;
|
||||
@@ -209,49 +210,21 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
|
||||
foreach (var unitId in newUnitIds)
|
||||
{
|
||||
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
|
||||
if (reusableTemplate != null)
|
||||
{
|
||||
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId);
|
||||
// Вычисляем флаги активности из конфигурации автоконтрола
|
||||
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState;
|
||||
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState;
|
||||
|
||||
// === Создаём временный Template для нормализации имени ===
|
||||
var tempTemplateForName = new Template
|
||||
{
|
||||
Id = reusableTemplate.Id,
|
||||
Name = reusableTemplate.Name,
|
||||
JobId = jobId,
|
||||
UnitId = unitId,
|
||||
Index = reusableTemplate.Index,
|
||||
Job = job, // загруженный job
|
||||
Unit = reusableTemplate.Unit, // может быть null — нормально
|
||||
UnitsInTemplate = new List<UnitsInTemplate>() // для простого шаблона
|
||||
};
|
||||
var request = new TemplateAllocationRequest(
|
||||
TargetJob: job,
|
||||
TargetUnitId: unitId,
|
||||
TargetUnit: null, // Для простого шаблона не критично, нормализатор загрузит при необходимости
|
||||
Index: null,
|
||||
UnitsInTemplate: new List<UnitInTemplateMessage>(),
|
||||
IsActiveTemplate: isActiveTemplate,
|
||||
IsActiveSchedule: isActiveSchedule,
|
||||
Initiator: initiator);
|
||||
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||||
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true);
|
||||
|
||||
var updateRequest = new TemplateUpdaterMessage
|
||||
{
|
||||
TemplateId = reusableTemplate.Id,
|
||||
JobId = jobId,
|
||||
UnitId = unitId,
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
|
||||
IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
Initiator = initiator,
|
||||
//NextRun = nextRun,
|
||||
IsNew = true,
|
||||
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
|
||||
};
|
||||
|
||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
|
||||
await CreateSimpleTemplateAsync(jobId, unitId, initiator);
|
||||
}
|
||||
await templateAllocationService.AllocateAsync(request);
|
||||
}
|
||||
|
||||
// === Обработка существующих шаблонов (проверка имени) ===
|
||||
@@ -280,7 +253,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
|
||||
};
|
||||
|
||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,7 +388,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
|
||||
};
|
||||
|
||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -662,7 +635,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
UnitsInTemplate = new List<UnitInTemplateMessage>()
|
||||
};
|
||||
|
||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class TemplateAllocationService : ITemplateAllocationService
|
||||
{
|
||||
private readonly ILogger<TemplateAllocationService> logger;
|
||||
private readonly ITemplateReuser reuser;
|
||||
private readonly ITemplateMqPublisher publisher;
|
||||
|
||||
public TemplateAllocationService(
|
||||
ILogger<TemplateAllocationService> logger,
|
||||
ITemplateReuser reuser,
|
||||
ITemplateMqPublisher publisher)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.reuser = reuser;
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
public async Task AllocateAsync(TemplateAllocationRequest request, CancellationToken ct = default)
|
||||
{
|
||||
// Пробуем переиспользовать
|
||||
bool reused = await reuser.TryReuseAsync(
|
||||
request.TargetJob,
|
||||
request.TargetUnitId,
|
||||
request.TargetUnit,
|
||||
request.Index,
|
||||
request.UnitsInTemplate,
|
||||
request.IsActiveTemplate,
|
||||
request.IsActiveSchedule,
|
||||
request.Initiator,
|
||||
ct);
|
||||
|
||||
if (!reused)
|
||||
{
|
||||
logger.LogDebug("Переиспользование не удалось. Запрос на создание шаблона для Job {JobId}, Unit {UnitId}, Index {Index}.",
|
||||
request.TargetJob.Id, request.TargetUnitId, request.Index);
|
||||
|
||||
var createMessage = new TemplateGeneratorMessage
|
||||
{
|
||||
JobId = request.TargetJob.Id,
|
||||
UnitId = request.TargetUnitId,
|
||||
Index = request.Index,
|
||||
UnitsInTemplate = request.UnitsInTemplate ?? new List<UnitInTemplateMessage>(),
|
||||
IsActiveTemplate = request.IsActiveTemplate,
|
||||
IsActiveSchedule = request.IsActiveSchedule,
|
||||
HistoryInitiator = request.Initiator
|
||||
};
|
||||
|
||||
await publisher.PublishCreateAsync(createMessage, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
||||
private readonly ITemplateRepository templateService;
|
||||
private readonly IJobRepository jobService;
|
||||
private readonly ITemplateNameNormalizer namenormalizer;
|
||||
private readonly ITemplateUpdaterMqSender sender;
|
||||
private readonly ITemplateMqPublisher templateMqPublisher;
|
||||
private readonly SettingsFromDb settingsFromDb;
|
||||
private readonly IOptions<TemplateSettings> templateSettings;
|
||||
|
||||
@@ -32,7 +32,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
||||
ITemplateRepository templateService,
|
||||
IJobRepository jobService,
|
||||
ITemplateNameNormalizer namenormalizer,
|
||||
ITemplateUpdaterMqSender sender,
|
||||
ITemplateMqPublisher templateMqPublisher,
|
||||
SettingsFromDb settingsFromDb,
|
||||
IOptions<TemplateSettings> templateSettings
|
||||
)
|
||||
@@ -41,7 +41,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
||||
this.templateService = templateService;
|
||||
this.jobService = jobService;
|
||||
this.namenormalizer = namenormalizer;
|
||||
this.sender = sender;
|
||||
this.templateMqPublisher = templateMqPublisher;
|
||||
this.settingsFromDb = settingsFromDb;
|
||||
this.templateSettings = templateSettings;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
||||
UnitsInTemplate = new List<UnitInTemplateMessage>()
|
||||
};
|
||||
|
||||
await sender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Common.Interfaces.RabbitServices;
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class TemplateMqPublisher : ITemplateMqPublisher
|
||||
{
|
||||
private readonly ILogger<TemplateMqPublisher> logger;
|
||||
private readonly IRabbitService mqService;
|
||||
private readonly MqSettings mqSettings;
|
||||
|
||||
public TemplateMqPublisher(ILogger<TemplateMqPublisher> logger, IRabbitService mqService, MqSettings mqSettings)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.mqService = mqService;
|
||||
this.mqSettings = mqSettings;
|
||||
}
|
||||
|
||||
public async Task PublishUpdateAsync(TemplateUpdaterMessage message, CancellationToken ct = default)
|
||||
{
|
||||
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new List<object> { message });
|
||||
if (!result.IsSuccess)
|
||||
logger.LogError("Ошибка публикации команды обновления шаблона {TemplateId}.", message.TemplateId);
|
||||
}
|
||||
|
||||
public async Task PublishCreateAsync(TemplateGeneratorMessage message, CancellationToken ct = default)
|
||||
{
|
||||
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { message });
|
||||
if (!result.IsSuccess)
|
||||
logger.LogError("Ошибка публикации команды создания шаблона для Job {JobId}, Unit {UnitId}.", message.JobId, message.UnitId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
@@ -10,71 +14,105 @@ namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
internal class TemplateReuser : ITemplateReuser
|
||||
{
|
||||
private readonly ILogger<TemplateReuser> logger;
|
||||
private readonly ITemplateRepository templateService;
|
||||
private readonly ITemplateRepository templateRepository;
|
||||
private readonly ITemplateNameNormalizer nameNormalizer;
|
||||
private readonly ITemplateMqPublisher mqPublisher;
|
||||
|
||||
public TemplateReuser(
|
||||
ILogger<TemplateReuser> logger,
|
||||
ITemplateRepository templateService)
|
||||
ITemplateRepository templateRepository,
|
||||
ITemplateNameNormalizer nameNormalizer,
|
||||
ITemplateMqPublisher mqPublisher)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.templateService = templateService;
|
||||
this.templateRepository = templateRepository;
|
||||
this.nameNormalizer = nameNormalizer;
|
||||
this.mqPublisher = mqPublisher;
|
||||
}
|
||||
|
||||
|
||||
public async Task<Template?> TryReuseOneUnusedTemplateAsync(
|
||||
Guid jobId,
|
||||
Guid unitId,
|
||||
public async Task<bool> TryReuseAsync(
|
||||
Job targetJob,
|
||||
Guid targetUnitId,
|
||||
Unit? targetUnit,
|
||||
int? index,
|
||||
List<UnitInTemplateMessage> unitsInTemplate,
|
||||
bool isActiveTemplate,
|
||||
bool isActiveSchedule,
|
||||
HistoryInitiator initiator,
|
||||
int maxAttempts = 3)
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const int maxAttempts = 3;
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
// Атомарно резервируем один шаблон через DAL
|
||||
var templateId = await templateService.ReserveUnusedTemplateAsync(unitId, initiator);
|
||||
|
||||
// 1. Атомарный захват шаблона
|
||||
var templateId = await templateRepository.ReserveUnusedTemplateAsync(targetUnitId, initiator);
|
||||
if (templateId == null)
|
||||
{
|
||||
logger.LogDebug("Нет доступных Unused-шаблонов для переиспользования (попытка {Attempt}).", attempt);
|
||||
return null;
|
||||
logger.LogDebug("Нет доступных Unused-шаблонов (попытка {Attempt}).", attempt);
|
||||
return false; // Шаблоны закончились
|
||||
}
|
||||
|
||||
// Загружаем зарезервированный шаблон
|
||||
var template = await templateService.Get()
|
||||
// Загружаем захваченный шаблон
|
||||
var template = await templateRepository.Get()
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Unit)
|
||||
.Include(t => t.Job)
|
||||
.ThenInclude(t => t!.Tnk)
|
||||
.Include(t => t.Job)
|
||||
.ThenInclude(t => t!.Group)
|
||||
.ThenInclude(t => t!.GroupType)
|
||||
.FirstOrDefaultAsync(t => t.Id == templateId);
|
||||
.FirstOrDefaultAsync(t => t.Id == templateId, ct);
|
||||
|
||||
if (template == null)
|
||||
{
|
||||
logger.LogWarning("Зарезервированный шаблон {TemplateId} не найден при загрузке.", templateId);
|
||||
continue;
|
||||
logger.LogWarning("Зарезервированный шаблон {TemplateId} не найден.", templateId);
|
||||
continue; // Попробовать еще раз
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Успешно захвачен шаблон {TemplateId} (старый Job {OldJobId}) для нового Job {NewJobId}, Unit {UnitId} (попытка {Attempt}).",
|
||||
template.Id, template.JobId, jobId, unitId, attempt);
|
||||
logger.LogInformation("Шаблон {TemplateId} захвачен для переиспользования.", templateId);
|
||||
|
||||
return template;
|
||||
// 2. Нормализация имени
|
||||
var templateForName = new Template
|
||||
{
|
||||
Id = template.Id,
|
||||
Name = template.Name,
|
||||
JobId = targetJob.Id,
|
||||
UnitId = targetUnitId,
|
||||
Index = index,
|
||||
Job = targetJob,
|
||||
Unit = targetUnit,
|
||||
UnitsInTemplate = unitsInTemplate.Select(m => new UnitsInTemplate { UnitId = m.UnitId, UnitFieldValueId = m.UnitFieldValueId }).ToList()
|
||||
};
|
||||
|
||||
var expectedName = await nameNormalizer.GetNormalizedTemplateNameAsync(templateForName);
|
||||
|
||||
// 3. Отправка команды в MQ
|
||||
var message = new TemplateUpdaterMessage
|
||||
{
|
||||
TemplateId = template.Id,
|
||||
JobId = targetJob.Id,
|
||||
UnitId = targetUnitId,
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = isActiveTemplate,
|
||||
IsActiveSchedule = isActiveSchedule,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
Initiator = initiator,
|
||||
IsNew = true,
|
||||
Index = index ?? template.Index,
|
||||
UnitsInTemplate = unitsInTemplate
|
||||
};
|
||||
|
||||
await mqPublisher.PublishUpdateAsync(message, ct);
|
||||
return true; // Успех
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка при попытке захвата шаблона (попытка {Attempt}).", attempt);
|
||||
|
||||
if (attempt == maxAttempts)
|
||||
throw;
|
||||
|
||||
// Небольшая задержка перед повтором
|
||||
await Task.Delay(Random.Shared.Next(10, 50));
|
||||
logger.LogError(ex, "Ошибка при переиспользовании (попытка {Attempt}).", attempt);
|
||||
if (attempt == maxAttempts) return false; // После 3 попыток сдаемся
|
||||
await Task.Delay(Random.Shared.Next(10, 50), ct);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Common.Interfaces.RabbitServices;
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
|
||||
internal class TemplateUpdaterMqSender : ITemplateUpdaterMqSender
|
||||
{
|
||||
private readonly ILogger<TemplateUpdaterMqSender> logger;
|
||||
private readonly IRabbitService mqService;
|
||||
private readonly MqSettings mqSettings;
|
||||
|
||||
public TemplateUpdaterMqSender(
|
||||
ILogger<TemplateUpdaterMqSender> logger,
|
||||
IRabbitService mqService,
|
||||
MqSettings mqSettings
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.mqService = mqService;
|
||||
this.mqSettings = mqSettings;
|
||||
}
|
||||
|
||||
public async Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest)
|
||||
{
|
||||
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
|
||||
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
|
||||
|
||||
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new List<object> { updateRequest });
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.",
|
||||
updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using PARR.TemplateMatcher.Models;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Оркестрирует выделение шаблона: попытка переиспользования или создание нового.
|
||||
/// </summary>
|
||||
public interface ITemplateAllocationService
|
||||
{
|
||||
Task AllocateAsync(TemplateAllocationRequest request, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Публикует команды создания и обновления шаблонов в RabbitMQ.
|
||||
/// </summary>
|
||||
public interface ITemplateMqPublisher
|
||||
{
|
||||
Task PublishUpdateAsync(TemplateUpdaterMessage message, CancellationToken ct = default);
|
||||
Task PublishCreateAsync(TemplateGeneratorMessage message, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,31 @@
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Отвечает за полный цикл переиспользования Unused-шаблона:
|
||||
/// 1. Атомарный захват в БД (с retry).
|
||||
/// 2. Нормализация имени.
|
||||
/// 3. Отправка команды на обновление (активацию) в MQ.
|
||||
/// </summary>
|
||||
public interface ITemplateReuser
|
||||
{
|
||||
/// <summary>
|
||||
/// Пытается переиспользовать неиспользуемый шаблон для указанной работы и ЭК.
|
||||
/// При резервировании шаблона для правильного расчета NextRun Id работы
|
||||
/// и ЭК устанавливается при захвате.
|
||||
/// Пытается переиспользовать шаблон. Возвращает true, если успешно захвачен и отправлена команда.
|
||||
/// </summary>
|
||||
/// <param name="jobId">Id работы.</param>
|
||||
/// <param name="unitId">Id ЭК.</param>
|
||||
/// <param name="initiator">Данные инициатора операции.</param>
|
||||
/// <param name="maxAttempts">Максимальное количество попыток (по умолчанию 3).</param>
|
||||
/// <returns>
|
||||
/// Зарезервированный шаблон или <see langword="null"/>,
|
||||
/// если не удалось получить шаблон после всех попыток.
|
||||
/// </returns>
|
||||
Task<Template?> TryReuseOneUnusedTemplateAsync(
|
||||
Guid jobId,
|
||||
Guid unitId,
|
||||
Task<bool> TryReuseAsync(
|
||||
Job targetJob,
|
||||
Guid targetUnitId,
|
||||
Unit? targetUnit,
|
||||
int? index,
|
||||
List<UnitInTemplateMessage> unitsInTemplate,
|
||||
bool isActiveTemplate,
|
||||
bool isActiveSchedule,
|
||||
HistoryInitiator initiator,
|
||||
int maxAttempts = 3);
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||
{
|
||||
public interface ITemplateUpdaterMqSender
|
||||
{
|
||||
Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest);
|
||||
}
|
||||
}
|
||||
@@ -26,20 +26,34 @@ namespace PARR.TemplateMatcher
|
||||
configuration.GetSection(nameof(TemplateSettings)).Bind(templateSettings);
|
||||
services.AddSingleton(templateSettings);
|
||||
|
||||
services.AddTransient<IMqTemplateMatcher, MqTemplateMatcher>();
|
||||
services.AddTransient<IJobValidatorService, JobValidatorService>();
|
||||
services.AddTransient<IJobGroupValidatorService, JobGroupValidatorService>();
|
||||
services.AddTransient<ITemplateMatcher, TemplateMatcher>();
|
||||
services.AddTransient<ITemplateReuser, TemplateReuser>();
|
||||
// === 1. Singleton: Долгоживущие сервисы и обработчики очередей ===
|
||||
services.AddSingleton<IMqTemplateMatcher, MqTemplateMatcher>();
|
||||
|
||||
// === 2. Scoped: Бизнес-логика и работа с БД (DbContext) ===
|
||||
// Создаются заново для каждого сообщения из очереди (внутри CreateAsyncScope)
|
||||
services.AddScoped<ITemplateMatcher, TemplateMatcher>();
|
||||
services.AddScoped<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
||||
services.AddScoped<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
||||
|
||||
// Пайплайн аллокации шаблонов
|
||||
services.AddScoped<ITemplateAllocationService, TemplateAllocationService>();
|
||||
services.AddScoped<ITemplateReuser, TemplateReuser>();
|
||||
services.AddScoped<ITemplateDeactivator, TemplateDeactivator>();
|
||||
|
||||
// Пайплайн групповых шаблонов
|
||||
services.AddScoped<IGroupedTemplateUnitFilter, GroupedTemplateUnitFilter>();
|
||||
services.AddScoped<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
|
||||
services.AddScoped<IGroupedTemplateBuilder, GroupedTemplateBuilder>();
|
||||
services.AddScoped<IGroupedTemplateProcessor, GroupedTemplateProcessor>();
|
||||
|
||||
// Валидаторы
|
||||
services.AddScoped<IJobValidatorService, JobValidatorService>();
|
||||
services.AddScoped<IJobGroupValidatorService, JobGroupValidatorService>();
|
||||
|
||||
// === 3. Transient: Stateless утилиты и инфраструктура ===
|
||||
// Легковесные сервисы без состояния, создаются по требованию
|
||||
services.AddTransient<ITemplateMqPublisher, TemplateMqPublisher>();
|
||||
services.AddTransient<ITemplateNameNormalizer, TemplateNameNormalizer>();
|
||||
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
|
||||
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
|
||||
services.AddTransient<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
|
||||
services.AddTransient<IGroupedTemplateUnitFilter, GroupedTemplateUnitFilter>();
|
||||
services.AddTransient<IGroupedTemplateBuilder, GroupedTemplateBuilder>();
|
||||
services.AddTransient<IGroupedTemplateProcessor, GroupedTemplateProcessor>();
|
||||
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
||||
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddTemplateMatcherConfigurations(this IConfigurationBuilder builder, IServiceCollection services)
|
||||
|
||||
Reference in New Issue
Block a user