refactor(templateMatcher): Переход на Pipeline-архитектуру для SimpleSync и GroupedSync.

- SimpleTemplateSynchronizer и GroupedTemplateSynchronizer переведены на паттерн Pipeline с разделением на Read/Write этапы
- Выделены контракты этапов (ISimpleSyncStage, IGroupedSyncStage) и контексты (SimpleSyncContext, GroupedSyncContext)
- Read-этапы безопасны для тестов (не пишут в БД/MQ), Write-этапы изолированы через отдельные интерфейсы
- Добавлено [Perf]-логирование каждого этапа с метриками времени выполнения
- Логи приведены к человекочитаемому формату 'Имя' (ID) для Job, JobGroup и Unit
- Устранено дублирование данных в контекстах (FilteredUnits перезаписывается, TemplateGroups строго типизирован)
- Константы неиспользуемых шаблонов вынесены в UnusedTemplateConstants
- Структура проекта реорганизована: SimpleSync, GroupedSync, Implementations, Interfaces
This commit is contained in:
Mikhail Kuznetsov
2026-06-11 15:09:46 +10:00
parent 16f4944a62
commit a438f18480
44 changed files with 1124 additions and 525 deletions

View File

@@ -1,6 +1,6 @@
namespace PARR.Core.Services.UnitFilterService.Models namespace PARR.Core.Services.UnitFilterService.Models
{ {
internal class UnitFilterMatchResult public class UnitFilterMatchResult
{ {
public Guid UnitId { get; set; } public Guid UnitId { get; set; }
public HashSet<Guid> ValidParentIds { get; set; } = new(); public HashSet<Guid> ValidParentIds { get; set; } = new();

View File

@@ -0,0 +1,25 @@
namespace PARR.TemplateMatcher.Constants
{
/// <summary>
/// Контрактные константы для обработки неиспользуемых шаблонов.
/// Являются соглашением между TemplateMatcher и внешними системами.
/// Изменение требует согласования со всеми участниками контракта.
/// </summary>
public static class UnusedTemplateConstants
{
/// <summary>
/// Системное имя поля зоны ответственности.
/// </summary>
public const string ResponsibilityAreaFieldName = "ЗОНА_ОТВЕТСТВЕННОСТИ";
/// <summary>
/// Системное имя поля тега ПАРР.
/// </summary>
public const string ParrTagFieldName = "ПАРР тег";
/// <summary>
/// Значение тега, обозначающее неиспользуемый юнит.
/// </summary>
public const string NotUsedTagValue = "ПАРР-НЕИСП";
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class BuildGroupsStage : IGroupedSyncStage
{
private readonly IGroupedTemplateBuilder _builder;
private readonly ILogger<BuildGroupsStage> _logger;
public string StageName => "Построение групп";
public BuildGroupsStage(IGroupedTemplateBuilder builder, ILogger<BuildGroupsStage> logger)
{
_builder = builder;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var groups = await _builder.BuildAsync(context.ReverseMapping, context.JobGroup, context.MaxJob);
if (!groups.Any())
throw new GroupedSyncEarlyExitException("Нет данных после построения групп");
context.TemplateGroups = groups;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): построено {Count} групп",
context.JobGroupName, context.JobGroupId, groups.Count);
return context;
}
}

View File

@@ -0,0 +1,71 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class DeactivateTemplatesStage : IGroupedSyncWriteStage
{
private readonly ITemplateRepository _templateRepository;
private readonly ITemplateDeactivator _deactivator;
private readonly ILogger<DeactivateTemplatesStage> _logger;
public string StageName => "Деактивация шаблонов";
public DeactivateTemplatesStage(
ITemplateRepository templateRepository,
ITemplateDeactivator deactivator,
ILogger<DeactivateTemplatesStage> logger)
{
_templateRepository = templateRepository;
_deactivator = deactivator;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var allJobIds = context.JobsInGroup.Select(j => j.Id).ToHashSet();
var existingTemplates = await _templateRepository.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.Job)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIds.Contains(t.JobId)
&& t.StatusTypeId == TemplateStatusTypeEnum.Used
&& t.Job!.GroupId == context.JobGroupId)
.ToListAsync(ct);
int deactivated = 0;
foreach (var template in existingTemplates)
{
var key = (template.JobId, template.UnitId, template.Index ?? -1);
if (!context.ExpectedTemplateKeys.Contains(key))
{
var unitLogName = context.UnitNames.TryGetValue(template.UnitId, out var unitName)
? $"'{unitName}' ({template.UnitId})"
: template.Unit != null
? $"'{template.Unit.Name}' ({template.UnitId})"
: $"({template.UnitId})";
_logger.LogInformation(
"JobGroup '{JobGroupName}' ({JobGroupId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Job '{JobName}' ({JobId}), Unit {Unit}",
context.JobGroupName, context.JobGroupId,
template.Name, template.Id,
template.Job?.Name ?? string.Empty, template.JobId,
unitLogName);
await _deactivator.DeactivateTemplateAsync(template, context.Initiator);
deactivated++;
}
}
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): деактивировано {Count} шаблонов из {Total}",
context.JobGroupName, context.JobGroupId, deactivated, existingTemplates.Count);
return context;
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Services.UnitFilterService;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class FilterUnitsStage : IGroupedSyncStage
{
private readonly IUnitFilterService _filterService;
private readonly ILogger<FilterUnitsStage> _logger;
public string StageName => "Фильтрация юнитов";
public FilterUnitsStage(IUnitFilterService filterService, ILogger<FilterUnitsStage> logger)
{
_filterService = filterService;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var result = await _filterService.GetUnitsByJobFilterAsync(context.MaxJob.Id, null, ct);
if (result == null || !result.Any())
throw new GroupedSyncEarlyExitException("Фильтры не дали Unit'ов с подходящими связями");
context.FilteredUnits = result.ToList();
context.UnitNames = result.ToDictionary(u => u.Id, u => u.Name);
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): отфильтровано {Count} юнитов",
context.JobGroupName, context.JobGroupId, context.FilteredUnits.Count);
return context;
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class GroupFilterStage : IGroupedSyncStage
{
private readonly IGroupedTemplateUnitFilter _groupedFilter;
private readonly ILogger<GroupFilterStage> _logger;
public string StageName => "Групповая фильтрация";
public GroupFilterStage(IGroupedTemplateUnitFilter groupedFilter, ILogger<GroupFilterStage> logger)
{
_groupedFilter = groupedFilter;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var finalFiltered = await _groupedFilter.FilterAsync(context.FilteredUnits, context.JobGroup);
if (!finalFiltered.Any())
throw new GroupedSyncEarlyExitException("Нет юнитов после групповой фильтрации");
context.FilteredUnits = finalFiltered;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): после групповой фильтрации {Count} юнитов",
context.JobGroupName, context.JobGroupId, finalFiltered.Count);
return context;
}
}

View File

@@ -0,0 +1,38 @@
using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
/// <summary>
/// Контекст групповой синхронизации. Передаётся между этапами.
/// </summary>
public class GroupedSyncContext
{
public Guid JobGroupId { get; init; }
public string JobGroupName { get; set; } = string.Empty;
public HistoryInitiator Initiator { get; init; } = null!;
public JobGroup JobGroup { get; set; } = null!;
public List<Job> JobsInGroup { get; set; } = new();
public Job MaxJob { get; set; } = null!;
public List<UnitFilterResultDto> FilteredUnits { get; set; } = new();
public Dictionary<Guid, List<Guid>> ReverseMapping { get; set; } = new();
public List<GroupedTemplateGroup> TemplateGroups { get; set; } = new();
public HashSet<(Guid JobId, Guid UnitId, int Index)> ExpectedTemplateKeys { get; set; } = new();
/// <summary>
/// Имена юнитов для логирования. Заполняется на этапе фильтрации.
/// </summary>
public Dictionary<Guid, string> UnitNames { get; set; } = new();
/// <summary>
/// Возвращает строку вида 'Имя' (ID) для логирования.
/// </summary>
public string FormatUnit(Guid unitId)
{
return UnitNames.TryGetValue(unitId, out var name)
? $"'{name}' ({unitId})"
: $"({unitId})";
}
}

