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
{
internal class UnitFilterMatchResult
public class UnitFilterMatchResult
{
public Guid UnitId { get; set; }
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.Job;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
{

View File

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

View File

@@ -2,9 +2,8 @@
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
namespace PARR.TemplateMatcher.Services.GroupedSync;
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.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.GroupedSync;
public interface IGroupedTemplateBuilder
{

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job;
namespace PARR.TemplateMatcher.Services.Interfaces
namespace PARR.TemplateMatcher.Services.GroupedSync
{
/// <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.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.GroupedSync;
internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
{

View File

@@ -1,260 +1,110 @@
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using Microsoft.Extensions.Logging;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces;
using System.Diagnostics;
namespace PARR.TemplateMatcher.Services.Implementations;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private readonly ILogger<GroupedTemplateSynchronizer> logger;
private readonly IJobGroupRepository jobGroupService;
private readonly IUnitFilterService unitFilterService;
private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter;
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
private readonly IGroupedTemplateBuilder groupedTemplateBuilder;
private readonly IGroupedTemplateProcessor groupedTemplateProcessor;
private readonly ITemplateRepository templateService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly IMatchingStatusService matchingStatusService;
private readonly IEnumerable<IGroupedSyncStage> _readStages;
private readonly IEnumerable<IGroupedSyncWriteStage> _writeStages;
private readonly IMatchingStatusService _matchingStatusService;
private readonly ILogger<GroupedTemplateSynchronizer> _logger;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
IJobGroupRepository jobGroupService,
IUnitFilterService unitFilterService,
IGroupedTemplateUnitFilter groupedTemplateUnitFilter,
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
IGroupedTemplateBuilder groupedTemplateBuilder,
IGroupedTemplateProcessor groupedTemplateProcessor,
ITemplateRepository templateService,
ITemplateDeactivator templateDeactivator,
IMatchingStatusService matchingStatusService)
IEnumerable<IGroupedSyncStage> readStages,
IEnumerable<IGroupedSyncWriteStage> writeStages,
IMatchingStatusService matchingStatusService,
ILogger<GroupedTemplateSynchronizer> logger)
{
this.logger = logger;
this.jobGroupService = jobGroupService;
this.unitFilterService = unitFilterService;
this.groupedTemplateUnitFilter = groupedTemplateUnitFilter;
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);
_readStages = readStages;
_writeStages = writeStages;
_matchingStatusService = matchingStatusService;
_logger = logger;
}
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)
{
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
_logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
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)
);
await SetStatusAsync(jobGroupId, "Начало синхронизации");
var totalSw = Stopwatch.StartNew();
try
{
// === ЭТАП 1: Загрузка JobGroup ===
var stageSw = Stopwatch.StartNew();
var jobGroup = await jobGroupService.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 == jobGroupId);
var context = new GroupedSyncContext { JobGroupId = jobGroupId, Initiator = initiator };
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
foreach (var stage in _readStages)
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return;
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
}
var jobsInGroup = jobGroup.Jobs.ToList();
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)
foreach (var stage in _writeStages)
{
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
return;
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
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: Фильтрация юнитов ===
stageSw.Restart();
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
stageSw.Stop();
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;
}
// === ЭТАП 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);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); return;
}
// === ЭТАП 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);
await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
_logger.LogInformation("Синхронизация шаблонов завершена для JobGroup '{JobGroupName}' ({JobGroupId})",
context.JobGroupName, jobGroupId);
}
catch (GroupedSyncEarlyExitException ex)
{
totalSw.Stop();
_logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)",
jobGroupId, ex.Reason, totalSw.ElapsedMilliseconds);
await SetStatusAsync(jobGroupId, ex.Reason);
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
}
catch (Exception ex)
{
totalSw.Stop();
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс", jobGroupId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
_logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс",
jobGroupId, totalSw.ElapsedMilliseconds);
await SetStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
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(
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
Guid jobGroupId,
List<Job> jobsInGroup,
HistoryInitiator initiator)
public Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.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);
}
}
_logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
return Task.CompletedTask;
}
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
private async Task SetStatusAsync(Guid jobGroupId, string comment)
{
var status = new MatchingStatusItemDto
{
@@ -262,12 +112,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
await _matchingStatusService.SetMatchingStatusAsync(
jobGroupId, SyncTaskEntityTypeEnum.JobGroup,
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.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions
namespace PARR.TemplateMatcher.Services.Implementations
{
internal class JobGroupValidatorService : IJobGroupValidatorService
{
private readonly ILogger<IJobValidatorService> logger;
private readonly IJobGroupRepository jobGroupService;
private readonly ILogger<IJobValidatorService> _logger;
private readonly IJobGroupRepository _jobGroupService;
public JobGroupValidatorService(
ILogger<IJobValidatorService> logger,
IJobGroupRepository jobGroupService
)
{
this.logger = logger;
this.jobGroupService = jobGroupService;
_logger = logger;
_jobGroupService = jobGroupService;
}
public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId)
{
var isExist = await jobGroupService.GetAsync(jobGroupId);
var isExist = await _jobGroupService.GetAsync(jobGroupId);
if (isExist == null)
{
logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
_logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
return false;
}

View File

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

View File

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

View File

@@ -15,8 +15,9 @@ 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.Constants;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
using PARR.TemplateMatcher.Settings;
using System.Diagnostics;
@@ -31,18 +32,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false;
// === Константы для логики неиспользуемых шаблонов ===
private const string FieldNameResponsibilityArea = "ЗОНА_ОТВЕТСТВЕННОСТИ";
private const string FieldNameParrTag = "ПАРР тег";
private const string TagValueNotWorking = "ПАРР-НЕИСП";
private readonly IEnumerable<ISimpleSyncStage> readStages;
private readonly IEnumerable<ISimpleSyncWriteStage> writeStages;
private readonly ILogger<SimpleTemplateSynchronizer> logger;
private readonly IUnitFilterService unitFilterService;
private readonly MqSettings mqSettings;
private readonly IRabbitService mqService;
private readonly ITemplateRepository templateService;
private readonly IJobRepository jobService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateAllocationService templateAllocationService;
private readonly ITemplateMqPublisher templateMqPublisher;
@@ -54,13 +51,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private readonly IUnitRepository unitRepository;
public SimpleTemplateSynchronizer(
IEnumerable<ISimpleSyncStage> readStages,
IEnumerable<ISimpleSyncWriteStage> writeStages,
ILogger<SimpleTemplateSynchronizer> logger,
IUnitFilterService unitFilterService,
MqSettings mqSettings,
IRabbitService mqService,
ITemplateRepository templateService,
IJobRepository jobService,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateAllocationService templateAllocationService,
ITemplateMqPublisher templateMqPublisher,
@@ -72,13 +70,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
IUnitRepository unitRepository
)
{
this.readStages = readStages;
this.writeStages = writeStages;
this.logger = logger;
this.unitFilterService = unitFilterService;
this.mqSettings = mqSettings;
this.mqService = mqService;
this.templateService = templateService;
this.jobService = jobService;
this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer;
this.templateAllocationService = templateAllocationService;
this.templateMqPublisher = templateMqPublisher;
@@ -92,18 +91,15 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
// === Специальная обработка для Job неиспользуемых шаблонов ===
if (jobId == settingsFromDb.JobIdForUnusedTemplates)
{
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId);
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId);
await SyncUnusedTemplatesAsync(jobId, initiator);
return;
}
var totalSw = Stopwatch.StartNew();
logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Any() == true)
{
@@ -111,7 +107,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
@@ -119,169 +114,49 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobId,
SyncTaskEntityTypeEnum.Job,
jobId, SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
TimeSpan.FromMinutes(35));
// Таймер запускается ПОСЛЕ инфраструктурных операций (статус, проверка блокировки)
var totalSw = Stopwatch.StartNew();
try
{
// === ЭТАП 1: Загрузка Job ===
var stageSw = Stopwatch.StartNew();
var job = await jobService.Get()
.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);
var context = new SimpleSyncContext { JobId = jobId, Initiator = initiator };
if (job == null)
foreach (var stage in readStages)
{
logger.LogWarning("Job {JobId} не найден.", jobId);
await UpdateMatchingStatusAsync(jobId, "Job не найден");
return;
}
stageSw.Stop();
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка Job | Время: {Ms} мс",
jobId, 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));
}
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
}
stageSw.Stop();
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)
foreach (var stage in writeStages)
{
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState;
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState;
var request = new TemplateAllocationRequest(
TargetJob: job,
TargetUnitId: unitId,
TargetUnit: null,
Index: null,
UnitsInTemplate: new List<UnitInTemplateMessage>(),
IsActiveTemplate: isActiveTemplate,
IsActiveSchedule: isActiveSchedule,
Initiator: initiator);
await templateAllocationService.AllocateAsync(request);
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
}
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();
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 matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})",
context.JobName, jobId);
}
catch (Exception ex)
{
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}");
throw;
}
@@ -293,6 +168,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
@@ -444,30 +320,29 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
try
{
// 1. Находим ID нужных полей
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(FieldNameResponsibilityArea);
var tagField = await unitFieldService.GetByAihitNameAsync(FieldNameParrTag);
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName);
var tagField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName);
if (responsableAreaField == null || tagField == null)
{
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", FieldNameResponsibilityArea, FieldNameParrTag);
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", UnusedTemplateConstants.ResponsibilityAreaFieldName, UnusedTemplateConstants.NotUsedTagValue);
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
return;
}
var responsableAreaFieldId = responsableAreaField.Id;
var tagFieldId = tagField.Id;
const string targetTagValue = TagValueNotWorking;
// 2. Находим ValueId для тега "ПАРР-НЕИСП"
var targetTagValueId = await unitInValueService.Get()
.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)
.FirstOrDefaultAsync(ct);
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()

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
using PARR.Domain.Entities.Base.History;
namespace PARR.TemplateMatcher
namespace PARR.TemplateMatcher.Services.Interfaces
{
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.DAL;
using PARR.Infrastructure;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Implementations;
using PARR.TemplateMatcher.Services.Implemetaions;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher
@@ -31,8 +32,31 @@ namespace PARR.TemplateMatcher
// === 2. Scoped: Бизнес-логика и работа с БД (DbContext) ===
// Создаются заново для каждого сообщения из очереди (внутри 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>();
// 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>();
// Пайплайн аллокации шаблонов

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums;
@@ -8,7 +9,11 @@ using PARR.EsppApi;
using PARR.EsppApi.Constants;
using PARR.EsppApi.Models.Query;
using PARR.TemplateMatcher;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Implementations;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.Test.NextRun;
using System.Diagnostics;
namespace PARR.Test
{
@@ -41,7 +46,8 @@ namespace PARR.Test
//var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0));
await TemplateMatcherTest();
//await TemplateMatcherTest();
await PreCommitValidationTest();
@@ -161,6 +167,91 @@ namespace PARR.Test
#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
private async Task TemplateMatcherTest()

View File

@@ -1,23 +1,30 @@
{
"ConnectionStrings": {
"RedisConnection": "10.99.253.216:6379,password=ParrP@ssPtk202MMdevDvs"
"ConnectionStrings": {
"RedisConnection": "10.99.253.216:6379,password=ParrP@ssPtk202MMdevDvs"
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"PARR.DAL": "Information",
"PARR.Infrastructure.Redis": "Information",
"PARR.TemplateMatcher.Services.GroupedSync.GroupedTemplateProcessor": "Information"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Debug"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "log/log-.txt",
"rollingInterval": "Day"
}
}
]
}
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "log/log-.txt",
"rollingInterval": "Day"
}
}
]
},
"MqSettings": {
"TemplateMatcher": { "HostName": "10.99.253.216" },
"TemplateGenerator": { "HostName": "10.99.253.216" },
"TemplateUpdater": { "HostName": "10.99.253.216" }
}
}

