feat(templateMatcher): Убрана избыточная проверка связанных юнитов, теперь это выполняется в UnitFilter. Добавлена группировка по рабочей группе или ответственному за ЭК

This commit is contained in:
Mikhail Kuznetsov
2026-03-26 18:08:13 +10:00
parent 4864b9b343
commit 73d7f4b200
7 changed files with 368 additions and 429 deletions

View File

@@ -5,13 +5,11 @@ 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.Models.Unit;
using PARR.DAL.NextRunServices;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
@@ -28,7 +26,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
private readonly ILogger<GroupedTemplateSynchronizer> logger;
private readonly IUnitFilterService unitFilterService;
private readonly IUnitInUnitService unitInUnitService;
private readonly IUnitInValueService unitInValueService;
private readonly IUnitService unitService;
private readonly MqSettings mqSettings;
@@ -42,12 +39,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService;
private readonly IShortcodesService shortcodesService;
//private readonly INextRunService nextRunService;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
IUnitFilterService unitFilterService,
IUnitInUnitService unitInUnitService,
IUnitInValueService unitInValueService,
IUnitService unitService,
MqSettings mqSettings,
@@ -60,13 +58,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService
IMatchingStatusService matchingStatusService,
IShortcodesService shortcodesService
//INextRunService nextRunService
)
{
this.logger = logger;
this.unitFilterService = unitFilterService;
this.unitInUnitService = unitInUnitService;
this.unitInValueService = unitInValueService;
this.unitService = unitService;
this.mqSettings = mqSettings;
@@ -80,6 +78,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService;
this.shortcodesService = shortcodesService;
//this.nextRunService = nextRunService;
}
@@ -87,7 +86,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
@@ -119,12 +117,14 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
// 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);
@@ -138,20 +138,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
var jobsInGroup = jobGroup.Jobs.ToList();
// --- Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗАК" ---
var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
if (workGroupField == null)
{
logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗАК' не найдено в справочнике полей. Синхронизация прервана.");
await UpdateMatchingStatusAsync(jobGroupId, "Ошибка: поле 'РАБОЧАЯ_ГР_ОТВ_ЗАК' не найдено");
return;
}
var workGroupFieldId = workGroupField.Id;
var relationshipGroupValueIds = regionalEkPtkGroupService.Get()
.Select(g => g.FieldValueId)
.ToList();
logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.", relationshipGroupValueIds.Count);
// 2. Найти Job с максимальным MaxValueRelationships
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
@@ -165,26 +151,25 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
return;
}
if (maxJob.UnitFilters == null)
{
logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id);
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
// 3. Использовать фильтры maxJob для получения отфильтрованных юнитов
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
if (filteredUnits == null || !filteredUnits.Any())
// 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'ов");
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
return;
}
// Извлекаем ID юнитов для последующих операций
var expectedUnitIds = filteredUnits.Select(u => u.Id).ToList();
logger.LogDebug("Получено {Count} юнитов с подходящими связями через UnitFilterService.", unitFilterResults.Count());
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId
// --- ФИЛЬТРАЦИЯ unitFilterResults (dto.Id) ---
// 4. Фильтрация unitFilterResults по GroupingUnitFieldId (проверяем dto.Id)
if (!jobGroup.GroupingUnitFieldId.HasValue)
{
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
@@ -193,413 +178,335 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по GroupingUnitFieldId (FieldId={FieldId}).", groupingFieldId);
// --- ОПТИМИЗАЦИЯ: Загрузка значений поля GroupingUnitFieldId отдельно ---
logger.LogDebug("Загружаем значения поля GroupingUnitFieldId (FieldId={FieldId}) для {Count} юнитов.", groupingFieldId, expectedUnitIds.Count);
// Загрузим значения поля GroupingUnitFieldId для всех Id из unitFilterResults
var allUnitFilterResultIds = unitFilterResults.Select(dto => dto.Id).ToList();
var groupingUnitValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitFilterResultIds, new HashSet<Guid> { groupingFieldId });
var groupingUnitValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(expectedUnitIds, new HashSet<Guid> { groupingFieldId });
var unitIdsWithValidGroupingFieldSet = groupingUnitValues
// Найдем 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} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} UnitFilterResultDto.", filteredUnitFilterResultsByGrouping.Count);
if (!unitIdsWithValidGroupingFieldSet.Any())
if (!filteredUnitFilterResultsByGrouping.Any())
{
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId");
return;
}
// --- Аналогично для фильтрации по "РАБОЧАЯ_ГР_ОТВ_ЗАК" ---
// var workGroupFieldId = workGroupField.Id; // УЖЕ ОПРЕДЕЛЕНО РАНЕЕ (строка 157)
// var relationshipGroupValueIds = regionalEkPtkGroupService.Get() // УДАЛИТЬ - УЖЕ ОПРЕДЕЛЕНО (строка 161)
// 5. Фильтрация unitFilterResults по РАБОЧАЯ_ГР_ОТВ_ЗАК (проверяем dto.Id)
var workGroupFieldId = await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
logger.LogDebug("Загружаем значения поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК' (FieldId={FieldId}) для {Count} юнитов.", workGroupFieldId, unitIdsWithValidGroupingFieldSet.Count);
logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по полю 'РАБОЧАЯ_ГР_ОТВ_ЗАК' (FieldId={FieldId}).", workGroupFieldId);
var workGroupValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(unitIdsWithValidGroupingFieldSet, new HashSet<Guid> { workGroupFieldId });
// Загрузим значения поля РАБОЧАЯ_ГР_ОТВ_ЗАК для Id из filteredUnitFilterResultsByGrouping
var allFilteredUnitFilterResultIds = filteredUnitFilterResultsByGrouping.Select(dto => dto.Id).ToList();
var workGroupValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allFilteredUnitFilterResultIds, new HashSet<Guid> { workGroupFieldId });
var unitIdsWithValidWorkGroupFieldSet = workGroupValues
.Where(uv => uv.Value != null && relationshipGroupValueIds.Contains(uv.Value.Id))
// Получим разрешенные значения из 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} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count);
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' осталось {Count} UnitFilterResultDto.", finalFilteredUnitFilterResults.Count);
if (!unitIdsWithValidWorkGroupFieldSet.Any())
if (!finalFilteredUnitFilterResults.Any())
{
logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК'");
return;
}
// --- ИСПОЛЬЗУЕМ ПОСЛЕДНИЙ РЕЗУЛЬТАТ ДАЛЬШЕ ---
var finalUnitIds = unitIdsWithValidWorkGroupFieldSet; // Более понятное имя
// 5. Получить RelationshipFilters из maxJob
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию...");
var potentialUnitInUnitLinks = await unitInUnitService.Get()
.AsNoTracking()
.Where(link => finalUnitIds.Contains(link.ParentUnitId) || finalUnitIds.Contains(link.ChildUnitId))
.ToListAsync();
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count);
var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet();
var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet();
// --- Оптимизация: Загрузка всех значений за ОДИН запрос ---
var allRelevantUnitIds = allParentIds.Concat(allChildIds).ToHashSet();
var allUnitValues = await unitInValueService.Get()
.AsNoTracking()
.Include(uv => uv.Field)
.Include(uv => uv.Value)
.Where(uv => allRelevantUnitIds.Contains(uv.UnitId))
.ToListAsync();
// --- Создание карт значений из одного списка ---
var parentValuesMap = allUnitValues
.Where(uv => allParentIds.Contains(uv.UnitId))
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
var childValuesMap = allUnitValues
.Where(uv => allChildIds.Contains(uv.UnitId))
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count);
var filteredUnitInUnitLinks = new List<UnitInUnit>();
foreach (var link in potentialUnitInUnitLinks)
// --- ПОСТРОЕНИЕ ОБРАТНОГО ОТОБРАЖЕНИЯ (после фильтрации) ---
logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
var reverseMapping = new Dictionary<Guid, List<Guid>>();
foreach (var dto in finalFilteredUnitFilterResults)
{
bool linkMatchesAllFilters = true;
foreach (var rf in relationshipFilters)
List<Guid> relatedUnitIds;
if (maxJob.IsParentRelationships == true)
{
var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List<UnitInValue>()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List<UnitInValue>());
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;
}
// 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();
}
if (linkMatchesAllFilters)
// dto.Id - это юнит, который прошел фильтры, он будет в UnitsInTemplate
var unitInTemplateId = dto.Id;
foreach (var relatedUnitId in relatedUnitIds)
{
filteredUnitInUnitLinks.Add(link);
// relatedUnitId уже прошел все фильтры, т.к. dto.Id (его связанный юнит) прошел фильтры
if (!reverseMapping.ContainsKey(relatedUnitId))
{
reverseMapping[relatedUnitId] = new List<Guid>();
}
reverseMapping[relatedUnitId].Add(unitInTemplateId);
}
}
logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count);
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
// --- Сгруппировать юниты ---
var groupedRelationships = new Dictionary<Guid, List<Guid>>();
foreach (var link in filteredUnitInUnitLinks)
if (!reverseMapping.Any())
{
var parentUnitId = link.ParentUnitId;
var childUnitId = link.ChildUnitId;
if (finalUnitIds.Contains(parentUnitId))
{
if (!groupedRelationships.ContainsKey(childUnitId))
{
groupedRelationships[childUnitId] = new List<Guid>();
}
groupedRelationships[childUnitId].Add(parentUnitId);
}
else if (finalUnitIds.Contains(childUnitId))
{
if (!groupedRelationships.ContainsKey(parentUnitId))
{
groupedRelationships[parentUnitId] = new List<Guid>();
}
groupedRelationships[parentUnitId].Add(childUnitId);
}
logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения");
return;
}
logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count);
// 6. Внутренняя группировка по ОТВЕТСТВЕННЫЙ_ЗАК / РАБОЧАЯ_ГР_ОТВ_ЗАК (для юнитов в UnitsInTemplate)
logger.LogDebug("Внутренняя группировка по полю (IsGroupByResponsible={IsGroupByResponsible}).", jobGroup.IsGroupByResponsible);
// --- Разрешение конфликта с детерминированной сортировкой ---
var unitToKeys = new Dictionary<Guid, List<Guid>>();
foreach (var kvp in groupedRelationships)
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<Guid> { 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 key = kvp.Key;
var units = kvp.Value;
foreach (var unitId in units)
{
if (!unitToKeys.ContainsKey(unitId))
{
unitToKeys[unitId] = new List<Guid>();
}
unitToKeys[unitId].Add(key);
}
}
var potentialUnitId = kvpOuter.Key;
var unitsInTemplateForThisPotentialUnitId = kvpOuter.Value;
// Получаем имена всех конфликтующих ключей
var conflictKeys = unitToKeys
.Where(kvp => kvp.Value.Count > 1)
.SelectMany(kvp => kvp.Value)
.Distinct()
.ToList();
logger.LogDebug("Обработка потенциального шаблона для UnitId {PotentialUnitId} с {Count} юнитами в UnitsInTemplate до внутренней группировки.", potentialUnitId, unitsInTemplateForThisPotentialUnitId.Count);
var unitNamesMap = await unitService.Get()
.Where(u => conflictKeys.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name ?? string.Empty);
foreach (var conflictedUnitEntry in unitToKeys.Where(kvp => kvp.Value.Count > 1))
{
var unitId = conflictedUnitEntry.Key;
var keysForUnit = conflictedUnitEntry.Value;
// Сортируем по: 1) кол-во юнитов (убывание), 2) имя ключа (возрастание)
var sortedKeys = keysForUnit
.Select(key => (
key,
count: groupedRelationships[key].Count,
name: unitNamesMap.GetValueOrDefault(key, "")
))
.OrderByDescending(x => x.count)
.ThenBy(x => x.name)
// --- ВНУТРЕННЯЯ ГРУППИРОВКА ---
// Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
.GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных")) // Используем маппинг
.ToList();
var bestKey = sortedKeys.First().key;
logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate.", potentialUnitId, innerGroupedUnitsInTemplate.Count);
// Удаляем юнит из ВСЕХ групп, кроме лучшей
foreach (var key in keysForUnit)
// 8. Цикл по внутренним группам UnitsInTemplate
foreach (var innerGroup in innerGroupedUnitsInTemplate)
{
if (key != bestKey && groupedRelationships.ContainsKey(key))
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
{
groupedRelationships[key].Remove(unitId);
}
}
}
var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции
var subGroupSize = unitsInTemplateSubGroup.Count;
logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", i, groupingValueName, potentialUnitId, subGroupSize);
// Удаляем пустые группы
var emptyKeys = groupedRelationships
.Where(kvp => !kvp.Value.Any())
.Select(kvp => kvp.Key)
.ToList();
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
foreach (var key in emptyKeys)
{
groupedRelationships.Remove(key);
}
logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count);
// === Основной цикл обработки ===
foreach (var kvp in groupedRelationships)
{
var relationshipUnitId = kvp.Key;
var childUnitIds = kvp.Value;
if (childUnitIds.Count == 0) continue;
logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count);
var childUnitNameMap = await unitService.Get()
.AsNoTracking()
.Where(u => childUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name);
var sortedChildUnitIds = childUnitIds
.OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString()))
.ToList();
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
var childUnitGroups = sortedChildUnitIds
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
logger.LogDebug("Связанный юнит {RelationshipUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count);
// Индекс начинается с 1
for (int i = 1; i <= childUnitGroups.Count; i++)
{
var subGroup = childUnitGroups[i - 1]; // корректируем индекс для доступа к коллекции
var subGroupSize = subGroup.Count;
logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RelationshipUnitId}, размер {Size}.", i, relationshipUnitId, 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 == relationshipUnitId && 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 = subGroup.ToList();
// === 2. Сравниваем детерминированно с сортировкой по имени ===
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
var unitNamesForSort = await unitService.Get()
var existingTemplatesForRelationship = await templateService.Get()
.AsNoTracking()
.Where(u => allUnitIdsForSort.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString());
.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 sortedCurrentUnitIds = currentUnitIds
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
.ToList();
var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault();
var sortedProposedUnitIds = proposedUnitIds
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
.ToList();
bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds);
if (unitsAreEqual)
if (existingTemplateForSubGroup != null)
{
logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id);
existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList();
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup);
// === 1. Получаем текущие и новые ID юнитов ===
var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
var proposedUnitIds = unitsInTemplateSubGroup.ToList();
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
// === 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);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(existingTemplateForSubGroup.Id, false);
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 updateRequest = new TemplateUpdaterMq
{
TemplateId = existingTemplateForSubGroup.Id,
TemplateId = reusableTemplate.Id,
JobId = targetJob.Id,
UnitId = relationshipUnitId,
UnitId = potentialUnitId,
Name = expectedName,
IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate,
IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule,
//LastRun = existingTemplateForSubGroup.LastRun,
//NextRun = nextRun,
IsNew = false,
Index = i,
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = sortedProposedUnitIds
IsNew = true,
Index = i,
UnitsInTemplate = unitsInTemplateSubGroup
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id);
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}, с {Count} юнитами.", targetJob.Id, potentialUnitId, i, unitsInTemplateSubGroup.Count);
await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, i, initiator);
}
}
else
{
logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов или их порядка (после сортировки).", existingTemplateForSubGroup.Id);
var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, 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, relationshipUnitId, initiator);
if (reusableTemplate != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i);
var tempTemplateForName = new Template
{
Id = reusableTemplate.Id,
Name = reusableTemplate.Name,
JobId = targetJob.Id,
UnitId = relationshipUnitId,
Index = i,
Job = targetJob,
Unit = reusableTemplate.Unit,
UnitsInTemplate = subGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
};
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true);
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,
IsNew = true,
Index = i,
UnitsInTemplate = subGroup
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count);
await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator);
}
}
}
}
// === Деактивация ===
// Собираем ожидаемые ключи шаблонов на основе внутренне сгруппированных результатов
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvp in groupedRelationships)
foreach (var kvpOuter in reverseMapping)
{
var relationshipUnitId = kvp.Key;
var childUnitIds = kvp.Value;
var childUnitNameMapForDeactivate = await unitService.Get()
.AsNoTracking()
.Where(u => childUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name);
var sortedChildUnitIdsForDeactivate = childUnitIds
.OrderBy(id => childUnitNameMapForDeactivate.GetValueOrDefault(id, id.ToString()))
var potentialUnitId = kvpOuter.Key;
var unitsInTemplateForThisPotentialUnitId = kvpOuter.Value;
// Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
.GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных"))
.ToList();
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
var childUnitGroups = sortedChildUnitIdsForDeactivate
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
// Индекс начинается с 1
for (int i = 1; i <= childUnitGroups.Count; i++)
foreach (var innerGroup in innerGroupedUnitsInTemplate)
{
var subGroup = childUnitGroups[i - 1]; // корректируем индекс для доступа к коллекции
var subGroupSize = subGroup.Count;
Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
expectedTemplateKeys.Add((targetJobForExpectedKey.Id, relationshipUnitId, i));
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 (не только для текущих relationshipUnitIds)
// Получаем ВСЕ шаблоны для JobGroup (не только для текущих potentialUnitIds)
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking()
@@ -724,8 +631,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
HistoryInitiator = initiator
};
//var msg = JsonSerializer.Serialize(mqRequest);
//var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg });
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
if (!result.IsSuccess)
@@ -749,4 +654,17 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
TimeSpan.FromMinutes(30)
);
}
#region Вспомогательные методы
private async Task<Guid> GetFieldIdByAihitNameAsync(string fieldName)
{
var field = await unitFieldService.GetByAihitNameAsync(fieldName);
if (field == null)
{
logger.LogError("Поле '{FieldName}' не найдено в справочнике полей.", fieldName);
throw new InvalidOperationException($"Поле '{fieldName}' не найдено в справочнике полей.");
}
return field.Id;
}
#endregion
}

View File

@@ -31,7 +31,7 @@ internal class TemplateReuser : ITemplateReuser
try
{
// Атомарно резервируем один шаблон через DAL
var templateId = await templateService.ReserveUnusedTemplateAsync(unitId, initiator);
var templateId = await templateService.ReserveUnusedTemplateAsync(unitId, jobId, initiator);
if (templateId == null)
{

View File

@@ -5,6 +5,19 @@ namespace PARR.TemplateMatcher.Services.Interfaces
{
public interface ITemplateReuser
{
/// <summary>
/// Пытается переиспользовать неиспользуемый шаблон для указанной работы и ЭК.
/// При резервировании шаблона для правильного расчета NextRun Id работы
/// и ЭК устанавливается при захвате.
/// </summary>
/// <param name="jobId">Id работы.</param>
/// <param name="unitId">Id ЭК.</param>
/// <param name="initiator">Данные инициатора операции.</param>
/// <param name="maxAttempts">Максимальное количество попыток (по умолчанию 3).</param>
/// <returns>
/// Зарезервированный шаблон или <see langword="null"/>,
/// если не удалось получить шаблон после всех попыток.
/// </returns>
Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId,
Guid unitId,