View File

@@ -0,0 +1,16 @@
namespace PARR.TemplateMatcher.Services.GroupedSync
{
/// <summary>
/// Штатное прерывание пайплайна (нет данных после этапа).
/// Не является ошибкой — оркестратор перехватывает и логирует как нормальное завершение.
/// </summary>
public class GroupedSyncEarlyExitException : Exception
{
public string Reason { get; }
public GroupedSyncEarlyExitException(string reason) : base(reason)
{
Reason = reason;
}
}
}

View File

@@ -6,9 +6,8 @@ using PARR.Domain.Constants;
using PARR.Domain.Entities; using PARR.Domain.Entities;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations; namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateBuilder : IGroupedTemplateBuilder internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
{ {

View File

@@ -12,7 +12,7 @@ using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations; namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateProcessor : IGroupedTemplateProcessor internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
{ {
@@ -136,7 +136,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate);
if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id); logger.LogInformation("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id);
var updateRequest = new TemplateUpdaterMessage var updateRequest = new TemplateUpdaterMessage
{ {
TemplateId = existingTemplate.Id, TemplateId = existingTemplate.Id,
@@ -160,7 +160,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
} }
else else
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id); logger.LogInformation("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id);
await UpdateTemplateUnitsAsync(existingTemplate, sortedProposed, targetJob, globalIndex, initiator); await UpdateTemplateUnitsAsync(existingTemplate, sortedProposed, targetJob, globalIndex, initiator);
} }
} }

View File

