feat(templateMatcher): Добавлена проверка принадлежности связанного ЭК к КИИ, который имеет больший приоритет.
This commit is contained in:
@@ -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<GroupedTemplateSynchronizer> 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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Строит обратное отображение: UnitId шаблона -> [UnitsInTemplate]
|
||||
/// Решает конфликты, когда юнит из UnitsInTemplate может быть связан с несколькими UnitId шаблона.
|
||||
/// </summary>
|
||||
/// <param name="unitFilterResults">Результаты фильтрации, содержащие связи.</param>
|
||||
/// <param name="maxJob">Job, используемый для определения направления связей (IsParentRelationships).</param>
|
||||
/// <returns>Словарь, где ключ - это UnitId шаблона, а значение - список юнитов, входящих в него (UnitsInTemplate).</returns>
|
||||
private async Task<Dictionary<Guid, List<Guid>>> BuildReverseMappingAsync(IEnumerable<UnitFilterResultDto> unitFilterResults, Job maxJob)
|
||||
{
|
||||
logger.LogDebug("Разрешение конфликта: определение, для какого UnitId выбрать каждый юнит из UnitsInTemplate.");
|
||||
|
||||
// 1. Собираем все потенциальные пары (relatedUnitId, unitInTemplateId)
|
||||
var potentialAssignments = new Dictionary<Guid, List<Guid>>(); // relatedUnitId -> [unitInTemplateId, ...]
|
||||
var allPotentialRelatedUnitIds = new HashSet<Guid>();
|
||||
var allUnitInTemplateIds = new HashSet<Guid>();
|
||||
|
||||
foreach (var dto in unitFilterResults)
|
||||
{
|
||||
List<Guid> 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<Guid>();
|
||||
}
|
||||
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<Guid, Guid>(); // 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<Guid, int>();
|
||||
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<Guid, List<Guid>>();
|
||||
foreach (var assignmentKvp in unitInTemplateToBestRelatedUnit)
|
||||
{
|
||||
var unitInTemplateId = assignmentKvp.Key;
|
||||
var bestRelatedUnitId = assignmentKvp.Value;
|
||||
|
||||
if (!reverseMapping.ContainsKey(bestRelatedUnitId))
|
||||
{
|
||||
reverseMapping[bestRelatedUnitId] = new List<Guid>();
|
||||
}
|
||||
reverseMapping[bestRelatedUnitId].Add(unitInTemplateId);
|
||||
}
|
||||
|
||||
return reverseMapping;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -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<UnitInTemplateConflictMapper> logger;
|
||||
private readonly IUnitRepository unitRepository;
|
||||
private readonly IUnitKiiUnitRepository unitKiiUnitRepository;
|
||||
|
||||
public UnitInTemplateConflictMapper(
|
||||
ILogger<UnitInTemplateConflictMapper> logger,
|
||||
IUnitRepository unitRepository,
|
||||
IUnitKiiUnitRepository unitKiiUnitRepository)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.unitRepository = unitRepository;
|
||||
this.unitKiiUnitRepository = unitKiiUnitRepository;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
||||
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
||||
Job maxJob,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger.LogDebug("Начало разрешения конфликтов и построения маппинга для юнитов в шаблонах.");
|
||||
|
||||
var unitFilterResultsList = unitFilterResults.ToList();
|
||||
if (!unitFilterResultsList.Any())
|
||||
return new Dictionary<Guid, List<Guid>>();
|
||||
|
||||
var potentialAssignments = new Dictionary<Guid, List<Guid>>();
|
||||
var allPotentialRelatedUnitIds = new HashSet<Guid>();
|
||||
var allUnitInTemplateIds = new HashSet<Guid>();
|
||||
|
||||
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<Guid>();
|
||||
potentialAssignments[relatedId] = list;
|
||||
}
|
||||
list.Add(unitInTemplateId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allPotentialRelatedUnitIds.Any())
|
||||
return new Dictionary<Guid, List<Guid>>();
|
||||
|
||||
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<Guid>(await unitKiiUnitRepository.Get()
|
||||
.Select(k => k.UnitId)
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
logger.LogDebug("Загружено {KiiCount} идентификаторов КИИ юнитов для приоритезации.", kiiUnitIds.Count);
|
||||
|
||||
var unitInTemplateToBestRelatedUnit = new Dictionary<Guid, Guid>();
|
||||
|
||||
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<Guid, int>();
|
||||
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<Guid, List<Guid>>();
|
||||
foreach (var kvp in unitInTemplateToBestRelatedUnit)
|
||||
{
|
||||
if (!reverseMapping.TryGetValue(kvp.Value, out var list))
|
||||
{
|
||||
list = new List<Guid>();
|
||||
reverseMapping[kvp.Value] = list;
|
||||
}
|
||||
list.Add(kvp.Key);
|
||||
}
|
||||
|
||||
return reverseMapping;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using PARR.Core.Services.UnitFilterService.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей.
|
||||
/// </summary>
|
||||
public interface IUnitInTemplateConflictMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Строит маппинг: UnitId шаблона -> [Список юнитов в UnitsInTemplate].
|
||||
/// При наличии нескольких кандидатов для одного юнита выбирается лучший по приоритету:
|
||||
/// 1. Наличие в таблице UnitKiiUnit
|
||||
/// 2. Наибольшее количество связей
|
||||
/// 3. Алфавитный порядок имени юнита
|
||||
/// </summary>
|
||||
Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
||||
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
||||
Job maxJob,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ namespace PARR.TemplateMatcher
|
||||
services.AddTransient<ITemplateNameNormalizer, TemplateNameNormalizer>();
|
||||
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
|
||||
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
|
||||
services.AddTransient<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
|
||||
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
||||
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user