diff --git a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs index faae576b..6faa91ec 100644 --- a/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs @@ -6,7 +6,6 @@ using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Services.MatchingStatusService; using PARR.Core.Services.UnitFilterService; -using PARR.Core.Services.UnitFilterService.Models; using PARR.Domain.Cache.Models; using PARR.Domain.Common.Rabbit.Messages; using PARR.Domain.Entities; @@ -38,7 +37,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly IMatchingStatusService matchingStatusService; - + private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper; public GroupedTemplateSynchronizer( ILogger logger, @@ -55,7 +54,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, ITemplateUpdaterMqSender templateUpdaterMqSender, - IMatchingStatusService matchingStatusService + IMatchingStatusService matchingStatusService, + IUnitInTemplateConflictMapper unitInTemplateConflictMapper ) { this.logger = logger; @@ -73,6 +73,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer this.templateNameNormalizer = templateNameNormalizer; this.templateUpdaterMqSender = templateUpdaterMqSender; this.matchingStatusService = matchingStatusService; + this.unitInTemplateConflictMapper = unitInTemplateConflictMapper; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) @@ -236,16 +237,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer } logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации)."); - var reverseMapping = await BuildReverseMappingAsync(finalFilteredUnitFilterResults, maxJob); - - logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count); - - if (!reverseMapping.Any()) - { - logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); - await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения"); - return; - } + var reverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnitFilterResults, maxJob); logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count); @@ -620,148 +612,5 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer return field.Id; } - - /// - /// Строит обратное отображение: UnitId шаблона -> [UnitsInTemplate] - /// Решает конфликты, когда юнит из UnitsInTemplate может быть связан с несколькими UnitId шаблона. - /// - /// Результаты фильтрации, содержащие связи. - /// Job, используемый для определения направления связей (IsParentRelationships). - /// Словарь, где ключ - это UnitId шаблона, а значение - список юнитов, входящих в него (UnitsInTemplate). - private async Task>> BuildReverseMappingAsync(IEnumerable unitFilterResults, Job maxJob) - { - logger.LogDebug("Разрешение конфликта: определение, для какого UnitId выбрать каждый юнит из UnitsInTemplate."); - - // 1. Собираем все потенциальные пары (relatedUnitId, unitInTemplateId) - var potentialAssignments = new Dictionary>(); // relatedUnitId -> [unitInTemplateId, ...] - var allPotentialRelatedUnitIds = new HashSet(); - var allUnitInTemplateIds = new HashSet(); - - foreach (var dto in unitFilterResults) - { - List relatedUnitIds; - if (maxJob.IsParentRelationships == true) - { - relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); // relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона - } - else - { - relatedUnitIds = dto.Parents.Select(p => p.UnitId).ToList(); // relatedUnitIds - это ParentUnitIds, которые станут UnitId шаблона - } - - var unitInTemplateId = dto.Id; // dto.Id - это юнит, который прошел фильтры, он будет в UnitsInTemplate - - allUnitInTemplateIds.Add(unitInTemplateId); - allPotentialRelatedUnitIds.UnionWith(relatedUnitIds); // Собираем все unique relatedUnitId - - foreach (var relatedUnitId in relatedUnitIds) - { - if (!potentialAssignments.ContainsKey(relatedUnitId)) - { - potentialAssignments[relatedUnitId] = new List(); - } - potentialAssignments[relatedUnitId].Add(unitInTemplateId); - } - } - - // 2. Загрузим имена всех potentialRelatedUnitIds для сортировки по алфавиту при равенстве связей - var relatedUnitNames = await unitService.Get() - .AsNoTracking() - .Where(u => allPotentialRelatedUnitIds.Contains(u.Id)) - .ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString()); - - // 3. Для каждого unitInTemplateId, найти лучший relatedUnitId - var unitInTemplateToBestRelatedUnit = new Dictionary(); // unitInTemplateId -> bestRelatedUnitId - - foreach (var unitInTemplateId in allUnitInTemplateIds) - { - var candidates = potentialAssignments - .Where(kvp => kvp.Value.Contains(unitInTemplateId)) - .Select(kvp => kvp.Key) - .ToList(); - - if (candidates.Count == 1) - { - // Только один кандидат, просто назначаем - unitInTemplateToBestRelatedUnit[unitInTemplateId] = candidates[0]; - } - else if (candidates.Count > 1) - { - // Несколько кандидатов, применяем правила: 1. Больше связей -> лучше. 2. По алфавиту. - Guid bestCandidate = candidates[0]; // Инициализируем первым кандидатом - - // Загрузим количество связей для каждого кандидата - // Количество связей - это общее число юнитов (в Parents или Children) в UnitFilterResultDto, связанном с *этим* relatedUnitId - var candidateRelationshipCounts = new Dictionary(); - foreach (var candidateId in candidates) - { - // Найдем все dto, которые привели к этому candidateId - // Это dto.Id, у которых candidateId был в Parents (если IsParentRelationships) или в Children (если !IsParentRelationships) - var relevantDtos = unitFilterResults.Where(dto => - { - if (maxJob.IsParentRelationships == true) - { - return dto.Children.Any(c => c.UnitId == candidateId); - } - else - { - return dto.Parents.Any(p => p.UnitId == candidateId); - } - }).ToList(); - - // Общее количество связей для этого candidateId - это сумма связей (Parents.Count или Children.Count) из *всех* relevantDtos - int totalRelationships = 0; - foreach (var relevantDto in relevantDtos) - { - if (maxJob.IsParentRelationships == true) - { - totalRelationships += relevantDto.Children.Count; - } - else - { - totalRelationships += relevantDto.Parents.Count; - } - } - - candidateRelationshipCounts[candidateId] = totalRelationships; - } - - // Применяем правило 1: больше связей -> лучше - int bestCount = candidateRelationshipCounts[bestCandidate]; - foreach (var candidateId in candidates.Skip(1)) - { - int candidateCount = candidateRelationshipCounts[candidateId]; - if (candidateCount > bestCount || - (candidateCount == bestCount && string.Compare(relatedUnitNames.GetValueOrDefault(candidateId, candidateId.ToString()), relatedUnitNames.GetValueOrDefault(bestCandidate, bestCandidate.ToString()), StringComparison.OrdinalIgnoreCase) < 0)) - { - bestCandidate = candidateId; - bestCount = candidateCount; - } - } - - unitInTemplateToBestRelatedUnit[unitInTemplateId] = bestCandidate; - } - // else: если candidates.Count == 0 (что маловероятно, если dto.Id гарантированно связан), то unitInTemplateId не будет в unitInTemplateToBestRelatedUnit - } - - logger.LogDebug("Разрешение конфликта завершено. Найдено {Count} однозначных назначений.", unitInTemplateToBestRelatedUnit.Count); - - // 4. Построение reverseMapping на основе решённых конфликтов - var reverseMapping = new Dictionary>(); - foreach (var assignmentKvp in unitInTemplateToBestRelatedUnit) - { - var unitInTemplateId = assignmentKvp.Key; - var bestRelatedUnitId = assignmentKvp.Value; - - if (!reverseMapping.ContainsKey(bestRelatedUnitId)) - { - reverseMapping[bestRelatedUnitId] = new List(); - } - reverseMapping[bestRelatedUnitId].Add(unitInTemplateId); - } - - return reverseMapping; - } - #endregion } \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Implemetaions/UnitInTemplateConflictMapper.cs b/PARR.TemplateMatcher/Services/Implemetaions/UnitInTemplateConflictMapper.cs new file mode 100644 index 00000000..99a285c8 --- /dev/null +++ b/PARR.TemplateMatcher/Services/Implemetaions/UnitInTemplateConflictMapper.cs @@ -0,0 +1,144 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.UnitFilterService.Models; +using PARR.Domain.Entities.Job; +using PARR.TemplateMatcher.Services.Interfaces; + +internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper +{ + private readonly ILogger logger; + private readonly IUnitRepository unitRepository; + private readonly IUnitKiiUnitRepository unitKiiUnitRepository; + + public UnitInTemplateConflictMapper( + ILogger logger, + IUnitRepository unitRepository, + IUnitKiiUnitRepository unitKiiUnitRepository) + { + this.logger = logger; + this.unitRepository = unitRepository; + this.unitKiiUnitRepository = unitKiiUnitRepository; + } + + public async Task>> BuildMappingAsync( + IEnumerable unitFilterResults, + Job maxJob, + CancellationToken cancellationToken = default) + { + logger.LogDebug("Начало разрешения конфликтов и построения маппинга для юнитов в шаблонах."); + + var unitFilterResultsList = unitFilterResults.ToList(); + if (!unitFilterResultsList.Any()) + return new Dictionary>(); + + var potentialAssignments = new Dictionary>(); + var allPotentialRelatedUnitIds = new HashSet(); + var allUnitInTemplateIds = new HashSet(); + + foreach (var dto in unitFilterResultsList) + { + var relatedUnitIds = maxJob.IsParentRelationships == true + ? dto.Children.Select(c => c.UnitId).ToList() + : dto.Parents.Select(p => p.UnitId).ToList(); + + var unitInTemplateId = dto.Id; + + allUnitInTemplateIds.Add(unitInTemplateId); + allPotentialRelatedUnitIds.UnionWith(relatedUnitIds); + + foreach (var relatedId in relatedUnitIds) + { + if (!potentialAssignments.TryGetValue(relatedId, out var list)) + { + list = new List(); + potentialAssignments[relatedId] = list; + } + list.Add(unitInTemplateId); + } + } + + if (!allPotentialRelatedUnitIds.Any()) + return new Dictionary>(); + + var relatedUnitNames = await unitRepository.Get() + .AsNoTracking() + .Where(u => allPotentialRelatedUnitIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString(), cancellationToken); + + // Загружаем идентификаторы КИИ через ваш реальный репозиторий + var kiiUnitIds = new HashSet(await unitKiiUnitRepository.Get() + .Select(k => k.UnitId) + .ToListAsync(cancellationToken)); + + logger.LogDebug("Загружено {KiiCount} идентификаторов КИИ юнитов для приоритезации.", kiiUnitIds.Count); + + var unitInTemplateToBestRelatedUnit = new Dictionary(); + + foreach (var unitInTemplateId in allUnitInTemplateIds) + { + var candidates = potentialAssignments + .Where(kvp => kvp.Value.Contains(unitInTemplateId)) + .Select(kvp => kvp.Key) + .ToList(); + + if (candidates.Count == 0) continue; + if (candidates.Count == 1) + { + unitInTemplateToBestRelatedUnit[unitInTemplateId] = candidates[0]; + continue; + } + + var kiiCandidates = candidates.Where(id => kiiUnitIds.Contains(id)).ToList(); + var candidatesToConsider = kiiCandidates.Any() ? kiiCandidates : candidates; + + var relationshipCounts = new Dictionary(); + foreach (var candidateId in candidatesToConsider) + { + int totalRelationships = 0; + foreach (var dto in unitFilterResultsList) + { + if (maxJob.IsParentRelationships == true && dto.Children.Any(c => c.UnitId == candidateId)) + totalRelationships += dto.Children.Count; + else if (maxJob.IsParentRelationships == false && dto.Parents.Any(p => p.UnitId == candidateId)) + totalRelationships += dto.Parents.Count; + } + relationshipCounts[candidateId] = totalRelationships; + } + + var bestCandidate = candidatesToConsider[0]; + int bestCount = relationshipCounts[bestCandidate]; + + foreach (var candidateId in candidatesToConsider.Skip(1)) + { + int currentCount = relationshipCounts[candidateId]; + if (currentCount > bestCount || + (currentCount == bestCount && string.Compare( + relatedUnitNames.GetValueOrDefault(candidateId, candidateId.ToString()), + relatedUnitNames.GetValueOrDefault(bestCandidate, bestCandidate.ToString()), + StringComparison.OrdinalIgnoreCase) < 0)) + { + bestCandidate = candidateId; + bestCount = currentCount; + } + } + + unitInTemplateToBestRelatedUnit[unitInTemplateId] = bestCandidate; + } + + logger.LogDebug("Разрешение конфликтов завершено. Найдено {Count} однозначных назначений.", unitInTemplateToBestRelatedUnit.Count); + + var reverseMapping = new Dictionary>(); + foreach (var kvp in unitInTemplateToBestRelatedUnit) + { + if (!reverseMapping.TryGetValue(kvp.Value, out var list)) + { + list = new List(); + reverseMapping[kvp.Value] = list; + } + list.Add(kvp.Key); + } + + return reverseMapping; + } +} \ No newline at end of file diff --git a/PARR.TemplateMatcher/Services/Interfaces/IUnitInTemplateConflictMapper.cs b/PARR.TemplateMatcher/Services/Interfaces/IUnitInTemplateConflictMapper.cs new file mode 100644 index 00000000..37ad61a2 --- /dev/null +++ b/PARR.TemplateMatcher/Services/Interfaces/IUnitInTemplateConflictMapper.cs @@ -0,0 +1,23 @@ +using PARR.Core.Services.UnitFilterService.Models; +using PARR.Domain.Entities.Job; + +namespace PARR.TemplateMatcher.Services.Interfaces +{ + /// + /// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей. + /// + public interface IUnitInTemplateConflictMapper + { + /// + /// Строит маппинг: UnitId шаблона -> [Список юнитов в UnitsInTemplate]. + /// При наличии нескольких кандидатов для одного юнита выбирается лучший по приоритету: + /// 1. Наличие в таблице UnitKiiUnit + /// 2. Наибольшее количество связей + /// 3. Алфавитный порядок имени юнита + /// + Task>> BuildMappingAsync( + IEnumerable unitFilterResults, + Job maxJob, + CancellationToken ct = default); + } +} diff --git a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs index a6bd8845..3c4bc89c 100644 --- a/PARR.TemplateMatcher/TemplateMatcherInstaller.cs +++ b/PARR.TemplateMatcher/TemplateMatcherInstaller.cs @@ -34,6 +34,7 @@ namespace PARR.TemplateMatcher services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); }