diff --git a/PARR.Core/Services/UnitFilterService/Models/UnitFilterMatchResult.cs b/PARR.Core/Services/UnitFilterService/Models/UnitFilterMatchResult.cs index 4ad0f42c..07ad534d 100644 --- a/PARR.Core/Services/UnitFilterService/Models/UnitFilterMatchResult.cs +++ b/PARR.Core/Services/UnitFilterService/Models/UnitFilterMatchResult.cs @@ -1,6 +1,6 @@ namespace PARR.Core.Services.UnitFilterService.Models { - internal class UnitFilterMatchResult + public class UnitFilterMatchResult { public Guid UnitId { get; set; } public HashSet ValidParentIds { get; set; } = new(); diff --git a/PARR.TemplateMatcher/Constants/UnusedTemplateConstants.cs b/PARR.TemplateMatcher/Constants/UnusedTemplateConstants.cs new file mode 100644 index 00000000..2e33d272 --- /dev/null +++ b/PARR.TemplateMatcher/Constants/UnusedTemplateConstants.cs @@ -0,0 +1,25 @@ +namespace PARR.TemplateMatcher.Constants +{ + /// + /// Контрактные константы для обработки неиспользуемых шаблонов. + /// Являются соглашением между TemplateMatcher и внешними системами. + /// Изменение требует согласования со всеми участниками контракта. + /// + public static class UnusedTemplateConstants + { + /// + /// Системное имя поля зоны ответственности. + /// + public const string ResponsibilityAreaFieldName = "ЗОНА_ОТВЕТСТВЕННОСТИ"; + + /// + /// Системное имя поля тега ПАРР. + /// + public const string ParrTagFieldName = "ПАРР тег"; + + /// + /// Значение тега, обозначающее неиспользуемый юнит. + /// + public const string NotUsedTagValue = "ПАРР-НЕИСП"; + } +} diff --git a/PARR.TemplateMatcher/Services/GroupedSync/BuildGroupsStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/BuildGroupsStage.cs new file mode 100644 index 00000000..e8ed1908 --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/BuildGroupsStage.cs @@ -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 _logger; + + public string StageName => "Построение групп"; + + public BuildGroupsStage(IGroupedTemplateBuilder builder, ILogger logger) + { + _builder = builder; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/GroupedSync/DeactivateTemplatesStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/DeactivateTemplatesStage.cs new file mode 100644 index 00000000..bcd2c8c8 --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/DeactivateTemplatesStage.cs @@ -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 _logger; + + public string StageName => "Деактивация шаблонов"; + + public DeactivateTemplatesStage( + ITemplateRepository templateRepository, + ITemplateDeactivator deactivator, + ILogger logger) + { + _templateRepository = templateRepository; + _deactivator = deactivator; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/GroupedSync/FilterUnitsStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/FilterUnitsStage.cs new file mode 100644 index 00000000..02761201 --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/FilterUnitsStage.cs @@ -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 _logger; + + public string StageName => "Фильтрация юнитов"; + + public FilterUnitsStage(IUnitFilterService filterService, ILogger logger) + { + _filterService = filterService; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/GroupedSync/GroupFilterStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/GroupFilterStage.cs new file mode 100644 index 00000000..e6b44d2e --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/GroupFilterStage.cs @@ -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 _logger; + + public string StageName => "Групповая фильтрация"; + + public GroupFilterStage(IGroupedTemplateUnitFilter groupedFilter, ILogger logger) + { + _groupedFilter = groupedFilter; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncContext.cs b/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncContext.cs new file mode 100644 index 00000000..73bfac5f --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncContext.cs @@ -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; + +/// +/// Контекст групповой синхронизации. Передаётся между этапами. +/// +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 JobsInGroup { get; set; } = new(); + public Job MaxJob { get; set; } = null!; + public List FilteredUnits { get; set; } = new(); + public Dictionary> ReverseMapping { get; set; } = new(); + public List TemplateGroups { get; set; } = new(); + public HashSet<(Guid JobId, Guid UnitId, int Index)> ExpectedTemplateKeys { get; set; } = new(); + + /// + /// Имена юнитов для логирования. Заполняется на этапе фильтрации. + /// + public Dictionary UnitNames { get; set; } = new(); + + /// + /// Возвращает строку вида 'Имя' (ID) для логирования. + /// + public string FormatUnit(Guid unitId) + { + return UnitNames.TryGetValue(unitId, out var name) + ? $"'{name}' ({unitId})" + : $"({unitId})"; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncEarlyExitException.cs b/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncEarlyExitException.cs new file mode 100644 index 00000000..660fab88 --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncEarlyExitException.cs @@ -0,0 +1,16 @@ +namespace PARR.TemplateMatcher.Services.GroupedSync +{ + /// + /// Штатное прерывание пайплайна (нет данных после этапа). + /// Не является ошибкой — оркестратор перехватывает и логирует как нормальное завершение. + /// + public class GroupedSyncEarlyExitException : Exception + { + public string Reason { get; } + + public GroupedSyncEarlyExitException(string reason) : base(reason) + { + Reason = reason; + } + } +} diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs b/PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateBuilder.cs similarity index 98% rename from PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs rename to PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateBuilder.cs index fb1456fb..8755bc49 100644 --- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateBuilder.cs @@ -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 { diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateProcessor.cs b/PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateProcessor.cs similarity index 97% rename from PARR.TemplateMatcher/Services/Implementations/GroupedTemplateProcessor.cs rename to PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateProcessor.cs index b5bcb655..0234c9f4 100644 --- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateProcessor.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateProcessor.cs @@ -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); } } diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateUnitFilter.cs b/PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateUnitFilter.cs similarity index 97% rename from PARR.TemplateMatcher/Services/Implementations/GroupedTemplateUnitFilter.cs rename to PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateUnitFilter.cs index 49f8cf74..882ed415 100644 --- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateUnitFilter.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateUnitFilter.cs @@ -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 { diff --git a/PARR.TemplateMatcher/Services/GroupedSync/IGroupedSyncStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedSyncStage.cs new file mode 100644 index 00000000..279a5fd0 --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedSyncStage.cs @@ -0,0 +1,14 @@ +using PARR.TemplateMatcher.Services.Implementations.GroupedSync; + +namespace PARR.TemplateMatcher.Services.GroupedSync +{ + /// + /// Этап групповой синхронизации, который только читает данные. + /// НЕ выполняет запись в БД, MQ или кэш. + /// + public interface IGroupedSyncStage + { + string StageName { get; } + Task ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default); + } +} diff --git a/PARR.TemplateMatcher/Services/GroupedSync/IGroupedSyncWriteStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedSyncWriteStage.cs new file mode 100644 index 00000000..60ff1727 --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedSyncWriteStage.cs @@ -0,0 +1,8 @@ +namespace PARR.TemplateMatcher.Services.GroupedSync +{ + /// + /// Этап групповой синхронизации с побочными эффектами (запись в БД, MQ). + /// В тестах не подключается — тип системы гарантирует безопасность. + /// + public interface IGroupedSyncWriteStage : IGroupedSyncStage { } +} diff --git a/PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateBuilder.cs b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateBuilder.cs similarity index 92% rename from PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateBuilder.cs rename to PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateBuilder.cs index 0333a167..ae0a89ac 100644 --- a/PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateBuilder.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateBuilder.cs @@ -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 { @@ -15,4 +15,4 @@ public interface IGroupedTemplateBuilder JobGroup jobGroup, Job maxJob, CancellationToken ct = default); -} +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateProcessor.cs b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateProcessor.cs similarity index 94% rename from PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateProcessor.cs rename to PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateProcessor.cs index 5d869eeb..7aee31be 100644 --- a/PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateProcessor.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateProcessor.cs @@ -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 { diff --git a/PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateUnitFilter.cs b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateUnitFilter.cs similarity index 93% rename from PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateUnitFilter.cs rename to PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateUnitFilter.cs index 11d83bd7..a07972bd 100644 --- a/PARR.TemplateMatcher/Services/Interfaces/IGroupedTemplateUnitFilter.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/IGroupedTemplateUnitFilter.cs @@ -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 { diff --git a/PARR.TemplateMatcher/Services/Interfaces/IUnitInTemplateConflictMapper.cs b/PARR.TemplateMatcher/Services/GroupedSync/IUnitInTemplateConflictMapper.cs similarity index 95% rename from PARR.TemplateMatcher/Services/Interfaces/IUnitInTemplateConflictMapper.cs rename to PARR.TemplateMatcher/Services/GroupedSync/IUnitInTemplateConflictMapper.cs index 37ad61a2..bebba37f 100644 --- a/PARR.TemplateMatcher/Services/Interfaces/IUnitInTemplateConflictMapper.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/IUnitInTemplateConflictMapper.cs @@ -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 { /// /// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей. diff --git a/PARR.TemplateMatcher/Services/GroupedSync/LoadJobGroupStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/LoadJobGroupStage.cs new file mode 100644 index 00000000..115be29f --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/LoadJobGroupStage.cs @@ -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 _logger; + + public string StageName => "Загрузка JobGroup"; + + public LoadJobGroupStage(IJobGroupRepository jobGroupRepository, ILogger logger) + { + _jobGroupRepository = jobGroupRepository; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/GroupedSync/ProcessGroupsStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/ProcessGroupsStage.cs new file mode 100644 index 00000000..52c3e10a --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/ProcessGroupsStage.cs @@ -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 _logger; + + public string StageName => "Обработка групп"; + + public ProcessGroupsStage(IGroupedTemplateProcessor processor, ILogger logger) + { + _processor = processor; + _logger = logger; + } + + public async Task ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default) + { + // Приведение типа обратно из object — пайплайн хранит как object для универсальности контекста + var typedGroups = context.TemplateGroups + .Cast() + .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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/GroupedSync/ResolveConflictsStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/ResolveConflictsStage.cs new file mode 100644 index 00000000..cd43a66c --- /dev/null +++ b/PARR.TemplateMatcher/Services/GroupedSync/ResolveConflictsStage.cs @@ -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 _logger; + + public string StageName => "Разрешение конфликтов"; + + public ResolveConflictsStage(IUnitInTemplateConflictMapper conflictMapper, ILogger logger) + { + _conflictMapper = conflictMapper; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Implementations/UnitInTemplateConflictMapper.cs b/PARR.TemplateMatcher/Services/GroupedSync/UnitInTemplateConflictMapper.cs similarity index 99% rename from PARR.TemplateMatcher/Services/Implementations/UnitInTemplateConflictMapper.cs rename to PARR.TemplateMatcher/Services/GroupedSync/UnitInTemplateConflictMapper.cs index 55ea2605..952ca8fd 100644 --- a/PARR.TemplateMatcher/Services/Implementations/UnitInTemplateConflictMapper.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/UnitInTemplateConflictMapper.cs @@ -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 { diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs index 4bd7bc48..7b2eaeef 100644 --- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs @@ -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 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 _readStages; + private readonly IEnumerable _writeStages; + private readonly IMatchingStatusService _matchingStatusService; + private readonly ILogger _logger; public GroupedTemplateSynchronizer( - ILogger logger, - IJobGroupRepository jobGroupService, - IUnitFilterService unitFilterService, - IGroupedTemplateUnitFilter groupedTemplateUnitFilter, - IUnitInTemplateConflictMapper unitInTemplateConflictMapper, - IGroupedTemplateBuilder groupedTemplateBuilder, - IGroupedTemplateProcessor groupedTemplateProcessor, - ITemplateRepository templateService, - ITemplateDeactivator templateDeactivator, - IMatchingStatusService matchingStatusService) + IEnumerable readStages, + IEnumerable writeStages, + IMatchingStatusService matchingStatusService, + ILogger 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 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)); } } \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Implementations/JobGroupValidatorService.cs b/PARR.TemplateMatcher/Services/Implementations/JobGroupValidatorService.cs index 23d95028..38c47ba7 100644 --- a/PARR.TemplateMatcher/Services/Implementations/JobGroupValidatorService.cs +++ b/PARR.TemplateMatcher/Services/Implementations/JobGroupValidatorService.cs @@ -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 logger; - private readonly IJobGroupRepository jobGroupService; + private readonly ILogger _logger; + private readonly IJobGroupRepository _jobGroupService; public JobGroupValidatorService( ILogger logger, IJobGroupRepository jobGroupService ) { - this.logger = logger; - this.jobGroupService = jobGroupService; + _logger = logger; + _jobGroupService = jobGroupService; } public async Task 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; } diff --git a/PARR.TemplateMatcher/Services/Implementations/JobValidatorService.cs b/PARR.TemplateMatcher/Services/Implementations/JobValidatorService.cs index b76d466b..528e0f51 100644 --- a/PARR.TemplateMatcher/Services/Implementations/JobValidatorService.cs +++ b/PARR.TemplateMatcher/Services/Implementations/JobValidatorService.cs @@ -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 { diff --git a/PARR.TemplateMatcher/MqTemplateMatcher.cs b/PARR.TemplateMatcher/Services/Implementations/MqTemplateMatcher.cs similarity index 99% rename from PARR.TemplateMatcher/MqTemplateMatcher.cs rename to PARR.TemplateMatcher/Services/Implementations/MqTemplateMatcher.cs index 2f8930e5..504359cd 100644 --- a/PARR.TemplateMatcher/MqTemplateMatcher.cs +++ b/PARR.TemplateMatcher/Services/Implementations/MqTemplateMatcher.cs @@ -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 { diff --git a/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs index 40a3dc82..00ba7f30 100644 --- a/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs @@ -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 readStages; + private readonly IEnumerable writeStages; private readonly ILogger 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 readStages, + IEnumerable writeStages, ILogger 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(); - - // === ЭТАП 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(), - 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() - }; - - 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() diff --git a/PARR.TemplateMatcher/TemplateMatcher.cs b/PARR.TemplateMatcher/Services/Implementations/TemplateMatcher.cs similarity index 56% rename from PARR.TemplateMatcher/TemplateMatcher.cs rename to PARR.TemplateMatcher/Services/Implementations/TemplateMatcher.cs index dd49c481..ccf35168 100644 --- a/PARR.TemplateMatcher/TemplateMatcher.cs +++ b/PARR.TemplateMatcher/Services/Implementations/TemplateMatcher.cs @@ -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 logger; - private readonly IJobRepository jobService; - private readonly IJobGroupRepository jobGroupService; - private readonly IEnumerable synchronizers; + private readonly ILogger _logger; + private readonly IJobRepository _jobService; + private readonly IJobGroupRepository _jobGroupService; + private readonly IEnumerable _synchronizers; public TemplateMatcher( ILogger logger, @@ -23,21 +23,21 @@ namespace PARR.TemplateMatcher IEnumerable 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 GetJobWithGroupAndAutoControlAsync(Guid jobId) { - return await jobService.Get() + return await _jobService.Get() .AsNoTracking() .Include(j => j.Group) .ThenInclude(j => j!.GroupType) diff --git a/PARR.TemplateMatcher/IMqTemplateMatcher.cs b/PARR.TemplateMatcher/Services/Interfaces/IMqTemplateMatcher.cs similarity index 66% rename from PARR.TemplateMatcher/IMqTemplateMatcher.cs rename to PARR.TemplateMatcher/Services/Interfaces/IMqTemplateMatcher.cs index bef4acc0..e9ec8da3 100644 --- a/PARR.TemplateMatcher/IMqTemplateMatcher.cs +++ b/PARR.TemplateMatcher/Services/Interfaces/IMqTemplateMatcher.cs @@ -1,4 +1,4 @@ -namespace PARR.TemplateMatcher +namespace PARR.TemplateMatcher.Services.Interfaces { public interface IMqTemplateMatcher { diff --git a/PARR.TemplateMatcher/ITemplateMatcher.cs b/PARR.TemplateMatcher/Services/Interfaces/ITemplateMatcher.cs similarity index 84% rename from PARR.TemplateMatcher/ITemplateMatcher.cs rename to PARR.TemplateMatcher/Services/Interfaces/ITemplateMatcher.cs index a80509ba..72cb5d25 100644 --- a/PARR.TemplateMatcher/ITemplateMatcher.cs +++ b/PARR.TemplateMatcher/Services/Interfaces/ITemplateMatcher.cs @@ -1,12 +1,12 @@ using PARR.Domain.Entities.Base.History; -namespace PARR.TemplateMatcher +namespace PARR.TemplateMatcher.Services.Interfaces { public interface ITemplateMatcher { Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator); Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator); Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator); - Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator); + Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator); } } diff --git a/PARR.TemplateMatcher/Services/SimpleSync/AllocateTemplatesStage.cs b/PARR.TemplateMatcher/Services/SimpleSync/AllocateTemplatesStage.cs new file mode 100644 index 00000000..5f648f00 --- /dev/null +++ b/PARR.TemplateMatcher/Services/SimpleSync/AllocateTemplatesStage.cs @@ -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 _logger; + + public string StageName => "Создание шаблонов"; + + public AllocateTemplatesStage( + ITemplateAllocationService allocationService, + ILogger logger) + { + _allocationService = allocationService; + _logger = logger; + } + + public async Task 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(), + 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/SimpleSync/AnalyzeChangesStage.cs b/PARR.TemplateMatcher/Services/SimpleSync/AnalyzeChangesStage.cs new file mode 100644 index 00000000..cd10b47e --- /dev/null +++ b/PARR.TemplateMatcher/Services/SimpleSync/AnalyzeChangesStage.cs @@ -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 _logger; + + public string StageName => "Анализ изменений"; + + public AnalyzeChangesStage( + ITemplateRepository templateRepository, + ITemplateNameNormalizer nameNormalizer, + ILogger logger) + { + _templateRepository = templateRepository; + _nameNormalizer = nameNormalizer; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/SimpleSync/DeactivateTemplatesStage.cs b/PARR.TemplateMatcher/Services/SimpleSync/DeactivateTemplatesStage.cs new file mode 100644 index 00000000..4dbc573a --- /dev/null +++ b/PARR.TemplateMatcher/Services/SimpleSync/DeactivateTemplatesStage.cs @@ -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 _logger; + + public string StageName => "Деактивация шаблонов"; + + public DeactivateTemplatesStage(ITemplateDeactivator deactivator, ILogger logger) + { + _deactivator = deactivator; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/SimpleSync/FilterUnitsStage.cs b/PARR.TemplateMatcher/Services/SimpleSync/FilterUnitsStage.cs new file mode 100644 index 00000000..6df32154 --- /dev/null +++ b/PARR.TemplateMatcher/Services/SimpleSync/FilterUnitsStage.cs @@ -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 _logger; + + public string StageName => "Фильтрация юнитов"; + + public FilterUnitsStage(IUnitFilterService filterService, ILogger logger) + { + _filterService = filterService; + _logger = logger; + } + + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncStage.cs b/PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncStage.cs new file mode 100644 index 00000000..6cf17437 --- /dev/null +++ b/PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncStage.cs @@ -0,0 +1,12 @@ +namespace PARR.TemplateMatcher.Services.SimpleSync +{ + /// + /// Этап синхронизации, который только читает данные и заполняет контекст. + /// НЕ выполняет запись в БД, MQ или кэш. + /// + public interface ISimpleSyncStage + { + string StageName { get; } + Task ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default); + } +} diff --git a/PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncWriteStage.cs b/PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncWriteStage.cs new file mode 100644 index 00000000..89a608e2 --- /dev/null +++ b/PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncWriteStage.cs @@ -0,0 +1,10 @@ +namespace PARR.TemplateMatcher.Services.SimpleSync +{ + /// + /// Этап синхронизации, который выполняет побочные эффекты (запись в БД, MQ). + /// В тестах не подключается — тип системы гарантирует безопасность. + /// + public interface ISimpleSyncWriteStage : ISimpleSyncStage + { + } +} diff --git a/PARR.TemplateMatcher/Services/SimpleSync/LoadJobStage.cs b/PARR.TemplateMatcher/Services/SimpleSync/LoadJobStage.cs new file mode 100644 index 00000000..2e0133b8 --- /dev/null +++ b/PARR.TemplateMatcher/Services/SimpleSync/LoadJobStage.cs @@ -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 _logger; + + public string StageName => "Загрузка Job"; + + public LoadJobStage(IJobRepository jobRepository, ILogger logger) + { + _jobRepository = jobRepository; + _logger = logger; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/SimpleSync/SimpleSyncContext.cs b/PARR.TemplateMatcher/Services/SimpleSync/SimpleSyncContext.cs new file mode 100644 index 00000000..4d73958b --- /dev/null +++ b/PARR.TemplateMatcher/Services/SimpleSync/SimpleSyncContext.cs @@ -0,0 +1,39 @@ +using PARR.Domain.Entities; +using PARR.Domain.Entities.Base.History; +using PARR.Domain.Entities.Job; + +namespace PARR.TemplateMatcher.Services.SimpleSync +{ + /// + /// Контекст синхронизации простого шаблона. Передаётся между этапами. + /// + 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 FilteredUnitIds { get; set; } = new(); + + /// + /// Имена юнитов для логирования. Заполняется на этапе фильтрации. + /// + public Dictionary UnitNames { get; set; } = new(); + + public List