using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.BLL.Domain.Mq; using PARR.BLL.Services.Interfaces; using PARR.Common.Domain; using PARR.Constants; using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.Models.Unit; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.TransformServices; using PARR.TemplateMatcher.Settings; using System.Text.Json; namespace PARR.TemplateMatcher { internal class TemplateMatcher : ITemplateMatcher { private const int UnusedCandidateBatchSize = 10; private const bool DefaultUnusedTemplateState = false; private const bool DefaultUnusedScheduleState = false; private const bool DefaultUsedTemplateState = false; private const bool DefaultUsedScheduleState = false; private readonly ILogger logger; private readonly IUnitFilterService unitFilterService; private readonly IUnitInUnitService unitInUnitService; private readonly IUnitInValueService unitInValueService; // Добавлено private readonly IUnitService unitService; // Добавлено private readonly MqSettings mqSettings; private readonly IMqService mqService; private readonly ITemplateService templateService; private readonly IJobService jobService; private readonly IJobGroupService jobGroupService; private readonly IShortcodesService shortcodesService; private readonly IEsppScheduleTransformService esppScheduleTransformService; public TemplateMatcher( ILogger logger, IUnitFilterService unitFilterService, IUnitInUnitService unitInUnitService, IUnitInValueService unitInValueService, IUnitService unitService, MqSettings mqSettings, IMqService mqService, ITemplateService templateService, IJobService jobService, IJobGroupService jobGroupService, IShortcodesService shortcodesService, IEsppScheduleTransformService esppScheduleTransformService) { this.logger = logger; this.unitFilterService = unitFilterService; this.unitInUnitService = unitInUnitService; this.unitInValueService = unitInValueService; this.unitService = unitService; this.mqSettings = mqSettings; this.mqService = mqService; this.templateService = templateService; this.jobService = jobService; this.jobGroupService = jobGroupService; this.shortcodesService = shortcodesService; this.esppScheduleTransformService = esppScheduleTransformService; } public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet(); logger.LogDebug("JobId {JobId}: найдено {Count} UnitId по фильтрам.", jobId, expectedUnitIds.Count); var job = await GetJobWithGroupAndAutoControlAsync(jobId); if (job == null) { logger.LogError("Job с Id {JobId} не найден.", jobId); return; } // Проверяем, является ли Job "групповым" bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group; if (isGroupJob && job.Group.GroupingUnitFieldId.HasValue) { logger.LogInformation("Job {JobId} является групповым. Используйте SyncTemplatesForJobGroup для синхронизации.", jobId); return; // Ничего не делаем для группового Job } else { await SyncSimpleTemplatesAsync(job, expectedUnitIds, initiator); } logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); } public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); // 1. Получить JobGroup и связанные Job'ы var jobGroup = await jobGroupService.Get() .AsNoTracking() // Добавлено .Include(jg => jg.Jobs) .ThenInclude(j => j.AutoControl) .Include(jg => jg.Jobs) .ThenInclude(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) .FirstOrDefaultAsync(jg => jg.Id == jobGroupId); if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) { logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); return; } var jobsInGroup = jobGroup.Jobs.ToList(); // 2. Найти Job с максимальным MaxValueRelationships var maxJob = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue) .OrderByDescending(j => j.MaxValueRelationships) .FirstOrDefault(); if (maxJob == null) { logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); // Возможно, нужно обработать случай, когда MaxValueRelationships не установлено ни у одного Job. // Пока просто выйдем. return; } // Проверяем, что UnitFilters и RelationshipFilters загружены if (maxJob.UnitFilters == null) { logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id); // Продолжить с пустыми фильтрами или выйти? // Пока продолжим с пустым списком. } logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships); // 3. Использовать фильтры maxJob для получения expectedUnitIds var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id); if (expectedUnitIds == null || !expectedUnitIds.Any()) { logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId); // Деактивировать все шаблоны для всех Job в группе? // Пока просто выйдем. return; } // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId if (!jobGroup.GroupingUnitFieldId.HasValue) { logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); return; } var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; // --- ИСПРАВЛЕНИЕ: Разбиваем запрос на части --- // Загрузить UnitValues для отфильтрованных юнитов, чтобы проверить GroupingUnitFieldId var filteredUnits = await unitService.Get() .AsNoTracking() // Добавлено .AsSplitQuery() // Добавлено .Include(t => t.UnitValues) // Добавлено .ThenInclude(t => t.Value) // Добавлено .Where(u => expectedUnitIds.Contains(u.Id)) .ToListAsync(); // Сначала загружаем Unit'ы // Затем фильтруем их UnitValues и собираем UnitId var unitIdsWithValidGroupingFieldSet = filteredUnits .Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))) .Select(u => u.Id) .ToHashSet(); // Создаем HashSet logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); if (!unitIdsWithValidGroupingFieldSet.Any()) { logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); return; } // --- НОВАЯ ЛОГИКА: Применение RelationshipFilters --- // Найти связи UnitInUnit для unitIdsWithValidGroupingFieldSet var potentialUnitInUnitLinks = await unitInUnitService.Get() .AsNoTracking() // Добавлено .Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId)) .ToListAsync(); logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit до применения RelationshipFilters.", potentialUnitInUnitLinks.Count); // Получить RelationshipFilters из maxJob var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List(); if (relationshipFilters.Any()) { // Загрузить UnitInValue для ParentUnitId и ChildUnitId из potentialUnitInUnitLinks var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet(); var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet(); var parentUnitValues = await unitInValueService.Get() .AsNoTracking() // Добавлено .Include(uv => uv.Field) // Добавлено .Include(uv => uv.Value) // Добавлено .Where(uv => allParentIds.Contains(uv.UnitId)) .ToListAsync(); var childUnitValues = await unitInValueService.Get() .AsNoTracking() // Добавлено .Include(uv => uv.Field) // Добавлено .Include(uv => uv.Value) // Добавлено .Where(uv => allChildIds.Contains(uv.UnitId)) .ToListAsync(); // Сгруппировать значения по UnitId для быстрого доступа var parentValuesMap = parentUnitValues .GroupBy(uv => uv.UnitId) .ToDictionary(g => g.Key, g => g.ToList()); var childValuesMap = childUnitValues .GroupBy(uv => uv.UnitId) .ToDictionary(g => g.Key, g => g.ToList()); // Применить фильтры к связям var filteredUnitInUnitLinks = new List(); foreach (var link in potentialUnitInUnitLinks) { bool linkMatchesAllFilters = true; foreach (var rf in relationshipFilters) { var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List()); bool filterMatch = valuesToCheck.Any(uv => uv.FieldId == rf.FieldId && uv.Value != null && uv.Value.Value != null && uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase) ); if (rf.IsInverse) filterMatch = !filterMatch; if (!filterMatch) { linkMatchesAllFilters = false; break; // Не подходит под один из фильтров } } if (linkMatchesAllFilters) { filteredUnitInUnitLinks.Add(link); } } potentialUnitInUnitLinks = filteredUnitInUnitLinks; // Заменяем на отфильтрованные } logger.LogDebug("Осталось {Count} связей UnitInUnit после применения RelationshipFilters.", potentialUnitInUnitLinks.Count); // 6. Сгруппировать ChildUnitId по ParentUnitId (региональный ЭК) из ОТФИЛЬТРОВАННЫХ связей var groupedByRegional = potentialUnitInUnitLinks .GroupBy(link => link.ParentUnitId) .ToDictionary(g => g.Key, g => g.Select(l => l.ChildUnitId).ToList()); logger.LogDebug("Сформировано {Count} групп по региональным юнитам.", groupedByRegional.Count); // 7. Разбить каждую группу и сопоставить с Job // Для каждого регионального юнита и его дочерних юнитов: foreach (var kvp in groupedByRegional) { var regionalUnitId = kvp.Key; var childUnitIds = kvp.Value; logger.LogDebug("Обработка регионального юнита {RegionalUnitId} с {Count} дочерними юнитами.", regionalUnitId, childUnitIds.Count); // Применяем ограничение MaxValueRelationships maxJob int maxValueForSplitting = maxJob.MaxValueRelationships.Value; // Уже проверили, что не null var childUnitGroups = childUnitIds .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .GroupBy(x => x.groupIndex) .Select(g => g.Select(x => x.id).ToList()) .ToList(); logger.LogDebug("Региональный юнит {RegionalUnitId}: разбит на {GroupCount} подгрупп.", regionalUnitId, childUnitGroups.Count); // Для каждой подгруппы: for (int i = 0; i < childUnitGroups.Count; i++) { var subGroup = childUnitGroups[i]; var subGroupSize = subGroup.Count; logger.LogDebug("Обработка подгруппы {Index} регионального юнита {RegionalUnitId}, размер {Size}.", i, regionalUnitId, subGroupSize); // 8. Найти подходящий Job для подгруппы // Попробовать найти Job с MaxValueRelationships, равным размеру подгруппы var targetJob = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) .FirstOrDefault(); if (targetJob == null) { // Найти Job с MaxValueRelationships >= размеру подгруппы, но минимально подходящее targetJob = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) .OrderBy(j => j.MaxValueRelationships.Value) .FirstOrDefault(); } if (targetJob == null) { // Если подходящий Job не найден, используем maxJob targetJob = maxJob; logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} не найден подходящий Job, используем maxJob {MaxJobId}.", i, regionalUnitId, maxJob.Id); } else { logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", i, regionalUnitId, targetJob.Id, targetJob.MaxValueRelationships); } // 9. Загрузить существующие шаблоны для targetJob, связанные с regionalUnitId var existingTemplatesForRegional = await templateService.Get() .AsNoTracking() // Добавлено .Include(t => t.UnitsInTemplate) .Where(t => t.JobId == targetJob.Id && t.UnitId == regionalUnitId && t.Index == i) .ToListAsync(); Template existingTemplateForSubGroup = existingTemplatesForRegional.FirstOrDefault(); if (existingTemplateForSubGroup != null) { // Проверить, изменились ли юниты var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet(); var newUnitIds = subGroup.ToHashSet(); if (existingUnitIds.SetEquals(newUnitIds)) { logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) актуален.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i); // Возможно, нужно обновить имя или статус, если изменились фильтры или AutoControl // Пока оставим как есть, если структура не изменилась. } else { logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i); // Обновляем существующий шаблон await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, targetJob, initiator); } } else { // --- ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ МЕТОД TryReuseOneUnusedTemplateAsync --- var reusableTemplate = await TryReuseOneUnusedTemplateAsync(targetJob.Id, regionalUnitId, initiator); // передаём regionalUnitId как unitId для старого метода if (reusableTemplate != null) // если захват успешен { logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, регионального юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, regionalUnitId, i); // Подготовить сообщение для TemplateUpdater с новыми параметрами var expectedName = await GetNormalizedTemplateNameAsync(targetJob, regionalUnitId, i); var nextRun = await GetNextRunAsync(targetJob); // всегда пересчитываем для нового назначения var updateRequest = new TemplateUpdaterMq { TemplateId = reusableTemplate.Id, // ID захваченного шаблона JobId = targetJob.Id, // Новый JobId UnitId = regionalUnitId, // Новый UnitId (региональный) Name = expectedName, IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, NextRun = nextRun, Index = i, // Новый Index UnitsInTemplate = subGroup // Новые UnitsInTemplate }; await SendTemplateUpdateMessage(updateRequest); } else { // Создать новый шаблон logger.LogDebug("Создание нового шаблона для Job {JobId}, Regional {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, regionalUnitId, i, subGroup.Count); await CreateGroupedTemplateAsync(targetJob.Id, regionalUnitId, subGroup, i, initiator); } } } } // 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе // Это требует сбора всех ожидаемых (JobId, UnitId, Index) и сравнения с существующими. // Соберем ожидаемые комбинации var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); foreach (var kvp in groupedByRegional) { var regionalUnitId = kvp.Key; var childUnitIds = kvp.Value; int maxValueForSplitting = maxJob.MaxValueRelationships.Value; var childUnitGroups = childUnitIds .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .GroupBy(x => x.groupIndex) .Select(g => g.Select(x => x.id).ToList()) .ToList(); for (int i = 0; i < childUnitGroups.Count; i++) { var subGroup = childUnitGroups[i]; var subGroupSize = subGroup.Count; var targetJob = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) .FirstOrDefault(); if (targetJob == null) { targetJob = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) .OrderBy(j => j.MaxValueRelationships.Value) .FirstOrDefault(); } if (targetJob == null) targetJob = maxJob; // Исправлено: используем конкретные типы для кортежа expectedTemplateKeys.Add((targetJob.Id, regionalUnitId, i)); } } // Загрузить *все* шаблоны для всех Job в группе, связанные с региональными юнитами из групп var allRegionalUnitIds = groupedByRegional.Keys.ToHashSet(); var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); var allExistingTemplatesInGroup = await templateService.Get() .AsNoTracking() // Добавлено .Include(t => t.UnitsInTemplate) .Where(t => allJobIdsInGroup.Contains(t.JobId) && allRegionalUnitIds.Contains(t.UnitId)) .ToListAsync(); foreach (var existingTemplate in allExistingTemplatesInGroup) { // Исправлено: используем конкретные типы для ключа var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); if (!expectedTemplateKeys.Contains(key)) { logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Regional {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index); await DeactivateTemplateAsync(existingTemplate, existingTemplate.JobId, initiator); } } logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); } private async Task SyncSimpleTemplatesAsync(Job job, HashSet expectedUnitIds, HistoryInitiator initiator) { logger.LogDebug("Синхронизация обычных шаблонов для JobId {JobId}", job.Id); var existingTemplates = await templateService.Get() .AsNoTracking() // Добавлено .Where(t => t.JobId == job.Id) .ToListAsync(); logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.", job.Id, expectedUnitIds.Count, existingTemplates.Count); // Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны if (!expectedUnitIds.Any()) { if (existingTemplates.Any()) { logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.", job.Id, existingTemplates.Count); foreach (var template in existingTemplates) { await DeactivateTemplateAsync(template, job.Id, initiator); } } else { logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", job.Id); } logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", job.Id); return; } // Деактивация шаблонов, которые вышли из фильтра var templatesToDeactivate = existingTemplates .Where(t => !expectedUnitIds.Contains(t.UnitId)) .ToList(); foreach (var template in templatesToDeactivate) { await DeactivateTemplateAsync(template, job.Id, initiator); } // Перечитываем шаблоны после деактивации existingTemplates = await templateService.Get() .AsNoTracking() // Добавлено .Where(t => t.JobId == job.Id) .ToListAsync(); var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t); // UnitId без шаблона → попытка переиспользования или создание var unitIdsMissingTemplates = expectedUnitIds .Where(unitId => !unitToTemplate.ContainsKey(unitId)) .ToList(); var unitIdsToCreateFresh = new List(); foreach (var unitId in unitIdsMissingTemplates) { var reused = await TryReuseOneUnusedTemplateAsync(job.Id, unitId, initiator); if (reused != null) { logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId); var expectedName = await GetNormalizedTemplateNameAsync(job, unitId); var nextRun = await GetNextRunAsync(job); // всегда пересчитываем для нового назначения var updateRequest = new TemplateUpdaterMq { TemplateId = reused.Id, JobId = job.Id, UnitId = unitId, Name = expectedName, IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, NextRun = nextRun, UnitsInTemplate = new List() }; await SendTemplateUpdateMessage(updateRequest); } else { logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId); unitIdsToCreateFresh.Add(unitId); } } // Обновление/реактивация шаблонов, оставшихся в фильтре var templatesInFilter = existingTemplates .Where(t => expectedUnitIds.Contains(t.UnitId)) .ToList(); foreach (var template in templatesInFilter) { var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); await ReactivateOrRenameTemplateAsync(template, job, expectedName, initiator); } // Создание новых шаблонов foreach (var unitId in unitIdsToCreateFresh) { await SendTemplateGeneratorMessageAsync(job.Id, unitId, initiator); } } public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); var existingTemplates = await templateService.Get() .AsNoTracking() // Добавлено .Where(t => t.JobId == jobId) .ToListAsync(); if (!existingTemplates.Any()) return; var job = await GetJobWithGroupAndAutoControlAsync(jobId); if (job == null) return; var currentUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet(); logger.LogDebug("JobId {JobId}: {Count} UnitId по текущему фильтру.", jobId, currentUnitIds.Count); foreach (var template in existingTemplates) { logger.LogDebug("Обработка шаблона {TemplateId} (UnitId {UnitId}).", template.Id, template.UnitId); bool unitStillInFilter = currentUnitIds.Contains(template.UnitId); var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); TemplateStatusTypeEnum targetStatus; bool targetIsActiveTemplate; bool targetIsActiveSchedule; if (unitStillInFilter) { targetStatus = TemplateStatusTypeEnum.Used; targetIsActiveTemplate = template.IsActiveTemplate; targetIsActiveSchedule = template.IsActiveSchedule; } else { targetStatus = TemplateStatusTypeEnum.Unused; targetIsActiveTemplate = DefaultUnusedTemplateState; targetIsActiveSchedule = DefaultUnusedScheduleState; expectedName = GetTemplateNameForUnused(expectedName); logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId); } bool needsUpdate = template.StatusTypeId != TemplateStatusTypeEnum.Updating && ( // 1. Статус изменился (например, Used → Unused или Unused → Used) template.StatusTypeId != targetStatus || // 2. Для Used — имя должно соответствовать шаблону (targetStatus == TemplateStatusTypeEnum.Used && template.Name != expectedName) || // 3. Флаги изменились (редко, но возможно через AutoControl изменение) template.IsActiveTemplate != targetIsActiveTemplate || template.IsActiveSchedule != targetIsActiveSchedule ); if (!needsUpdate) continue; template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); continue; } var nextRun = await GetNextRunAsync(job, template.NextRun); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = jobId, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = targetIsActiveTemplate, IsActiveSchedule = targetIsActiveSchedule, LastRun = template.LastRun, NextRun = nextRun, Index = template.Index, StatusTypeId = targetStatus, Initiator = initiator, UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() }; await SendTemplateUpdateMessage(updateRequest); } logger.LogInformation("Обновление шаблонов завершено для JobId {JobId}.", jobId); } private async Task UpdateTemplateUnitsAsync(Template template, List newUnitIds, Job job, HistoryInitiator initiator) { // Обновляем шаблон как "Updating" template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); return; } // Здесь нужно обновить UnitsInTemplate. // Это может быть сделано через TemplateUpdaterMq, если он поддерживает передачу нового списка юнитов. // Или напрямую в сервисе шаблонов, если логика обновления простая. // Пока отправим сообщение в TemplateUpdater. var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); var nextRun = await GetNextRunAsync(job, template.NextRun); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = job.Id, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, LastRun = template.LastRun, NextRun = nextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, // Предполагаем, что используется Initiator = initiator, UnitsInTemplate = newUnitIds // Передаем обновленный список юнитов }; await SendTemplateUpdateMessage(updateRequest); } private async Task CreateGroupedTemplateAsync(Guid jobId, Guid regionalUnitId, List unitIds, int index, HistoryInitiator initiator) { logger.LogInformation("Создание нового группового шаблона для Job {JobId}, регионального юнита {RegionalUnitId}, Index {Index}, с {Count} юнитами.", jobId, regionalUnitId, index, unitIds.Count); // Предполагаем, что TemplateGeneratorMq может обрабатывать UnitsInTemplate и Index var mqRequest = new TemplateGeneratorMq { JobId = jobId, UnitId = regionalUnitId, // UnitId шаблона UnitsInTemplate = unitIds, // Юниты для UnitsInTemplate Index = index, // Индекс шаблона HistoryInitiator = initiator }; var msg = JsonSerializer.Serialize(mqRequest); var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); if (!result.IsSuccess) logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, регионального юнита {RegionalUnitId}, Index {Index}.", jobId, regionalUnitId, index); } private async Task DeactivateTemplateAsync( Template template, Guid jobId, HistoryInitiator initiator) { if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) return true; // уже в обработке logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId); template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); return false; } var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = jobId, UnitId = template.UnitId, Name = GetTemplateNameForUnused(template.Name), IsActiveTemplate = DefaultUnusedTemplateState, IsActiveSchedule = DefaultUnusedScheduleState, LastRun = template.LastRun, NextRun = template.NextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Unused, Initiator = initiator, UnitsInTemplate = new List() }; await SendTemplateUpdateMessage(updateRequest); return true; } private async Task ReactivateOrRenameTemplateAsync( Template template, Job job, string expectedName, HistoryInitiator initiator) { if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) return true; bool needsUpdate = template.Name != expectedName || template.StatusTypeId != TemplateStatusTypeEnum.Used; if (!needsUpdate) return true; logger.LogInformation("Шаблон {TemplateId}: требуется обновление имени или реактивация.", template.Id); template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); return false; } var nextRun = await GetNextRunAsync(job, template.NextRun); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = job.Id, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, LastRun = template.LastRun, NextRun = nextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() }; await SendTemplateUpdateMessage(updateRequest); return true; } private async Task SendTemplateGeneratorMessageAsync( Guid jobId, Guid unitId, HistoryInitiator initiator) { logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId); var mqRequest = new TemplateGeneratorMq { JobId = jobId, UnitId = unitId, HistoryInitiator = initiator, UnitsInTemplate = new List() }; var msg = JsonSerializer.Serialize(mqRequest); var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); if (!result.IsSuccess) logger.LogError("Ошибка отправки команды создания шаблона для UnitId {UnitId}.", unitId); return result.IsSuccess; } private async Task SendTemplateUpdateMessage(TemplateUpdaterMq updateRequest) { logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}", mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId); var msg = JsonSerializer.Serialize(updateRequest); var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new[] { msg }); if (result.IsSuccess) { logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId); } else { logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.", updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName); } } // --- ИЗМЕНЕННЫЙ МЕТОД: Теперь используется как для простых, так и для групповых шаблонов --- private async Task TryReuseOneUnusedTemplateAsync( Guid jobId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync) Guid unitId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync) HistoryInitiator initiator, int maxAttempts = 3) { for (int attempt = 1; attempt <= maxAttempts; attempt++) { try { var unusedCandidates = await templateService.Get() .AsNoTracking() // Добавлено .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) .OrderBy(t => t.DateModified ?? t.DateCreated) .Take(UnusedCandidateBatchSize) .ToListAsync(); if (!unusedCandidates.Any()) { logger.LogDebug("Нет Unused-шаблонов (попытка {Attempt}).", attempt); return null; } foreach (var candidate in unusedCandidates) { var originalStatus = candidate.StatusTypeId; var originalModified = candidate.DateModified; try { candidate.StatusTypeId = TemplateStatusTypeEnum.Updating; candidate.DateModified = DateTimeOffset.UtcNow; if (await templateService.CommitAsync(initiator)) { logger.LogInformation("Успешно захвачен шаблон {TemplateId} для переиспользования (попытка {Attempt}).", candidate.Id, attempt); return candidate; // Возвращаем захваченный шаблон } // Откат при неудаче candidate.StatusTypeId = originalStatus; candidate.DateModified = originalModified; } catch (Exception ex) when ( ex is DbUpdateException || ex.InnerException?.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase) == true || ex.InnerException?.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) == true) { logger.LogWarning(ex, "Конфликт при захвате шаблона {TemplateId} (попытка {Attempt}).", candidate.Id, attempt); candidate.StatusTypeId = originalStatus; candidate.DateModified = originalModified; } } if (attempt < maxAttempts) await Task.Delay(Random.Shared.Next(5, 15) * attempt); } catch (Exception ex) { logger.LogError(ex, "Ошибка в попытке захвата (попытка {Attempt}).", attempt); if (attempt == maxAttempts) throw; } } return null; } private string GetTemplateNameForUnused(string templateName) { return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds(); } private async Task GetJobWithGroupAndAutoControlAsync(Guid jobId) { return await jobService.Get() .AsNoTracking() // Добавлено .Include(j => j.Group) .ThenInclude(j => j.GroupType) .Include(j => j.AutoControl) .FirstOrDefaultAsync(j => j.Id == jobId); } private async Task> GetExpectedUnitIdsAsync(Guid jobId) { var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); return units?.ToHashSet() ?? new HashSet(); } private async Task GetNormalizedTemplateNameAsync(Job job, Guid unitId, int? index = null) { var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id, index); return rawName.ToUpper(); } private async Task GetNextRunAsync(Job job, DateTimeOffset? currentNextRun = null) { var now = DateTimeOffset.UtcNow; if (currentNextRun.HasValue && currentNextRun.Value > now) { return currentNextRun.Value; } var referenceDate = job.Group?.ReferenceDate ?? now; return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); } } }