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.Cache.Models; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.DomainServices.UnitFilterService; using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Settings; namespace PARR.TemplateMatcher.Services.Implementations; internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { private const bool DefaultUsedTemplateState = false; private const bool DefaultUsedScheduleState = false; private readonly ILogger logger; private readonly IUnitFilterService unitFilterService; 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 IUnitRegionalEkPtkGroupService regionalEkPtkGroupService; private readonly IUnitFieldService unitFieldService; private readonly ITemplateDeactivator templateDeactivator; private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly IMatchingStatusService matchingStatusService; private readonly IShortcodesService shortcodesService; //private readonly INextRunService nextRunService; public GroupedTemplateSynchronizer( ILogger logger, IUnitFilterService unitFilterService, IUnitInValueService unitInValueService, IUnitService unitService, MqSettings mqSettings, IMqService mqService, ITemplateService templateService, IJobGroupService jobGroupService, ITemplateReuser templateReuser, IUnitRegionalEkPtkGroupService regionalEkPtkGroupService, IUnitFieldService unitFieldService, ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, ITemplateUpdaterMqSender templateUpdaterMqSender, IMatchingStatusService matchingStatusService, IShortcodesService shortcodesService //INextRunService nextRunService ) { this.logger = logger; this.unitFilterService = unitFilterService; this.unitInValueService = unitInValueService; this.unitService = unitService; this.mqSettings = mqSettings; this.mqService = mqService; this.templateService = templateService; this.jobGroupService = jobGroupService; this.templateReuser = templateReuser; this.regionalEkPtkGroupService = regionalEkPtkGroupService; this.unitFieldService = unitFieldService; this.templateDeactivator = templateDeactivator; this.templateNameNormalizer = templateNameNormalizer; this.templateUpdaterMqSender = templateUpdaterMqSender; this.matchingStatusService = matchingStatusService; this.shortcodesService = shortcodesService; //this.nextRunService = nextRunService; } 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); // === Проверка: уже запущена? === var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); if (existingStatus.DetailsJobGroups?.Any() == true) { logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId); return; } // === Устанавливаем статус "в процессе" === var initialStatus = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = "Начало синхронизации" }; await matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup, new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) }, TimeSpan.FromMinutes(35) ); try { // 1. Получить JobGroup и связанные Job'ы var jobGroup = await jobGroupService.Get() .AsNoTracking() .AsSingleQuery() .Include(jg => jg.GroupType) .Include(jg => jg.Jobs) .ThenInclude(j => j.AutoControl) .Include(jg => jg.Jobs) .ThenInclude(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) .ThenInclude(rf => rf.UnitField) // Подгрузим поля для фильтрации .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); await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст"); 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); await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships"); return; } logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships); // 3. Использовать unitFilterService для получения отфильтрованных юнитов с их связями // Это включает в себя все фильтры: UnitFilter, FieldFilter, RelationshipFilter, UmbrellaFilter logger.LogDebug("Получение отфильтрованных юнитов с их связями через UnitFilterService для Job {JobId}.", maxJob.Id); var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id); if (unitFilterResults == null || !unitFilterResults.Any()) { logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями"); return; } logger.LogDebug("Получено {Count} юнитов с подходящими связями через UnitFilterService.", unitFilterResults.Count()); // --- ФИЛЬТРАЦИЯ unitFilterResults (dto.Id) --- // 4. Фильтрация unitFilterResults по GroupingUnitFieldId (проверяем dto.Id) if (!jobGroup.GroupingUnitFieldId.HasValue) { logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Отсутствует GroupingUnitFieldId"); return; } var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по GroupingUnitFieldId (FieldId={FieldId}).", groupingFieldId); // Загрузим значения поля GroupingUnitFieldId для всех Id из unitFilterResults var allUnitFilterResultIds = unitFilterResults.Select(dto => dto.Id).ToList(); var groupingUnitValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitFilterResultIds, new HashSet { groupingFieldId }); // Найдем Id юнитов, у которых есть значение в GroupingUnitFieldId var validUnitFilterResultIds = groupingUnitValues .Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value)) .Select(uv => uv.UnitId) .ToHashSet(); // Оставляем только те UnitFilterResultDto, чей Id проходит фильтр var filteredUnitFilterResultsByGrouping = unitFilterResults .Where(dto => validUnitFilterResultIds.Contains(dto.Id)) .ToList(); logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} UnitFilterResultDto.", filteredUnitFilterResultsByGrouping.Count); if (!filteredUnitFilterResultsByGrouping.Any()) { logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId"); return; } // 5. Фильтрация unitFilterResults по РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК (проверяем dto.Id) var workGroupFieldId = await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"); logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по полю 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' (FieldId={FieldId}).", workGroupFieldId); // Загрузим значения поля РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК для Id из filteredUnitFilterResultsByGrouping var allFilteredUnitFilterResultIds = filteredUnitFilterResultsByGrouping.Select(dto => dto.Id).ToList(); var workGroupValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allFilteredUnitFilterResultIds, new HashSet { workGroupFieldId }); // Получим разрешенные значения из regionalEkPtkGroupService var allowedValueIds = regionalEkPtkGroupService.Get() .Select(g => g.FieldValueId) .ToHashSet(); logger.LogDebug("Найдено {Count} разрешенных значений для поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", allowedValueIds.Count); // Найдем Id юнитов, у которых значение в РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК разрешено var validUnitFilterResultIdsForWorkGroup = workGroupValues .Where(uv => uv.Value != null && allowedValueIds.Contains(uv.Value.Id)) .Select(uv => uv.UnitId) .ToHashSet(); // Оставляем только те UnitFilterResultDto, чей Id проходит фильтр var finalFilteredUnitFilterResults = filteredUnitFilterResultsByGrouping .Where(dto => validUnitFilterResultIdsForWorkGroup.Contains(dto.Id)) .ToList(); logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} UnitFilterResultDto.", finalFilteredUnitFilterResults.Count); if (!finalFilteredUnitFilterResults.Any()) { logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'"); return; } // --- ПОСТРОЕНИЕ ОБРАТНОГО ОТОБРАЖЕНИЯ (после фильтрации) --- logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации)."); var reverseMapping = new Dictionary>(); foreach (var dto in finalFilteredUnitFilterResults) { List relatedUnitIds; if (maxJob.IsParentRelationships == true) { // dto.Id - это ParentUnitId, связанные - ChildUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate // relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); // <-- Исправлено } else { // dto.Id - это ChildUnitId, связанные - ParentUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate // relatedUnitIds - это ParentUnitIds, которые станут UnitId шаблона relatedUnitIds = dto.Parents.Select(p => p.UnitId).ToList(); } // dto.Id - это юнит, который прошел фильтры, он будет в UnitsInTemplate var unitInTemplateId = dto.Id; foreach (var relatedUnitId in relatedUnitIds) { // relatedUnitId уже прошел все фильтры, т.к. dto.Id (его связанный юнит) прошел фильтры if (!reverseMapping.ContainsKey(relatedUnitId)) { reverseMapping[relatedUnitId] = new List(); } reverseMapping[relatedUnitId].Add(unitInTemplateId); } } logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count); if (!reverseMapping.Any()) { logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения"); return; } // 6. Внутренняя группировка по ОТВЕТСТВЕННЫЙ_ЗА_ЭК / РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК (для юнитов в UnitsInTemplate) logger.LogDebug("Внутренняя группировка по полю (IsGroupByResponsible={IsGroupByResponsible}).", jobGroup.IsGroupByResponsible); var groupingFieldIdForInnerGrouping = jobGroup.IsGroupByResponsible == true ? await GetFieldIdByAihitNameAsync("ОТВЕТСТВЕННЫЙ_ЗА_ЭК") : await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"); // Загрузим значения поля для *всех* юнитов, которые могут быть в UnitsInTemplate // Это все юниты из всех списков в reverseMapping.Values var allUnitsInTemplate = reverseMapping.Values.SelectMany(list => list).Distinct().ToList(); var innerGroupingValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitsInTemplate, new HashSet { groupingFieldIdForInnerGrouping }); // Создадим маппинг UnitId (из UnitsInTemplate) -> значение поля для внутренней группировки var unitInTemplateToInnerGroupingValueMap = innerGroupingValues .Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value)) .ToDictionary(uv => uv.UnitId, uv => uv.Value.Value); // 7. Основной цикл обработки: итерируемся по potentialUnitIds (UnitId шаблонов) foreach (var kvpOuter in reverseMapping) { var potentialUnitId = kvpOuter.Key; var unitsInTemplateForThisPotentialUnitId = kvpOuter.Value; logger.LogDebug("Обработка потенциального шаблона для UnitId {PotentialUnitId} с {Count} юнитами в UnitsInTemplate до внутренней группировки.", potentialUnitId, unitsInTemplateForThisPotentialUnitId.Count); // --- ВНУТРЕННЯЯ ГРУППИРОВКА --- // Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId .GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных")) // Используем маппинг .ToList(); logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate.", potentialUnitId, innerGroupedUnitsInTemplate.Count); // 8. Цикл по внутренним группам UnitsInTemplate foreach (var innerGroup in innerGroupedUnitsInTemplate) { var groupingValueName = innerGroup.Key; // Значение поля var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Список юнитов (UnitId), связанных с potentialUnitId и имеющих одно и то же значение поля logger.LogDebug("Обработка внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId} с {Count} юнитами.", groupingValueName, potentialUnitId, unitsInTemplateInInnerGroup.Count); // Разбиваем юниты из *этой* внутренней группы на подгруппы по maxJob.MaxValueRelationships int maxValueForSplitting = maxJob.MaxValueRelationships!.Value; var unitsInTemplateSubGroups = unitsInTemplateInInnerGroup .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .GroupBy(x => x.groupIndex) .Select(g => g.Select(x => x.id).ToList()) .ToList(); logger.LogDebug("Внутренняя группа '{GroupingValue}' для UnitId {PotentialUnitId}: разбит на {GroupCount} подгрупп UnitsInTemplate.", groupingValueName, potentialUnitId, unitsInTemplateSubGroups.Count); // 9. Цикл по подгруппам UnitsInTemplate для создания/обновления шаблонов for (int i = 1; i <= unitsInTemplateSubGroups.Count; i++) // Индекс начинается с 1 { var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции var subGroupSize = unitsInTemplateSubGroup.Count; logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", i, groupingValueName, potentialUnitId, subGroupSize); Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); var existingTemplatesForRelationship = await templateService.Get() .AsNoTracking() .Include(t => t.Unit) .Include(t => t.Job) .ThenInclude(t => t!.Tnk) .Include(t => t.Job) .ThenInclude(t => t!.Group) .ThenInclude(t => t!.GroupType) .Include(t => t.UnitsInTemplate) .ThenInclude(uit => uit.Unit) .Where(t => t.JobId == targetJob.Id && t.UnitId == potentialUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToListAsync(); var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault(); if (existingTemplateForSubGroup != null) { // === 1. Получаем текущие и новые ID юнитов === var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); var proposedUnitIds = unitsInTemplateSubGroup.ToList(); // === 2. Сравниваем детерминированно с сортировкой по имени === var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList(); var unitNamesForSort = await unitService.Get() .AsNoTracking() .Where(u => allUnitIdsForSort.Contains(u.Id)) .ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString()); var sortedCurrentUnitIds = currentUnitIds .OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString())) .ToList(); var sortedProposedUnitIds = proposedUnitIds .OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString())) .ToList(); bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds); if (unitsAreEqual) { logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id); existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList(); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup); if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id); var updateRequest = new TemplateUpdaterMq { TemplateId = existingTemplateForSubGroup.Id, JobId = targetJob.Id, UnitId = potentialUnitId, Name = expectedName, IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate, IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule, IsNew = false, Index = i, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = sortedProposedUnitIds }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } else { logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id); } } else { logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов или их порядка (после сортировки).", existingTemplateForSubGroup.Id); var newTargetJob = SelectTargetJob(jobsInGroup, unitsInTemplateSubGroup.Count, maxJob); // Размер - из подмножества if (newTargetJob.Id != existingTemplateForSubGroup.JobId) { logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id); } await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, sortedProposedUnitIds, newTargetJob, initiator, i); } } else { var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator); if (reusableTemplate != null) { logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, potentialUnitId, i); var tempTemplateForName = new Template { Id = reusableTemplate.Id, Name = reusableTemplate.Name, JobId = targetJob.Id, UnitId = potentialUnitId, Index = i, Job = targetJob, Unit = reusableTemplate.Unit, UnitsInTemplate = unitsInTemplateSubGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList() }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); //var existingTemplateByName = await templateService.Get().AsNoTracking() // .FirstOrDefaultAsync(t => EF.Functions.ILike(t.Name, expectedName)); //if (existingTemplateByName != null) // logger.LogInformation("Переиспользован шаблон"); var updateRequest = new TemplateUpdaterMq { TemplateId = reusableTemplate.Id, JobId = targetJob.Id, UnitId = potentialUnitId, Name = expectedName, IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, IsNew = true, Index = i, UnitsInTemplate = unitsInTemplateSubGroup }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } else { logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}, с {Count} юнитами.", targetJob.Id, potentialUnitId, i, unitsInTemplateSubGroup.Count); await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, i, initiator); } } } } } // === Деактивация === // Собираем ожидаемые ключи шаблонов на основе внутренне сгруппированных результатов var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); foreach (var kvpOuter in reverseMapping) { var potentialUnitId = kvpOuter.Key; var unitsInTemplateForThisPotentialUnitId = kvpOuter.Value; // Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId .GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных")) .ToList(); foreach (var innerGroup in innerGroupedUnitsInTemplate) { var groupingValueName = innerGroup.Key; var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Разбиваем юниты из *этой* внутренней группы на подгруппы по maxJob.MaxValueRelationships int maxValueForSplitting = maxJob.MaxValueRelationships!.Value; var unitsInTemplateSubGroups = unitsInTemplateInInnerGroup .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .GroupBy(x => x.groupIndex) .Select(g => g.Select(x => x.id).ToList()) .ToList(); // Для каждой подгруппы UnitsInTemplate for (int i = 1; i <= unitsInTemplateSubGroups.Count; i++) // Индекс начинается с 1 { var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции var subGroupSize = unitsInTemplateSubGroup.Count; Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); expectedTemplateKeys.Add((targetJobForExpectedKey.Id, potentialUnitId, i)); // potentialUnitId - это UnitId шаблона } } } // Получаем ВСЕ шаблоны для JobGroup (не только для текущих potentialUnitIds) var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); var allExistingTemplatesInGroup = await templateService.Get() .AsNoTracking() .Include(t => t.Unit) .Include(t => t.UnitsInTemplate) .Where(t => allJobIdsInGroup.Contains(t.JobId) && t.StatusTypeId == TemplateStatusTypeEnum.Used && t.Job!.GroupId == jobGroupId) .ToListAsync(); foreach (var existingTemplate in allExistingTemplatesInGroup) { var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); if (!expectedTemplateKeys.Contains(key)) { logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index); await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator); } } // === Успешное завершение === await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно"); await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); } catch (Exception ex) { logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}"); throw; } } public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); return; } 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; 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, int newIndex) { template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id); return; } var tempTemplateForName = new Template { Id = template.Id, Name = template.Name, JobId = targetJob.Id, UnitId = template.UnitId, Index = newIndex, Job = targetJob, Unit = template.Unit, UnitsInTemplate = newUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList() }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); //var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false); 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, IsNew = false, Index = newIndex, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = newUnitIds }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } private async Task CreateGroupedTemplateAsync(Guid jobId, Guid relationshipUnitId, List unitIds, int index, HistoryInitiator initiator) { logger.LogInformation("Создание нового группового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", jobId, relationshipUnitId, index, unitIds.Count); var mqRequest = new TemplateGeneratorMq { JobId = jobId, UnitId = relationshipUnitId, UnitsInTemplate = unitIds, Index = index, HistoryInitiator = initiator }; var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List { mqRequest }); if (!result.IsSuccess) logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}.", jobId, relationshipUnitId, index); } private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment) { var status = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = comment }; await matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup, new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) }, TimeSpan.FromMinutes(30) ); } #region Вспомогательные методы private async Task GetFieldIdByAihitNameAsync(string fieldName) { var field = await unitFieldService.GetByAihitNameAsync(fieldName); if (field == null) { logger.LogError("Поле '{FieldName}' не найдено в справочнике полей.", fieldName); throw new InvalidOperationException($"Поле '{fieldName}' не найдено в справочнике полей."); } return field.Id; } #endregion }