Files
parr_api/PARR.TemplateMatcher/Services/Implemetaions/GroupedTemplateSynchronizer.cs

767 lines
44 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Core.Repositories.Interfaces;
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;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false;
private readonly ILogger<GroupedTemplateSynchronizer> logger;
private readonly IUnitFilterService unitFilterService;
private readonly IUnitInValueRepository unitInValueService;
private readonly IUnitRepository unitService;
private readonly MqSettings mqSettings;
private readonly IRabbitService mqService;
private readonly ITemplateRepository templateService;
private readonly IJobGroupRepository jobGroupService;
private readonly ITemplateReuser templateReuser;
private readonly IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupService;
private readonly IUnitFieldRepository unitFieldService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
IUnitFilterService unitFilterService,
IUnitInValueRepository unitInValueService,
IUnitRepository unitService,
MqSettings mqSettings,
IRabbitService mqService,
ITemplateRepository templateService,
IJobGroupRepository jobGroupService,
ITemplateReuser templateReuser,
IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupService,
IUnitFieldRepository unitFieldService,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService
)
{
this.logger = logger;
this.unitFilterService = unitFilterService;
this.unitInValueService = unitInValueService;
this.unitService = unitService;
this.mqSettings = mqSettings;
this.mqService = mqService;
this.templateService = templateService;
this.jobGroupService = jobGroupService;
this.templateReuser = templateReuser;
this.regionalEkPtkGroupService = regionalEkPtkGroupService;
this.unitFieldService = unitFieldService;
this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Any() == true)
{
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
try
{
// 1. Получить JobGroup и связанные Job'ы
var jobGroup = await jobGroupService.Get()
.AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.ThenInclude(rf => rf.UnitField) // Подгрузим поля для фильтрации
.Include(jg => jg.Jobs)
.ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return;
}
var jobsInGroup = jobGroup.Jobs.ToList();
// 2. Найти Job с максимальным MaxValueRelationships
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
{
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
return;
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
// 3. Использовать unitFilterService для получения отфильтрованных юнитов с их связями
// Это включает в себя все фильтры: UnitFilter, FieldFilter, RelationshipFilter, UmbrellaFilter
logger.LogDebug("Получение отфильтрованных юнитов с их связями через UnitFilterService для Job {JobId}.", maxJob.Id);
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
if (unitFilterResults == null || !unitFilterResults.Any())
{
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
return;
}
logger.LogDebug("Получено {Count} юнитов с подходящими связями через UnitFilterService.", unitFilterResults.Count());
// --- ФИЛЬТРАЦИЯ unitFilterResults (dto.Id) ---
// 4. Фильтрация unitFilterResults по GroupingUnitFieldId (проверяем dto.Id)
if (!jobGroup.GroupingUnitFieldId.HasValue)
{
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Отсутствует GroupingUnitFieldId");
return;
}
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по GroupingUnitFieldId (FieldId={FieldId}).", groupingFieldId);
// Загрузим значения поля GroupingUnitFieldId для всех Id из unitFilterResults
var allUnitFilterResultIds = unitFilterResults.Select(dto => dto.Id).ToList();
var groupingUnitValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitFilterResultIds, new HashSet<Guid> { groupingFieldId });
// Найдем Id юнитов, у которых есть значение в GroupingUnitFieldId
var validUnitFilterResultIds = groupingUnitValues
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
.Select(uv => uv.UnitId)
.ToHashSet();
// Оставляем только те UnitFilterResultDto, чей Id проходит фильтр
var filteredUnitFilterResultsByGrouping = unitFilterResults
.Where(dto => validUnitFilterResultIds.Contains(dto.Id))
.ToList();
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} UnitFilterResultDto.", filteredUnitFilterResultsByGrouping.Count);
if (!filteredUnitFilterResultsByGrouping.Any())
{
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId");
return;
}
// 5. Фильтрация unitFilterResults по РАБОЧАЯ_ГР_ОТВ_ЗАК (проверяем dto.Id)
var workGroupFieldId = await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по полю 'РАБОЧАЯ_ГР_ОТВ_ЗАК' (FieldId={FieldId}).", workGroupFieldId);
// Загрузим значения поля РАБОЧАЯ_ГР_ОТВ_ЗАК для Id из filteredUnitFilterResultsByGrouping
var allFilteredUnitFilterResultIds = filteredUnitFilterResultsByGrouping.Select(dto => dto.Id).ToList();
var workGroupValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allFilteredUnitFilterResultIds, new HashSet<Guid> { workGroupFieldId });
// Получим разрешенные значения из regionalEkPtkGroupService
var allowedValueIds = regionalEkPtkGroupService.Get()
.Select(g => g.FieldValueId)
.ToHashSet();
logger.LogDebug("Найдено {Count} разрешенных значений для поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.", allowedValueIds.Count);
// Найдем Id юнитов, у которых значение в РАБОЧАЯ_ГР_ОТВ_ЗАК разрешено
var validUnitFilterResultIdsForWorkGroup = workGroupValues
.Where(uv => uv.Value != null && allowedValueIds.Contains(uv.Value.Id))
.Select(uv => uv.UnitId)
.ToHashSet();
// Оставляем только те UnitFilterResultDto, чей Id проходит фильтр
var finalFilteredUnitFilterResults = filteredUnitFilterResultsByGrouping
.Where(dto => validUnitFilterResultIdsForWorkGroup.Contains(dto.Id))
.ToList();
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' осталось {Count} UnitFilterResultDto.", finalFilteredUnitFilterResults.Count);
if (!finalFilteredUnitFilterResults.Any())
{
logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК'");
return;
}
logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
var reverseMapping = await BuildReverseMappingAsync(finalFilteredUnitFilterResults, maxJob);
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
if (!reverseMapping.Any())
{
logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения");
return;
}
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
if (!reverseMapping.Any())
{
logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения");
return;
}
// 6. Внутренняя группировка по ОТВЕТСТВЕННЫЙ_ЗАК / РАБОЧАЯ_ГР_ОТВ_ЗАК (для юнитов в UnitsInTemplate)
logger.LogDebug("Внутренняя группировка по полю (IsGroupByResponsible={IsGroupByResponsible}).", jobGroup.IsGroupByResponsible);
var groupingFieldIdForInnerGrouping = jobGroup.IsGroupByResponsible == true
? await GetFieldIdByAihitNameAsync("ОТВЕТСТВЕННЫЙ_ЗАК")
: await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
// Загрузим значения поля для *всех* юнитов, которые могут быть в UnitsInTemplate
// Это все юниты из всех списков в reverseMapping.Values
var allUnitsInTemplate = reverseMapping.Values.SelectMany(list => list).Distinct().ToList();
var innerGroupingValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitsInTemplate, new HashSet<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 шаблонов)
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvpOuter in reverseMapping)
{
var potentialUnitId = kvpOuter.Key;
var unitsInTemplateForThisPotentialUnitId = kvpOuter.Value;
logger.LogDebug("Обработка потенциального шаблона для UnitId {PotentialUnitId} с {Count} юнитами в UnitsInTemplate до внутренней группировки.", potentialUnitId, unitsInTemplateForThisPotentialUnitId.Count);
// --- ВНУТРЕННЯЯ ГРУППИРОВКА ---
// Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
.GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных"))
.OrderBy(g => g.Key, StringComparer.Ordinal) // <-- Сортировка по имени внутренней группы
.ToList();
logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate (отсортировано).", potentialUnitId, innerGroupedUnitsInTemplate.Count);
// --- СОБЕРЕМ ВСЕ ИТОГОВЫЕ ПОДГРУППЫ ДЛЯ ЭТОГО 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 })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
logger.LogDebug("Внутренняя группа '{GroupingValue}' для UnitId {PotentialUnitId}: разбит на {GroupCount} подгрупп UnitsInTemplate.", groupingValueName, potentialUnitId, unitsInTemplateSubGroups.Count);
// Добавим каждую *итоговую* подгруппу в общий список
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)
{
// === 1. Получаем текущие и новые ID юнитов ===
var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
var proposedUnitIds = unitsInTemplateSubGroup.ToList();
// === 2. Сравниваем детерминированно с сортировкой по имени ===
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
var unitNamesForSort = await unitService.Get()
.AsNoTracking()
.Where(u => allUnitIdsForSort.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString());
var sortedCurrentUnitIds = currentUnitIds
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
.ToList();
var sortedProposedUnitIds = proposedUnitIds
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
.ToList();
bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds);
if (unitsAreEqual)
{
logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id);
existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList();
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup);
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = existingTemplateForSubGroup.Id,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Name = expectedName,
IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate,
IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule,
IsNew = false,
Index = globalIndexForThisUnitId,
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, 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++; // Увеличиваем индекс для следующей итоговой подгруппы
}
}
// === Деактивация ===
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) &&
t.StatusTypeId == TemplateStatusTypeEnum.Used &&
t.Job!.GroupId == jobGroupId)
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedTemplateKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {UnitId}, Index {Index}).",
existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
}
}
// === Успешное завершение ===
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
return;
}
private Job SelectTargetJob(List<Job> jobsInGroup, int subGroupSize, Job maxJob)
{
Job? targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize)
.FirstOrDefault();
if (targetJob == null)
{
targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize)
.OrderBy(j => j.MaxValueRelationships!.Value)
.FirstOrDefault();
}
if (targetJob == null)
{
targetJob = maxJob;
logger.LogDebug("Для подгруппы размером {Size} не найден подходящий Job, используем maxJob {MaxJobId}.", subGroupSize, maxJob.Id);
}
else
{
logger.LogDebug("Для подгруппы размером {Size} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", subGroupSize, targetJob.Id, targetJob.MaxValueRelationships);
}
return targetJob;
}
private async Task UpdateTemplateUnitsAsync(Template template, List<Guid> newUnitIds, Job targetJob, HistoryInitiator initiator, int newIndex)
{
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id);
return;
}
var tempTemplateForName = new Template
{
Id = template.Id,
Name = template.Name,
JobId = targetJob.Id,
UnitId = template.UnitId,
Index = newIndex,
Job = targetJob,
Unit = template.Unit,
UnitsInTemplate = newUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
};
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = targetJob.Id,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
IsNew = false,
Index = newIndex,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = newUnitIds
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
private async Task CreateGroupedTemplateAsync(Guid jobId, Guid relationshipUnitId, List<Guid> unitIds, int index, HistoryInitiator initiator)
{
logger.LogInformation("Создание нового группового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", jobId, relationshipUnitId, index, unitIds.Count);
var mqRequest = new TemplateGeneratorMq
{
JobId = jobId,
UnitId = relationshipUnitId,
UnitsInTemplate = unitIds,
Index = index,
HistoryInitiator = initiator
};
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}.", jobId, relationshipUnitId, index);
}
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
{
var status = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
}
#region Вспомогательные методы
private async Task<Guid> GetFieldIdByAihitNameAsync(string fieldName)
{
var field = await unitFieldService.GetByAihitNameAsync(fieldName);
if (field == null)
{
logger.LogError("Поле '{FieldName}' не найдено в справочнике полей.", fieldName);
throw new InvalidOperationException($"Поле '{fieldName}' не найдено в справочнике полей.");
}
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
}