@@ -2,9 +2,8 @@
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations; namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateUnitFilter : IGroupedTemplateUnitFilter internal class GroupedTemplateUnitFilter : IGroupedTemplateUnitFilter
{ {

View File

@@ -0,0 +1,14 @@
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync
{
/// <summary>
/// Этап групповой синхронизации, который только читает данные.
/// НЕ выполняет запись в БД, MQ или кэш.
/// </summary>
public interface IGroupedSyncStage
{
string StageName { get; }
Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,8 @@
namespace PARR.TemplateMatcher.Services.GroupedSync
{
/// <summary>
/// Этап групповой синхронизации с побочными эффектами (запись в БД, MQ).
/// В тестах не подключается — тип системы гарантирует безопасность.
/// </summary>
public interface IGroupedSyncWriteStage : IGroupedSyncStage { }
}

View File

@@ -1,7 +1,7 @@
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Interfaces; namespace PARR.TemplateMatcher.Services.GroupedSync;
public interface IGroupedTemplateBuilder public interface IGroupedTemplateBuilder
{ {

View File

@@ -2,7 +2,7 @@
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Interfaces; namespace PARR.TemplateMatcher.Services.GroupedSync;
public interface IGroupedTemplateProcessor public interface IGroupedTemplateProcessor
{ {

View File

@@ -1,7 +1,7 @@
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
namespace PARR.TemplateMatcher.Services.Interfaces namespace PARR.TemplateMatcher.Services.GroupedSync
{ {
public interface IGroupedTemplateUnitFilter public interface IGroupedTemplateUnitFilter
{ {

View File

@@ -1,7 +1,7 @@
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
namespace PARR.TemplateMatcher.Services.Interfaces namespace PARR.TemplateMatcher.Services.GroupedSync
{ {
/// <summary> /// <summary>
/// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей. /// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей.

View File

@@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class LoadJobGroupStage : IGroupedSyncStage
{
private readonly IJobGroupRepository _jobGroupRepository;
private readonly ILogger<LoadJobGroupStage> _logger;
public string StageName => "Загрузка JobGroup";
public LoadJobGroupStage(IJobGroupRepository jobGroupRepository, ILogger<LoadJobGroupStage> logger)
{
_jobGroupRepository = jobGroupRepository;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var jobGroup = await _jobGroupRepository.Get()
.AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType)
.Include(jg => jg.Jobs).ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs).ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters).ThenInclude(rf => rf.UnitField)
.Include(jg => jg.Jobs).ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == context.JobGroupId, ct);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
throw new GroupedSyncEarlyExitException("JobGroup не найден или пуст");
var jobsInGroup = jobGroup.Jobs.ToList();
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
throw new GroupedSyncEarlyExitException("Не найден Job с MaxValueRelationships");
context.JobGroup = jobGroup;
context.JobGroupName = jobGroup.GroupName;
context.JobsInGroup = jobsInGroup;
context.MaxJob = maxJob;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): загружено {JobCount} Job'ов, эталонный Job '{MaxJobName}' ({MaxJobId})",
jobGroup.GroupName, jobGroup.Id, jobsInGroup.Count, maxJob.Name, maxJob.Id);
return context;
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class ProcessGroupsStage : IGroupedSyncWriteStage
{
private readonly IGroupedTemplateProcessor _processor;
private readonly ILogger<ProcessGroupsStage> _logger;
public string StageName => "Обработка групп";
public ProcessGroupsStage(IGroupedTemplateProcessor processor, ILogger<ProcessGroupsStage> logger)
{
_processor = processor;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
// Приведение типа обратно из object — пайплайн хранит как object для универсальности контекста
var typedGroups = context.TemplateGroups
.Cast<GroupedTemplateGroup>()
.ToList();
var expectedKeys = await _processor.ProcessAsync(
typedGroups, context.JobsInGroup, context.MaxJob, context.Initiator);
context.ExpectedTemplateKeys = expectedKeys;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): обработано групп, ожидаемых ключей={Count}",
context.JobGroupName, context.JobGroupId, expectedKeys.Count);
return context;
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
internal class ResolveConflictsStage : IGroupedSyncStage
{
private readonly IUnitInTemplateConflictMapper _conflictMapper;
private readonly ILogger<ResolveConflictsStage> _logger;
public string StageName => "Разрешение конфликтов";
public ResolveConflictsStage(IUnitInTemplateConflictMapper conflictMapper, ILogger<ResolveConflictsStage> logger)
{
_conflictMapper = conflictMapper;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
{
var mapping = await _conflictMapper.BuildMappingAsync(context.FilteredUnits, context.MaxJob, ct);
if (!mapping.Any())
throw new GroupedSyncEarlyExitException("Нет связей после разрешения конфликтов");
context.ReverseMapping = mapping;
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): разрешено конфликтов, связей={Count}",
context.JobGroupName, context.JobGroupId, mapping.Count);
return context;
}
}

View File

@@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitFilterService.Models; using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.GroupedSync;
internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
{ {

View File

@@ -1,260 +1,110 @@
using System.Diagnostics; using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.MatchingStatusService; using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models; using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using System.Diagnostics;
namespace PARR.TemplateMatcher.Services.Implementations; namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{ {
private readonly ILogger<GroupedTemplateSynchronizer> logger; private readonly IEnumerable<IGroupedSyncStage> _readStages;
private readonly IJobGroupRepository jobGroupService; private readonly IEnumerable<IGroupedSyncWriteStage> _writeStages;
private readonly IUnitFilterService unitFilterService; private readonly IMatchingStatusService _matchingStatusService;
private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter; private readonly ILogger<GroupedTemplateSynchronizer> _logger;
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
private readonly IGroupedTemplateBuilder groupedTemplateBuilder;
private readonly IGroupedTemplateProcessor groupedTemplateProcessor;
private readonly ITemplateRepository templateService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly IMatchingStatusService matchingStatusService;
public GroupedTemplateSynchronizer( public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger, IEnumerable<IGroupedSyncStage> readStages,
IJobGroupRepository jobGroupService, IEnumerable<IGroupedSyncWriteStage> writeStages,
IUnitFilterService unitFilterService, IMatchingStatusService matchingStatusService,
IGroupedTemplateUnitFilter groupedTemplateUnitFilter, ILogger<GroupedTemplateSynchronizer> logger)
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
IGroupedTemplateBuilder groupedTemplateBuilder,
IGroupedTemplateProcessor groupedTemplateProcessor,
ITemplateRepository templateService,
ITemplateDeactivator templateDeactivator,
IMatchingStatusService matchingStatusService)
{ {
this.logger = logger; _readStages = readStages;
this.jobGroupService = jobGroupService; _writeStages = writeStages;
this.unitFilterService = unitFilterService; _matchingStatusService = matchingStatusService;
this.groupedTemplateUnitFilter = groupedTemplateUnitFilter; _logger = logger;
this.unitInTemplateConflictMapper = unitInTemplateConflictMapper;
this.groupedTemplateBuilder = groupedTemplateBuilder;
this.groupedTemplateProcessor = groupedTemplateProcessor;
this.templateService = templateService;
this.templateDeactivator = templateDeactivator;
this.matchingStatusService = matchingStatusService;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
} }
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{ {
var totalSw = Stopwatch.StartNew(); _logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// === Проверка: уже запущена? === var existingStatus = await _matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Any() == true) if (existingStatus.DetailsJobGroups?.Any() == true)
{ {
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId); _logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return; return;
} }
// === Устанавливаем статус "в процессе" === await SetStatusAsync(jobGroupId, "Начало синхронизации");
var initialStatus = new MatchingStatusItemDto
{ var totalSw = Stopwatch.StartNew();
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
try try
{ {
// === ЭТАП 1: Загрузка JobGroup === var context = new GroupedSyncContext { JobGroupId = jobGroupId, Initiator = initiator };
foreach (var stage in _readStages)
{
var stageSw = Stopwatch.StartNew(); var stageSw = Stopwatch.StartNew();
var jobGroup = await jobGroupService.Get() await stage.ExecuteAsync(context);
.AsNoTracking() stageSw.Stop();
.AsSingleQuery() _logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
.Include(jg => jg.GroupType) context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
.Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.ThenInclude(rf => rf.UnitField)
.Include(jg => jg.Jobs)
.ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return;
} }
var jobsInGroup = jobGroup.Jobs.ToList(); foreach (var stage in _writeStages)
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Загрузка JobGroup | Время: {Ms} мс | Jobs: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, jobsInGroup.Count);
// === Поиск эталонного Job ===
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
{ {
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); var stageSw = Stopwatch.StartNew();
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships"); await stage.ExecuteAsync(context);
return; stageSw.Stop();
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
} }
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}).", maxJob.Id, maxJob.MaxValueRelationships); totalSw.Stop();
_logger.LogInformation("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | ИТОГО: {TotalMs} мс",
context.JobGroupName, jobGroupId, totalSw.ElapsedMilliseconds);
// === ЭТАП 2: Фильтрация юнитов === await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно");
stageSw.Restart(); await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id); _logger.LogInformation("Синхронизация шаблонов завершена для JobGroup '{JobGroupName}' ({JobGroupId})",
stageSw.Stop(); context.JobGroupName, jobGroupId);
var filterCount = unitFilterResults?.Count() ?? 0;
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, filterCount);
if (unitFilterResults == null || !unitFilterResults.Any())
{
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
return;
} }
catch (GroupedSyncEarlyExitException ex)
// === ЭТАП 3: Групповая фильтрация ===
stageSw.Restart();
var finalFilteredUnits = await groupedTemplateUnitFilter.FilterAsync(unitFilterResults, jobGroup);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Групповая фильтрация | Время: {Ms} мс | Результат: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, finalFilteredUnits.Count);
if (!finalFilteredUnits.Any())
{ {
logger.LogInformation("После применения правил фильтрации в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); totalSw.Stop();
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации"); _logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)",
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); return; jobGroupId, ex.Reason, totalSw.ElapsedMilliseconds);
} await SetStatusAsync(jobGroupId, ex.Reason);
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
// === ЭТАП 4: Разрешение конфликтов ===
stageSw.Restart();
var initialReverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnits, maxJob);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Разрешение конфликтов | Время: {Ms} мс | Связей: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, initialReverseMapping.Count);
if (!initialReverseMapping.Any())
{
logger.LogInformation("После разрешения конфликтов в JobGroup {JobGroupId} не осталось связей.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет связей после разрешения конфликтов");
return;
}
// === ЭТАП 5: Построение структуры групп ===
stageSw.Restart();
var templateGroups = await groupedTemplateBuilder.BuildAsync(initialReverseMapping, jobGroup, maxJob);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Построение групп | Время: {Ms} мс | Групп: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, templateGroups.Count);
if (!templateGroups.Any())
{
logger.LogInformation("После построения структуры групп в JobGroup {JobGroupId} не осталось данных.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет данных после построения групп");
return;
}
// === ЭТАП 6: Обработка групп (сравнение, обновление, MQ) ===
stageSw.Restart();
var expectedTemplateKeys = await groupedTemplateProcessor.ProcessAsync(
templateGroups,
jobsInGroup,
maxJob,
initiator);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Обработка групп | Время: {Ms} мс | Ключей: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, expectedTemplateKeys.Count);
// === ЭТАП 7: Деактивация лишних шаблонов ===
stageSw.Restart();
await DeactivateUnusedTemplatesAsync(expectedTemplateKeys, jobGroupId, jobsInGroup, initiator);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Деактивация | Время: {Ms} мс",
jobGroupId, stageSw.ElapsedMilliseconds);
// === ИТОГО === totalSw.Stop();
logger.LogInformation(
"[Perf] JobGroup {JobGroupId} | ИТОГО: {TotalMs} мс",
jobGroupId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
} }
catch (Exception ex) catch (Exception ex)
{ {
totalSw.Stop(); totalSw.Stop();
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс", jobGroupId, totalSw.ElapsedMilliseconds); _logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс",
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}"); jobGroupId, totalSw.ElapsedMilliseconds);
await SetStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw; throw;
} }
} }
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) public Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); _logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
return Task.CompletedTask;
} }
private async Task DeactivateUnusedTemplatesAsync( public Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
Guid jobGroupId,
List<Job> jobsInGroup,
HistoryInitiator initiator)
{ {
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); _logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
var allExistingTemplatesInGroup = await templateService.Get() return Task.CompletedTask;
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) &&
t.StatusTypeId == TemplateStatusTypeEnum.Used &&
t.Job!.GroupId == jobGroupId)
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Unit {UnitId}, Index {Index}).",
existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
}
}
} }
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment) private async Task SetStatusAsync(Guid jobGroupId, string comment)
{ {
var status = new MatchingStatusItemDto var status = new MatchingStatusItemDto
{ {
@@ -262,12 +112,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
Action = TemplateMatcherActionEnum.Sync, Action = TemplateMatcherActionEnum.Sync,
Comment = comment Comment = comment
}; };
await _matchingStatusService.SetMatchingStatusAsync(
await matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup,
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) }, new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30) TimeSpan.FromMinutes(30));
);
} }
} }

View File

