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; 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 ) { 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; } 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) .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); // 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); // 8. Найти подходящий Job для подгруппы (логика без изменений) 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("Для подгруппы {Index} связанного юнита {RegionalUnitId} не найден подходящий Job, используем maxJob {MaxJobId}.", i, relationshipUnitId, maxJob.Id); } else { logger.LogDebug("Для подгруппы {Index} связанного юнита {RegionalUnitId} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", i, relationshipUnitId, targetJob.Id, targetJob.MaxValueRelationships); } // 9. Загрузить существующие шаблоны для targetJob, связанные с regionalUnitId var existingTemplatesForRelationship = await templateService.Get() .AsNoTracking() .Include(t => t.UnitsInTemplate) .Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i) .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); } else { logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i); await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, targetJob, 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 = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) .FirstOrDefault(); if (targetJobForExpectedKey == null) { targetJobForExpectedKey = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) .OrderBy(j => j.MaxValueRelationships!.Value) .FirstOrDefault(); } if (targetJobForExpectedKey == null) targetJobForExpectedKey = maxJob; // maxJob уже проверен на null 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, initiator); } } logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); } public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); return; } // --- Вспомогательные методы --- 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, 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 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(); } // Изменённая сигнатура: добавлен templateUnitIds 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, 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); } }