refactor(templateMatcher): Переход на Pipeline-архитектуру для SimpleSync и GroupedSync.

- SimpleTemplateSynchronizer и GroupedTemplateSynchronizer переведены на паттерн Pipeline с разделением на Read/Write этапы
- Выделены контракты этапов (ISimpleSyncStage, IGroupedSyncStage) и контексты (SimpleSyncContext, GroupedSyncContext)
- Read-этапы безопасны для тестов (не пишут в БД/MQ), Write-этапы изолированы через отдельные интерфейсы
- Добавлено [Perf]-логирование каждого этапа с метриками времени выполнения
- Логи приведены к человекочитаемому формату 'Имя' (ID) для Job, JobGroup и Unit
- Устранено дублирование данных в контекстах (FilteredUnits перезаписывается, TemplateGroups строго типизирован)
- Константы неиспользуемых шаблонов вынесены в UnusedTemplateConstants
- Структура проекта реорганизована: SimpleSync, GroupedSync, Implementations, Interfaces
This commit is contained in:
Mikhail Kuznetsov
2026-06-11 15:09:46 +10:00
parent 16f4944a62
commit a438f18480
44 changed files with 1124 additions and 525 deletions

View File

@@ -0,0 +1,296 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations
{
internal class TemplateMatcher : ITemplateMatcher
{
private readonly ILogger<TemplateMatcher> _logger;
private readonly IJobRepository _jobService;
private readonly IJobGroupRepository _jobGroupService;
private readonly IEnumerable<ITemplateSynchronizer> _synchronizers;
public TemplateMatcher(
ILogger<TemplateMatcher> logger,
IJobRepository jobService,
IJobGroupRepository jobGroupService,
IEnumerable<ITemplateSynchronizer> synchronizers
)
{
_logger = logger;
_jobService = jobService;
_jobGroupService = jobGroupService;
_synchronizers = synchronizers;
}
public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{
_logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId);
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null)
{
_logger.LogError("Job с Id {JobId} не найден.", jobId);
return;
}
// Проверяем, является ли Job "групповым"
bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group;
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
{
_logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer.", jobId);
// Находим нужный синхронизатор
var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null)
{
// Так как Job групповой, вызываем синхронизацию для его JobGroup
await synchronizer.SyncTemplatesForJobGroupAsync(job.GroupId, initiator);
}
else
{
_logger.LogError("GroupedTemplateSynchronizer не найден.");
}
return;
}
else
{
_logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer.", jobId);
// Находим нужный синхронизатор
var synchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (synchronizer != null)
{
await synchronizer.SyncTemplatesForJobAsync(jobId, initiator);
}
else
{
_logger.LogError("SimpleTemplateSynchronizer не найден.");
}
return;
}
}
public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
{
_logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
var jobGroup = await _jobGroupService.Get()
.AsNoTracking()
.Include(jg => jg.GroupType)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.GroupType == null)
{
_logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
return;
}
// Определяем стратегию по типу группы
switch (jobGroup.GroupType.Code)
{
case JobGroupTypesEnum.Group:
// Проверяем, есть ли GroupingUnitFieldId — это признак "настоящей" группировки
if (jobGroup.GroupingUnitFieldId.HasValue)
{
_logger.LogInformation("JobGroup {JobGroupId} является Group с GroupingUnitFieldId. Передаём в GroupedTemplateSynchronizer.", jobGroupId);
var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null)
{
await synchronizer.SyncTemplatesForJobGroupAsync(jobGroupId, initiator);
}
else
{
_logger.LogError("GroupedTemplateSynchronizer не найден для JobGroup {JobGroupId}.", jobGroupId);
}
}
else
{
_logger.LogInformation("JobGroup {JobGroupId} является Group, но не имеет GroupingUnitFieldId. Обрабатываем как Collection.", jobGroupId);
//await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
}
break;
case JobGroupTypesEnum.Umbrella:
_logger.LogInformation("JobGroup {JobGroupId} является Umbrella. Обрабатываем как Collection (каждый Job — независимо).", jobGroupId);
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
break;
case JobGroupTypesEnum.Simple:
default:
_logger.LogInformation("JobGroup {JobGroupId} имеет тип Simple. Обрабатываем как Collection.", jobGroupId);
await SyncJobGroupAsCollectionAsync(jobGroupId, initiator);
break;
}
}
public async Task UpdateTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
{
_logger.LogDebug("Начало обновления шаблонов для JobGroup {JobGroupId}", jobGroupId);
var jobGroup = await _jobGroupService.Get()
.AsNoTracking()
.Include(jg => jg.GroupType)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.GroupType == null)
{
_logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит GroupType.", jobGroupId);
return;
}
switch (jobGroup.GroupType.Code)
{
case JobGroupTypesEnum.Group:
if (jobGroup.GroupingUnitFieldId.HasValue)
{
_logger.LogWarning("UpdateTemplatesForJobGroup не поддерживается для Group с GroupingUnitFieldId. Id: {JobGroupId}", jobGroupId);
}
else
{
_logger.LogInformation("JobGroup {JobGroupId} — Group без GroupingUnitFieldId. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
}
break;
case JobGroupTypesEnum.Umbrella:
_logger.LogInformation("JobGroup {JobGroupId} — Umbrella. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
break;
case JobGroupTypesEnum.Simple:
default:
_logger.LogInformation("JobGroup {JobGroupId} — Simple. Обновляем как Collection.", jobGroupId);
await UpdateJobGroupAsCollectionAsync(jobGroupId, initiator);
break;
}
}
public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{
_logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId);
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null)
{
_logger.LogError("Job с Id {JobId} не найден.", jobId);
return;
}
// Проверяем, является ли Job "групповым"
bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group;
if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue)
{
_logger.LogInformation("Job {JobId} является групповым. Передаём в GroupedSynchronizer для Update.", jobId);
// Находим нужный синхронизатор
var synchronizer = _synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (synchronizer != null)
{
// Вызов UpdateTemplatesForJobAsync для GroupedTemplateSynchronizer (который делает предупреждение)
await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator);
}
else
{
_logger.LogError("GroupedTemplateSynchronizer не найден.");
}
return;
}
else
{
_logger.LogInformation("Job {JobId} является обычным. Передаём в SimpleSynchronizer для Update.", jobId);
// Находим нужный синхронизатор
var synchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (synchronizer != null)
{
await synchronizer.UpdateTemplatesForJobAsync(jobId, initiator);
}
else
{
_logger.LogError("SimpleTemplateSynchronizer не найден.");
}
return;
}
}
// --- Вспомогательные методы ---
private async Task SyncJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator)
{
_logger.LogDebug("Синхронизация JobGroup {JobGroupId} как Collection (по каждому Job'у отдельно)", jobGroupId);
var jobIds = await _jobService.Get()
.AsNoTracking()
.Where(j => j.GroupId == jobGroupId)
.Select(j => j.Id)
.ToListAsync();
if (!jobIds.Any())
{
_logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
return;
}
_logger.LogDebug("Найдено {Count} Job'ов в JobGroup {JobGroupId}", jobIds.Count, jobGroupId);
var simpleSynchronizer = _synchronizers.FirstOrDefault(s => s is SimpleTemplateSynchronizer);
if (simpleSynchronizer == null)
{
_logger.LogError("SimpleTemplateSynchronizer не найден для синхронизации Job'ов в JobGroup {JobGroupId}.", jobGroupId);
return;
}
foreach (var jobId in jobIds)
{
_logger.LogDebug("Синхронизация Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId);
await simpleSynchronizer.SyncTemplatesForJobAsync(jobId, initiator);
}
_logger.LogInformation("Синхронизация JobGroup {JobGroupId} как Collection завершена.", jobGroupId);
}
private async Task UpdateJobGroupAsCollectionAsync(Guid jobGroupId, HistoryInitiator initiator)
{
_logger.LogDebug("Обновление JobGroup {JobGroupId} как Collection (по каждому Job'у)", jobGroupId);
var jobIds = await _jobService.Get()
.AsNoTracking()
.Where(j => j.GroupId == jobGroupId)
.Select(j => j.Id)
.ToListAsync();
if (!jobIds.Any())
{
_logger.LogWarning("JobGroup {JobGroupId} не содержит Job'ов.", jobGroupId);
return;
}
foreach (var jobId in jobIds)
{
_logger.LogDebug("Обновление шаблонов для Job {JobId} в рамках JobGroup {JobGroupId}", jobId, jobGroupId);
await UpdateTemplatesForJob(jobId, initiator);
}
_logger.LogInformation("Обновление JobGroup {JobGroupId} как Collection завершено.", jobGroupId);
}
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
{
return await _jobService.Get()
.AsNoTracking()
.Include(j => j.Group)
.ThenInclude(j => j!.GroupType)
.Include(j => j.AutoControl)
.FirstOrDefaultAsync(j => j.Id == jobId);
}
}
}