refactor(templateMatcher):

- унификация деактивации шаблонов между разными синхронизаторами
- поиск ЭК выделен в отдельную сущность
This commit is contained in:
Mikhail Kuznetsov
2026-10-14 14:28:58 +10:00
parent 394fc059ba
commit f9f234d119
26 changed files with 643 additions and 166 deletions

View File

@@ -21,5 +21,15 @@
/// Значение тега, обозначающее неиспользуемый юнит.
/// </summary>
public const string NotUsedTagValue = "ПАРР-НЕИСП";
/// <summary>
/// Состояние шаблонов по-умолчанию
/// </summary>
public const bool DefaultUnusedTemplateState = false;
/// <summary>
/// Состояние расписаний по-умолчанию
/// </summary>
public const bool DefaultUnusedScheduleState = false;
}
}

View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class BuildGroupsStage : IGroupedSyncStage
{

View File

@@ -2,15 +2,15 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class DeactivateTemplatesStage : IGroupedSyncWriteStage
{
private readonly ITemplateRepository _templateRepository;
private readonly ITemplateDeactivator _deactivator;
private readonly IUnusedTemplateTargetUnitResolver _targetUnitResolver;
private readonly ILogger<DeactivateTemplatesStage> _logger;
public string StageName => "Деактивация шаблонов";
@@ -18,16 +18,22 @@ internal class DeactivateTemplatesStage : IGroupedSyncWriteStage
public DeactivateTemplatesStage(
ITemplateRepository templateRepository,
ITemplateDeactivator deactivator,
IUnusedTemplateTargetUnitResolver targetUnitResolver,
ILogger<DeactivateTemplatesStage> logger)
{
_templateRepository = templateRepository;
_deactivator = deactivator;
_targetUnitResolver = targetUnitResolver;
_logger = logger;
}
public async Task<GroupedSyncContext> ExecuteAsync(GroupedSyncContext context, CancellationToken ct = default)
public async Task<GroupedSyncContext> ExecuteAsync(
GroupedSyncContext context,
CancellationToken ct = default)
{
var allJobIds = context.JobsInGroup.Select(j => j.Id).ToHashSet();
var allJobIds = context.JobsInGroup
.Select(j => j.Id)
.ToHashSet();
var existingTemplates = await _templateRepository.Get()
.AsNoTracking()
@@ -39,45 +45,76 @@ internal class DeactivateTemplatesStage : IGroupedSyncWriteStage
&& t.Job!.GroupId == context.JobGroupId)
.ToListAsync(ct);
int deactivated = 0;
foreach (var template in existingTemplates)
var staleTemplates = existingTemplates
.Where(t => !context.ExpectedTemplateKeys.Contains(
(t.JobId, t.UnitId, t.Index ?? -1)))
.ToList();
if (staleTemplates.Count == 0)
{
var key = (template.JobId, template.UnitId, template.Index ?? -1);
if (!context.ExpectedTemplateKeys.Contains(key))
_logger.LogDebug(
"JobGroup '{JobGroupName}' ({JobGroupId}): шаблонов для деактивации нет.",
context.JobGroupName,
context.JobGroupId);
return context;
}
var targetUnits = await _targetUnitResolver.ResolveAsync(staleTemplates, ct);
int deactivated = 0;
foreach (var template in staleTemplates)
{
var unitLogName = context.UnitNames.TryGetValue(template.UnitId, out var unitName)
? $"'{unitName}' ({template.UnitId})"
: template.Unit != null
? $"'{template.Unit.Name}' ({template.UnitId})"
: $"({template.UnitId})";
_logger.LogInformation(
"JobGroup '{JobGroupName}' ({JobGroupId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Job '{JobName}' ({JobId}), Unit {Unit}",
context.JobGroupName,
context.JobGroupId,
template.Name,
template.Id,
template.Job?.Name ?? string.Empty,
template.JobId,
unitLogName);
if (!targetUnits.TryGetValue(template.Id, out var targetUnit))
{
var unitLogName = context.UnitNames.TryGetValue(template.UnitId, out var unitName)
? $"'{unitName}' ({template.UnitId})"
: template.Unit != null
? $"'{template.Unit.Name}' ({template.UnitId})"
: $"({template.UnitId})";
_logger.LogError(
"Не удалось определить целевой Unit для деактивации шаблона {TemplateId}.",
template.Id);
_logger.LogInformation(
"JobGroup '{JobGroupName}' ({JobGroupId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Job '{JobName}' ({JobId}), Unit {Unit}",
context.JobGroupName, context.JobGroupId,
template.Name, template.Id,
template.Job?.Name ?? string.Empty, template.JobId,
unitLogName);
continue;
}
var success = await _deactivator.DeactivateTemplateAsync(template, context.Initiator);
var success = await _deactivator.DeactivateTemplateAsync(
template,
targetUnit,
context.Initiator,
ct);
if (success)
{
deactivated++;
}
else
{
_logger.LogError(
"JobGroup '{JobGroupName}' ({JobGroupId}): не удалось деактивировать шаблон '{TemplateName}' ({TemplateId}).",
context.JobGroupName,
context.JobGroupId,
template.Name,
template.Id);
}
if (success)
{
deactivated++;
}
else
{
_logger.LogError(
"Не удалось деактивировать шаблон {TemplateId}.",
template.Id);
}
}
_logger.LogDebug("JobGroup '{JobGroupName}' ({JobGroupId}): деактивировано {Count} шаблонов из {Total}",
context.JobGroupName, context.JobGroupId, deactivated, existingTemplates.Count);
_logger.LogDebug(
"JobGroup '{JobGroupName}' ({JobGroupId}): деактивировано {Count} шаблонов из {Total}",
context.JobGroupName,
context.JobGroupId,
deactivated,
staleTemplates.Count);
return context;
}

View File

@@ -1,8 +1,7 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Services.UnitFilterService;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class FilterUnitsStage : IGroupedSyncStage
{

View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupFilterStage : IGroupedSyncStage
{

View File

@@ -4,7 +4,7 @@ using PARR.Domain.Entities.JobEntities;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync;
/// <summary>
/// Контекст групповой синхронизации. Передаётся между этапами.

View File

@@ -1,6 +1,4 @@
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync
namespace PARR.TemplateMatcher.Services.GroupedSync
{
/// <summary>
/// Этап групповой синхронизации, который только читает данные.

View File

@@ -2,9 +2,8 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
using PARR.TemplateMatcher.Exceptions;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class LoadJobGroupStage : IGroupedSyncStage
{

View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class ProcessGroupsStage : IGroupedSyncWriteStage
{

View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.GroupedSync;
namespace PARR.TemplateMatcher.Services.Implementations.GroupedSync;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class ResolveConflictsStage : IGroupedSyncStage
{

View File

@@ -5,7 +5,6 @@ using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Exceptions;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces;
using System.Diagnostics;

View File

@@ -3,10 +3,10 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.JobRepositories;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.JobEntities;
using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums;
using PARR.Domain.Settings;
using PARR.TemplateMatcher.Services.Interfaces;
@@ -16,106 +16,113 @@ namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateDeactivator : ITemplateDeactivator
{
private const bool DefaultUnusedTemplateState = false;
private const bool DefaultUnusedScheduleState = false;
private readonly ILogger<TemplateDeactivator> _logger;
private readonly ITemplateRepository _templateRepository;
private readonly IJobRepository _jobRepository;
private readonly ITemplateMqPublisher _templateMqPublisher;
private readonly IUnusedTemplateMessageFactory _unusedTemplateMessageFactory;
private readonly SettingsFromDb _settingsFromDb;
private readonly IOptions<TemplateSettings> _templateSettings;
private readonly ILogger<TemplateDeactivator> logger;
private readonly ITemplateRepository templateService;
private readonly IJobRepository jobService;
private readonly ITemplateNameNormalizer namenormalizer;
private readonly ITemplateMqPublisher templateMqPublisher;
private readonly SettingsFromDb settingsFromDb;
private readonly IOptions<TemplateSettings> templateSettings;
private Job? _unusedJob;
public TemplateDeactivator(
ILogger<TemplateDeactivator> logger,
ITemplateRepository templateService,
IJobRepository jobService,
ITemplateNameNormalizer namenormalizer,
ITemplateRepository templateRepository,
IJobRepository jobRepository,
ITemplateMqPublisher templateMqPublisher,
IUnusedTemplateMessageFactory unusedTemplateMessageFactory,
SettingsFromDb settingsFromDb,
IOptions<TemplateSettings> templateSettings
)
IOptions<TemplateSettings> templateSettings)
{
this.logger = logger;
this.templateService = templateService;
this.jobService = jobService;
this.namenormalizer = namenormalizer;
this.templateMqPublisher = templateMqPublisher;
this.settingsFromDb = settingsFromDb;
this.templateSettings = templateSettings;
_logger = logger;
_templateRepository = templateRepository;
_jobRepository = jobRepository;
_templateMqPublisher = templateMqPublisher;
_unusedTemplateMessageFactory = unusedTemplateMessageFactory;
_settingsFromDb = settingsFromDb;
_templateSettings = templateSettings;
}
public async Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator)
public async Task<bool> DeactivateTemplateAsync(
Template template,
Unit targetUnit,
HistoryInitiator initiator,
CancellationToken ct = default)
{
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
return true;
if (template.StatusTypeId == TemplateStatusTypeEnum.Unused)
{
logger.LogDebug("Шаблон {TemplateId} уже неактивен (Unused), пропускаем деактивацию.", template.Id);
_logger.LogDebug(
"Шаблон {TemplateId} уже находится в статусе Unused. Пропускаем деактивацию.",
template.Id);
return true;
}
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.",
template.Id, template.UnitId);
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
{
_logger.LogDebug(
"Шаблон {TemplateId} уже находится в статусе Updating. Пропускаем повторную деактивацию.",
template.Id);
return true;
}
var unusedJob = await GetUnusedJobAsync(ct);
if (unusedJob == null)
{
_logger.LogError(
"Job для неиспользуемых шаблонов не найден. Шаблон {TemplateId} не деактивирован.",
template.Id);
return false;
}
_logger.LogInformation(
"Деактивация шаблона {TemplateId}. Текущий UnitId: {CurrentUnitId}, целевой UnitId: {TargetUnitId}.",
template.Id,
template.UnitId,
targetUnit.Id);
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
if (!await _templateRepository.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
_logger.LogError(
"Не удалось перевести шаблон {TemplateId} в статус Updating.",
template.Id);
return false;
}
var unusedJob = await jobService.Get()
var updateRequest = await _unusedTemplateMessageFactory.CreateAsync(
template,
unusedJob,
targetUnit,
initiator,
ct);
await _templateMqPublisher.PublishUpdateAsync(updateRequest);
return true;
}
private async Task<Job?> GetUnusedJobAsync(CancellationToken ct)
{
if (_unusedJob != null)
return _unusedJob;
_unusedJob = await _jobRepository.Get()
.AsNoTracking()
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g!.GroupType)
.FirstOrDefaultAsync(j => j.Id == settingsFromDb.JobIdForUnusedTemplates);
.FirstOrDefaultAsync(
j => j.Id == _settingsFromDb.JobIdForUnusedTemplates,
ct);
if (unusedJob == null)
{
logger.LogError("Job для деактивированных шаблонов не найден.");
return false;
}
unusedJob.TemplateNameMask = templateSettings.Value.UnusedTemplateNameMask;
// === Создаём временный Template для нормализации имени ===
var tempTemplateForName = new Template
{
Id = template.Id,
Name = template.Name,
JobId = unusedJob.Id,
UnitId = template.UnitId,
Index = template.Index,
Job = unusedJob,
Unit = template.Unit,
UnitsInTemplate = new List<UnitsInTemplate>()
};
var expectedName = await namenormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = unusedJob.Id,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = DefaultUnusedTemplateState,
IsActiveSchedule = DefaultUnusedScheduleState,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await templateMqPublisher.PublishUpdateAsync(updateRequest);
return true;
return _unusedJob;
}
}

View File

@@ -0,0 +1,83 @@
using Microsoft.Extensions.Options;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.JobEntities;
using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Constants;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class UnusedTemplateMessageFactory : IUnusedTemplateMessageFactory
{
private readonly ITemplateNameNormalizer _templateNameNormalizer;
private readonly IOptions<TemplateSettings> _templateSettings;
public UnusedTemplateMessageFactory(
ITemplateNameNormalizer templateNameNormalizer,
IOptions<TemplateSettings> templateSettings)
{
_templateNameNormalizer = templateNameNormalizer;
_templateSettings = templateSettings;
}
public async Task<TemplateUpdaterMessage> CreateAsync(
Template template,
Job unusedJob,
Unit targetUnit,
HistoryInitiator initiator,
CancellationToken ct = default)
{
var jobForName = new Job
{
Id = unusedJob.Id,
Name = unusedJob.Name,
WorkName = unusedJob.WorkName,
MinValueRelationships = unusedJob.MinValueRelationships,
MaxValueRelationships = unusedJob.MaxValueRelationships,
IsParentRelationships = unusedJob.IsParentRelationships,
TemplateNameMask = _templateSettings.Value.UnusedTemplateNameMask,
WorkGroupMask = unusedJob.WorkGroupMask,
ResponseAreaMask = unusedJob.ResponseAreaMask,
TnkId = unusedJob.TnkId,
GroupId = unusedJob.GroupId,
Group = unusedJob.Group,
Tnk = unusedJob.Tnk,
UnitFilters = unusedJob.UnitFilters,
Templates = unusedJob.Templates,
AutoControl = unusedJob.AutoControl
};
var templateForName = new Template
{
Id = template.Id,
Name = template.Name,
JobId = unusedJob.Id,
UnitId = targetUnit.Id,
Index = null,
Job = jobForName,
Unit = targetUnit,
UnitsInTemplate = new List<UnitsInTemplate>()
};
var expectedName = await _templateNameNormalizer.GetNormalizedTemplateNameAsync(templateForName);
return new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = unusedJob.Id,
UnitId = targetUnit.Id,
Name = expectedName,
IsActiveTemplate = UnusedTemplateConstants.DefaultUnusedTemplateState,
IsActiveSchedule = UnusedTemplateConstants.DefaultUnusedScheduleState,
IsNew = false,
Index = null,
StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
}
}

View File

@@ -0,0 +1,209 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Unit;
using PARR.TemplateMatcher.Constants;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class UnusedTemplateTargetUnitResolver : IUnusedTemplateTargetUnitResolver
{
private readonly IUnitFieldRepository _unitFieldRepository;
private readonly IUnitInValueRepository _unitInValueRepository;
private readonly IUnitRepository _unitRepository;
private readonly ILogger<UnusedTemplateTargetUnitResolver> _logger;
public UnusedTemplateTargetUnitResolver(
IUnitFieldRepository unitFieldRepository,
IUnitInValueRepository unitInValueRepository,
IUnitRepository unitRepository,
ILogger<UnusedTemplateTargetUnitResolver> logger)
{
_unitFieldRepository = unitFieldRepository;
_unitInValueRepository = unitInValueRepository;
_unitRepository = unitRepository;
_logger = logger;
}
public async Task<IReadOnlyDictionary<Guid, Unit>> ResolveAsync(
IReadOnlyCollection<Template> templates,
CancellationToken ct = default)
{
var result = new Dictionary<Guid, Unit>();
foreach (var template in templates.Where(t => t.Unit != null))
{
result[template.Id] = template.Unit!;
}
if (templates.Count == 0)
return result;
var responsableAreaField = await _unitFieldRepository.GetByAihitNameAsync(
UnusedTemplateConstants.ResponsibilityAreaFieldName,
ct);
var tagField = await _unitFieldRepository.GetByAihitNameAsync(
UnusedTemplateConstants.ParrTagFieldName,
ct);
if (responsableAreaField == null || tagField == null)
{
_logger.LogWarning(
"Не найдены поля '{ResponsibilityAreaField}' или '{TagField}'. Целевой Unit для неиспользуемых шаблонов будет выбран как текущий.",
UnusedTemplateConstants.ResponsibilityAreaFieldName,
UnusedTemplateConstants.ParrTagFieldName);
await AddFallbackUnitsAsync(templates, result, ct);
return result;
}
var targetTagValueId = await _unitInValueRepository.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == tagField.Id
&& uiv.Value != null
&& uiv.Value.Value == UnusedTemplateConstants.NotUsedTagValue)
.Select(uiv => uiv.ValueId)
.FirstOrDefaultAsync(ct);
if (targetTagValueId == Guid.Empty)
{
_logger.LogWarning(
"Значение '{TagValue}' для поля '{FieldName}' не найдено. Целевой Unit для неиспользуемых шаблонов будет выбран как текущий.",
UnusedTemplateConstants.NotUsedTagValue,
UnusedTemplateConstants.ParrTagFieldName);
await AddFallbackUnitsAsync(templates, result, ct);
return result;
}
var unitIds = templates
.Select(t => t.UnitId)
.Distinct()
.ToList();
var unitResponsableAreaValues = await _unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(
unitIds,
new List<Guid> { responsableAreaField.Id },
ct);
var unitToResponsableAreaValueMap = unitResponsableAreaValues
.Where(uiv => uiv.ValueId != Guid.Empty)
.ToDictionary(uiv => uiv.UnitId, uiv => uiv.ValueId);
if (unitToResponsableAreaValueMap.Count == 0)
{
_logger.LogDebug(
"Для юнитов не найдены значения поля '{FieldName}'. Целевой Unit для неиспользуемых шаблонов будет выбран как текущий.",
UnusedTemplateConstants.ResponsibilityAreaFieldName);
await AddFallbackUnitsAsync(templates, result, ct);
return result;
}
var distinctResponsableAreaValues = unitToResponsableAreaValueMap.Values
.Distinct()
.ToList();
var matches = await _unitInValueRepository.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == responsableAreaField.Id
&& distinctResponsableAreaValues.Contains(uiv.ValueId))
.Join(
_unitInValueRepository.Get()
.AsNoTracking()
.Where(t => t.FieldId == tagField.Id && t.ValueId == targetTagValueId),
responsableArea => responsableArea.UnitId,
tag => tag.UnitId,
(responsableArea, tag) => new
{
responsableArea.ValueId,
responsableArea.UnitId
})
.ToListAsync(ct);
var responsableAreaToTargetUnitMap = matches
.GroupBy(x => x.ValueId)
.ToDictionary(g => g.Key, g => g.First().UnitId);
var templatesToReload = new Dictionary<Guid, Guid>();
foreach (var template in templates)
{
if (!unitToResponsableAreaValueMap.TryGetValue(template.UnitId, out var responsableAreaValueId))
continue;
if (!responsableAreaToTargetUnitMap.TryGetValue(responsableAreaValueId, out var targetUnitId))
continue;
if (targetUnitId == Guid.Empty || targetUnitId == template.UnitId)
continue;
templatesToReload[template.Id] = targetUnitId;
}
if (templatesToReload.Count > 0)
{
var targetUnitIds = templatesToReload.Values
.Distinct()
.ToList();
var targetUnits = await _unitRepository.Get()
.AsNoTracking()
.Where(u => targetUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, ct);
foreach (var kvp in templatesToReload)
{
if (targetUnits.TryGetValue(kvp.Value, out var targetUnit))
{
result[kvp.Key] = targetUnit;
}
else
{
_logger.LogWarning(
"Не удалось загрузить целевой Unit {TargetUnitId} для шаблона {TemplateId}.",
kvp.Value,
kvp.Key);
}
}
}
await AddFallbackUnitsAsync(templates, result, ct);
return result;
}
private async Task AddFallbackUnitsAsync(
IReadOnlyCollection<Template> templates,
Dictionary<Guid, Unit> result,
CancellationToken ct)
{
var missingUnitIds = templates
.Where(t => !result.ContainsKey(t.Id) && t.UnitId != Guid.Empty)
.Select(t => t.UnitId)
.Distinct()
.ToList();
if (missingUnitIds.Count == 0)
return;
var fallbackUnits = await _unitRepository.Get()
.AsNoTracking()
.Where(u => missingUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, ct);
foreach (var template in templates.Where(t => !result.ContainsKey(t.Id) && t.UnitId != Guid.Empty))
{
if (fallbackUnits.TryGetValue(template.UnitId, out var fallbackUnit))
{
result[template.Id] = fallbackUnit;
}
}
}
}

View File

@@ -1,10 +1,15 @@
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Unit;
namespace PARR.TemplateMatcher.Services.Interfaces
{
public interface ITemplateDeactivator
{
Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator);
Task<bool> DeactivateTemplateAsync(
Template template,
Unit targetUnit,
HistoryInitiator initiator,
CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,18 @@
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.JobEntities;
using PARR.Domain.Entities.Unit;
namespace PARR.TemplateMatcher.Services.Interfaces
{
internal interface IUnusedTemplateMessageFactory
{
Task<TemplateUpdaterMessage> CreateAsync(
Template template,
Job unusedJob,
Unit targetUnit,
HistoryInitiator initiator,
CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,12 @@
using PARR.Domain.Entities;
using PARR.Domain.Entities.Unit;
namespace PARR.TemplateMatcher.Services.Interfaces
{
internal interface IUnusedTemplateTargetUnitResolver
{
Task<IReadOnlyDictionary<Guid, Unit>> ResolveAsync(
IReadOnlyCollection<Template> templates,
CancellationToken ct = default);
}
}

View File

@@ -1,10 +1,10 @@
using Microsoft.Extensions.Logging;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Implementations;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
namespace PARR.TemplateMatcher.Services.SimpleSync;
internal class AllocateTemplatesStage : ISimpleSyncWriteStage
{

View File

@@ -3,9 +3,8 @@ using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
namespace PARR.TemplateMatcher.Services.SimpleSync;
internal class AnalyzeChangesStage : ISimpleSyncStage
{

View File

@@ -1,33 +1,81 @@
using Microsoft.Extensions.Logging;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
namespace PARR.TemplateMatcher.Services.SimpleSync;
internal class DeactivateTemplatesStage : ISimpleSyncWriteStage
{
private readonly ITemplateDeactivator _deactivator;
private readonly IUnusedTemplateTargetUnitResolver _targetUnitResolver;
private readonly ILogger<DeactivateTemplatesStage> _logger;
public string StageName => "Деактивация шаблонов";
public DeactivateTemplatesStage(ITemplateDeactivator deactivator, ILogger<DeactivateTemplatesStage> logger)
public DeactivateTemplatesStage(
ITemplateDeactivator deactivator,
IUnusedTemplateTargetUnitResolver targetUnitResolver,
ILogger<DeactivateTemplatesStage> logger)
{
_deactivator = deactivator;
_targetUnitResolver = targetUnitResolver;
_logger = logger;
}
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
public async Task<SimpleSyncContext> ExecuteAsync(
SimpleSyncContext context,
CancellationToken ct = default)
{
if (!context.UnusedTemplates.Any())
return context;
var targetUnits = await _targetUnitResolver.ResolveAsync(context.UnusedTemplates, ct);
int deactivated = 0;
foreach (var template in context.UnusedTemplates)
{
_logger.LogInformation("Job '{JobName}' ({JobId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Unit {Unit}",
context.JobName, context.JobId, template.Name, template.Id,
_logger.LogInformation(
"Job '{JobName}' ({JobId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Unit {Unit}",
context.JobName,
context.JobId,
template.Name,
template.Id,
context.FormatUnit(template.UnitId));
await _deactivator.DeactivateTemplateAsync(template, context.Initiator);
if (!targetUnits.TryGetValue(template.Id, out var targetUnit))
{
_logger.LogError(
"Не удалось определить целевой Unit для деактивации шаблона {TemplateId}.",
template.Id);
continue;
}
var success = await _deactivator.DeactivateTemplateAsync(
template,
targetUnit,
context.Initiator,
ct);
if (success)
{
deactivated++;
}
else
{
_logger.LogError(
"Не удалось деактивировать шаблон {TemplateId}.",
template.Id);
}
}
_logger.LogDebug(
"Job '{JobName}' ({JobId}): деактивировано {Count} шаблонов из {Total}.",
context.JobName,
context.JobId,
deactivated,
context.UnusedTemplates.Count);
return context;
}
}

View File

@@ -1,8 +1,7 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Services.UnitFilterService;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
namespace PARR.TemplateMatcher.Services.SimpleSync;
internal class FilterUnitsStage : ISimpleSyncStage
{

View File

@@ -1,9 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.JobRepositories;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
namespace PARR.TemplateMatcher.Services.SimpleSync;
internal class LoadJobStage : ISimpleSyncStage
{

View File

@@ -2,9 +2,8 @@
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Services.SimpleSync;
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
namespace PARR.TemplateMatcher.Services.SimpleSync;
internal class UpdateNamesStage : ISimpleSyncWriteStage
{

View File

@@ -35,27 +35,27 @@ namespace PARR.TemplateMatcher
services.AddScoped<ITemplateMatcher, Services.Implementations.TemplateMatcher>();
// Simple pipeline: Read-этапы
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.LoadJobStage>(); //1
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.FilterUnitsStage>(); //2
services.AddScoped<ISimpleSyncStage, Services.Implementations.SimpleSync.AnalyzeChangesStage>(); //3
services.AddScoped<ISimpleSyncStage, LoadJobStage>(); //1
services.AddScoped<ISimpleSyncStage, Services.SimpleSync.FilterUnitsStage>(); //2
services.AddScoped<ISimpleSyncStage, AnalyzeChangesStage>(); //3
// Simple pipeline: Write-этапы
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.AllocateTemplatesStage>(); //4
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.UpdateNamesStage>(); //5
services.AddScoped<ISimpleSyncWriteStage, Services.Implementations.SimpleSync.DeactivateTemplatesStage>(); //6
services.AddScoped<ISimpleSyncWriteStage, AllocateTemplatesStage>(); //4
services.AddScoped<ISimpleSyncWriteStage, UpdateNamesStage>(); //5
services.AddScoped<ISimpleSyncWriteStage, Services.SimpleSync.DeactivateTemplatesStage>(); //6
services.AddScoped<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
// Grouped pipeline: Read-этапы
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.LoadJobGroupStage>(); //1
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.FilterUnitsStage>(); //2
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.GroupFilterStage>(); //3
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.ResolveConflictsStage>(); //4
services.AddScoped<IGroupedSyncStage, Services.Implementations.GroupedSync.BuildGroupsStage>(); //5
services.AddScoped<IGroupedSyncStage, LoadJobGroupStage>(); //1
services.AddScoped<IGroupedSyncStage, Services.GroupedSync.FilterUnitsStage>(); //2
services.AddScoped<IGroupedSyncStage, GroupFilterStage>(); //3
services.AddScoped<IGroupedSyncStage, ResolveConflictsStage>(); //4
services.AddScoped<IGroupedSyncStage, BuildGroupsStage>(); //5
// Grouped pipeline: Write-этапы
services.AddScoped<IGroupedSyncWriteStage, Services.Implementations.GroupedSync.ProcessGroupsStage>(); //6
services.AddScoped<IGroupedSyncWriteStage, Services.Implementations.GroupedSync.DeactivateTemplatesStage>();//7
services.AddScoped<IGroupedSyncWriteStage, ProcessGroupsStage>(); //6
services.AddScoped<IGroupedSyncWriteStage, Services.GroupedSync.DeactivateTemplatesStage>();//7
services.AddScoped<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
@@ -81,6 +81,8 @@ namespace PARR.TemplateMatcher
//Сервис для неактуальных шаблонов
services.AddScoped<IUnusedTemplatesSyncService, UnusedTemplatesSyncService>();
services.AddTransient<IUnusedTemplateMessageFactory, UnusedTemplateMessageFactory>();
services.AddScoped<IUnusedTemplateTargetUnitResolver, UnusedTemplateTargetUnitResolver>();
}
public static IConfigurationBuilder AddTemplateMatcherConfigurations(this IConfigurationBuilder builder, IServiceCollection services)

View File

@@ -25,4 +25,8 @@
<ProjectReference Include="..\PARR.TemplateDistributor\PARR.TemplateDistributor.csproj" />
<ProjectReference Include="..\PARR.TemplateMatcher\PARR.TemplateMatcher.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="log\" />
</ItemGroup>
</Project>

View File

@@ -1,8 +1,6 @@
using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.JobGroupRepositories;
using PARR.Core.Repositories.Interfaces.JobRepositories;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.JobEntities;
using PARR.Domain.Enums;
using PARR.EsppApi;
using PARR.EsppApi.Constants;
@@ -43,12 +41,19 @@ namespace PARR.Test
//var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0));
await TemplateMatcherTest();
//await TemplateMatcherTest();
await using var scope = serviceProvider.CreateAsyncScope();
//var unitFilterService = scope.ServiceProvider.GetRequiredService<IUnitFilterService>();
//var t = await unitFilterService.GetRelatedUnitNamesAsync(Guid.Parse("4c35d6c3-f07d-4910-98af-b88247d22247"), Guid.Parse("4d4a1503-3a56-4e2a-bf92-a615c91c14ef"));
//t.ForEach(unit =>
//{
// _logger.LogInformation(unit);
//});
//await NextRunTest();
//await GroupedSyncTest();
await SimpleSyncTest();
#region test
//using (var scope = serviceProvider.CreateScope())
@@ -164,7 +169,6 @@ namespace PARR.Test
}
#region SimpleSyncTest
private async Task SimpleSyncTest()
{
await using var scope = serviceProvider.CreateAsyncScope();
@@ -182,7 +186,7 @@ namespace PARR.Test
{
//Guid.Parse("cfb0d2dd-192a-4ed5-a29c-25f0abf61895"),//11010
//Guid.Parse("79976eaf-be7b-42a0-a4e3-a89d7f68a18e"),//17
Guid.Parse("055ac0cc-10f7-4f96-baf6-9c8145b1332a")//318
Guid.Parse("edcabe55-7f89-45ae-b946-d136b8594c53")//48
};
if (!jobIds.Any())
@@ -207,7 +211,8 @@ namespace PARR.Test
InitiatorIp = "127.0.0.1",
InitiatorParrComponentId = ParrComponentsEnum.Master,
InitiatorComment = "Тестовая синхронизация SimpleTemplateSynchronizer"
});
}, dryRun: false
);
sw.Stop();
passed++;
@@ -236,7 +241,7 @@ namespace PARR.Test
var templateMatcher = scope.ServiceProvider.GetRequiredService<ITemplateMatcher>();
var groupRepository = scope.ServiceProvider.GetRequiredService<IJobGroupRepository>();
await templateMatcher.SyncTemplatesForJob(Guid.Parse("055ac0cc-10f7-4f96-baf6-9c8145b1332a"),
await templateMatcher.SyncTemplatesForJobGroup(Guid.Parse("9ed70f1e-69f6-4559-9139-b4947cce8d54"),
new HistoryInitiator
{
InitiatorIp = "10.99.246.156",
@@ -267,6 +272,56 @@ namespace PARR.Test
return;
}
#endregion
#region GroupedSyncTest
private async Task GroupedSyncTest()
{
_logger.LogInformation("=== DryRun-тестирование GroupedTemplateSynchronizer ===");
await using var scope = serviceProvider.CreateAsyncScope();
var synchronizers = scope.ServiceProvider.GetRequiredService<IEnumerable<ITemplateSynchronizer>>();
var groupedSync = synchronizers.FirstOrDefault(s => s is GroupedTemplateSynchronizer);
if (groupedSync == null)
{
_logger.LogError("[FAIL] GroupedTemplateSynchronizer не найден в DI");
return;
}
// Подставьте ID нужных JobGroup для тестирования
var jobGroupIds = new List<Guid>
{
Guid.Parse("fb8678d0-aa8b-457d-8ea8-6452040c0692"), // Анализ производительности БД
// Добавьте другие ID для проверки
};
foreach (var jobGroupId in jobGroupIds)
{
_logger.LogInformation("--- DryRun: JobGroup {JobGroupId} ---", jobGroupId);
try
{
await groupedSync.SyncTemplatesForJobGroupAsync(
jobGroupId,
new HistoryInitiator
{
InitiatorIp = "127.0.0.1",
InitiatorParrComponentId = ParrComponentsEnum.Master,
InitiatorComment = "DryRun-тест"
},
dryRun: true // Ключевой флаг
);
}
catch (Exception ex)
{
_logger.LogError(ex, "[FAIL] DryRun для JobGroup {JobGroupId}", jobGroupId);
}
_logger.LogInformation("--- DryRun завершён ---");
_logger.LogInformation("");
}
}
#endregion
private async Task NextRunTest()
{