@@ -2,28 +2,28 @@
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions namespace PARR.TemplateMatcher.Services.Implementations
{ {
internal class JobGroupValidatorService : IJobGroupValidatorService internal class JobGroupValidatorService : IJobGroupValidatorService
{ {
private readonly ILogger<IJobValidatorService> logger; private readonly ILogger<IJobValidatorService> _logger;
private readonly IJobGroupRepository jobGroupService; private readonly IJobGroupRepository _jobGroupService;
public JobGroupValidatorService( public JobGroupValidatorService(
ILogger<IJobValidatorService> logger, ILogger<IJobValidatorService> logger,
IJobGroupRepository jobGroupService IJobGroupRepository jobGroupService
) )
{ {
this.logger = logger; _logger = logger;
this.jobGroupService = jobGroupService; _jobGroupService = jobGroupService;
} }
public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId) public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId)
{ {
var isExist = await jobGroupService.GetAsync(jobGroupId); var isExist = await _jobGroupService.GetAsync(jobGroupId);
if (isExist == null) if (isExist == null)
{ {
logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}"); _logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
return false; return false;
} }

View File

@@ -2,7 +2,7 @@
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions namespace PARR.TemplateMatcher.Services.Implementations
{ {
internal class JobValidatorService : IJobValidatorService internal class JobValidatorService : IJobValidatorService
{ {

View File

@@ -7,7 +7,7 @@ using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher.Services.Implementations
{ {
internal class MqTemplateMatcher : IMqTemplateMatcher internal class MqTemplateMatcher : IMqTemplateMatcher
{ {

View File

@@ -15,8 +15,9 @@ using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit; using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.Domain.Settings; using PARR.Domain.Settings;
using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Constants;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
using System.Diagnostics; using System.Diagnostics;
@@ -31,18 +32,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private const bool DefaultUsedTemplateState = false; private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false; private const bool DefaultUsedScheduleState = false;
// === Константы для логики неиспользуемых шаблонов === private readonly IEnumerable<ISimpleSyncStage> readStages;
private const string FieldNameResponsibilityArea = "ЗОНА_ОТВЕТСТВЕННОСТИ"; private readonly IEnumerable<ISimpleSyncWriteStage> writeStages;
private const string FieldNameParrTag = "ПАРР тег";
private const string TagValueNotWorking = "ПАРР-НЕИСП";
private readonly ILogger<SimpleTemplateSynchronizer> logger; private readonly ILogger<SimpleTemplateSynchronizer> logger;
private readonly IUnitFilterService unitFilterService; private readonly IUnitFilterService unitFilterService;
private readonly MqSettings mqSettings; private readonly MqSettings mqSettings;
private readonly IRabbitService mqService; private readonly IRabbitService mqService;
private readonly ITemplateRepository templateService; private readonly ITemplateRepository templateService;
private readonly IJobRepository jobService; private readonly IJobRepository jobService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateAllocationService templateAllocationService; private readonly ITemplateAllocationService templateAllocationService;
private readonly ITemplateMqPublisher templateMqPublisher; private readonly ITemplateMqPublisher templateMqPublisher;
@@ -54,13 +51,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private readonly IUnitRepository unitRepository; private readonly IUnitRepository unitRepository;
public SimpleTemplateSynchronizer( public SimpleTemplateSynchronizer(
IEnumerable<ISimpleSyncStage> readStages,
IEnumerable<ISimpleSyncWriteStage> writeStages,
ILogger<SimpleTemplateSynchronizer> logger, ILogger<SimpleTemplateSynchronizer> logger,
IUnitFilterService unitFilterService, IUnitFilterService unitFilterService,
MqSettings mqSettings, MqSettings mqSettings,
IRabbitService mqService, IRabbitService mqService,
ITemplateRepository templateService, ITemplateRepository templateService,
IJobRepository jobService, IJobRepository jobService,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer, ITemplateNameNormalizer templateNameNormalizer,
ITemplateAllocationService templateAllocationService, ITemplateAllocationService templateAllocationService,
ITemplateMqPublisher templateMqPublisher, ITemplateMqPublisher templateMqPublisher,
@@ -72,13 +70,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
IUnitRepository unitRepository IUnitRepository unitRepository
) )
{ {
this.readStages = readStages;
this.writeStages = writeStages;
this.logger = logger; this.logger = logger;
this.unitFilterService = unitFilterService; this.unitFilterService = unitFilterService;
this.mqSettings = mqSettings; this.mqSettings = mqSettings;
this.mqService = mqService; this.mqService = mqService;
this.templateService = templateService; this.templateService = templateService;
this.jobService = jobService; this.jobService = jobService;
this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer; this.templateNameNormalizer = templateNameNormalizer;
this.templateAllocationService = templateAllocationService; this.templateAllocationService = templateAllocationService;
this.templateMqPublisher = templateMqPublisher; this.templateMqPublisher = templateMqPublisher;
@@ -92,18 +91,15 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{ {
// === Специальная обработка для Job неиспользуемых шаблонов ===
if (jobId == settingsFromDb.JobIdForUnusedTemplates) if (jobId == settingsFromDb.JobIdForUnusedTemplates)
{ {
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId); logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId);
await SyncUnusedTemplatesAsync(jobId, initiator); await SyncUnusedTemplatesAsync(jobId, initiator);
return; return;
} }
var totalSw = Stopwatch.StartNew();
logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId); logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Any() == true) if (existingStatus.DetailsJobs?.Any() == true)
{ {
@@ -111,7 +107,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return; return;
} }
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto var initialStatus = new MatchingStatusItemDto
{ {
DateStart = DateTimeOffset.UtcNow, DateStart = DateTimeOffset.UtcNow,
@@ -119,169 +114,49 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Comment = "Начало синхронизации" Comment = "Начало синхронизации"
}; };
await matchingStatusService.SetMatchingStatusAsync( await matchingStatusService.SetMatchingStatusAsync(
jobId, jobId, SyncTaskEntityTypeEnum.Job,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(35) TimeSpan.FromMinutes(35));
);
// Таймер запускается ПОСЛЕ инфраструктурных операций (статус, проверка блокировки)
var totalSw = Stopwatch.StartNew();
try try
{ {
// === ЭТАП 1: Загрузка Job === var context = new SimpleSyncContext { JobId = jobId, Initiator = initiator };
foreach (var stage in readStages)
{
var stageSw = Stopwatch.StartNew(); var stageSw = Stopwatch.StartNew();
var job = await jobService.Get() await stage.ExecuteAsync(context);
.AsNoTracking()
.Include(j => j.AutoControl)
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g!.GroupType)
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == jobId);
if (job == null)
{
logger.LogWarning("Job {JobId} не найден.", jobId);
await UpdateMatchingStatusAsync(jobId, "Job не найден");
return;
}
stageSw.Stop(); stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка Job | Время: {Ms} мс", logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
jobId, stageSw.ElapsedMilliseconds); context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
// === ЭТАП 2: Фильтрация юнитов ===
stageSw.Restart();
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
stageSw.Stop();
var filterCount = filteredUnits?.Count() ?? 0;
logger.LogDebug("[Perf] Job {JobId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
jobId, stageSw.ElapsedMilliseconds, filterCount);
var unitIds = filteredUnits?.Select(u => u.Id).ToHashSet() ?? new HashSet<Guid>();
// === ЭТАП 3: Загрузка существующих шаблонов ===
stageSw.Restart();
var existingTemplates = await templateService.Get()
.Include(t => t.UnitsInTemplate)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t => t!.GroupType)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Include(t => t.Unit)
.Where(t => t.JobId == jobId)
.ToListAsync();
var existingUsedTemplates = existingTemplates
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToList();
var existingUnitIds = existingUsedTemplates.Select(t => t.UnitId).ToHashSet();
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка шаблонов | Время: {Ms} мс | Используется: {Count}",
jobId, stageSw.ElapsedMilliseconds, existingUsedTemplates.Count);
// === ЭТАП 4: Расчёт диффа (создание / деактивация / переименование) ===
stageSw.Restart();
var newUnitIds = unitIds.Except(existingUnitIds).ToList();
var unusedTemplates = existingUsedTemplates
.Where(t => !unitIds.Contains(t.UnitId))
.ToList();
// Проверка имён существующих шаблонов
var templatesToRename = new List<(Template Template, string ExpectedName)>();
foreach (var template in existingUsedTemplates)
{
if (!unitIds.Contains(template.UnitId))
continue;
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
templatesToRename.Add((template, expectedName));
}
} }
stageSw.Stop(); foreach (var stage in writeStages)
logger.LogDebug("[Perf] Job {JobId} | Этап: Расчёт диффа | Время: {Ms} мс | Создать: {Create}, Деактивировать: {Deactivate}, Переименовать: {Rename}",
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count, unusedTemplates.Count, templatesToRename.Count);
// === ЭТАП 5: Создание новых шаблонов ===
stageSw.Restart();
foreach (var unitId in newUnitIds)
{ {
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState; var stageSw = Stopwatch.StartNew();
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState; await stage.ExecuteAsync(context);
stageSw.Stop();
var request = new TemplateAllocationRequest( logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
TargetJob: job, context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
TargetUnitId: unitId,
TargetUnit: null,
Index: null,
UnitsInTemplate: new List<UnitInTemplateMessage>(),
IsActiveTemplate: isActiveTemplate,
IsActiveSchedule: isActiveSchedule,
Initiator: initiator);
await templateAllocationService.AllocateAsync(request);
} }
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Создание шаблонов | Время: {Ms} мс | Количество: {Count}",
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count);
// === ЭТАП 6: Обновление имён существующих шаблонов ===
stageSw.Restart();
foreach (var (template, expectedName) in templatesToRename)
{
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: '{OldName}' → '{NewName}'",
template.Id, template.Name, expectedName);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await templateMqPublisher.PublishUpdateAsync(updateRequest);
}
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Обновление имён | Время: {Ms} мс | Количество: {Count}",
jobId, stageSw.ElapsedMilliseconds, templatesToRename.Count);
// === ЭТАП 7: Деактивация лишних шаблонов ===
stageSw.Restart();
foreach (var unusedTemplate in unusedTemplates)
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).",
unusedTemplate.Id, jobId, unusedTemplate.UnitId);
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
}
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Деактивация | Время: {Ms} мс | Количество: {Count}",
jobId, stageSw.ElapsedMilliseconds, unusedTemplates.Count);
// === ИТОГО ===
totalSw.Stop(); totalSw.Stop();
logger.LogInformation("[Perf] Job {JobId} | ИТОГО: {TotalMs} мс", jobId, totalSw.ElapsedMilliseconds); logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс",
context.JobName, jobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно"); await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})",
context.JobName, jobId);
} }
catch (Exception ex) catch (Exception ex)
{ {
totalSw.Stop(); totalSw.Stop();
logger.LogError(ex, "Ошибка при синхронизации Job {JobId} через {ElapsedMs} мс", jobId, totalSw.ElapsedMilliseconds); logger.LogError(ex, "Ошибка при синхронизации Job '{JobName}' ({JobId}) через {ElapsedMs} мс",
string.Empty, jobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}"); await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
throw; throw;
} }
@@ -293,6 +168,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId); logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
} }
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId); logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
@@ -444,30 +320,29 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
try try
{ {
// 1. Находим ID нужных полей // 1. Находим ID нужных полей
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(FieldNameResponsibilityArea); var responsableAreaField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName);
var tagField = await unitFieldService.GetByAihitNameAsync(FieldNameParrTag); var tagField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName);
if (responsableAreaField == null || tagField == null) if (responsableAreaField == null || tagField == null)
{ {
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", FieldNameResponsibilityArea, FieldNameParrTag); logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", UnusedTemplateConstants.ResponsibilityAreaFieldName, UnusedTemplateConstants.NotUsedTagValue);
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей"); await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
return; return;
} }
var responsableAreaFieldId = responsableAreaField.Id; var responsableAreaFieldId = responsableAreaField.Id;
var tagFieldId = tagField.Id; var tagFieldId = tagField.Id;
const string targetTagValue = TagValueNotWorking;
// 2. Находим ValueId для тега "ПАРР-НЕИСП" // 2. Находим ValueId для тега "ПАРР-НЕИСП"
var targetTagValueId = await unitInValueService.Get() var targetTagValueId = await unitInValueService.Get()
.AsNoTracking() .AsNoTracking()
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == targetTagValue) .Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == UnusedTemplateConstants.NotUsedTagValue)
.Select(uiv => uiv.ValueId) .Select(uiv => uiv.ValueId)
.FirstOrDefaultAsync(ct); .FirstOrDefaultAsync(ct);
if (targetTagValueId == Guid.Empty) if (targetTagValueId == Guid.Empty)
{ {
logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", targetTagValue, FieldNameParrTag); logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", UnusedTemplateConstants.NotUsedTagValue, UnusedTemplateConstants.ParrTagFieldName);
} }
var unusedJob = await jobService.Get().AsNoTracking() var unusedJob = await jobService.Get().AsNoTracking()

