fix(core, templateMatcher): Исправлен метод ApplyRelationshipFiltersToUnitsAsync - неверно фильтровались ЭК при одновременно включенном IsFullMatch и IsInverse, теперь учитываются все связи; В GroupedTemplateSynchronizer изменен порядок индексации шаблонов.
This commit is contained in:
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -78,6 +79,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
{
|
||||
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
|
||||
}
|
||||
|
||||
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
|
||||
@@ -154,6 +156,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
{
|
||||
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
|
||||
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -231,37 +235,16 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
return;
|
||||
}
|
||||
|
||||
// --- ПОСТРОЕНИЕ ОБРАТНОГО ОТОБРАЖЕНИЯ (после фильтрации) ---
|
||||
logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
|
||||
var reverseMapping = new Dictionary<Guid, List<Guid>>();
|
||||
foreach (var dto in finalFilteredUnitFilterResults)
|
||||
var reverseMapping = await BuildReverseMappingAsync(finalFilteredUnitFilterResults, maxJob);
|
||||
|
||||
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
|
||||
|
||||
if (!reverseMapping.Any())
|
||||
{
|
||||
List<Guid> 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<Guid>();
|
||||
}
|
||||
reverseMapping[relatedUnitId].Add(unitInTemplateId);
|
||||
}
|
||||
logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
|
||||
@@ -288,7 +271,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
// Создадим маппинг UnitId (из UnitsInTemplate) -> значение поля для внутренней группировки
|
||||
var unitInTemplateToInnerGroupingValueMap = innerGroupingValues
|
||||
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
|
||||
.ToDictionary(uv => uv.UnitId, uv => uv.Value.Value);
|
||||
.ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value);
|
||||
|
||||
// 7. Основной цикл обработки: итерируемся по potentialUnitIds (UnitId шаблонов)
|
||||
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
|
||||
@@ -304,20 +287,23 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
// Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
|
||||
var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
|
||||
.GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных"))
|
||||
.OrderBy(g => g.Key, StringComparer.Ordinal) // <-- Сортировка по имени внутренней группы
|
||||
.ToList();
|
||||
|
||||
logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate.", potentialUnitId, innerGroupedUnitsInTemplate.Count);
|
||||
logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate (отсортировано).", potentialUnitId, innerGroupedUnitsInTemplate.Count);
|
||||
|
||||
// 8. Цикл по внутренним группам UnitsInTemplate
|
||||
foreach (var innerGroup in innerGroupedUnitsInTemplate)
|
||||
// --- СОБЕРЕМ ВСЕ ИТОГОВЫЕ ПОДГРУППЫ ДЛЯ ЭТОГО potentialUnitId ---
|
||||
var allFinalSubGroups = new List<(List<Guid> UnitsInTemplateSubGroup, string InnerGroupName, int SubGroupSizeWithinInnerGroup)>(); // (UnitsInTemplate, Имя_внутренней_группы, размер_подгруппы_внутри_внутренней_группы)
|
||||
|
||||
foreach (var innerGroup in innerGroupedUnitsInTemplate) // Теперь проходит в отсортированном порядке по groupingValueName
|
||||
{
|
||||
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 })
|
||||
@@ -327,149 +313,154 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
|
||||
logger.LogDebug("Внутренняя группа '{GroupingValue}' для UnitId {PotentialUnitId}: разбит на {GroupCount} подгрупп UnitsInTemplate.", groupingValueName, potentialUnitId, unitsInTemplateSubGroups.Count);
|
||||
|
||||
// 9. Цикл по подгруппам UnitsInTemplate для создания/обновления шаблонов
|
||||
for (int i = 1; i <= unitsInTemplateSubGroups.Count; i++) // Индекс начинается с 1
|
||||
// Добавим каждую *итоговую* подгруппу в общий список
|
||||
foreach (var subGroup in unitsInTemplateSubGroups)
|
||||
allFinalSubGroups.Add((subGroup, groupingValueName!, subGroup.Count));
|
||||
}
|
||||
|
||||
int globalIndexForThisUnitId = 1;
|
||||
foreach (var finalSubGroupData in allFinalSubGroups)
|
||||
{
|
||||
var unitsInTemplateSubGroup = finalSubGroupData.UnitsInTemplateSubGroup;
|
||||
var subGroupSize = finalSubGroupData.SubGroupSizeWithinInnerGroup;
|
||||
var originatingInnerGroupName = finalSubGroupData.InnerGroupName;
|
||||
|
||||
logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", globalIndexForThisUnitId, originatingInnerGroupName, potentialUnitId, subGroupSize);
|
||||
|
||||
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
|
||||
|
||||
// --- Добавляем УНИКАЛЬНЫЙ ключ в список ожидаемых ---
|
||||
expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, globalIndexForThisUnitId));
|
||||
|
||||
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 == globalIndexForThisUnitId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
||||
.ToListAsync();
|
||||
|
||||
var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault();
|
||||
|
||||
if (existingTemplateForSubGroup != null)
|
||||
{
|
||||
var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции
|
||||
var subGroupSize = unitsInTemplateSubGroup.Count;
|
||||
logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", i, groupingValueName, potentialUnitId, subGroupSize);
|
||||
// === 1. Получаем текущие и новые ID юнитов ===
|
||||
var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
|
||||
var proposedUnitIds = unitsInTemplateSubGroup.ToList();
|
||||
|
||||
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
|
||||
|
||||
// --- Добавляем ключ в список ожидаемых прямо здесь ---
|
||||
expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, i)); // potentialUnitId - это UnitId шаблона
|
||||
|
||||
var existingTemplatesForRelationship = await templateService.Get()
|
||||
// === 2. Сравниваем детерминированно с сортировкой по имени ===
|
||||
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
|
||||
var unitNamesForSort = await unitService.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();
|
||||
.Where(u => allUnitIdsForSort.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString());
|
||||
|
||||
var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault();
|
||||
var sortedCurrentUnitIds = currentUnitIds
|
||||
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
|
||||
.ToList();
|
||||
|
||||
if (existingTemplateForSubGroup != null)
|
||||
var sortedProposedUnitIds = proposedUnitIds
|
||||
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
|
||||
.ToList();
|
||||
|
||||
bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds);
|
||||
|
||||
if (unitsAreEqual)
|
||||
{
|
||||
// === 1. Получаем текущие и новые ID юнитов ===
|
||||
var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
|
||||
var proposedUnitIds = unitsInTemplateSubGroup.ToList();
|
||||
logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id);
|
||||
existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList();
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup);
|
||||
|
||||
// === 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)
|
||||
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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);
|
||||
|
||||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id);
|
||||
var updateRequest = new TemplateUpdaterMq
|
||||
{
|
||||
TemplateId = reusableTemplate.Id,
|
||||
TemplateId = existingTemplateForSubGroup.Id,
|
||||
JobId = targetJob.Id,
|
||||
UnitId = potentialUnitId,
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
|
||||
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
|
||||
IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate,
|
||||
IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule,
|
||||
IsNew = false,
|
||||
Index = globalIndexForThisUnitId,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
Initiator = initiator,
|
||||
IsNew = true,
|
||||
Index = i,
|
||||
UnitsInTemplate = unitsInTemplateSubGroup
|
||||
UnitsInTemplate = sortedProposedUnitIds
|
||||
};
|
||||
|
||||
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);
|
||||
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, globalIndexForThisUnitId);
|
||||
}
|
||||
}
|
||||
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, globalIndexForThisUnitId);
|
||||
|
||||
var tempTemplateForName = new Template
|
||||
{
|
||||
Id = reusableTemplate.Id,
|
||||
Name = reusableTemplate.Name,
|
||||
JobId = targetJob.Id,
|
||||
UnitId = potentialUnitId,
|
||||
Index = globalIndexForThisUnitId,
|
||||
Job = targetJob,
|
||||
Unit = reusableTemplate.Unit,
|
||||
UnitsInTemplate = unitsInTemplateSubGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
|
||||
};
|
||||
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||||
|
||||
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 = globalIndexForThisUnitId,
|
||||
UnitsInTemplate = unitsInTemplateSubGroup
|
||||
};
|
||||
|
||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}, с {Count} юнитами.", targetJob.Id, potentialUnitId, globalIndexForThisUnitId, unitsInTemplateSubGroup.Count);
|
||||
await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, globalIndexForThisUnitId, initiator);
|
||||
}
|
||||
}
|
||||
|
||||
globalIndexForThisUnitId++; // Увеличиваем индекс для следующей итоговой подгруппы
|
||||
}
|
||||
}
|
||||
|
||||
// === Деактивация ===
|
||||
// Теперь expectedTemplateKeys уже собран
|
||||
|
||||
// Получаем ВСЕ шаблоны для JobGroup (не только для текущих potentialUnitIds)
|
||||
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
|
||||
var allExistingTemplatesInGroup = await templateService.Get()
|
||||
.AsNoTracking()
|
||||
@@ -560,7 +551,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
};
|
||||
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||||
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
||||
|
||||
var updateRequest = new TemplateUpdaterMq
|
||||
{
|
||||
TemplateId = template.Id,
|
||||
@@ -569,8 +560,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = template.IsActiveTemplate,
|
||||
IsActiveSchedule = template.IsActiveSchedule,
|
||||
//LastRun = template.LastRun,
|
||||
//NextRun = nextRun,
|
||||
IsNew = false,
|
||||
Index = newIndex,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
@@ -618,6 +607,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#region Вспомогательные методы
|
||||
private async Task<Guid> GetFieldIdByAihitNameAsync(string fieldName)
|
||||
{
|
||||
@@ -629,5 +619,149 @@ 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
|
||||
}
|
||||
@@ -48,7 +48,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
||||
public async Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator)
|
||||
{
|
||||
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
|
||||
return true; // уже в обработке
|
||||
return true;
|
||||
|
||||
if (template.StatusTypeId == TemplateStatusTypeEnum.Unused)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user