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.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.DomainServices.Shortcodes.Models; 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.Services.Interfaces; using PARR.TemplateMatcher.Settings; using System.Text.Json; namespace PARR.TemplateMatcher.Services.Implemetaions; internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { 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 IJobGroupService jobGroupService; private readonly ITemplateReuser templateReuser; private readonly IShortcodesService shortcodesService; private readonly IEsppScheduleTransformService esppScheduleTransformService; private readonly IUnitRegionalEkPtkGroupService regionalEkPtkGroupService; private readonly IUnitFieldService unitFieldService; private readonly IJobService jobService; public GroupedTemplateSynchronizer( ILogger logger, IUnitFilterService unitFilterService, IUnitInUnitService unitInUnitService, IUnitInValueService unitInValueService, IUnitService unitService, MqSettings mqSettings, IMqService mqService, ITemplateService templateService, IJobGroupService jobGroupService, ITemplateReuser templateReuser, IShortcodesService shortcodesService, IEsppScheduleTransformService esppScheduleTransformService, IUnitRegionalEkPtkGroupService regionalEkPtkGroupService, IUnitFieldService unitFieldService, IJobService jobService ) { 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.jobGroupService = jobGroupService; this.templateReuser = templateReuser; this.shortcodesService = shortcodesService; this.esppScheduleTransformService = esppScheduleTransformService; this.regionalEkPtkGroupService = regionalEkPtkGroupService; this.unitFieldService = unitFieldService; this.jobService = jobService; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); } public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); // 1. Получить JobGroup и связанные Job'ы var jobGroup = await jobGroupService.Get() .AsNoTracking() .Include(jg => jg.GroupType) .Include(jg => jg.Jobs) .ThenInclude(j => j.AutoControl) .Include(jg => jg.Jobs) .ThenInclude(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) .Include(jg => jg.Jobs) .ThenInclude(jg => jg.Tnk) .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(); // --- Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"); if (workGroupField == null) { logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено в справочнике полей. Синхронизация прервана."); return; } var workGroupFieldId = workGroupField.Id; var regionalGroupValueIds = regionalEkPtkGroupService.Get() .Select(g => g.FieldValueId) .ToList(); // Получаем список UnitFieldValue.Id logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", regionalGroupValueIds.Count); // Загружаем заранее подготовленный Job (например, для неиспользуемых шаблонов) var unusedJobId = Guid.Parse("8f85a91c-a223-4686-bb69-1f0ee73624f2"); var unusedJob = await jobService.Get() .AsNoTracking() .Include(j => j.Tnk) .Include(j => j.Group) .ThenInclude(g => g.GroupType) .FirstOrDefaultAsync(j => j.Id == unusedJobId); // 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); return; } // Проверяем, что UnitFilters и RelationshipFilters загружены (если используется для выбора targetJob) 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); return; } // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId (дополнительный фильтр) if (!jobGroup.GroupingUnitFieldId.HasValue) { logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); return; } var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; // Загрузить UnitValues для юнитов из expectedUnitIds, чтобы проверить GroupingUnitFieldId var expectedUnitsWithGroupingField = await unitService.Get() .AsNoTracking() .AsSplitQuery() // Для Unit -> UnitValues .Include(u => u.UnitValues) .ThenInclude(uv => uv.Value) .Where(u => expectedUnitIds.Contains(u.Id)) .ToListAsync(); var unitIdsWithValidGroupingFieldSet = expectedUnitsWithGroupingField .Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))) .Select(u => u.Id) .ToHashSet(); logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); if (!unitIdsWithValidGroupingFieldSet.Any()) { logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); return; } // --- Дополнительная фильтрация по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" --- var unitIdsWithValidWorkGroupFieldSet = expectedUnitsWithGroupingField .Where(u => unitIdsWithValidGroupingFieldSet.Contains(u.Id) && // Убедимся, что юнит уже прошёл фильтр по GroupingFieldId u.UnitValues.Any(uv => uv.FieldId == workGroupFieldId && // Поле "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" uv.Value != null && // Значение существует regionalGroupValueIds.Contains(uv.Value.Id) // Значение в списке разрешённых )) .Select(u => u.Id) // Выбираем Id юнита .ToHashSet(); // И снова в HashSet logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count); if (!unitIdsWithValidWorkGroupFieldSet.Any()) { logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); return; } // Обновляем список юнитов, прошедших оба фильтра unitIdsWithValidGroupingFieldSet = unitIdsWithValidWorkGroupFieldSet; // 5. Получить RelationshipFilters из maxJob var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List(); // --- Найти и отфильтровать UnitInUnit связи --- logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию по GroupingUnitFieldId и 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'."); var potentialUnitInUnitLinks = await unitInUnitService.Get() .AsNoTracking() .Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId)) .ToListAsync(); logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count); // Загрузить 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()); // Применить фильтры к связям logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count); 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); } } logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count); // --- Сгруппировать юниты из unitIdsWithValidGroupingFieldSet по связанному юниту --- var groupedRelationships = new Dictionary>(); foreach (var link in filteredUnitInUnitLinks) { var parentUnitId = link.ParentUnitId; var childUnitId = link.ChildUnitId; if (unitIdsWithValidGroupingFieldSet.Contains(parentUnitId)) { if (!groupedRelationships.ContainsKey(childUnitId)) { groupedRelationships[childUnitId] = new List(); } groupedRelationships[childUnitId].Add(parentUnitId); } else if (unitIdsWithValidGroupingFieldSet.Contains(childUnitId)) { if (!groupedRelationships.ContainsKey(parentUnitId)) { groupedRelationships[parentUnitId] = new List(); } groupedRelationships[parentUnitId].Add(childUnitId); } } logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count); // --- Разрешение конфликта - один юнит из unitIdsWithValidGroupingFieldSet только в одном списке значений --- var unitToKeys = new Dictionary>(); // Карта: юнит из списка -> список ключей, где он встречается foreach (var kvp in groupedRelationships) { var key = kvp.Key; var units = kvp.Value; foreach (var unitId in units) { if (!unitToKeys.ContainsKey(unitId)) { unitToKeys[unitId] = new List(); } unitToKeys[unitId].Add(key); } } // Найти юниты, которые находятся в нескольких списках var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList(); foreach (var conflictedUnitEntry in conflictedUnits) { var unitId = conflictedUnitEntry.Key; var keysForUnit = conflictedUnitEntry.Value; Guid bestKey = keysForUnit[0]; // Инициализируем первым ключом int maxCount = groupedRelationships[bestKey].Count; for (int i = 1; i < keysForUnit.Count; i++) { var currentKey = keysForUnit[i]; var currentCount = groupedRelationships[currentKey].Count; if (currentCount > maxCount) { bestKey = currentKey; maxCount = currentCount; } } // Удалить юнит из списков всех ключей, кроме bestKey foreach (var key in keysForUnit) { if (key != bestKey) { groupedRelationships[key].Remove(unitId); logger.LogDebug("Юнит {UnitId} перемещён из группы {OldKey} в группу {BestKey} (по кол-ву).", unitId, key, bestKey); } } } // Удаляем ключи, у которых список стал пустым после разрешения конфликтов var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList(); foreach (var key in keysToRemove) { groupedRelationships.Remove(key); logger.LogDebug("Ключ {Key} удалён, так как его список юнитов стал пустым после разрешения конфликтов.", key); } logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count); // 7. Разбить каждую группу и сопоставить с Job foreach (var kvp in groupedRelationships) { var relationshipUnitId = kvp.Key; // Связанный юнит (не из unitIdsWithValidGroupingFieldSet) var childUnitIds = kvp.Value; // Юниты из unitIdsWithValidGroupingFieldSet, связанные с regionalUnitId logger.LogDebug("Обработка связанного юнита {RegionalUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count); // Применяем ограничение MaxValueRelationships maxJob if (!maxJob.MaxValueRelationships.HasValue) { logger.LogWarning("Job {JobId} не заполнено MaxValueRelationships.", maxJob.Id); return; } 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(); logger.LogDebug("Связанный юнит {RegionalUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count); // Для каждой подгруппы: for (int i = 0; i < childUnitGroups.Count; i++) { var subGroup = childUnitGroups[i]; var subGroupSize = subGroup.Count; logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RegionalUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize); // Выбираем targetJob для подгруппы Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); // 9. Загрузить существующие шаблоны для targetJob, связанные с relationshipUnitId var existingTemplatesForRelationship = await templateService.Get() .AsNoTracking() .Include(t => t.UnitsInTemplate) .Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToListAsync(); var existingTemplateForSubGroup = existingTemplatesForRelationship.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, relationshipUnitId, i); // Проверить, изменилось ли имя шаблона (например, из-за %МАКС:...% или %ТНК-КРАТКО%) var expectedName = await GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup); if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.Name, expectedName); var nextRun = await GetNextRunAsync(targetJob, existingTemplateForSubGroup.NextRun); var updateRequest = new TemplateUpdaterMq { TemplateId = existingTemplateForSubGroup.Id, JobId = targetJob.Id, UnitId = relationshipUnitId, Name = expectedName, IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate, IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule, LastRun = existingTemplateForSubGroup.LastRun, NextRun = nextRun, Index = i , StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = subGroup }; await SendTemplateUpdateMessage(updateRequest); } else { logger.LogDebug("Шаблон {TemplateId} полностью актуален (юниты и имя).", existingTemplateForSubGroup.Id); } } else { logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i); // Пересчитать targetJob для нового размера subGroup var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); // Если Job изменился — нужно обновить шаблон с новым JobId if (newTargetJob.Id != existingTemplateForSubGroup.JobId) { logger.LogDebug("Job для шаблона {TemplateId} изменился: старый {OldJobId}, новый {NewJobId}.", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.JobId, newTargetJob.Id); } await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, newTargetJob, initiator); } } else { var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, relationshipUnitId, initiator); if (reusableTemplate != null) { logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i); var expectedName = await GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup); var nextRun = await GetNextRunAsync(targetJob); var updateRequest = new TemplateUpdaterMq { TemplateId = reusableTemplate.Id, JobId = targetJob.Id, UnitId = relationshipUnitId, Name = expectedName, IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, NextRun = nextRun, Index = i, UnitsInTemplate = subGroup }; await SendTemplateUpdateMessage(updateRequest); } else { logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count); await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator); } } } } // 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе (логика без изменений) var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); foreach (var kvp in groupedRelationships) { 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; Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); expectedTemplateKeys.Add((targetJobForExpectedKey.Id, regionalUnitId, i)); } } var allRegionalUnitIds = groupedRelationships.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, unusedJob, initiator); } } logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); } public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); return; } // --- Вспомогательные методы --- /// /// Выбирает Job, соответствующий размеру подгруппы /// /// Список Job'ов в группе /// Размер подгруппы /// Job с максимальным MaxValueRelationships /// Найденный Job или maxJob, если не найден подходящий private Job SelectTargetJob(List jobsInGroup, int subGroupSize, Job maxJob) { Job? 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; // maxJob уже проверен на null ранее logger.LogDebug("Для подгруппы размером {Size} не найден подходящий Job, используем maxJob {MaxJobId}.", subGroupSize, maxJob.Id); } else { logger.LogDebug("Для подгруппы размером {Size} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", subGroupSize, targetJob.Id, targetJob.MaxValueRelationships); } return targetJob; } private async Task UpdateTemplateUnitsAsync(Template template, List newUnitIds, Job targetJob, HistoryInitiator initiator) { template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); return; } var expectedName = await GetNormalizedTemplateNameAsync(targetJob, template.UnitId, template.Index, newUnitIds); var nextRun = await GetNextRunAsync(targetJob, template.NextRun); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = targetJob.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); 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, Job unusedJob, HistoryInitiator initiator) { if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) return true; if (template.StatusTypeId == TemplateStatusTypeEnum.Unused) { logger.LogDebug("Шаблон {TemplateId} уже неактивен (Unused), пропускаем деактивацию.", template.Id); 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; } if (unusedJob == null) { logger.LogError("Job для деактивированных шаблонов не найден."); return false; } // Вычисляем имя шаблона с новым Job var expectedName = await GetNormalizedTemplateNameAsync(unusedJob, template.UnitId); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = unusedJob.Id, UnitId = template.UnitId, Name = GetTemplateNameForUnused(expectedName), IsActiveTemplate = DefaultUnusedTemplateState, IsActiveSchedule = DefaultUnusedScheduleState, LastRun = template.LastRun, NextRun = template.NextRun, Index = null, StatusTypeId = TemplateStatusTypeEnum.Unused, Initiator = initiator, UnitsInTemplate = new List() }; await SendTemplateUpdateMessage(updateRequest); return true; } 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 string GetTemplateNameForUnused(string templateName) { return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds(); } private async Task GetNormalizedTemplateNameAsync(Job targetJob, Guid unitId, int? index = null, List? templateUnitIds = null) { // Подготовка объекта TemplateForShortcodes для передачи в ShortcodesService var templateForShortcodes = new TemplateForShortcodes { Id = Guid.Empty, // Не используется в подстановке, но нужен для структуры Index = index+1, JobId = targetJob.Id, UnitId = unitId, Job = new JobForShortcodes { Group = targetJob.Group != null ? new JobGroupForShortcodes { Id = targetJob.Group.Id, GroupingUnitFieldId = targetJob.Group.GroupingUnitFieldId, GroupType = targetJob.Group.GroupType != null ? new JobGroupTypeForShortcodes { Code = targetJob.Group.GroupType.Code } : null, GroupName = targetJob.Group.GroupName } : null, Tnk = targetJob.Tnk != null ? new TnkForShortcodes { Name = targetJob.Tnk.Name, ShortName = targetJob.Tnk.ShortName ?? "" } : null, WorkName = targetJob.WorkName, Name = targetJob.Name }, // Преобразование List в List UnitsInTemplate = templateUnitIds?.Select(id => new UnitInTemplateForShortcodes { UnitId = id }).ToList() ?? new List() }; var rawName = await shortcodesService.ApplyShortcodesAsync(targetJob.TemplateNameMask, templateForShortcodes); return rawName.ToUpper(); } private async Task GetNextRunAsync(Job targetJob, DateTimeOffset? currentNextRun = null) { var now = DateTimeOffset.UtcNow; if (currentNextRun.HasValue && currentNextRun.Value > now) { return currentNextRun.Value; } var referenceDate = targetJob.Group?.ReferenceDate ?? now; return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate); } }