View File

@@ -1,41 +1,61 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;",
"RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "log/log-.txt",
"rollingInterval": "Day"
}
}
]
},
"EsppOrderSettings": {
//dev
//"Url": "http://rzd-espp-t-rpa-app-1.gvc.oao.rzd:8080/espp_api/OperationExecutor/",
"Url": "http://espp.gvc.rzd/esppapi_prom/OperationExecutor/",
"UserName": "Auto-PTK-INFO-0002-DVS",
"AccountName": "АВТО ТЕХНОЛОГ ПТК-ИНФО-0002-ДВС (AUTO-PTK-INFO-0002-DVS)",
"Password": "123456789",
"EsppUserTimeZone": 3,
"RobotEk": "РПА-РОБИН-ГВЦ-ЕСПП-ТС-513-ДВС",
"CodeRRO": "ЦТС-РЦТ-9999"
"ConnectionStrings": {
"DefaultConnection": "Server=10.99.253.184;Database=parr;User Id=app_parr; Password=PosdfkhT&)%sdfligL&%5546;",
"RedisConnection": "parr-redis:6379,password=ParrP@ssPtk202MMdevDvs"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "log/log-.txt",
"rollingInterval": "Day"
}
}
]
},
"EsppOrderSettings": {
//dev
//"Url": "http://rzd-espp-t-rpa-app-1.gvc.oao.rzd:8080/espp_api/OperationExecutor/",
"Url": "http://espp.gvc.rzd/esppapi_prom/OperationExecutor/",
"UserName": "Auto-PTK-INFO-0002-DVS",
"AccountName": "АВТО ТЕХНОЛОГ ПТК-ИНФО-0002-ДВС (AUTO-PTK-INFO-0002-DVS)",
"Password": "123456789",
"EsppUserTimeZone": 3,
"RobotEk": "РПА-РОБИН-ГВЦ-ЕСПП-ТС-513-ДВС",
"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"
}
}
}