From 0629579cf019af6ce0f3ccf91ff6dda5f41bec30 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Thu, 10 Sep 2026 14:12:47 +1000 Subject: [PATCH] =?UTF-8?q?reafctor(templateMatcher):=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BD=D0=B5=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D1=88=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2=20=D0=B2=D1=8B=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=B5=D0=BD=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Exceptions/SyncEarlyExitException.cs | 16 + .../Services/GroupedSync/FilterUnitsStage.cs | 3 +- .../Services/GroupedSync/GroupFilterStage.cs | 3 +- .../GroupedSyncEarlyExitException.cs | 16 - .../Services/GroupedSync/LoadJobGroupStage.cs | 5 +- .../GroupedSync/ResolveConflictsStage.cs | 3 +- .../GroupedTemplateSynchronizer.cs | 17 +- .../SimpleTemplateSynchronizer.cs | 389 ++---------------- .../UnusedTemplatesSyncService.cs | 383 +++++++++++++++++ .../Interfaces/ITemplateSynchronizer.cs | 6 +- .../Interfaces/IUnusedTemplatesSyncService.cs | 9 + .../TemplateMatcherInstaller.cs | 3 + 12 files changed, 477 insertions(+), 376 deletions(-) create mode 100644 PARR.TemplateMatcher/Exceptions/SyncEarlyExitException.cs delete mode 100644 PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncEarlyExitException.cs create mode 100644 PARR.TemplateMatcher/Services/Implementations/UnusedTemplatesSyncService.cs create mode 100644 PARR.TemplateMatcher/Services/Interfaces/IUnusedTemplatesSyncService.cs diff --git a/PARR.TemplateMatcher/Exceptions/SyncEarlyExitException.cs b/PARR.TemplateMatcher/Exceptions/SyncEarlyExitException.cs new file mode 100644 index 00000000..5e7faa80 --- /dev/null +++ b/PARR.TemplateMatcher/Exceptions/SyncEarlyExitException.cs @@ -0,0 +1,16 @@ +namespace PARR.TemplateMatcher.Exceptions +{ + /// + /// Контролируемый ранний выход из синхронизации без ошибки. + /// Например: нет данных для обработки, фильтры не вернули юнитов. + /// + public class SyncEarlyExitException : Exception + { + public string Reason { get; } + + public SyncEarlyExitException(string reason) : base(reason) + { + Reason = reason; + } + } +} diff --git a/PARR.TemplateMatcher/Services/GroupedSync/FilterUnitsStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/FilterUnitsStage.cs index 02761201..1435c51a 100644 --- a/PARR.TemplateMatcher/Services/GroupedSync/FilterUnitsStage.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/FilterUnitsStage.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using PARR.Core.Services.UnitFilterService; +using PARR.TemplateMatcher.Exceptions; using PARR.TemplateMatcher.Services.GroupedSync; namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync; @@ -22,7 +23,7 @@ internal class FilterUnitsStage : IGroupedSyncStage var result = await _filterService.GetUnitsByJobFilterAsync(context.MaxJob.Id, null, ct); if (result == null || !result.Any()) - throw new GroupedSyncEarlyExitException("Фильтры не дали Unit'ов с подходящими связями"); + throw new SyncEarlyExitException("Фильтры не дали Unit'ов с подходящими связями"); context.FilteredUnits = result.ToList(); context.UnitNames = result.ToDictionary(u => u.Id, u => u.Name); diff --git a/PARR.TemplateMatcher/Services/GroupedSync/GroupFilterStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/GroupFilterStage.cs index e6b44d2e..234273f4 100644 --- a/PARR.TemplateMatcher/Services/GroupedSync/GroupFilterStage.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/GroupFilterStage.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using PARR.TemplateMatcher.Exceptions; using PARR.TemplateMatcher.Services.GroupedSync; namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync; @@ -21,7 +22,7 @@ internal class GroupFilterStage : IGroupedSyncStage var finalFiltered = await _groupedFilter.FilterAsync(context.FilteredUnits, context.JobGroup); if (!finalFiltered.Any()) - throw new GroupedSyncEarlyExitException("Нет юнитов после групповой фильтрации"); + throw new SyncEarlyExitException("Нет юнитов после групповой фильтрации"); context.FilteredUnits = finalFiltered; diff --git a/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncEarlyExitException.cs b/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncEarlyExitException.cs deleted file mode 100644 index 660fab88..00000000 --- a/PARR.TemplateMatcher/Services/GroupedSync/GroupedSyncEarlyExitException.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/GroupedSync/LoadJobGroupStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/LoadJobGroupStage.cs index a7e8fb67..d5284ad0 100644 --- a/PARR.TemplateMatcher/Services/GroupedSync/LoadJobGroupStage.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/LoadJobGroupStage.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces.JobGroupRepositories; +using PARR.TemplateMatcher.Exceptions; using PARR.TemplateMatcher.Services.GroupedSync; namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync; @@ -32,7 +33,7 @@ internal class LoadJobGroupStage : IGroupedSyncStage .FirstOrDefaultAsync(jg => jg.Id == context.JobGroupId, ct); if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) - throw new GroupedSyncEarlyExitException("JobGroup не найден или пуст"); + throw new SyncEarlyExitException("JobGroup не найден или пуст"); var jobsInGroup = jobGroup.Jobs.ToList(); var maxJob = jobsInGroup @@ -41,7 +42,7 @@ internal class LoadJobGroupStage : IGroupedSyncStage .FirstOrDefault(); if (maxJob == null) - throw new GroupedSyncEarlyExitException("Не найден Job с MaxValueRelationships"); + throw new SyncEarlyExitException("Не найден Job с MaxValueRelationships"); context.JobGroup = jobGroup; context.JobGroupName = jobGroup.GroupName; diff --git a/PARR.TemplateMatcher/Services/GroupedSync/ResolveConflictsStage.cs b/PARR.TemplateMatcher/Services/GroupedSync/ResolveConflictsStage.cs index cd43a66c..ec063a0c 100644 --- a/PARR.TemplateMatcher/Services/GroupedSync/ResolveConflictsStage.cs +++ b/PARR.TemplateMatcher/Services/GroupedSync/ResolveConflictsStage.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using PARR.TemplateMatcher.Exceptions; using PARR.TemplateMatcher.Services.GroupedSync; namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync; @@ -21,7 +22,7 @@ internal class ResolveConflictsStage : IGroupedSyncStage var mapping = await _conflictMapper.BuildMappingAsync(context.FilteredUnits, context.MaxJob, ct); if (!mapping.Any()) - throw new GroupedSyncEarlyExitException("Нет связей после разрешения конфликтов"); + throw new SyncEarlyExitException("Нет связей после разрешения конфликтов"); context.ReverseMapping = mapping; diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs index 85e1670b..3a189a6f 100644 --- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs @@ -3,6 +3,7 @@ using PARR.Core.Services.MatchingStatusService; using PARR.Domain.Cache.Models; using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; +using PARR.TemplateMatcher.Exceptions; using PARR.TemplateMatcher.Services.GroupedSync; using PARR.TemplateMatcher.Services.Implementations.GroupedSync; using PARR.TemplateMatcher.Services.Interfaces; @@ -17,6 +18,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer private readonly IMatchingStatusService _matchingStatusService; private readonly ILogger _logger; + public GroupedTemplateSynchronizer( IEnumerable readStages, IEnumerable writeStages, @@ -30,7 +32,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer _logger = logger; } - public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false) + + public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default) { _logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId} (DryRun={DryRun})", jobGroupId, dryRun); @@ -93,7 +96,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer _logger.LogInformation("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | ИТОГО: {TotalMs} мс", context.JobGroupName, jobGroupId, totalSw.ElapsedMilliseconds); } - catch (GroupedSyncEarlyExitException ex) + catch (SyncEarlyExitException ex) { totalSw.Stop(); _logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)", @@ -111,6 +114,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } } + private void PrintDryRunReport(GroupedSyncContext context) { _logger.LogInformation("========== [DryRun] ОТЧЁТ по JobGroup '{JobGroupName}' ({JobGroupId}) ==========", @@ -140,7 +144,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer foreach (var entry in subGroup.Entries) { - _logger.LogInformation( + _logger.LogDebug( "[DryRun] - {Unit} (ValueId: {ValueId})", context.FormatUnit(entry.UnitId), entry.UnitFieldValueId); @@ -157,18 +161,21 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer _logger.LogInformation("========== [DryRun] КОНЕЦ ОТЧЁТА =========="); } - public Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) + + public Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default) { _logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); return Task.CompletedTask; } - public Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) + + public Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, CancellationToken ct = default) { _logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); return Task.CompletedTask; } + private async Task SetStatusAsync(Guid jobGroupId, string comment) { var status = new MatchingStatusItemDto diff --git a/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs index bb00e23f..97b86fcb 100644 --- a/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs @@ -3,18 +3,14 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.JobRepositories; -using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Services.MatchingStatusService; using PARR.Core.Services.UnitFilterService; using PARR.Domain.Cache.Models; using PARR.Domain.Common.Rabbit.Messages.TemplateMatching; -using PARR.Domain.Entities; using PARR.Domain.Entities.Base.History; -using PARR.Domain.Entities.JobEntities; -using PARR.Domain.Entities.Unit; using PARR.Domain.Enums; using PARR.Domain.Settings; -using PARR.TemplateMatcher.Constants; +using PARR.TemplateMatcher.Exceptions; using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Services.SimpleSync; using PARR.TemplateMatcher.Settings; @@ -38,10 +34,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer private readonly ITemplateMqPublisher _templateMqPublisher; private readonly IMatchingStatusService _matchingStatusService; private readonly SettingsFromDb _settingsFromDb; - private readonly IOptions _templateSettings; - private readonly IUnitFieldRepository _unitFieldService; - private readonly IUnitInValueRepository _unitInValueService; - private readonly IUnitRepository _unitRepository; + private readonly IUnusedTemplatesSyncService _unusedTemplatesSyncService; + public SimpleTemplateSynchronizer( IEnumerable readStages, @@ -55,33 +49,28 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer IMatchingStatusService matchingStatusService, SettingsFromDb settingsFromDb, IOptions templateSettings, - IUnitFieldRepository unitFieldService, - IUnitInValueRepository unitInValueService, - IUnitRepository unitRepository + IUnusedTemplatesSyncService unusedTemplatesSyncService ) { - this._readStages = readStages; - this._writeStages = writeStages; - this._logger = logger; - this._unitFilterService = unitFilterService; - this._templateService = templateService; - this._jobService = jobService; - this._templateNameNormalizer = templateNameNormalizer; - this._templateMqPublisher = templateMqPublisher; - this._matchingStatusService = matchingStatusService; - this._settingsFromDb = settingsFromDb; - this._templateSettings = templateSettings; - this._unitFieldService = unitFieldService; - this._unitInValueService = unitInValueService; - this._unitRepository = unitRepository; + _readStages = readStages; + _writeStages = writeStages; + _logger = logger; + _unitFilterService = unitFilterService; + _templateService = templateService; + _jobService = jobService; + _templateNameNormalizer = templateNameNormalizer; + _templateMqPublisher = templateMqPublisher; + _matchingStatusService = matchingStatusService; + _settingsFromDb = settingsFromDb; + _unusedTemplatesSyncService = unusedTemplatesSyncService; } - public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, bool dryRun = false) + + public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default) { if (jobId == _settingsFromDb.JobIdForUnusedTemplates) { - _logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId); - await SyncUnusedTemplatesAsync(jobId, initiator, dryRun); + await _unusedTemplatesSyncService.SyncAsync(jobId, initiator, dryRun); return; } @@ -134,14 +123,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds); } - await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно"); + await SetStatusAsync(jobId, "Синхронизация завершена успешно"); await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); _logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})", context.JobName, jobId); } else { _logger.LogInformation("[DryRun] Write-этапы пропущены. Изменения в БД и MQ не выполнены."); - await UpdateMatchingStatusAsync(jobId, "DryRun: Анализ завершён без изменений"); + await SetStatusAsync(jobId, "DryRun: Анализ завершён без изменений"); await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); } @@ -149,12 +138,20 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer _logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс", context.JobName, jobId, totalSw.ElapsedMilliseconds); } + catch (SyncEarlyExitException ex) + { + totalSw.Stop(); + _logger.LogInformation("Job {JobId}: {Reason} ({ElapsedMs} мс)", + jobId, ex.Reason, totalSw.ElapsedMilliseconds); + await SetStatusAsync(jobId, ex.Reason); + await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); + } catch (Exception ex) { totalSw.Stop(); _logger.LogError(ex, "Ошибка при синхронизации Job '{JobName}' ({JobId}) через {ElapsedMs} мс", context.JobName ?? string.Empty, jobId, totalSw.ElapsedMilliseconds); - await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}"); + await SetStatusAsync(jobId, $"Ошибка: {ex.Message}"); throw; } } @@ -173,29 +170,29 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer if (context.NewUnitIds.Any()) { - _logger.LogInformation("[DryRun] --- Новые юниты (будут созданы шаблоны) ---"); + _logger.LogDebug("[DryRun] --- Новые юниты (будут созданы шаблоны) ---"); foreach (var unitId in context.NewUnitIds) { - _logger.LogInformation("[DryRun] + {Unit}", context.FormatUnit(unitId)); + _logger.LogDebug("[DryRun] + {Unit}", context.FormatUnit(unitId)); } } if (context.UnusedTemplates.Any()) { - _logger.LogInformation("[DryRun] --- Шаблоны для деактивации ---"); + _logger.LogDebug("[DryRun] --- Шаблоны для деактивации ---"); foreach (var template in context.UnusedTemplates) { - _logger.LogInformation("[DryRun] - {TemplateId} (Unit: {Unit}, Name: '{Name}')", + _logger.LogDebug("[DryRun] - {TemplateId} (Unit: {Unit}, Name: '{Name}')", template.Id, context.FormatUnit(template.UnitId), template.Name); } } if (context.TemplatesToRename.Any()) { - _logger.LogInformation("[DryRun] --- Шаблоны для переименования ---"); + _logger.LogDebug("[DryRun] --- Шаблоны для переименования ---"); foreach (var (template, expectedName) in context.TemplatesToRename) { - _logger.LogInformation("[DryRun] ~ {TemplateId} (Unit: {Unit}): '{OldName}' -> '{NewName}'", + _logger.LogDebug("[DryRun] ~ {TemplateId} (Unit: {Unit}): '{OldName}' -> '{NewName}'", template.Id, context.FormatUnit(template.UnitId), template.Name, expectedName); } } @@ -203,7 +200,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer _logger.LogInformation("========== [DryRun] КОНЕЦ ОТЧЁТА =========="); } - public Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false) + public Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default) { _logger.LogWarning( "SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId} (DryRun={DryRun}). Это не поддерживаемая операция.", @@ -212,7 +209,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer } - public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) + public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, CancellationToken ct = default) { _logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId); @@ -252,7 +249,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer if (job == null) { _logger.LogWarning("Job {JobId} не найден.", jobId); - await UpdateMatchingStatusAsync(jobId, "Job не найден"); + await SetStatusAsync(jobId, "Job не найден"); return; } @@ -261,7 +258,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer if (filteredUnits == null || !filteredUnits.Any()) { _logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); - await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется"); + await SetStatusAsync(jobId, "Нет Unit'ов — обновление не требуется"); await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); return; } @@ -322,322 +319,20 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer } } - await UpdateMatchingStatusAsync(jobId, "Обновление завершено"); + await SetStatusAsync(jobId, "Обновление завершено"); await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); _logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId); } catch (Exception ex) { _logger.LogError(ex, "Ошибка при обновлении Job {JobId}", jobId); - await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}"); + await SetStatusAsync(jobId, $"Ошибка: {ex.Message}"); throw; } } - private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default) - { - // Проверка отмены в самом начале - ct.ThrowIfCancellationRequested(); - - var existingStatus = await _matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); - if (existingStatus.DetailsJobs?.Count > 0) - { - _logger.LogWarning("Синхронизация для Job неиспользуемых шаблонов {JobId} уже запущена. Пропускаем.", unusedJobId); - return; - } - - var initialStatus = new MatchingStatusItemDto - { - DateStart = DateTimeOffset.UtcNow, - Action = TemplateMatcherActionEnum.Sync, - Comment = "Синхронизация неиспользуемых шаблонов" - }; - await _matchingStatusService.SetMatchingStatusAsync( - unusedJobId, - SyncTaskEntityTypeEnum.Job, - new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, - TimeSpan.FromMinutes(30) - ); - - var totalSw = Stopwatch.StartNew(); - - try - { - // 1. Находим ID нужных полей - var responsableAreaField = await _unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName, ct); - var tagField = await _unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName, ct); - - if (responsableAreaField == null || tagField == null) - { - _logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", UnusedTemplateConstants.ResponsibilityAreaFieldName, UnusedTemplateConstants.NotUsedTagValue); - await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей"); - return; - } - - var responsableAreaFieldId = responsableAreaField.Id; - var tagFieldId = tagField.Id; - - // 2. Находим ValueId для тега "ПАРР-НЕИСП" - var targetTagValueId = await _unitInValueService.Get() - .AsNoTracking() - .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.", UnusedTemplateConstants.NotUsedTagValue, UnusedTemplateConstants.ParrTagFieldName); - } - - var unusedJob = await _jobService.Get().AsNoTracking() - .Include(t => t!.Group).ThenInclude(t => t!.GroupType) - .Include(t => t!.Tnk) - .FirstOrDefaultAsync(j => j.Id == unusedJobId, ct); - - if (unusedJob == null) - { - _logger.LogError("Job неиспользуемых шаблонов {JobId} не найден.", unusedJobId); - await UpdateMatchingStatusAsync(unusedJobId, "Job не найден"); - return; - } - - var unusedTemplates = await _templateService.Get() - .Include(t => t.Unit) - .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) - .ToListAsync(ct); - - if (!unusedTemplates.Any()) - { - _logger.LogInformation("Не найдено шаблонов со статусом Unused."); - await UpdateMatchingStatusAsync(unusedJobId, "Нет шаблонов для обработки"); - await _matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); - return; - } - - await UpdateMatchingStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки"); - - int processed = 0; - var allTemplateUnitIds = unusedTemplates.Select(t => t.UnitId).Distinct().ToList(); - - // Получаем значения ЗОНА_ОТВЕТСТВЕННОСТИ для всех юнитов шаблонов - var unitResponsableAreaValues = await _unitInValueService.GetByUnitIdsAndFieldIdsAsync(allTemplateUnitIds, new List { responsableAreaFieldId }, ct); - var unitToResponsableAreaValueMap = unitResponsableAreaValues - .Where(uiv => uiv.ValueId != Guid.Empty) - .ToDictionary(uiv => uiv.UnitId, uiv => uiv.ValueId); - - // Пакетный поиск целевых юнитов (Один запрос к БД вместо N) - var responsableAreaToTargetUnitMap = new Dictionary(); - var distinctResponsableAreaValues = unitToResponsableAreaValueMap.Values.Distinct().ToList(); - - if (targetTagValueId != Guid.Empty && - distinctResponsableAreaValues.Any()) - { - _logger.LogDebug("Пакетный поиск целевых юнитов с тегом 'ПАРР-НЕИСП' для {Count} уникальных значений ЗОНА_ОТВЕТСТВЕННОСТИ.", distinctResponsableAreaValues.Count); - - // Передаем ct в ToListAsync - var matches = await _unitInValueService.Get().AsNoTracking() - .Where(uiv => uiv.FieldId == responsableAreaFieldId && distinctResponsableAreaValues.Contains(uiv.ValueId)) - .Join( - _unitInValueService.Get().AsNoTracking().Where(t => t.FieldId == tagFieldId && t.ValueId == targetTagValueId), - responsableArea => responsableArea.UnitId, - tag => tag.UnitId, - (responsableArea, tag) => new { responsableArea.ValueId, responsableArea.UnitId } - ) - .ToListAsync(ct); - - responsableAreaToTargetUnitMap = matches - .GroupBy(x => x.ValueId) - .ToDictionary(g => g.Key, g => g.First().UnitId); - - _logger.LogDebug("Сформирован кэш соответствий: найдено {Count} целевых юнитов.", responsableAreaToTargetUnitMap.Count); - } - - foreach (var template in unusedTemplates) - { - try - { - // Проверка отмены внутри цикла (на случай долгих вычислений) - ct.ThrowIfCancellationRequested(); - - if (template.Unit == null) - { - _logger.LogWarning("У шаблона {TemplateId} отсутствует Unit. Пропускаем.", template.Id); - processed++; - continue; - } - - // 1. Определяем текущее значение ЗОНА_ОТВЕТСТВЕННОСТИ - var currentResponsableAreaValueId = Guid.Empty; - var hasResponsableArea = unitToResponsableAreaValueMap.TryGetValue(template.UnitId, out currentResponsableAreaValueId); - - // 2. Ищем целевой юнит в кэше - Guid? targetUnitId = null; - if (hasResponsableArea && currentResponsableAreaValueId != Guid.Empty) - { - if (responsableAreaToTargetUnitMap.TryGetValue(currentResponsableAreaValueId, out var foundUnitId) && foundUnitId != Guid.Empty) - { - targetUnitId = foundUnitId; - } - } - - // 3. Финализируем UnitId и Unit - Guid finalUnitId = targetUnitId ?? template.UnitId; - Unit finalUnit = template.Unit; - - if (targetUnitId.HasValue && targetUnitId.Value != template.UnitId) - { - _logger.LogInformation("Для шаблона {TemplateId} найден новый UnitId {NewUnitId} (был {OldUnitId}).", - template.Id, targetUnitId.Value, template.UnitId); - - // Передаем ct в запрос - var newUnit = await _unitRepository.Get().AsNoTracking() - .FirstOrDefaultAsync(u => u.Id == targetUnitId.Value, ct); - - if (newUnit != null) - { - finalUnit = newUnit; - } - else - { - _logger.LogWarning("Не удалось загрузить новый юнит {UnitId}. Используем старый.", targetUnitId.Value); - finalUnitId = template.UnitId; - } - } - else if (!hasResponsableArea) - { - _logger.LogDebug("У юнита {UnitId} шаблона {TemplateId} нет значения поля ЗОНА_ОТВЕТСТВЕННОСТИ. Оставляем текущий UnitId.", template.UnitId, template.Id); - } - - // 4. Генерация целевого имени - var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob, finalUnit); - - // 5. Проверка необходимости обновления - bool unitChanged = template.UnitId != finalUnitId; - bool jobChanged = template.JobId != unusedJobId; - bool nameChanged = !string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase); - - if (!unitChanged && !jobChanged && !nameChanged) - { - _logger.LogDebug("Шаблон {TemplateId} уже актуален. Пропуск отправки в MQ.", template.Id); - processed++; - continue; - } - - // Условие DryRun для отправки в MQ - if (!dryRun) - { - await SendUpdateRequest(template, unusedJobId, expectedName, initiator, finalUnitId); - _logger.LogDebug("Отправлен запрос на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}", - template.Id, unitChanged, jobChanged, nameChanged); - } - else - { - _logger.LogInformation("[DryRun] Пропущена отправка запроса на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}, ExpectedName='{Name}'", - template.Id, unitChanged, jobChanged, nameChanged, expectedName); - } - - processed++; - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка при обработке шаблона {TemplateId}", template.Id); - } - } - - - totalSw.Stop(); - - _logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс", - unusedJob.Name, unusedJob.Id, totalSw.ElapsedMilliseconds); - - if (dryRun) - { - await UpdateMatchingStatusAsync(unusedJobId, "DryRun: Анализ неиспользуемых шаблонов завершён без изменений"); - } - else - { - await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена"); - } - - await _matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); - - _logger.LogInformation( - "Синхронизация неиспользуемых шаблонов завершена. Обработано {Count} шаблонов", - unusedTemplates.Count); - } - catch (Exception ex) - { - totalSw.Stop(); - _logger.LogError(ex, - "Ошибка при синхронизации неиспользуемых шаблонов для Job {JobId} через {ElapsedMs} мс", - unusedJobId, totalSw.ElapsedMilliseconds); - await UpdateMatchingStatusAsync(unusedJobId, $"Ошибка: {ex.Message}"); - throw; - } - } - - - private async Task SendUpdateRequest(Template template, Guid jobId, string name, HistoryInitiator initiator, Guid unitId) - { - var updateRequest = new TemplateUpdaterMessage - { - TemplateId = template.Id, - JobId = jobId, - UnitId = unitId, - Name = name, - IsActiveTemplate = false, - IsActiveSchedule = false, - IsNew = false, - Index = null, - StatusTypeId = template.StatusTypeId, - Initiator = initiator, - UnitsInTemplate = new List() - }; - - await _templateMqPublisher.PublishUpdateAsync(updateRequest); - } - - - private async Task GenerateUnusedTemplateNameAsync(Template template, Job unusedJob, Unit unit) - { - var tempJob = new Job - { - Id = unusedJob.Id, - Name = unusedJob.Name, - WorkName = unusedJob.WorkName, - MinValueRelationships = unusedJob.MinValueRelationships, - MaxValueRelationships = unusedJob.MaxValueRelationships, - IsParentRelationships = unusedJob.IsParentRelationships, - TemplateNameMask = _templateSettings.Value.UnusedTemplateNameMask, - WorkGroupMask = unusedJob.WorkGroupMask, - ResponseAreaMask = unusedJob.ResponseAreaMask, - TnkId = unusedJob.TnkId, - GroupId = unusedJob.GroupId, - Group = unusedJob.Group, - Tnk = unusedJob.Tnk, - UnitFilters = unusedJob.UnitFilters, - Templates = unusedJob.Templates, - AutoControl = unusedJob.AutoControl - }; - - var tempTemplateForName = new Template - { - Id = template.Id, - Name = template.Name, - JobId = unusedJob.Id, - UnitId = unit.Id, - Index = null, - Job = tempJob, - Unit = unit, - UnitsInTemplate = new List() - }; - - return await _templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); - } - - - private async Task UpdateMatchingStatusAsync(Guid jobId, string comment) + private async Task SetStatusAsync(Guid jobId, string comment) { var status = new MatchingStatusItemDto { diff --git a/PARR.TemplateMatcher/Services/Implementations/UnusedTemplatesSyncService.cs b/PARR.TemplateMatcher/Services/Implementations/UnusedTemplatesSyncService.cs new file mode 100644 index 00000000..8ec3a4d1 --- /dev/null +++ b/PARR.TemplateMatcher/Services/Implementations/UnusedTemplatesSyncService.cs @@ -0,0 +1,383 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using PARR.Core.Repositories.Interfaces; +using PARR.Core.Repositories.Interfaces.JobRepositories; +using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.MatchingStatusService; +using PARR.Domain.Cache.Models; +using PARR.Domain.Common.Rabbit.Messages.TemplateMatching; +using PARR.Domain.Entities; +using PARR.Domain.Entities.Base.History; +using PARR.Domain.Entities.JobEntities; +using PARR.Domain.Entities.Unit; +using PARR.Domain.Enums; +using PARR.TemplateMatcher.Constants; +using PARR.TemplateMatcher.Services.Interfaces; +using PARR.TemplateMatcher.Settings; +using System.Diagnostics; + +namespace PARR.TemplateMatcher.Services.Implementations; + +/// +/// Синхронизация неиспользуемых шаблонов. +/// Вынесен из SimpleTemplateSynchronizer для разделения ответственности. +/// +internal sealed class UnusedTemplatesSyncService: IUnusedTemplatesSyncService +{ + private readonly ITemplateRepository _templateService; + private readonly IJobRepository _jobService; + private readonly IUnitFieldRepository _unitFieldService; + private readonly IUnitInValueRepository _unitInValueService; + private readonly IUnitRepository _unitRepository; + private readonly ITemplateMqPublisher _templateMqPublisher; + private readonly IMatchingStatusService _matchingStatusService; + private readonly ITemplateNameNormalizer _templateNameNormalizer; + private readonly IOptions _templateSettings; + private readonly ILogger _logger; + + public UnusedTemplatesSyncService( + ITemplateRepository templateService, + IJobRepository jobService, + IUnitFieldRepository unitFieldService, + IUnitInValueRepository unitInValueService, + IUnitRepository unitRepository, + ITemplateMqPublisher templateMqPublisher, + IMatchingStatusService matchingStatusService, + ITemplateNameNormalizer templateNameNormalizer, + IOptions templateSettings, + ILogger logger) + { + _templateService = templateService; + _jobService = jobService; + _unitFieldService = unitFieldService; + _unitInValueService = unitInValueService; + _unitRepository = unitRepository; + _templateMqPublisher = templateMqPublisher; + _matchingStatusService = matchingStatusService; + _templateNameNormalizer = templateNameNormalizer; + _templateSettings = templateSettings; + _logger = logger; + } + + + public async Task SyncAsync( + Guid unusedJobId, + HistoryInitiator initiator, + bool dryRun = false, + CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + var existingStatus = await _matchingStatusService.GetStatusAsync( + unusedJobId, SyncTaskEntityTypeEnum.Job); + + if (existingStatus.DetailsJobs?.Count > 0) + { + _logger.LogWarning( + "Синхронизация для Job неиспользуемых шаблонов {JobId} уже запущена. Пропускаем.", + unusedJobId); + return; + } + + var statusComment = dryRun + ? "DryRun: Анализ неиспользуемых шаблонов" + : "Синхронизация неиспользуемых шаблонов"; + + await SetStatusAsync(unusedJobId, statusComment); + + var totalSw = Stopwatch.StartNew(); + + try + { + // Поиск полей + var responsableAreaField = await _unitFieldService + .GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName, ct); + var tagField = await _unitFieldService + .GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName, ct); + + if (responsableAreaField == null || tagField == null) + { + _logger.LogError( + "Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", + UnusedTemplateConstants.ResponsibilityAreaFieldName, + UnusedTemplateConstants.NotUsedTagValue); + await SetStatusAsync(unusedJobId, "Ошибка конфигурации полей"); + return; + } + + var responsableAreaFieldId = responsableAreaField.Id; + var tagFieldId = tagField.Id; + + // 2. Находим ValueId для тега "ПАРР-НЕИСП" + var targetTagValueId = await _unitInValueService.Get() + .AsNoTracking() + .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.", UnusedTemplateConstants.NotUsedTagValue, UnusedTemplateConstants.ParrTagFieldName); + } + + var unusedJob = await _jobService.Get().AsNoTracking() + .Include(t => t!.Group).ThenInclude(t => t!.GroupType) + .Include(t => t!.Tnk) + .FirstOrDefaultAsync(j => j.Id == unusedJobId, ct); + + if (unusedJob == null) + { + _logger.LogError("Job неиспользуемых шаблонов {JobId} не найден.", unusedJobId); + await SetStatusAsync(unusedJobId, "Job не найден"); + return; + } + + var unusedTemplates = await _templateService.Get() + .Include(t => t.Unit) + .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) + .ToListAsync(ct); + + if (!unusedTemplates.Any()) + { + _logger.LogInformation("Не найдено шаблонов со статусом Unused."); + await SetStatusAsync(unusedJobId, "Нет шаблонов для обработки"); + await _matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); + return; + } + + await SetStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки"); + + int processed = 0; + var allTemplateUnitIds = unusedTemplates.Select(t => t.UnitId).Distinct().ToList(); + + // Получаем значения ЗОНА_ОТВЕТСТВЕННОСТИ для всех юнитов шаблонов + var unitResponsableAreaValues = await _unitInValueService.GetByUnitIdsAndFieldIdsAsync(allTemplateUnitIds, new List { responsableAreaFieldId }, ct); + var unitToResponsableAreaValueMap = unitResponsableAreaValues + .Where(uiv => uiv.ValueId != Guid.Empty) + .ToDictionary(uiv => uiv.UnitId, uiv => uiv.ValueId); + + // Пакетный поиск целевых юнитов (Один запрос к БД вместо N) + var responsableAreaToTargetUnitMap = new Dictionary(); + var distinctResponsableAreaValues = unitToResponsableAreaValueMap.Values.Distinct().ToList(); + + if (targetTagValueId != Guid.Empty && + distinctResponsableAreaValues.Any()) + { + _logger.LogDebug("Пакетный поиск целевых юнитов с тегом 'ПАРР-НЕИСП' для {Count} уникальных значений ЗОНА_ОТВЕТСТВЕННОСТИ.", distinctResponsableAreaValues.Count); + + // Передаем ct в ToListAsync + var matches = await _unitInValueService.Get().AsNoTracking() + .Where(uiv => uiv.FieldId == responsableAreaFieldId && distinctResponsableAreaValues.Contains(uiv.ValueId)) + .Join( + _unitInValueService.Get().AsNoTracking().Where(t => t.FieldId == tagFieldId && t.ValueId == targetTagValueId), + responsableArea => responsableArea.UnitId, + tag => tag.UnitId, + (responsableArea, tag) => new { responsableArea.ValueId, responsableArea.UnitId } + ) + .ToListAsync(ct); + + responsableAreaToTargetUnitMap = matches + .GroupBy(x => x.ValueId) + .ToDictionary(g => g.Key, g => g.First().UnitId); + + _logger.LogDebug("Сформирован кэш соответствий: найдено {Count} целевых юнитов.", responsableAreaToTargetUnitMap.Count); + } + + foreach (var template in unusedTemplates) + { + try + { + // Проверка отмены внутри цикла (на случай долгих вычислений) + ct.ThrowIfCancellationRequested(); + + if (template.Unit == null) + { + _logger.LogWarning("У шаблона {TemplateId} отсутствует Unit. Пропускаем.", template.Id); + processed++; + continue; + } + + // 1. Определяем текущее значение ЗОНА_ОТВЕТСТВЕННОСТИ + var currentResponsableAreaValueId = Guid.Empty; + var hasResponsableArea = unitToResponsableAreaValueMap.TryGetValue(template.UnitId, out currentResponsableAreaValueId); + + // 2. Ищем целевой юнит в кэше + Guid? targetUnitId = null; + if (hasResponsableArea && currentResponsableAreaValueId != Guid.Empty) + { + if (responsableAreaToTargetUnitMap.TryGetValue(currentResponsableAreaValueId, out var foundUnitId) && foundUnitId != Guid.Empty) + { + targetUnitId = foundUnitId; + } + } + + // 3. Финализируем UnitId и Unit + Guid finalUnitId = targetUnitId ?? template.UnitId; + Unit finalUnit = template.Unit; + + if (targetUnitId.HasValue && targetUnitId.Value != template.UnitId) + { + _logger.LogInformation("Для шаблона {TemplateId} найден новый UnitId {NewUnitId} (был {OldUnitId}).", + template.Id, targetUnitId.Value, template.UnitId); + + // Передаем ct в запрос + var newUnit = await _unitRepository.Get().AsNoTracking() + .FirstOrDefaultAsync(u => u.Id == targetUnitId.Value, ct); + + if (newUnit != null) + { + finalUnit = newUnit; + } + else + { + _logger.LogWarning("Не удалось загрузить новый юнит {UnitId}. Используем старый.", targetUnitId.Value); + finalUnitId = template.UnitId; + } + } + else if (!hasResponsableArea) + { + _logger.LogDebug("У юнита {UnitId} шаблона {TemplateId} нет значения поля ЗОНА_ОТВЕТСТВЕННОСТИ. Оставляем текущий UnitId.", template.UnitId, template.Id); + } + + // 4. Генерация целевого имени + var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob, finalUnit); + + // 5. Проверка необходимости обновления + bool unitChanged = template.UnitId != finalUnitId; + bool jobChanged = template.JobId != unusedJobId; + bool nameChanged = !string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase); + + if (!unitChanged && !jobChanged && !nameChanged) + { + _logger.LogDebug("Шаблон {TemplateId} уже актуален. Пропуск отправки в MQ.", template.Id); + processed++; + continue; + } + + // Условие DryRun для отправки в MQ + if (!dryRun) + { + await SendUpdateRequest(template, unusedJobId, expectedName, initiator, finalUnitId); + _logger.LogDebug("Отправлен запрос на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}", + template.Id, unitChanged, jobChanged, nameChanged); + } + else + { + _logger.LogInformation("[DryRun] Пропущена отправка запроса на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}, ExpectedName='{Name}'", + template.Id, unitChanged, jobChanged, nameChanged, expectedName); + } + + processed++; + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка при обработке шаблона {TemplateId}", template.Id); + } + } + + totalSw.Stop(); + + _logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс", + unusedJob.Name, unusedJob.Id, totalSw.ElapsedMilliseconds); + + var finalComment = dryRun + ? "DryRun: Анализ неиспользуемых шаблонов завершён без изменений" + : "Синхронизация неиспользуемых шаблонов завершена"; + + await SetStatusAsync(unusedJobId, finalComment); + await _matchingStatusService.DeleteMatchingStatusAsync( + unusedJobId, SyncTaskEntityTypeEnum.Job); + } + catch (Exception ex) + { + totalSw.Stop(); + _logger.LogError(ex, + "Ошибка при синхронизации неиспользуемых шаблонов для Job {JobId} через {ElapsedMs} мс", + unusedJobId, totalSw.ElapsedMilliseconds); + await SetStatusAsync(unusedJobId, $"Ошибка: {ex.Message}"); + throw; + } + } + + + private async Task SendUpdateRequest(Template template, Guid jobId, string name, HistoryInitiator initiator, Guid unitId) + { + var updateRequest = new TemplateUpdaterMessage + { + TemplateId = template.Id, + JobId = jobId, + UnitId = unitId, + Name = name, + IsActiveTemplate = false, + IsActiveSchedule = false, + IsNew = false, + Index = null, + StatusTypeId = template.StatusTypeId, + Initiator = initiator, + UnitsInTemplate = new List() + }; + + await _templateMqPublisher.PublishUpdateAsync(updateRequest); + } + + + private async Task GenerateUnusedTemplateNameAsync(Template template, Job unusedJob, Unit unit) + { + var tempJob = new Job + { + Id = unusedJob.Id, + Name = unusedJob.Name, + WorkName = unusedJob.WorkName, + MinValueRelationships = unusedJob.MinValueRelationships, + MaxValueRelationships = unusedJob.MaxValueRelationships, + IsParentRelationships = unusedJob.IsParentRelationships, + TemplateNameMask = _templateSettings.Value.UnusedTemplateNameMask, + WorkGroupMask = unusedJob.WorkGroupMask, + ResponseAreaMask = unusedJob.ResponseAreaMask, + TnkId = unusedJob.TnkId, + GroupId = unusedJob.GroupId, + Group = unusedJob.Group, + Tnk = unusedJob.Tnk, + UnitFilters = unusedJob.UnitFilters, + Templates = unusedJob.Templates, + AutoControl = unusedJob.AutoControl + }; + + var tempTemplateForName = new Template + { + Id = template.Id, + Name = template.Name, + JobId = unusedJob.Id, + UnitId = unit.Id, + Index = null, + Job = tempJob, + Unit = unit, + UnitsInTemplate = new List() + }; + + return await _templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); + } + + + private async Task SetStatusAsync(Guid jobId, string comment) + { + var status = new MatchingStatusItemDto + { + DateStart = DateTimeOffset.UtcNow, + Action = TemplateMatcherActionEnum.Sync, + Comment = comment + }; + + await _matchingStatusService.SetMatchingStatusAsync( + jobId, + SyncTaskEntityTypeEnum.Job, + new MatchingStatusItem + { + Data = status, + Timestamp = DateTimeOffset.UtcNow, + Source = nameof(UnusedTemplatesSyncService) + }, + TimeSpan.FromMinutes(30)); + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Interfaces/ITemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Interfaces/ITemplateSynchronizer.cs index b931f818..00177bbd 100644 --- a/PARR.TemplateMatcher/Services/Interfaces/ITemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Interfaces/ITemplateSynchronizer.cs @@ -4,8 +4,8 @@ namespace PARR.TemplateMatcher.Services.Interfaces { public interface ITemplateSynchronizer { - Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, bool dryRun = false); - Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false); - Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator); + Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default); + Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default); + Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, CancellationToken ct = default); } } diff --git a/PARR.TemplateMatcher/Services/Interfaces/IUnusedTemplatesSyncService.cs b/PARR.TemplateMatcher/Services/Interfaces/IUnusedTemplatesSyncService.cs new file mode 100644 index 00000000..a8d3405b --- /dev/null +++ b/PARR.TemplateMatcher/Services/Interfaces/IUnusedTemplatesSyncService.cs @@ -0,0 +1,9 @@ +using PARR.Domain.Entities.Base.History; + +namespace PARR.TemplateMatcher.Services.Interfaces +{ + internal interface IUnusedTemplatesSyncService + { + Task SyncAsync(Guid unusedJobId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default); + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs index 3a0c61d9..a5bb5b86 100644 --- a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs +++ b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs @@ -78,6 +78,9 @@ namespace PARR.TemplateMatcher // Легковесные сервисы без состояния, создаются по требованию services.AddTransient(); services.AddTransient(); + + //Сервис для неактуальных шаблонов + services.AddScoped(); } public static IConfigurationBuilder AddTemplateMatcherConfigurations(this IConfigurationBuilder builder, IServiceCollection services)