diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs index f9089bb6..1d251f42 100644 --- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs @@ -56,7 +56,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { - logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); + logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); // === Проверка: уже запущена? === var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); diff --git a/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs index 01ff266d..ad7c4a10 100644 --- a/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs @@ -29,6 +29,11 @@ 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 ILogger logger; private readonly IUnitFilterService unitFilterService; private readonly MqSettings mqSettings; @@ -395,8 +400,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer { logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); - //var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false); - var updateRequest = new TemplateUpdaterMessage { TemplateId = template.Id, @@ -405,8 +408,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer Name = expectedName, IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, - //LastRun = template.LastRun, - //NextRun = nextRun, IsNew = false, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, @@ -432,8 +433,11 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer } - private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator) + private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator, CancellationToken ct = default) { + // Проверка отмены в самом начале + ct.ThrowIfCancellationRequested(); + var existingStatus = await matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); if (existingStatus.DetailsJobs?.Any() == true) { @@ -457,36 +461,36 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer try { // 1. Находим ID нужных полей - var zoRgField = await unitFieldService.GetByAihitNameAsync("ЗОНА_ОТВЕТСТВЕННОСТИ"); - var tagField = await unitFieldService.GetByAihitNameAsync("ПАРР тег"); + var responsableAreaField = await unitFieldService.GetByAihitNameAsync(FieldNameResponsibilityArea); + var tagField = await unitFieldService.GetByAihitNameAsync(FieldNameParrTag); - if (zoRgField == null || tagField == null) + if (responsableAreaField == null || tagField == null) { - logger.LogError("Не найдены поля 'ЗОНА_ОТВЕТСТВЕННОСТИ' или 'Тег'. Синхронизация прервана."); + logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", FieldNameResponsibilityArea, FieldNameParrTag); await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей"); return; } - var zoRgFieldId = zoRgField.Id; + var responsableAreaFieldId = responsableAreaField.Id; var tagFieldId = tagField.Id; - const string targetTagValue = "ПАРР-НЕИСП"; + const string targetTagValue = TagValueNotWorking; // 2. Находим ValueId для тега "ПАРР-НЕИСП" var targetTagValueId = await unitInValueService.Get() .AsNoTracking() .Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == targetTagValue) .Select(uiv => uiv.ValueId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (targetTagValueId == Guid.Empty) { - logger.LogWarning("Значение '{TagValue}' для поля 'Тег' не найдено в справочнике UnitFieldValue.", targetTagValue); + logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", targetTagValue, FieldNameParrTag); } - var unusedJob = await jobService.Get() - .AsNoTracking() - .Include(j => j.Group) - .FirstOrDefaultAsync(j => j.Id == unusedJobId); + 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) { @@ -496,14 +500,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer } var unusedTemplates = await templateService.Get() - .Include(t => t.Unit) - .Include(t => t.Job) - .ThenInclude(t => t.Group) - .ThenInclude(t => t.GroupType) - .Include(t => t.Job) - .ThenInclude(t => t.Tnk) - .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) - .ToListAsync(); + .Include(t => t.Unit) + .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) + .ToListAsync(ct); if (!unusedTemplates.Any()) { @@ -519,15 +518,44 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer var allTemplateUnitIds = unusedTemplates.Select(t => t.UnitId).Distinct().ToList(); // Получаем значения ЗОНА_ОТВЕТСТВЕННОСТИ для всех юнитов шаблонов - var unitZoRgValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allTemplateUnitIds, new List { zoRgFieldId }); - var unitToZoRgValueMap = unitZoRgValues + var unitResponsableAreaValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allTemplateUnitIds, new List { responsableAreaFieldId }); + 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); @@ -535,45 +563,21 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer continue; } - if (!unitToZoRgValueMap.TryGetValue(template.UnitId, out var currentZoRgValueId)) - { - logger.LogDebug("У юнита {UnitId} шаблона {TemplateId} нет значения поля ЗОНА_ОТВЕТСТВЕННОСТИ. Пропускаем замену UnitId.", template.UnitId, template.Id); - - // Исправлено: используем другое имя или просто вызываем метод - var nameForUpdate = await GenerateUnusedTemplateNameAsync(template, unusedJob, template.Unit); - await SendUpdateRequest(template, unusedJobId, nameForUpdate, initiator, template.UnitId); - processed++; - continue; - } + // 1. Определяем текущее значение ЗОНА_ОТВЕТСТВЕННОСТИ + var currentResponsableAreaValueId = Guid.Empty; + var hasResponsableArea = unitToResponsableAreaValueMap.TryGetValue(template.UnitId, out currentResponsableAreaValueId); + // 2. Ищем целевой юнит в кэше Guid? targetUnitId = null; - - if (targetTagValueId != Guid.Empty) + if (hasResponsableArea && currentResponsableAreaValueId != Guid.Empty) { - var candidatesWithZoRg = await unitInValueService.Get() - .AsNoTracking() - .Where(uiv => uiv.FieldId == zoRgFieldId && uiv.ValueId == currentZoRgValueId) - .Select(uiv => uiv.UnitId) - .Distinct() - .ToListAsync(); - - if (candidatesWithZoRg.Any()) + if (responsableAreaToTargetUnitMap.TryGetValue(currentResponsableAreaValueId, out var foundUnitId) && foundUnitId != Guid.Empty) { - // Сначала проверим, есть ли вообще такие юниты - var foundUnitId = await unitInValueService.Get() - .AsNoTracking() - .Where(uiv => uiv.FieldId == tagFieldId && uiv.ValueId == targetTagValueId && candidatesWithZoRg.Contains(uiv.UnitId)) - .Select(uiv => uiv.UnitId) - .FirstOrDefaultAsync(); - - // Если нашли реальный Guid (не Empty), то используем его - if (foundUnitId != Guid.Empty) - { - targetUnitId = foundUnitId; - } + targetUnitId = foundUnitId; } } + // 3. Финализируем UnitId и Unit Guid finalUnitId = targetUnitId ?? template.UnitId; Unit finalUnit = template.Unit; @@ -582,25 +586,45 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer 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); + .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); + } - // Здесь expectedName объявляется впервые в этой итерации цикла, конфликта нет + // 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; + } + await SendUpdateRequest(template, unusedJobId, expectedName, initiator, finalUnitId); + logger.LogDebug("Отправлен запрос на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}", + template.Id, unitChanged, jobChanged, nameChanged); processed++; - await UpdateMatchingStatusAsync(unusedJobId, $"Обработано: {processed}/{unusedTemplates.Count}"); } catch (Exception ex) { @@ -620,6 +644,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer } } + private async Task SendUpdateRequest(Template template, Guid jobId, string name, HistoryInitiator initiator, Guid unitId) { var updateRequest = new TemplateUpdaterMessage