640 lines
37 KiB
C#
640 lines
37 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using PARR.BLL.Domain.Mq;
|
||
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.Services.Interfaces;
|
||
using PARR.DAL.Services.Interfaces.Job;
|
||
using PARR.DAL.Services.Interfaces.Unit;
|
||
|
||
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 IUnitInValueService unitInValueService;
|
||
private readonly IUnitService unitService;
|
||
private readonly MqSettings mqSettings;
|
||
private readonly IMqService mqService;
|
||
private readonly ITemplateService templateService;
|
||
private readonly IJobGroupService jobGroupService;
|
||
private readonly ITemplateReuser templateReuser;
|
||
private readonly IUnitRegionalEkPtkGroupService regionalEkPtkGroupService;
|
||
private readonly IUnitFieldService unitFieldService;
|
||
private readonly ITemplateDeactivator templateDeactivator;
|
||
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,
|
||
IUnitInValueService unitInValueService,
|
||
IUnitService unitService,
|
||
MqSettings mqSettings,
|
||
IMqService mqService,
|
||
ITemplateService templateService,
|
||
IJobGroupService jobGroupService,
|
||
ITemplateReuser templateReuser,
|
||
IUnitRegionalEkPtkGroupService regionalEkPtkGroupService,
|
||
IUnitFieldService unitFieldService,
|
||
ITemplateDeactivator templateDeactivator,
|
||
ITemplateNameNormalizer templateNameNormalizer,
|
||
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
||
IMatchingStatusService matchingStatusService,
|
||
IShortcodesService shortcodesService
|
||
//INextRunService nextRunService
|
||
)
|
||
{
|
||
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;
|
||
this.shortcodesService = shortcodesService;
|
||
//this.nextRunService = nextRunService;
|
||
}
|
||
|
||
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'ов с подходящими связями");
|
||
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 = new Dictionary<Guid, List<Guid>>();
|
||
foreach (var dto in finalFilteredUnitFilterResults)
|
||
{
|
||
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.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, "Нет данных"))
|
||
.ToList();
|
||
|
||
logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate.", potentialUnitId, innerGroupedUnitsInTemplate.Count);
|
||
|
||
// 8. Цикл по внутренним группам UnitsInTemplate
|
||
foreach (var innerGroup in innerGroupedUnitsInTemplate)
|
||
{
|
||
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
|
||
{
|
||
var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции
|
||
var subGroupSize = unitsInTemplateSubGroup.Count;
|
||
logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", i, groupingValueName, potentialUnitId, subGroupSize);
|
||
|
||
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
|
||
|
||
// --- Добавляем ключ в список ожидаемых прямо здесь ---
|
||
expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, i)); // potentialUnitId - это UnitId шаблона
|
||
|
||
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 == 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 = 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 = 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 = 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 = i,
|
||
UnitsInTemplate = unitsInTemplateSubGroup
|
||
};
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// === Деактивация ===
|
||
// Теперь expectedTemplateKeys уже собран
|
||
|
||
// Получаем ВСЕ шаблоны для JobGroup (не только для текущих potentialUnitIds)
|
||
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 nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
||
var updateRequest = new TemplateUpdaterMq
|
||
{
|
||
TemplateId = template.Id,
|
||
JobId = targetJob.Id,
|
||
UnitId = template.UnitId,
|
||
Name = expectedName,
|
||
IsActiveTemplate = template.IsActiveTemplate,
|
||
IsActiveSchedule = template.IsActiveSchedule,
|
||
//LastRun = template.LastRun,
|
||
//NextRun = nextRun,
|
||
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;
|
||
}
|
||
#endregion
|
||
} |