From 91fa14f3f27df697f2504c95dfda4274c2aa7e7b Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Sat, 17 Jan 2026 09:32:34 +1000 Subject: [PATCH] =?UTF-8?q?feat(templateMatcher):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=20=D0=BD=D0=B5=D0=B8=D0=BC=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=D1=8B=D0=BC=D0=B8=20=D1=88=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=BD=D0=B0=D0=BC=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SimpleTemplateSynchronizer.cs | 243 ++++++++++++++---- 1 file changed, 194 insertions(+), 49 deletions(-) diff --git a/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs index d8ab7bdd..1d1af62d 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/SimpleTemplateSynchronizer.cs @@ -5,6 +5,7 @@ using PARR.BLL.Services.Interfaces; using PARR.Common.Domain; using PARR.Constants; using PARR.DAL.Cache.Models; +using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.Models; @@ -28,6 +29,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer private const bool DefaultUsedTemplateState = false; private const bool DefaultUsedScheduleState = false; + // Маска для неиспользуемых шаблонов — хардкод, недоступна из GUI + private const string UnusedTemplateNameMask = "%П-1%_НЕАКТУАЛЬНЫЕ РАБОТЫ_%ЭК%_%ТИКТАК%"; + private readonly ILogger logger; private readonly IUnitFilterService unitFilterService; private readonly MqSettings mqSettings; @@ -40,6 +44,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly IMatchingStatusService matchingStatusService; + private readonly SettingsFromDb settingsFromDb; public SimpleTemplateSynchronizer( ILogger logger, @@ -59,7 +64,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, ITemplateUpdaterMqSender templateUpdaterMqSender, - IMatchingStatusService matchingStatusService + IMatchingStatusService matchingStatusService, + SettingsFromDb settingsFromDb ) { this.logger = logger; @@ -74,10 +80,20 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer this.templateNameNormalizer = templateNameNormalizer; this.templateUpdaterMqSender = templateUpdaterMqSender; this.matchingStatusService = matchingStatusService; + this.settingsFromDb = settingsFromDb; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { + // === Специальная обработка для Job неиспользуемых шаблонов === + if (jobId == settingsFromDb.JobIdForUnusedTemplates) + { + logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId); + await SyncUnusedTemplatesAsync(jobId, initiator); + return; + } + + // === Обычная логика для всех остальных Job === logger.LogDebug("Начало синхронизации шаблонов для Job {JobId}", jobId); // === Проверка: уже запущена? === @@ -120,34 +136,34 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer await UpdateMatchingStatusAsync(jobId, "Job не найден"); return; } - + var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); + if (unitIds == null || !unitIds.Any()) { - if (unitIds == null || !unitIds.Any()) + logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); + + var existingTemplatesForDeactivation = await templateService.Get() + .AsNoTracking() + .Include(t => t.UnitsInTemplate) + .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) + .ToListAsync(); + + await UpdateMatchingStatusAsync(jobId, $"Нет Unit'ов. Деактивация {existingTemplatesForDeactivation.Count} шаблонов..."); + + foreach (var unusedTemplate in existingTemplatesForDeactivation) { - logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); - - var existingTemplatesForDeactivation = await templateService.Get() - .AsNoTracking() - .Include(t => t.UnitsInTemplate) - .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) - .ToListAsync(); - - await UpdateMatchingStatusAsync(jobId, $"Нет Unit'ов. Деактивация {existingTemplatesForDeactivation.Count} шаблонов..."); - - foreach (var unusedTemplate in existingTemplatesForDeactivation) - { - logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); - await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); - } - - await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена: нет Unit'ов"); - await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); - logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); - return; + logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); + await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); } + + await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена: нет Unit'ов"); + await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); + logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); + return; } + #if DEBUG + // Отладка: проверить, есть ли юнит в unitIds if (unitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); @@ -185,7 +201,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer // === Подсчёт операций === int toCreate = newUnitIds.Count; int toUpdate = 0; - int toDeactivate = unusedTemplates.Count; + int toDeactivate = unusedTemplates.Count; // Подсчёт обновлений по имени foreach (var template in existingUsedTemplates) @@ -209,6 +225,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer { logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId); + // === Создаём временный Template для нормализации имени === var tempTemplateForName = new Template { Id = reusableTemplate.Id, @@ -216,9 +233,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer JobId = jobId, UnitId = unitId, Index = reusableTemplate.Index, - Job = job, - Unit = reusableTemplate.Unit, - UnitsInTemplate = new List() + Job = job, // загруженный job + Unit = reusableTemplate.Unit, // может быть null — нормально + UnitsInTemplate = new List() // для простого шаблона }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); @@ -235,7 +252,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, NextRun = nextRun, - UnitsInTemplate = new List() + UnitsInTemplate = new List() // для простого шаблона }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); @@ -257,7 +274,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { - logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", template.Id); + logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); var nextRun = await GetNextRunAsync(job, template.NextRun); @@ -274,7 +291,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, - UnitsInTemplate = new List() + UnitsInTemplate = new List() // для простого шаблона }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); @@ -365,6 +382,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer } #if DEBUG + // Отладка: проверить, есть ли юнит в unitIds if (unitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); @@ -387,20 +405,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToListAsync(); - int toUpdate = 0; - foreach (var template in existingTemplates) - { - if (unitIds.Contains(template.UnitId)) - { - var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); - if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) - toUpdate++; - } - } - - await UpdateMatchingStatusAsync(jobId, $"Обновление имён: {toUpdate} шаблонов"); - - int updated = 0; foreach (var template in existingTemplates) { if (unitIds.Contains(template.UnitId)) @@ -408,7 +412,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { - logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", template.Id); + logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); var nextRun = await GetNextRunAsync(job, template.NextRun); @@ -425,12 +429,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, - UnitsInTemplate = new List() + UnitsInTemplate = new List() // для простого шаблона }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); - updated++; - await UpdateMatchingStatusAsync(jobId, $"Обновлено: {updated}/{toUpdate}"); } } } @@ -447,6 +449,149 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer } } + private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator) + { + // === Проверка: уже запущена? === + var existingStatus = await matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); + if (existingStatus.DetailsJobs?.Any() == true) + { + 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) + ); + + try + { + // Загружаем Job неиспользуемых шаблонов с Group + var unusedJob = await jobService.Get() + .AsNoTracking() + .Include(j => j.Group) + .FirstOrDefaultAsync(j => j.Id == unusedJobId); + + if (unusedJob == null) + { + logger.LogError("Job неиспользуемых шаблонов {JobId} не найден.", unusedJobId); + await UpdateMatchingStatusAsync(unusedJobId, "Job не найден"); + return; + } + + // Находим все шаблоны со статусом Unused + var unusedTemplates = await templateService.Get() + .Include(t => t.Unit) + .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) + .ToListAsync(); + + if (!unusedTemplates.Any()) + { + logger.LogInformation("Не найдено шаблонов со статусом Unused."); + await UpdateMatchingStatusAsync(unusedJobId, "Нет шаблонов для обработки"); + await matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); + return; + } + + await UpdateMatchingStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки"); + + int processed = 0; + foreach (var template in unusedTemplates) + { + // Генерируем ожидаемое имя один раз + var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob); + + // Проверяем, нужно ли обновление + bool needsUpdate = template.JobId != unusedJobId || + !string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase); + + if (!needsUpdate) + { + logger.LogDebug("Шаблон {TemplateId} уже имеет актуальное имя и JobId. Пропускаем.", template.Id); + continue; + } + + var nextRun = await GetNextRunAsync(unusedJob); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = unusedJobId, + UnitId = template.UnitId, + Name = expectedName, + IsActiveTemplate = false, + IsActiveSchedule = false, + LastRun = template.LastRun, + NextRun = nextRun, + Index = null, + StatusTypeId = TemplateStatusTypeEnum.Unused, + Initiator = initiator, + UnitsInTemplate = new List() + }; + + await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); + processed++; + await UpdateMatchingStatusAsync(unusedJobId, $"Обработано: {processed}/{unusedTemplates.Count}"); + } + + await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена"); + await matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); + logger.LogInformation("Синхронизация неиспользуемых шаблонов завершена. Обработано {Count} шаблонов.", unusedTemplates.Count); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при синхронизации неиспользуемых шаблонов для Job {JobId}", unusedJobId); + await UpdateMatchingStatusAsync(unusedJobId, $"Ошибка: {ex.Message}"); + throw; + } + } + + private async Task GenerateUnusedTemplateNameAsync(Template template, Job unusedJob) + { + var tempJob = new Job + { + Id = unusedJob.Id, + Name = unusedJob.Name, + WorkName = unusedJob.WorkName, + MinValueRelationships = unusedJob.MinValueRelationships, + MaxValueRelationships = unusedJob.MaxValueRelationships, + IsParentRelationships = unusedJob.IsParentRelationships, + TemplateNameMask = 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 = template.UnitId, + Index = null, + Job = tempJob, + Unit = template.Unit, + UnitsInTemplate = new List() + }; + + return await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); + } + private async Task CreateSimpleTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator) { logger.LogInformation("Создание нового простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId); @@ -455,7 +600,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer { JobId = jobId, UnitId = unitId, - UnitsInTemplate = new List(), + UnitsInTemplate = new List(), // для простого шаблона HistoryInitiator = initiator };