View File

@@ -4,17 +4,17 @@ using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job; using PARR.Domain.Entities.Job;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Implementations; using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher.Services.Implementations
{ {
internal class TemplateMatcher : ITemplateMatcher internal class TemplateMatcher : ITemplateMatcher
{ {
private readonly ILogger<TemplateMatcher> logger; private readonly ILogger<TemplateMatcher> _logger;
private readonly IJobRepository jobService; private readonly IJobRepository _jobService;
private readonly IJobGroupRepository jobGroupService; private readonly IJobGroupRepository _jobGroupService;
private readonly IEnumerable<ITemplateSynchronizer> synchronizers; private readonly IEnumerable<ITemplateSynchronizer> _synchronizers;
public TemplateMatcher( public TemplateMatcher(
ILogger<TemplateMatcher> logger, ILogger<TemplateMatcher> logger,
@@ -23,21 +23,21 @@ namespace PARR.TemplateMatcher
IEnumerable<ITemplateSynchronizer> synchronizers IEnumerable<ITemplateSynchronizer> synchronizers
) )
{ {
this.logger = logger; _logger = logger;
this.jobService = jobService; _jobService = jobService;
this.jobGroupService = jobGroupService; _jobGroupService = jobGroupService;
this.synchronizers = synchronizers; _synchronizers = synchronizers;
} }
public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator) public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); _logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId);
var job = await GetJobWithGroupAndAutoControlAsync(jobId); var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null) if (job == null)
{ {
logger.LogError("Job с Id {JobId} не найден.", jobId); _logger.LogError("Job с Id {JobId} не найден.", jobId);
return; return;
} }
@@ -46,9 +46,9 @@ namespace PARR.TemplateMatcher
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue) if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
{ {
logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer.", jobId); _logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer.", jobId);
// Находим нужный синхронизатор // Находим нужный синхронизатор
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
// Так как Job групповой, вызываем синхронизацию для его JobGroup // Так как Job групповой, вызываем синхронизацию для его JobGroup
@@ -56,22 +56,22 @@ namespace PARR.TemplateMatcher
} }
else else
{ {
logger.LogError("GroupedTemplateSynchronizer не найден."); _logger.LogError("GroupedTemplateSynchronizer не найден.");
} }
return; return;
} }
else else
{ {
logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer.", jobId); _logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer.", jobId);
// Находим нужный синхронизатор // Находим нужный синхронизатор
var synchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
await synchronizer.SyncTemplatesForJobAsync(jobId, initiator); await synchronizer.SyncTemplatesForJobAsync(jobId, initiator);
} }
else else
{ {
logger.LogError("SimpleTemplateSynchronizer не найден."); _logger.LogError("SimpleTemplateSynchronizer не найден.");
} }
return; return;
} }
@@ -79,16 +79,16 @@ namespace PARR.TemplateMatcher
public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); _logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
var jobGroup = await jobGroupService.Get() var jobGroup = await _jobGroupService.Get()
.AsNoTracking() .AsNoTracking()
.Include(jg => jg.GroupType) .Include(jg => jg.GroupType)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId); .FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.GroupType == null) if (jobGroup == null || jobGroup.GroupType == null)
{ {
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId); _logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
return; return;
} }
@@ -99,32 +99,32 @@ namespace PARR.TemplateMatcher
// Проверяем, есть ли GroupingUnitFieldId — это признак "настоящей" группировки // Проверяем, есть ли GroupingUnitFieldId — это признак "настоящей" группировки
if (jobGroup.GroupingUnitFieldId.HasValue) if (jobGroup.GroupingUnitFieldId.HasValue)
{ {
logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId);
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator); await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator);
} }
else else
{ {
logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId); _logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId);
} }
} }
else else
{ {
logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId);
//await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); //await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
} }
break; break;
case JobGroupTypesEnum.Umbrella: case JobGroupTypesEnum.Umbrella:
logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId);
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
break; break;
case JobGroupTypesEnum.Simple: case JobGroupTypesEnum.Simple:
default: default:
logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId);
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator); await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
break; break;
} }
@@ -132,16 +132,16 @@ namespace PARR.TemplateMatcher
public async Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) public async Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало обновления шаблонов для JobGroup {JobGroupId}", jobGroupId); _logger.LogDebug("Начало обновления шаблонов для JobGroup {JobGroupId}", jobGroupId);
var jobGroup = await jobGroupService.Get() var jobGroup = await _jobGroupService.Get()
.AsNoTracking() .AsNoTracking()
.Include(jg => jg.GroupType) .Include(jg => jg.GroupType)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId); .FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.GroupType == null) if (jobGroup == null || jobGroup.GroupType == null)
{ {
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId); _logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
return; return;
} }
@@ -150,23 +150,23 @@ namespace PARR.TemplateMatcher
case JobGroupTypesEnum.Group: case JobGroupTypesEnum.Group:
if (jobGroup.GroupingUnitFieldId.HasValue) if (jobGroup.GroupingUnitFieldId.HasValue)
{ {
logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId); _logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId);
} }
else else
{ {
logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
} }
break; break;
case JobGroupTypesEnum.Umbrella: case JobGroupTypesEnum.Umbrella:
logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
break; break;
case JobGroupTypesEnum.Simple: case JobGroupTypesEnum.Simple:
default: default:
logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId); _logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator); await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
break; break;
} }
@@ -175,12 +175,12 @@ namespace PARR.TemplateMatcher
public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); _logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId);
var job = await GetJobWithGroupAndAutoControlAsync(jobId); var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null) if (job == null)
{ {
logger.LogError("Job с Id {JobId} не найден.", jobId); _logger.LogError("Job с Id {JobId} не найден.", jobId);
return; return;
} }
@@ -189,9 +189,9 @@ namespace PARR.TemplateMatcher
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue) if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
{ {
logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer для Update.", jobId); _logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer для Update.", jobId);
// Находим нужный синхронизатор // Находим нужный синхронизатор
var synchronizer = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
// Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение) // Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение)
@@ -199,22 +199,22 @@ namespace PARR.TemplateMatcher
} }
else else
{ {
logger.LogError("GroupedTemplateSynchronizer не найден."); _logger.LogError("GroupedTemplateSynchronizer не найден.");
} }
return; return;
} }
else else
{ {
logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer для Update.", jobId); _logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer для Update.", jobId);
// Находим нужный синхронизатор // Находим нужный синхронизатор
var synchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); var synchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (synchronizer != null) if (synchronizer != null)
{ {
await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator); await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator);
} }
else else
{ {
logger.LogError("SimpleTemplateSynchronizer не найден."); _logger.LogError("SimpleTemplateSynchronizer не найден.");
} }
return; return;
} }
@@ -223,9 +223,9 @@ namespace PARR.TemplateMatcher
// --- Вспомогательные методы --- // --- Вспомогательные методы ---
private async Task SyncJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator) private async Task SyncJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Синхронизация JobGroup {JobGroupId} как Collection (по каждому Job'у отдельно)", jobGroupId); _logger.LogDebug("Синхронизация JobGroup {JobGroupId} как Collection (по каждому Job'у отдельно)", jobGroupId);
var jobIds = await jobService.Get() var jobIds = await _jobService.Get()
.AsNoTracking() .AsNoTracking()
.Where(j => j.GroupId == jobGroupId) .Where(j => j.GroupId == jobGroupId)
.Select(j => j.Id) .Select(j => j.Id)
@@ -233,35 +233,35 @@ namespace PARR.TemplateMatcher
if (!jobIds.Any()) if (!jobIds.Any())
{ {
logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId); _logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
return; return;
} }
logger.LogDebug("Найдено {Count} Job'ов в JobGroup {JobGroupId}", jobIds.Count, jobGroupId); _logger.LogDebug("Найдено {Count} Job'ов в JobGroup {JobGroupId}", jobIds.Count, jobGroupId);
var simpleSynchronizer = synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer); var simpleSynchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (simpleSynchronizer == null) if (simpleSynchronizer == null)
{ {
logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId); _logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId);
return; return;
} }
foreach (var jobId in jobIds) foreach (var jobId in jobIds)
{ {
logger.LogDebug("Синхронизация Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId); _logger.LogDebug("Синхронизация Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId);
await simpleSynchronizer.SyncTemplatesForJobAsync(jobId, initiator); await simpleSynchronizer.SyncTemplatesForJobAsync(jobId, initiator);
} }
logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId); _logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId);
} }
private async Task UpdateJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator) private async Task UpdateJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Обновление JobGroup {JobGroupId} как Collection (по каждому Job'у)", jobGroupId); _logger.LogDebug("Обновление JobGroup {JobGroupId} как Collection (по каждому Job'у)", jobGroupId);
var jobIds = await jobService.Get() var jobIds = await _jobService.Get()
.AsNoTracking() .AsNoTracking()
.Where(j => j.GroupId == jobGroupId) .Where(j => j.GroupId == jobGroupId)
.Select(j => j.Id) .Select(j => j.Id)
@@ -269,23 +269,23 @@ namespace PARR.TemplateMatcher
if (!jobIds.Any()) if (!jobIds.Any())
{ {
logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId); _logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
return; return;
} }
foreach (var jobId in jobIds) foreach (var jobId in jobIds)
{ {
logger.LogDebug("Обновление шаблонов для Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId); _logger.LogDebug("Обновление шаблонов для Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId);
await UpdateTemplatesForJob(jobId, initiator); await UpdateTemplatesForJob(jobId, initiator);
} }
logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId); _logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId);
} }
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId) private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
{ {
return await jobService.Get() return await _jobService.Get()
.AsNoTracking() .AsNoTracking()
.Include(j => j.Group) .Include(j => j.Group)
.ThenInclude(j => j!.GroupType) .ThenInclude(j => j!.GroupType)

View File

@@ -1,4 +1,4 @@
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher.Services.Interfaces
{ {
public interface IMqTemplateMatcher public interface IMqTemplateMatcher
{ {

View File

@@ -1,6 +1,6 @@
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher.Services.Interfaces
{ {
public interface ITemplateMatcher public interface ITemplateMatcher
{ {

View File

@@ -0,0 +1,52 @@
using Microsoft.Extensions.Logging;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class AllocateTemplatesStage : ISimpleSyncWriteStage
{
private readonly ITemplateAllocationService _allocationService;
private readonly ILogger<AllocateTemplatesStage> _logger;
public string StageName => "Создание шаблонов";
public AllocateTemplatesStage(
ITemplateAllocationService allocationService,
ILogger<AllocateTemplatesStage> logger)
{
_allocationService = allocationService;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
const bool defaultUsedTemplateState = false;
const bool defaultUsedScheduleState = false;
foreach (var unitId in context.NewUnitIds)
{
var isActiveTemplate = context.Job.AutoControl?.InitUsedTemplateState ?? defaultUsedTemplateState;
var isActiveSchedule = context.Job.AutoControl?.InitUsedScheduleState ?? defaultUsedScheduleState;
var request = new TemplateAllocationRequest(
TargetJob: context.Job,
TargetUnitId: unitId,
TargetUnit: null,
Index: null,
UnitsInTemplate: new List<UnitInTemplateMessage>(),
IsActiveTemplate: isActiveTemplate,
IsActiveSchedule: isActiveSchedule,
Initiator: context.Initiator);
await _allocationService.AllocateAsync(request);
_logger.LogDebug("Job '{JobName}' ({JobId}): создан шаблон для Unit {Unit}",
context.JobName, context.JobId, context.FormatUnit(unitId));
}
return context;
}
}

View File

@@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class AnalyzeChangesStage : ISimpleSyncStage
{
private readonly ITemplateRepository _templateRepository;
private readonly ITemplateNameNormalizer _nameNormalizer;
private readonly ILogger<AnalyzeChangesStage> _logger;
public string StageName => "Анализ изменений";
public AnalyzeChangesStage(
ITemplateRepository templateRepository,
ITemplateNameNormalizer nameNormalizer,
ILogger<AnalyzeChangesStage> logger)
{
_templateRepository = templateRepository;
_nameNormalizer = nameNormalizer;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
var existing = await _templateRepository.Get()
.Include(t => t.UnitsInTemplate)
.Include(t => t.Job).ThenInclude(t => t!.Group).ThenInclude(t => t!.GroupType)
.Include(t => t.Job).ThenInclude(t => t!.Tnk)
.Include(t => t.Unit)
.Where(t => t.JobId == context.JobId)
.ToListAsync(ct);
var used = existing.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used).ToList();
var existingUnitIds = used.Select(t => t.UnitId).ToHashSet();
context.ExistingUsedTemplates = used;
context.NewUnitIds = context.FilteredUnitIds.Except(existingUnitIds).ToList();
context.UnusedTemplates = used.Where(t => !context.FilteredUnitIds.Contains(t.UnitId)).ToList();
foreach (var template in used)
{
if (!context.FilteredUnitIds.Contains(template.UnitId)) continue;
var expectedName = await _nameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
context.TemplatesToRename.Add((template, expectedName));
}
_logger.LogDebug(
"Job '{JobName}' ({JobId}): существующих шаблонов={Existing}, создать={Create}, деактивировать={Deactivate}, переименовать={Rename}",
context.JobName, context.JobId,
used.Count, context.NewUnitIds.Count, context.UnusedTemplates.Count, context.TemplatesToRename.Count);
return context;
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class DeactivateTemplatesStage : ISimpleSyncWriteStage
{
private readonly ITemplateDeactivator _deactivator;
private readonly ILogger<DeactivateTemplatesStage> _logger;
public string StageName => "Деактивация шаблонов";
public DeactivateTemplatesStage(ITemplateDeactivator deactivator, ILogger<DeactivateTemplatesStage> logger)
{
_deactivator = deactivator;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
foreach (var template in context.UnusedTemplates)
{
_logger.LogInformation("Job '{JobName}' ({JobId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Unit {Unit}",
context.JobName, context.JobId, template.Name, template.Id,
context.FormatUnit(template.UnitId));
await _deactivator.DeactivateTemplateAsync(template, context.Initiator);
}
return context;
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Services.UnitFilterService;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class FilterUnitsStage : ISimpleSyncStage
{
private readonly IUnitFilterService _filterService;
private readonly ILogger<FilterUnitsStage> _logger;
public string StageName => "Фильтрация юнитов";
public FilterUnitsStage(IUnitFilterService filterService, ILogger<FilterUnitsStage> logger)
{
_filterService = filterService;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
var filtered = await _filterService.GetUnitsByJobFilterAsync(context.JobId, null, ct);
if (filtered != null)
{
context.FilteredUnitIds = filtered.Select(u => u.Id).ToHashSet();
context.UnitNames = filtered.ToDictionary(u => u.Id, u => u.Name);
}
_logger.LogDebug("Job '{JobName}' ({JobId}): отфильтровано {Count} юнитов",
context.JobName, context.JobId, context.FilteredUnitIds.Count);
return context;
}
}

View File

@@ -0,0 +1,12 @@
namespace PARR.TemplateMatcher.Services.SimpleSync
{
/// <summary>
/// Этап синхронизации, который только читает данные и заполняет контекст.
/// НЕ выполняет запись в БД, MQ или кэш.
/// </summary>
public interface ISimpleSyncStage
{
string StageName { get; }
Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,10 @@
namespace PARR.TemplateMatcher.Services.SimpleSync
{
/// <summary>
/// Этап синхронизации, который выполняет побочные эффекты (запись в БД, MQ).
/// В тестах не подключается — тип системы гарантирует безопасность.
/// </summary>
public interface ISimpleSyncWriteStage : ISimpleSyncStage
{
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class LoadJobStage : ISimpleSyncStage
{
private readonly IJobRepository _jobRepository;
private readonly ILogger<LoadJobStage> _logger;
public string StageName => "Загрузка Job";
public LoadJobStage(IJobRepository jobRepository, ILogger<LoadJobStage> logger)
{
_jobRepository = jobRepository;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
var job = await _jobRepository.Get()
.AsNoTracking()
.AsSplitQuery()
.Include(j => j.AutoControl)
.Include(j => j.Tnk)
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
.FirstOrDefaultAsync(j => j.Id == context.JobId, ct);
if (job == null)
throw new InvalidOperationException($"Job '{context.JobId}' не найден");
context.Job = job;
context.JobName = job.Name;
_logger.LogDebug("Job '{JobName}' ({JobId}) загружен", job.Name, job.Id);
return context;
}
}

View File

@@ -0,0 +1,39 @@
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
namespace PARR.TemplateMatcher.Services.SimpleSync
{
/// <summary>
/// Контекст синхронизации простого шаблона. Передаётся между этапами.
/// </summary>
public class SimpleSyncContext
{
public Guid JobId { get; init; }
public string JobName { get; set; } = string.Empty;
public HistoryInitiator Initiator { get; init; } = null!;
public Job Job { get; set; } = null!;
public HashSet<Guid> FilteredUnitIds { get; set; } = new();
/// <summary>
/// Имена юнитов для логирования. Заполняется на этапе фильтрации.
/// </summary>
public Dictionary<Guid, string> UnitNames { get; set; } = new();
public List<Template> ExistingUsedTemplates { get; set; } = new();
public List<Guid> NewUnitIds { get; set; } = new();
public List<Template> UnusedTemplates { get; set; } = new();
public List<(Template Template, string ExpectedName)> TemplatesToRename { get; set; } = new();
/// <summary>
/// Возвращает строку вида 'Имя' (ID) для логирования.
/// Если имя неизвестно — возвращает только ID.
/// </summary>
public string FormatUnit(Guid unitId)
{
return UnitNames.TryGetValue(unitId, out var name)
? $"'{name}' ({unitId})"
: $"({unitId})";
}
}
}

View File

@@ -0,0 +1,50 @@
using Microsoft.Extensions.Logging;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
internal class UpdateNamesStage : ISimpleSyncWriteStage
{
private readonly ITemplateMqPublisher _mqPublisher;
private readonly ILogger<UpdateNamesStage> _logger;
public string StageName => "Обновление имён";
public UpdateNamesStage(ITemplateMqPublisher mqPublisher, ILogger<UpdateNamesStage> logger)
{
_mqPublisher = mqPublisher;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
{
foreach (var (template, expectedName) in context.TemplatesToRename)
{
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = context.JobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = context.Initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await _mqPublisher.PublishUpdateAsync(updateRequest);
_logger.LogDebug("Job '{JobName}' ({JobId}): шаблон '{TemplateName}' ({TemplateId}) для Unit {Unit} переименован в '{NewName}'",
context.JobName, context.JobId, template.Name, template.Id,
context.FormatUnit(template.UnitId), expectedName);
}
return context;
}
}

View File

@@ -3,9 +3,10 @@ using Microsoft.Extensions.DependencyInjection;
using PARR.Core; using PARR.Core;
using PARR.DAL; using PARR.DAL;
using PARR.Infrastructure; using PARR.Infrastructure;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Implementations; using PARR.TemplateMatcher.Services.Implementations;
using PARR.TemplateMatcher.Services.Implemetaions;
using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
using PARR.TemplateMatcher.Settings; using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher namespace PARR.TemplateMatcher
@@ -31,8 +32,31 @@ namespace PARR.TemplateMatcher
// === 2. Scoped: Бизнес-логика и работа с БД (DbContext) === // === 2. Scoped: Бизнес-логика и работа с БД (DbContext) ===
// Создаются заново для каждого сообщения из очереди (внутри CreateAsyncScope) // Создаются заново для каждого сообщения из очереди (внутри CreateAsyncScope)
services.AddScoped<ITemplateMatcher, TemplateMatcher>(); services.AddScoped<ITemplateMatcher, Services.Implementations.TemplateMatcher>();
// Simple pipeline: Read-этапы
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.LoadJobStage>(); //1
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.FilterUnitsStage>(); //2
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.AnalyzeChangesStage>(); //3
// Simple pipeline: Write-этапы
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.AllocateTemplatesStage>(); //4
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.UpdateNamesStage>(); //5
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.DeactivateTemplatesStage>(); //6
services.AddScoped<ITemplateSynchronizer, SimpleTemplateSynchronizer>(); services.AddScoped<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
// Grouped pipeline: Read-этапы
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.LoadJobGroupStage>(); //1
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.FilterUnitsStage>(); //2
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.GroupFilterStage>(); //3
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.ResolveConflictsStage>(); //4
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.BuildGroupsStage>(); //5
// Grouped pipeline: Write-этапы
services.AddScoped<IGroupedSyncWriteStage, Services.Implementations.GroupedSync.ProcessGroupsStage>(); //6
services.AddScoped<IGroupedSyncWriteStage, Services.Implementations.GroupedSync.DeactivateTemplatesStage>();//7
services.AddScoped<ITemplateSynchronizer, GroupedTemplateSynchronizer>(); services.AddScoped<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
// Пайплайн аллокации шаблонов // Пайплайн аллокации шаблонов

View File

@@ -1,4 +1,4 @@
using PARR.TemplateMatcher; using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcherWorker namespace PARR.TemplateMatcherWorker
{ {

View File

@@ -25,4 +25,8 @@
<ProjectReference Include="..\PARR.TemplateDistributor\PARR.TemplateDistributor.csproj" /> <ProjectReference Include="..\PARR.TemplateDistributor\PARR.TemplateDistributor.csproj" />
<ProjectReference Include="..\PARR.TemplateMatcher\PARR.TemplateMatcher.csproj" /> <ProjectReference Include="..\PARR.TemplateMatcher\PARR.TemplateMatcher.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="log\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PARR.Core.Common.Interfaces; using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache; using PARR.Domain.Cache;
using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums; using PARR.Domain.Enums;
@@ -8,7 +9,11 @@ using PARR.EsppApi;
using PARR.EsppApi.Constants; using PARR.EsppApi.Constants;
using PARR.EsppApi.Models.Query; using PARR.EsppApi.Models.Query;
using PARR.TemplateMatcher; using PARR.TemplateMatcher;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Implementations;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.Test.NextRun; using PARR.Test.NextRun;
using System.Diagnostics;
namespace PARR.Test namespace PARR.Test
{ {
@@ -41,7 +46,8 @@ namespace PARR.Test
//var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0)); //var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0));
await TemplateMatcherTest(); //await TemplateMatcherTest();
await PreCommitValidationTest();
@@ -161,6 +167,91 @@ namespace PARR.Test
#endregion #endregion
} }
#region PreCommitValidation
private async Task PreCommitValidationTest()
{
_logger.LogInformation("=== НАЧАЛО PRE-COMMIT ВАЛИДАЦИИ ===");
await using var scope = serviceProvider.CreateAsyncScope();
// === 1. Проверка Simple Pipeline ===
_logger.LogInformation("--- Simple Pipeline ---");
var simpleSync = scope.ServiceProvider
.GetRequiredService<IEnumerable<ITemplateSynchronizer>>()
.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (simpleSync == null)
{
_logger.LogError("[FAIL] SimpleTemplateSynchronizer не найден в DI");
return;
}
// Подставьте реальный JobId для Simple
var simpleJobId = Guid.Parse("6ff1de05-80c3-4b38-846b-0c793fd7fc8c");
try
{
var sw = Stopwatch.StartNew();
await simpleSync.SyncTemplatesForJobAsync(simpleJobId, new HistoryInitiator
{
InitiatorIp = "127.0.0.1",
InitiatorParrComponentId = ParrComponentsEnum.Master,
InitiatorComment = "Pre-commit validation: Simple Pipeline"
});
sw.Stop();
_logger.LogInformation("[OK] Simple Pipeline: завершено за {Ms} мс", sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "[FAIL] Simple Pipeline: исключение");
}
// === 2. Проверка Grouped Pipeline ===
_logger.LogInformation("--- Grouped Pipeline ---");
var groupedSync = scope.ServiceProvider
.GetRequiredService<IEnumerable<ITemplateSynchronizer>>()
.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (groupedSync == null)
{
_logger.LogError("[FAIL] GroupedTemplateSynchronizer не найден в DI");
return;
}
// Подставьте реальный JobGroupId для Grouped
var groupedJobGroupId = Guid.Parse("51acaa95-08bf-425f-9a09-87b6a4cbc77e");
try
{
var sw = Stopwatch.StartNew();
await groupedSync.SyncTemplatesForJobGroupAsync(groupedJobGroupId, new HistoryInitiator
{
InitiatorIp = "127.0.0.1",
InitiatorParrComponentId = ParrComponentsEnum.Master,
InitiatorComment = "Pre-commit validation: Grouped Pipeline"
});
sw.Stop();
_logger.LogInformation("[OK] Grouped Pipeline: завершено за {Ms} мс", sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "[FAIL] Grouped Pipeline: исключение");
}
// === 3. Проверка контрактных констант ===
_logger.LogInformation("--- Контрактные константы ---");
var constantsType = typeof(PARR.TemplateMatcher.Constants.UnusedTemplateConstants);
var fields = constantsType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
foreach (var field in fields)
{
var value = field.GetValue(null);
_logger.LogInformation("[OK] Константа {Name} = '{Value}'", field.Name, value);
}
}
#endregion
#region TemplateMatcher #region TemplateMatcher
private async Task TemplateMatcherTest() private async Task TemplateMatcherTest()

View File

@@ -7,7 +7,9 @@
"Default": "Debug", "Default": "Debug",
"Override": { "Override": {
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Debug" "PARR.DAL": "Information",
"PARR.Infrastructure.Redis": "Information",
"PARR.TemplateMatcher.Services.GroupedSync.GroupedTemplateProcessor": "Information"
} }
}, },
"WriteTo": [ "WriteTo": [
@@ -19,5 +21,10 @@
} }
} }
] ]
},
"MqSettings": {
"TemplateMatcher": { "HostName": "10.99.253.216" },
"TemplateGenerator": { "HostName": "10.99.253.216" },
"TemplateUpdater": { "HostName": "10.99.253.216" }
} }
} }

View File

@@ -37,5 +37,25 @@
"EsppUserTimeZone": 3, "EsppUserTimeZone": 3,
"RobotEk": "РПА-РОБИН-ГВЦ-ЕСПП-ТС-513-ДВС", "RobotEk": "РПА-РОБИН-ГВЦ-ЕСПП-ТС-513-ДВС",
"CodeRRO": "ЦТС-РЦТ-9999" "CodeRRO": "ЦТС-РЦТ-9999"
},
"MqSettings": {
"TemplateMatcher": {
"HostName": "parr-rabbitmq",
"QueueName": "parr-template-matcher",
"User": "template_matcher_reader",
"Password": "wzqj$Z@3:poasad;lk324@oot"
},
"TemplateGenerator": {
"HostName": "parr-rabbitmq",
"QueueName": "parr-template-generator",
"User": "template_generator_writer",
"Password": "B;6h+yF$zQ0OSkLX"
},
"TemplateUpdater": {
"HostName": "parr-rabbitmq",
"QueueName": "parr-template-updater",
"User": "template_updater_writer",
"Password": "sjdhgfkJHGIUFDi14asd^12"
}
} }
} }