2026-05-14 16:47:27 +10:00
using Microsoft.EntityFrameworkCore ;
using Microsoft.Extensions.Logging ;
using PARR.Core.Repositories.Interfaces ;
using PARR.Core.Repositories.Interfaces.Job ;
using PARR.Core.Services.MatchingStatusService ;
using PARR.Core.Services.UnitFilterService ;
using PARR.Domain.Cache.Models ;
using PARR.Domain.Entities.Base.History ;
using PARR.Domain.Entities.Job ;
using PARR.Domain.Enums ;
using PARR.TemplateMatcher.Services.Interfaces ;
namespace PARR.TemplateMatcher.Services.Implementations ;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private readonly ILogger < GroupedTemplateSynchronizer > logger ;
private readonly IJobGroupRepository jobGroupService ;
private readonly IUnitFilterService unitFilterService ;
private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter ;
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper ;
private readonly IGroupedTemplateBuilder groupedTemplateBuilder ;
private readonly IGroupedTemplateProcessor groupedTemplateProcessor ;
private readonly ITemplateRepository templateService ;
private readonly ITemplateDeactivator templateDeactivator ;
private readonly IMatchingStatusService matchingStatusService ;
public GroupedTemplateSynchronizer (
ILogger < GroupedTemplateSynchronizer > logger ,
IJobGroupRepository jobGroupService ,
IUnitFilterService unitFilterService ,
IGroupedTemplateUnitFilter groupedTemplateUnitFilter ,
IUnitInTemplateConflictMapper unitInTemplateConflictMapper ,
IGroupedTemplateBuilder groupedTemplateBuilder ,
IGroupedTemplateProcessor groupedTemplateProcessor ,
ITemplateRepository templateService ,
ITemplateDeactivator templateDeactivator ,
IMatchingStatusService matchingStatusService )
{
this . logger = logger ;
this . jobGroupService = jobGroupService ;
this . unitFilterService = unitFilterService ;
this . groupedTemplateUnitFilter = groupedTemplateUnitFilter ;
this . unitInTemplateConflictMapper = unitInTemplateConflictMapper ;
this . groupedTemplateBuilder = groupedTemplateBuilder ;
this . groupedTemplateProcessor = groupedTemplateProcessor ;
this . templateService = templateService ;
this . templateDeactivator = templateDeactivator ;
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 )
{
2026-05-18 12:29:55 +10:00
logger . LogInformation ( "Начало синхронизации шаблонов для JobGroup {JobGroupId}" , jobGroupId ) ;
2026-05-14 16:47:27 +10:00
// === Проверка: уже запущена? ===
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
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 ) ;
return ;
}
// 4. Применение специфичных правил фильтрации для групповых шаблонов
logger . LogDebug ( "Применение специфичных правил фильтрации для групповых шаблонов." ) ;
var finalFilteredUnits = await groupedTemplateUnitFilter . FilterAsync ( unitFilterResults , jobGroup ) ;
if ( ! finalFilteredUnits . Any ( ) )
{
logger . LogInformation ( "После применения правил фильтрации в JobGroup {JobGroupId} не осталось юнитов." , jobGroupId ) ;
await UpdateMatchingStatusAsync ( jobGroupId , "Нет юнитов после фильтрации" ) ;
await matchingStatusService . DeleteMatchingStatusAsync ( jobGroupId , SyncTaskEntityTypeEnum . JobGroup ) ;
return ;
}
// 5. Разрешение конфликтов связей и построение первичного маппинга
logger . LogDebug ( "Разрешение конфликтов связей и построение первичного маппинга." ) ;
var initialReverseMapping = await unitInTemplateConflictMapper . BuildMappingAsync ( finalFilteredUnits , maxJob ) ;
if ( ! initialReverseMapping . Any ( ) )
{
logger . LogInformation ( "После разрешения конфликтов в JobGroup {JobGroupId} не осталось связей." , jobGroupId ) ;
await UpdateMatchingStatusAsync ( jobGroupId , "Нет связей после разрешения конфликтов" ) ;
return ;
}
// 6. Построение структуры групп (трансформация, внутренняя группировка, разбиение)
logger . LogDebug ( "Построение структуры групп для шаблонов." ) ;
var templateGroups = await groupedTemplateBuilder . BuildAsync ( initialReverseMapping , jobGroup , maxJob ) ;
if ( ! templateGroups . Any ( ) )
{
logger . LogInformation ( "После построения структуры групп в JobGroup {JobGroupId} не осталось данных." , jobGroupId ) ;
await UpdateMatchingStatusAsync ( jobGroupId , "Нет данных после построения групп" ) ;
return ;
}
// 7. Обработка групп: сравнение, обновление, создание, отправка MQ
logger . LogDebug ( "Обработка групп шаблонов: сравнение, обновление и создание." ) ;
var expectedTemplateKeys = await groupedTemplateProcessor . ProcessAsync (
templateGroups ,
jobsInGroup ,
maxJob ,
initiator ) ;
// 8. Деактивация лишних шаблонов
await DeactivateUnusedTemplatesAsync ( expectedTemplateKeys , jobGroupId , jobsInGroup , initiator ) ;
// 9. Успешное завершение
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 ) ;
}
private async Task DeactivateUnusedTemplatesAsync (
HashSet < ( Guid JobId , Guid UnitId , int Index ) > expectedKeys ,
Guid jobGroupId ,
List < Job > jobsInGroup ,
HistoryInitiator initiator )
{
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 ( ! expectedKeys . Contains ( key ) )
{
logger . LogInformation ( "Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Unit {UnitId}, Index {Index})." ,
existingTemplate . Id , existingTemplate . JobId , existingTemplate . UnitId , existingTemplate . Index ) ;
await templateDeactivator . DeactivateTemplateAsync ( existingTemplate , initiator ) ;
}
}
}
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 )
) ;
}
}