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:
@@ -1,216 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Core.Services.Shortcodes;
|
||||
using PARR.Domain.Constants;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
|
||||
internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
|
||||
{
|
||||
private readonly ILogger<GroupedTemplateBuilder> logger;
|
||||
private readonly IUnitInValueRepository unitInValueRepository;
|
||||
private readonly IUnitFieldRepository unitFieldRepository;
|
||||
private readonly IShortcodesService shortcodesService;
|
||||
|
||||
public GroupedTemplateBuilder(
|
||||
ILogger<GroupedTemplateBuilder> logger,
|
||||
IUnitInValueRepository unitInValueRepository,
|
||||
IUnitFieldRepository unitFieldRepository,
|
||||
IShortcodesService shortcodesService
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.unitInValueRepository = unitInValueRepository;
|
||||
this.unitFieldRepository = unitFieldRepository;
|
||||
this.shortcodesService = shortcodesService;
|
||||
}
|
||||
|
||||
public async Task<List<GroupedTemplateGroup>> BuildAsync(
|
||||
Dictionary<Guid, List<Guid>> initialReverseMapping,
|
||||
JobGroup jobGroup,
|
||||
Job maxJob,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
logger.LogDebug("Начало построения структуры групп для JobGroup {JobGroupId}.", jobGroup.Id);
|
||||
|
||||
if (!initialReverseMapping.Any())
|
||||
return new List<GroupedTemplateGroup>();
|
||||
|
||||
// 1. Определяем стратегию внутренней группировки
|
||||
// Если IsGroupByResponsible != true, используем WorkGroupMask через ShortcodesService
|
||||
bool useWorkGroupMask = jobGroup.IsGroupByResponsible != true;
|
||||
|
||||
logger.LogDebug("Стратегия внутренней группировки: {Strategy}",
|
||||
useWorkGroupMask ? "WorkGroupMask" : "Поле 'Ответственный за ЭК'");
|
||||
|
||||
// 2. Собираем все исходные UnitId
|
||||
var allSourceUnitIds = initialReverseMapping.Values.SelectMany(ids => ids).Distinct().ToList();
|
||||
|
||||
// 3. Загружаем UnitInValue для трансформации по полю группировки из настроек JobGroup
|
||||
var groupingFieldId = jobGroup.GroupingUnitFieldId!.Value;
|
||||
var relevantUnitInValues = await unitInValueRepository.Get()
|
||||
.AsNoTracking()
|
||||
.Where(uiv => allSourceUnitIds.Contains(uiv.UnitId) && uiv.FieldId == groupingFieldId)
|
||||
.OrderBy(uiv => uiv.UnitId)
|
||||
.ThenBy(uiv => uiv.ValueId)
|
||||
.Select(uiv => new { uiv.UnitId, uiv.ValueId })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var uivLookup = relevantUnitInValues
|
||||
.GroupBy(x => x.UnitId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(x => x.ValueId).ToList());
|
||||
|
||||
// 4. Трансформируем в reverseMapping с парами
|
||||
var reverseMapping = new Dictionary<Guid, List<(Guid UnitId, Guid UnitFieldValueId)>>();
|
||||
|
||||
foreach (var kvp in initialReverseMapping.OrderBy(k => k.Key))
|
||||
{
|
||||
var potentialUnitId = kvp.Key;
|
||||
var sourceDtoIds = kvp.Value;
|
||||
var entries = new List<(Guid UnitId, Guid UnitFieldValueId)>();
|
||||
|
||||
foreach (var dtoId in sourceDtoIds)
|
||||
{
|
||||
if (uivLookup.TryGetValue(dtoId, out var valueIds))
|
||||
{
|
||||
foreach (Guid valId in valueIds)
|
||||
entries.Add((UnitId: dtoId, UnitFieldValueId: valId));
|
||||
}
|
||||
}
|
||||
|
||||
var uniqueEntries = entries
|
||||
.GroupBy(e => (e.UnitId, e.UnitFieldValueId))
|
||||
.Select(g => g.First())
|
||||
.OrderBy(e => e.UnitId)
|
||||
.ThenBy(e => e.UnitFieldValueId)
|
||||
.ToList();
|
||||
|
||||
if (uniqueEntries.Any())
|
||||
reverseMapping[potentialUnitId] = uniqueEntries;
|
||||
}
|
||||
|
||||
if (!reverseMapping.Any())
|
||||
return new List<GroupedTemplateGroup>();
|
||||
|
||||
// 5. Определяем значения для внутренней группировки
|
||||
Dictionary<Guid, string> unitIdToGroupingValueMap;
|
||||
|
||||
if (useWorkGroupMask)
|
||||
{
|
||||
// Используем ShortcodesService для получения финальных значений WorkGroupMask
|
||||
unitIdToGroupingValueMap = new Dictionary<Guid, string>();
|
||||
|
||||
foreach (var potentialUnitId in reverseMapping.Keys)
|
||||
{
|
||||
var relatedUnitIds = reverseMapping[potentialUnitId].Select(e => e.UnitId).Distinct().ToList();
|
||||
|
||||
foreach (var relatedUnitId in relatedUnitIds)
|
||||
{
|
||||
// Создаем временный Template для применения шорткодов
|
||||
var tempTemplate = new Template
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "temp",
|
||||
JobId = maxJob.Id,
|
||||
UnitId = potentialUnitId, // Родительский юнит шаблона
|
||||
Job = maxJob,
|
||||
UnitsInTemplate = new List<UnitsInTemplate>
|
||||
{
|
||||
new UnitsInTemplate { UnitId = relatedUnitId, UnitFieldValueId = Guid.Empty }
|
||||
}
|
||||
};
|
||||
|
||||
var workGroupValue = await shortcodesService.ApplyShortcodesAsync(
|
||||
maxJob.WorkGroupMask,
|
||||
tempTemplate,
|
||||
nameof(GroupedTemplateBuilder));
|
||||
|
||||
unitIdToGroupingValueMap[relatedUnitId] = workGroupValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Логика разделения по ответственному за ЭК через служебный код поля
|
||||
var innerGroupingField = await unitFieldRepository.GetByCodeAsync(UnitFieldCodes.Responsible, ct)
|
||||
?? throw new InvalidOperationException($"Поле с кодом '{UnitFieldCodes.Responsible}' не найдено в справочнике UnitField.");
|
||||
|
||||
var innerGroupingFieldId = innerGroupingField.Id;
|
||||
var allUnitsInTemplatePairs = reverseMapping.Values.SelectMany(list => list).ToList();
|
||||
var allUnitIdsForInnerGrouping = allUnitsInTemplatePairs.Select(e => e.UnitId).Distinct().ToList();
|
||||
|
||||
var innerGroupingValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(
|
||||
allUnitIdsForInnerGrouping,
|
||||
new HashSet<Guid> { innerGroupingFieldId },
|
||||
ct);
|
||||
|
||||
unitIdToGroupingValueMap = innerGroupingValues
|
||||
.Where(uv => uv.Value != null && uv.Value.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
|
||||
.ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value!);
|
||||
}
|
||||
|
||||
// 6. Формируем итоговую структуру
|
||||
var templateGroups = new List<GroupedTemplateGroup>();
|
||||
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
|
||||
|
||||
foreach (var kvp in reverseMapping.OrderBy(k => k.Key))
|
||||
{
|
||||
var potentialUnitId = kvp.Key;
|
||||
var unitsInTemplateForThisPotentialUnitId = kvp.Value;
|
||||
|
||||
var innerGroupedUnits = unitsInTemplateForThisPotentialUnitId
|
||||
.GroupBy(entry => unitIdToGroupingValueMap.GetValueOrDefault(entry.UnitId, "Нет данных"))
|
||||
.OrderBy(g => g.Key, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var subGroups = new List<GroupedTemplateSubGroup>();
|
||||
int globalIndex = 1;
|
||||
|
||||
foreach (var innerGroup in innerGroupedUnits)
|
||||
{
|
||||
var innerGroupName = innerGroup.Key;
|
||||
if (innerGroupName == null)
|
||||
continue;
|
||||
|
||||
var unitsInInnerGroup = innerGroup.ToList();
|
||||
|
||||
var splitSubGroups = unitsInInnerGroup
|
||||
.Select((entry, index) => new { entry, groupIndex = index / maxValueForSplitting })
|
||||
.GroupBy(x => x.groupIndex)
|
||||
.Select(g => g.Select(x => x.entry).ToList())
|
||||
.ToList();
|
||||
|
||||
foreach (var subGroupEntries in splitSubGroups)
|
||||
{
|
||||
var sortedEntries = subGroupEntries
|
||||
.OrderBy(e => e.UnitId)
|
||||
.ThenBy(e => e.UnitFieldValueId)
|
||||
.ToList();
|
||||
|
||||
subGroups.Add(new GroupedTemplateSubGroup(
|
||||
Entries: sortedEntries,
|
||||
InnerGroupName: innerGroupName,
|
||||
GlobalIndex: globalIndex
|
||||
));
|
||||
globalIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (subGroups.Any())
|
||||
{
|
||||
templateGroups.Add(new GroupedTemplateGroup(
|
||||
PotentialUnitId: potentialUnitId,
|
||||
SubGroups: subGroups
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug("Построено {Count} групп шаблонов.", templateGroups.Count);
|
||||
return templateGroups;
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Common.Interfaces.RabbitServices;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
|
||||
internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
|
||||
{
|
||||
private readonly ILogger<GroupedTemplateProcessor> logger;
|
||||
private readonly ITemplateRepository templateRepository;
|
||||
private readonly IUnitRepository unitRepository;
|
||||
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
||||
private readonly ITemplateAllocationService templateAllocationService;
|
||||
private readonly ITemplateMqPublisher templateMqPublisher;
|
||||
|
||||
public GroupedTemplateProcessor(
|
||||
ILogger<GroupedTemplateProcessor> logger,
|
||||
ITemplateRepository templateRepository,
|
||||
IUnitRepository unitRepository,
|
||||
ITemplateNameNormalizer templateNameNormalizer,
|
||||
ITemplateAllocationService templateAllocationService,
|
||||
ITemplateMqPublisher templateMqPublisher,
|
||||
MqSettings mqSettings,
|
||||
IRabbitService mqService)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.templateRepository = templateRepository;
|
||||
this.unitRepository = unitRepository;
|
||||
this.templateNameNormalizer = templateNameNormalizer;
|
||||
this.templateAllocationService = templateAllocationService;
|
||||
this.templateMqPublisher = templateMqPublisher;
|
||||
}
|
||||
|
||||
public async Task<HashSet<(Guid JobId, Guid UnitId, int Index)>> ProcessAsync(
|
||||
List<GroupedTemplateGroup> groups,
|
||||
List<Job> jobsInGroup,
|
||||
Job maxJob,
|
||||
HistoryInitiator initiator,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var potentialUnitId = group.PotentialUnitId;
|
||||
|
||||
foreach (var subGroup in group.SubGroups)
|
||||
{
|
||||
var unitsInTemplateSubGroup = subGroup.Entries;
|
||||
var globalIndex = subGroup.GlobalIndex;
|
||||
var originatingInnerGroupName = subGroup.InnerGroupName;
|
||||
|
||||
logger.LogDebug("Обработка подгруппы {Index} ('{GroupingValue}') для UnitId {PotentialUnitId}, размер {Size}.",
|
||||
globalIndex, originatingInnerGroupName, potentialUnitId, unitsInTemplateSubGroup.Count);
|
||||
|
||||
Job targetJob = SelectTargetJob(jobsInGroup, unitsInTemplateSubGroup.Count, maxJob);
|
||||
expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, globalIndex));
|
||||
|
||||
// Поиск существующего шаблона
|
||||
var existingTemplate = await templateRepository.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 == globalIndex &&
|
||||
t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (existingTemplate != null)
|
||||
{
|
||||
await HandleExistingTemplateAsync(existingTemplate, unitsInTemplateSubGroup, targetJob, globalIndex, initiator, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expectedTemplateKeys;
|
||||
}
|
||||
|
||||
private async Task HandleExistingTemplateAsync(
|
||||
Template existingTemplate,
|
||||
List<(Guid UnitId, Guid UnitFieldValueId)> proposedEntries,
|
||||
Job targetJob,
|
||||
int globalIndex,
|
||||
HistoryInitiator initiator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var currentEntries = existingTemplate.UnitsInTemplate
|
||||
.Select(uit => (uit.UnitId, uit.UnitFieldValueId))
|
||||
.ToList();
|
||||
|
||||
// Сравнение
|
||||
var allUnitIdsForSort = currentEntries.Select(e => e.UnitId)
|
||||
.Concat(proposedEntries.Select(e => e.UnitId))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var unitNamesForSort = await unitRepository.Get()
|
||||
.AsNoTracking()
|
||||
.Where(u => allUnitIdsForSort.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString(), ct);
|
||||
|
||||
var sortedCurrent = currentEntries
|
||||
.OrderBy(e => unitNamesForSort.GetValueOrDefault(e.UnitId, e.UnitId.ToString()))
|
||||
.ThenBy(e => e.UnitFieldValueId)
|
||||
.ToList();
|
||||
|
||||
var sortedProposed = proposedEntries
|
||||
.OrderBy(e => unitNamesForSort.GetValueOrDefault(e.UnitId, e.UnitId.ToString()))
|
||||
.ThenBy(e => e.UnitFieldValueId)
|
||||
.ToList();
|
||||
|
||||
bool unitsAreEqual = sortedCurrent.SequenceEqual(sortedProposed);
|
||||
|
||||
if (unitsAreEqual)
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} актуален по составу.", existingTemplate.Id);
|
||||
|
||||
// Проверка имени
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate);
|
||||
if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id);
|
||||
var updateRequest = new TemplateUpdaterMessage
|
||||
{
|
||||
TemplateId = existingTemplate.Id,
|
||||
JobId = targetJob.Id,
|
||||
UnitId = existingTemplate.UnitId,
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = existingTemplate.IsActiveTemplate,
|
||||
IsActiveSchedule = existingTemplate.IsActiveSchedule,
|
||||
IsNew = false,
|
||||
Index = globalIndex,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
Initiator = initiator,
|
||||
UnitsInTemplate = sortedProposed.Select(t => new UnitInTemplateMessage
|
||||
{
|
||||
UnitId = t.UnitId,
|
||||
UnitFieldValueId = t.UnitFieldValueId
|
||||
}).ToList()
|
||||
};
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id);
|
||||
await UpdateTemplateUnitsAsync(existingTemplate, sortedProposed, targetJob, globalIndex, initiator);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleNewOrReusableTemplateAsync(
|
||||
Guid potentialUnitId,
|
||||
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplateSubGroup,
|
||||
Job targetJob,
|
||||
int globalIndex,
|
||||
HistoryInitiator initiator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Вычисляем флаги активности
|
||||
var isActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false;
|
||||
var isActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false;
|
||||
|
||||
// Маппим кортежи в сообщения
|
||||
var unitsInTemplateMsg = unitsInTemplateSubGroup
|
||||
.Select(e => new UnitInTemplateMessage
|
||||
{
|
||||
UnitId = e.UnitId,
|
||||
UnitFieldValueId = e.UnitFieldValueId
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var request = new TemplateAllocationRequest(
|
||||
TargetJob: targetJob,
|
||||
TargetUnitId: potentialUnitId,
|
||||
TargetUnit: null,
|
||||
Index: globalIndex,
|
||||
UnitsInTemplate: unitsInTemplateMsg,
|
||||
IsActiveTemplate: isActiveTemplate,
|
||||
IsActiveSchedule: isActiveSchedule,
|
||||
Initiator: initiator);
|
||||
|
||||
await templateAllocationService.AllocateAsync(request, ct);
|
||||
}
|
||||
|
||||
private async Task UpdateTemplateUnitsAsync(
|
||||
Template template,
|
||||
List<(Guid UnitId, Guid UnitFieldValueId)> newUnitEntries,
|
||||
Job targetJob,
|
||||
int newIndex,
|
||||
HistoryInitiator initiator)
|
||||
{
|
||||
// 1. Устанавливаем статус и дату
|
||||
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
|
||||
template.DateModified = DateTimeOffset.UtcNow;
|
||||
|
||||
// 2. КОММИТ В БАЗУ СРАЗУ
|
||||
// Важно зафиксировать изменение статуса до отправки сообщения в очередь
|
||||
if (!await templateRepository.CommitAsync(initiator))
|
||||
{
|
||||
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Формируем временный объект для генерации имени
|
||||
var tempTemplateForName = new Template
|
||||
{
|
||||
Id = template.Id,
|
||||
Name = template.Name,
|
||||
JobId = targetJob.Id,
|
||||
UnitId = template.UnitId,
|
||||
Index = newIndex,
|
||||
Job = targetJob,
|
||||
Unit = template.Unit,
|
||||
UnitsInTemplate = newUnitEntries.Select(e => new UnitsInTemplate { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
|
||||
};
|
||||
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||||
|
||||
// 4. Отправляем сообщение в очередь
|
||||
var updateRequest = new TemplateUpdaterMessage
|
||||
{
|
||||
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 = newUnitEntries.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
|
||||
};
|
||||
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
|
||||
|
||||
private static 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();
|
||||
}
|
||||
|
||||
return targetJob ?? maxJob;
|
||||
}
|
||||
}
|
||||
@@ -1,260 +1,110 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using Microsoft.Extensions.Logging;
|
||||
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.Implementations.GroupedSync;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
namespace PARR.TemplateMatcher.Services.GroupedSync;
|
||||
|
||||
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;
|
||||
private readonly IEnumerable<IGroupedSyncStage> _readStages;
|
||||
private readonly IEnumerable<IGroupedSyncWriteStage> _writeStages;
|
||||
private readonly IMatchingStatusService _matchingStatusService;
|
||||
private readonly ILogger<GroupedTemplateSynchronizer> _logger;
|
||||
|
||||
public GroupedTemplateSynchronizer(
|
||||
ILogger<GroupedTemplateSynchronizer> logger,
|
||||
IJobGroupRepository jobGroupService,
|
||||
IUnitFilterService unitFilterService,
|
||||
IGroupedTemplateUnitFilter groupedTemplateUnitFilter,
|
||||
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
|
||||
IGroupedTemplateBuilder groupedTemplateBuilder,
|
||||
IGroupedTemplateProcessor groupedTemplateProcessor,
|
||||
ITemplateRepository templateService,
|
||||
ITemplateDeactivator templateDeactivator,
|
||||
IMatchingStatusService matchingStatusService)
|
||||
IEnumerable<IGroupedSyncStage> readStages,
|
||||
IEnumerable<IGroupedSyncWriteStage> writeStages,
|
||||
IMatchingStatusService matchingStatusService,
|
||||
ILogger<GroupedTemplateSynchronizer> logger)
|
||||
{
|
||||
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);
|
||||
_readStages = readStages;
|
||||
_writeStages = writeStages;
|
||||
_matchingStatusService = matchingStatusService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
|
||||
{
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
|
||||
_logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
|
||||
|
||||
// === Проверка: уже запущена? ===
|
||||
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
var existingStatus = await _matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
if (existingStatus.DetailsJobGroups?.Any() == true)
|
||||
{
|
||||
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
|
||||
_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)
|
||||
);
|
||||
await SetStatusAsync(jobGroupId, "Начало синхронизации");
|
||||
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// === ЭТАП 1: Загрузка JobGroup ===
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
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);
|
||||
var context = new GroupedSyncContext { JobGroupId = jobGroupId, Initiator = initiator };
|
||||
|
||||
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
|
||||
foreach (var stage in _readStages)
|
||||
{
|
||||
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
|
||||
return;
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
var jobsInGroup = jobGroup.Jobs.ToList();
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Загрузка JobGroup | Время: {Ms} мс | Jobs: {Count}",
|
||||
jobGroupId, stageSw.ElapsedMilliseconds, jobsInGroup.Count);
|
||||
|
||||
// === Поиск эталонного Job ===
|
||||
var maxJob = jobsInGroup
|
||||
.Where(j => j.MaxValueRelationships.HasValue)
|
||||
.OrderByDescending(j => j.MaxValueRelationships)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (maxJob == null)
|
||||
foreach (var stage in _writeStages)
|
||||
{
|
||||
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
|
||||
return;
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}).", maxJob.Id, maxJob.MaxValueRelationships);
|
||||
totalSw.Stop();
|
||||
_logger.LogInformation("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | ИТОГО: {TotalMs} мс",
|
||||
context.JobGroupName, jobGroupId, totalSw.ElapsedMilliseconds);
|
||||
|
||||
// === ЭТАП 2: Фильтрация юнитов ===
|
||||
stageSw.Restart();
|
||||
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
|
||||
stageSw.Stop();
|
||||
var filterCount = unitFilterResults?.Count() ?? 0;
|
||||
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
|
||||
jobGroupId, stageSw.ElapsedMilliseconds, filterCount);
|
||||
|
||||
if (unitFilterResults == null || !unitFilterResults.Any())
|
||||
{
|
||||
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
|
||||
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
// === ЭТАП 3: Групповая фильтрация ===
|
||||
stageSw.Restart();
|
||||
var finalFilteredUnits = await groupedTemplateUnitFilter.FilterAsync(unitFilterResults, jobGroup);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Групповая фильтрация | Время: {Ms} мс | Результат: {Count}",
|
||||
jobGroupId, stageSw.ElapsedMilliseconds, finalFilteredUnits.Count);
|
||||
|
||||
if (!finalFilteredUnits.Any())
|
||||
{
|
||||
logger.LogInformation("После применения правил фильтрации в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации");
|
||||
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); return;
|
||||
}
|
||||
|
||||
// === ЭТАП 4: Разрешение конфликтов ===
|
||||
stageSw.Restart();
|
||||
var initialReverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnits, maxJob);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Разрешение конфликтов | Время: {Ms} мс | Связей: {Count}",
|
||||
jobGroupId, stageSw.ElapsedMilliseconds, initialReverseMapping.Count);
|
||||
|
||||
if (!initialReverseMapping.Any())
|
||||
{
|
||||
logger.LogInformation("После разрешения конфликтов в JobGroup {JobGroupId} не осталось связей.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Нет связей после разрешения конфликтов");
|
||||
return;
|
||||
}
|
||||
|
||||
// === ЭТАП 5: Построение структуры групп ===
|
||||
stageSw.Restart();
|
||||
var templateGroups = await groupedTemplateBuilder.BuildAsync(initialReverseMapping, jobGroup, maxJob);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Построение групп | Время: {Ms} мс | Групп: {Count}",
|
||||
jobGroupId, stageSw.ElapsedMilliseconds, templateGroups.Count);
|
||||
|
||||
if (!templateGroups.Any())
|
||||
{
|
||||
logger.LogInformation("После построения структуры групп в JobGroup {JobGroupId} не осталось данных.", jobGroupId);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Нет данных после построения групп");
|
||||
return;
|
||||
}
|
||||
|
||||
// === ЭТАП 6: Обработка групп (сравнение, обновление, MQ) ===
|
||||
stageSw.Restart();
|
||||
var expectedTemplateKeys = await groupedTemplateProcessor.ProcessAsync(
|
||||
templateGroups,
|
||||
jobsInGroup,
|
||||
maxJob,
|
||||
initiator);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Обработка групп | Время: {Ms} мс | Ключей: {Count}",
|
||||
jobGroupId, stageSw.ElapsedMilliseconds, expectedTemplateKeys.Count);
|
||||
|
||||
// === ЭТАП 7: Деактивация лишних шаблонов ===
|
||||
stageSw.Restart();
|
||||
await DeactivateUnusedTemplatesAsync(expectedTemplateKeys, jobGroupId, jobsInGroup, initiator);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Деактивация | Время: {Ms} мс",
|
||||
jobGroupId, stageSw.ElapsedMilliseconds);
|
||||
|
||||
// === ИТОГО === totalSw.Stop();
|
||||
logger.LogInformation(
|
||||
"[Perf] JobGroup {JobGroupId} | ИТОГО: {TotalMs} мс",
|
||||
jobGroupId, totalSw.ElapsedMilliseconds);
|
||||
|
||||
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
|
||||
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
|
||||
await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно");
|
||||
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
_logger.LogInformation("Синхронизация шаблонов завершена для JobGroup '{JobGroupName}' ({JobGroupId})",
|
||||
context.JobGroupName, jobGroupId);
|
||||
}
|
||||
catch (GroupedSyncEarlyExitException ex)
|
||||
{
|
||||
totalSw.Stop();
|
||||
_logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)",
|
||||
jobGroupId, ex.Reason, totalSw.ElapsedMilliseconds);
|
||||
await SetStatusAsync(jobGroupId, ex.Reason);
|
||||
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
totalSw.Stop();
|
||||
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс", jobGroupId, totalSw.ElapsedMilliseconds);
|
||||
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
|
||||
_logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс",
|
||||
jobGroupId, totalSw.ElapsedMilliseconds);
|
||||
await SetStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
public Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
|
||||
_logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task DeactivateUnusedTemplatesAsync(
|
||||
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
|
||||
Guid jobGroupId,
|
||||
List<Job> jobsInGroup,
|
||||
HistoryInitiator initiator)
|
||||
public Task UpdateTemplatesForJobAsync(Guid jobId, 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);
|
||||
}
|
||||
}
|
||||
_logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
|
||||
private async Task SetStatusAsync(Guid jobGroupId, string comment)
|
||||
{
|
||||
var status = new MatchingStatusItemDto
|
||||
{
|
||||
@@ -262,12 +112,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||||
Action = TemplateMatcherActionEnum.Sync,
|
||||
Comment = comment
|
||||
};
|
||||
|
||||
await matchingStatusService.SetMatchingStatusAsync(
|
||||
jobGroupId,
|
||||
SyncTaskEntityTypeEnum.JobGroup,
|
||||
await _matchingStatusService.SetMatchingStatusAsync(
|
||||
jobGroupId, SyncTaskEntityTypeEnum.JobGroup,
|
||||
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
|
||||
TimeSpan.FromMinutes(30)
|
||||
);
|
||||
TimeSpan.FromMinutes(30));
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Core.Services.UnitFilterService.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||
|
||||
internal class GroupedTemplateUnitFilter : IGroupedTemplateUnitFilter
|
||||
{
|
||||
private readonly ILogger<GroupedTemplateUnitFilter> logger;
|
||||
private readonly IUnitInValueRepository unitInValueRepository;
|
||||
private readonly IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupRepository;
|
||||
private readonly IUnitFieldRepository unitFieldRepository;
|
||||
|
||||
public GroupedTemplateUnitFilter(
|
||||
ILogger<GroupedTemplateUnitFilter> logger,
|
||||
IUnitInValueRepository unitInValueRepository,
|
||||
IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupRepository,
|
||||
IUnitFieldRepository unitFieldRepository)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.unitInValueRepository = unitInValueRepository;
|
||||
this.regionalEkPtkGroupRepository = regionalEkPtkGroupRepository;
|
||||
this.unitFieldRepository = unitFieldRepository;
|
||||
}
|
||||
|
||||
public async Task<List<UnitFilterResultDto>> FilterAsync(
|
||||
IEnumerable<UnitFilterResultDto> initialUnits,
|
||||
JobGroup jobGroup,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var unitsList = initialUnits.ToList();
|
||||
if (!unitsList.Any())
|
||||
{
|
||||
logger.LogDebug("Входной список юнитов пуст. Фильтрация пропущена.");
|
||||
return unitsList;
|
||||
}
|
||||
|
||||
logger.LogDebug("Начало фильтрации юнитов для JobGroup {JobGroupId}.", jobGroup.Id);
|
||||
|
||||
// 1. Проверка наличия GroupingUnitFieldId
|
||||
if (!jobGroup.GroupingUnitFieldId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException($"JobGroup {jobGroup.Id} не имеет GroupingUnitFieldId, необходимого для группировки.");
|
||||
}
|
||||
|
||||
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
|
||||
logger.LogDebug("Фильтрация по GroupingUnitFieldId (FieldId={FieldId}).", groupingFieldId);
|
||||
|
||||
// 2. Фильтрация по GroupingUnitFieldId
|
||||
var allUnitIds = unitsList.Select(u => u.Id).ToList();
|
||||
var groupingValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(allUnitIds, new HashSet<Guid> { groupingFieldId });
|
||||
|
||||
var validUnitIdsAfterGrouping = groupingValues
|
||||
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
|
||||
.Select(uv => uv.UnitId)
|
||||
.ToHashSet();
|
||||
|
||||
var filteredByGrouping = unitsList
|
||||
.Where(u => validUnitIdsAfterGrouping.Contains(u.Id))
|
||||
.ToList();
|
||||
|
||||
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", filteredByGrouping.Count);
|
||||
|
||||
if (!filteredByGrouping.Any())
|
||||
{
|
||||
return filteredByGrouping;
|
||||
}
|
||||
|
||||
// 3. Фильтрация по РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК
|
||||
var workGroupField = await unitFieldRepository.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК")
|
||||
?? throw new InvalidOperationException("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено в справочнике полей.");
|
||||
|
||||
var workGroupFieldId = workGroupField.Id;
|
||||
logger.LogDebug("Фильтрация по полю 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' (FieldId={FieldId}).", workGroupFieldId);
|
||||
|
||||
var filteredUnitIds = filteredByGrouping.Select(u => u.Id).ToList();
|
||||
var workGroupValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(filteredUnitIds, new HashSet<Guid> { workGroupFieldId });
|
||||
|
||||
var allowedValueIds = regionalEkPtkGroupRepository.Get()
|
||||
.Select(g => g.FieldValueId)
|
||||
.ToHashSet();
|
||||
|
||||
logger.LogDebug("Найдено {Count} разрешенных значений для поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", allowedValueIds.Count);
|
||||
|
||||
var validUnitIdsAfterWorkGroup = workGroupValues
|
||||
.Where(uv => uv.Value != null && allowedValueIds.Contains(uv.Value.Id))
|
||||
.Select(uv => uv.UnitId)
|
||||
.ToHashSet();
|
||||
|
||||
var finalFiltered = filteredByGrouping
|
||||
.Where(u => validUnitIdsAfterWorkGroup.Contains(u.Id))
|
||||
.ToList();
|
||||
|
||||
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", finalFiltered.Count);
|
||||
|
||||
return finalFiltered;
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,28 @@
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implemetaions
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class JobGroupValidatorService : IJobGroupValidatorService
|
||||
{
|
||||
private readonly ILogger<IJobValidatorService> logger;
|
||||
private readonly IJobGroupRepository jobGroupService;
|
||||
private readonly ILogger<IJobValidatorService> _logger;
|
||||
private readonly IJobGroupRepository _jobGroupService;
|
||||
|
||||
public JobGroupValidatorService(
|
||||
ILogger<IJobValidatorService> logger,
|
||||
IJobGroupRepository jobGroupService
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.jobGroupService = jobGroupService;
|
||||
_logger = logger;
|
||||
_jobGroupService = jobGroupService;
|
||||
}
|
||||
public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId)
|
||||
{
|
||||
var isExist = await jobGroupService.GetAsync(jobGroupId);
|
||||
var isExist = await _jobGroupService.GetAsync(jobGroupId);
|
||||
|
||||
if (isExist == null)
|
||||
{
|
||||
logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
|
||||
_logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implemetaions
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class JobValidatorService : IJobValidatorService
|
||||
{
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Common.Interfaces;
|
||||
using PARR.Core.Common.Interfaces.RabbitServices;
|
||||
using PARR.Domain.Common.Rabbit.Messages;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations
|
||||
{
|
||||
internal class MqTemplateMatcher : IMqTemplateMatcher
|
||||
{
|
||||
private readonly ILogger<MqTemplateMatcher> logger;
|
||||
private readonly MqSettings mqSettings;
|
||||
private readonly IRabbitService mqService;
|
||||
private readonly ITransformService transformService;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public MqTemplateMatcher(
|
||||
ILogger<MqTemplateMatcher> logger,
|
||||
MqSettings mqSettings,
|
||||
IRabbitService mqService,
|
||||
ITransformService transformService,
|
||||
IServiceProvider serviceProvider
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.mqSettings = mqSettings;
|
||||
this.mqService = mqService;
|
||||
this.transformService = transformService;
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
logger.LogInformation("Запускаем обработчик TemplateMatcher для очереди {QueueName}", mqSettings.TemplateMatcher.QueueName);
|
||||
|
||||
var isConnected = await mqService.InitConsumerAsync(mqSettings.TemplateMatcher, HandleMessageAsync);
|
||||
|
||||
if (!isConnected)
|
||||
{
|
||||
logger.LogError("Ошибка при подключении к RabbitMq для очереди {QueueName}", mqSettings.TemplateMatcher.QueueName);
|
||||
throw new Exception("Ошибка при подключении к RabbitMq");
|
||||
}
|
||||
|
||||
logger.LogInformation("Успешно подключились к RabbitMq для очереди {QueueName}", mqSettings.TemplateMatcher.QueueName);
|
||||
}
|
||||
|
||||
|
||||
private async Task HandleMessageAsync(string msg)
|
||||
{
|
||||
logger.LogDebug("Получили запрос: {Message}", msg);
|
||||
|
||||
var query = transformService.GetModelFromJson<TemplateMatcherMq>(msg);
|
||||
if (query == null)
|
||||
{
|
||||
logger.LogWarning("Не удалось десериализовать сообщение в TemplateMatcherMq: {Message}", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogTrace("Обрабатываем сообщение: EntityId={EntityId}, EntityType={EntityType}, Action={Action}",
|
||||
query.Id, query.EntityType, query.Action);
|
||||
|
||||
await using (var scope = serviceProvider.CreateAsyncScope())
|
||||
{
|
||||
var templateMatcherService = GetServiceInScope<ITemplateMatcher>(scope);
|
||||
|
||||
switch (query.EntityType)
|
||||
{
|
||||
case SyncTaskEntityTypeEnum.Job:
|
||||
var jobValidatorService = GetServiceInScope<IJobValidatorService>(scope);
|
||||
if (!await jobValidatorService.IsValidJobAsync(query.Id))
|
||||
{
|
||||
logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogDebug("Сущность {EntityType} с Id {Id} прошла валидацию", query.EntityType, query.Id);
|
||||
|
||||
switch (query.Action)
|
||||
{
|
||||
case TemplateMatcherActionEnum.Sync:
|
||||
// Создать недостающие шаблоны, привязать к нужному Job, включить/выключить по фильтрам
|
||||
await templateMatcherService.SyncTemplatesForJob(query.Id, query.Initiator);
|
||||
break;
|
||||
case TemplateMatcherActionEnum.Update:
|
||||
// Обновить существующие шаблоны: имя, привязка к Job, вкл/выкл по фильтрам
|
||||
await templateMatcherService.UpdateTemplatesForJob(query.Id, query.Initiator);
|
||||
break;
|
||||
default:
|
||||
logger.LogWarning("Неизвестное действие для {EntityType}: {Action}", query.EntityType, query.Action);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case SyncTaskEntityTypeEnum.JobGroup:
|
||||
var jobGroupValidatorService = GetServiceInScope<IJobGroupValidatorService>(scope);
|
||||
if (!await jobGroupValidatorService.IsValidJobGroupAsync(query.Id))
|
||||
{
|
||||
logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogDebug("Сущность {EntityType} с Id {Id} прошла валидацию", query.EntityType, query.Id);
|
||||
|
||||
switch (query.Action)
|
||||
{
|
||||
case TemplateMatcherActionEnum.Sync:
|
||||
await templateMatcherService.SyncTemplatesForJobGroup(query.Id, query.Initiator);
|
||||
break;
|
||||
|
||||
case TemplateMatcherActionEnum.Update:
|
||||
//await templateMatcherService.UpdateTemplatesForJob(query.Id, query.Initiator);
|
||||
await templateMatcherService.UpdateTemplatesForJobGroup(query.Id, query.Initiator);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.LogWarning("Неизвестное действие для {EntityType}: {Action}", query.EntityType, query.Action);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SyncTaskEntityTypeEnum.Template:
|
||||
logger.LogWarning("Обработка EntityType Template не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.LogWarning("Неизвестный тип сущности: {EntityType}", query.EntityType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug("Обработка сообщения завершена: {Message}", msg);
|
||||
}
|
||||
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
logger.LogInformation("Останавливаем обработчик TemplateMatcher");
|
||||
await mqService.DisposeAsync();
|
||||
logger.LogInformation("Обработчик TemplateMatcher остановлен");
|
||||
}
|
||||
|
||||
|
||||
private Service GetServiceInScope<Service>(IServiceScope scope)
|
||||
{
|
||||
var service = scope.ServiceProvider.GetService<Service>();
|
||||
if (service == null)
|
||||
throw new Exception($"Не найден сервис: {nameof(Service)}");
|
||||
|
||||
return service;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,9 @@ using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.Domain.Settings;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Constants;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -31,18 +32,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
private const bool DefaultUsedTemplateState = false;
|
||||
private const bool DefaultUsedScheduleState = false;
|
||||
|
||||
// === Константы для логики неиспользуемых шаблонов ===
|
||||
private const string FieldNameResponsibilityArea = "ЗОНА_ОТВЕТСТВЕННОСТИ";
|
||||
private const string FieldNameParrTag = "ПАРР тег";
|
||||
private const string TagValueNotWorking = "ПАРР-НЕИСП";
|
||||
|
||||
private readonly IEnumerable<ISimpleSyncStage> readStages;
|
||||
private readonly IEnumerable<ISimpleSyncWriteStage> writeStages;
|
||||
private readonly ILogger<SimpleTemplateSynchronizer> logger;
|
||||
private readonly IUnitFilterService unitFilterService;
|
||||
private readonly MqSettings mqSettings;
|
||||
private readonly IRabbitService mqService;
|
||||
private readonly ITemplateRepository templateService;
|
||||
private readonly IJobRepository jobService;
|
||||
private readonly ITemplateDeactivator templateDeactivator;
|
||||
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
||||
private readonly ITemplateAllocationService templateAllocationService;
|
||||
private readonly ITemplateMqPublisher templateMqPublisher;
|
||||
@@ -54,13 +51,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
private readonly IUnitRepository unitRepository;
|
||||
|
||||
public SimpleTemplateSynchronizer(
|
||||
IEnumerable<ISimpleSyncStage> readStages,
|
||||
IEnumerable<ISimpleSyncWriteStage> writeStages,
|
||||
ILogger<SimpleTemplateSynchronizer> logger,
|
||||
IUnitFilterService unitFilterService,
|
||||
MqSettings mqSettings,
|
||||
IRabbitService mqService,
|
||||
ITemplateRepository templateService,
|
||||
IJobRepository jobService,
|
||||
ITemplateDeactivator templateDeactivator,
|
||||
ITemplateNameNormalizer templateNameNormalizer,
|
||||
ITemplateAllocationService templateAllocationService,
|
||||
ITemplateMqPublisher templateMqPublisher,
|
||||
@@ -72,13 +70,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
IUnitRepository unitRepository
|
||||
)
|
||||
{
|
||||
this.readStages = readStages;
|
||||
this.writeStages = writeStages;
|
||||
this.logger = logger;
|
||||
this.unitFilterService = unitFilterService;
|
||||
this.mqSettings = mqSettings;
|
||||
this.mqService = mqService;
|
||||
this.templateService = templateService;
|
||||
this.jobService = jobService;
|
||||
this.templateDeactivator = templateDeactivator;
|
||||
this.templateNameNormalizer = templateNameNormalizer;
|
||||
this.templateAllocationService = templateAllocationService;
|
||||
this.templateMqPublisher = templateMqPublisher;
|
||||
@@ -92,18 +91,15 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
|
||||
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
// === Специальная обработка для Job неиспользуемых шаблонов ===
|
||||
if (jobId == settingsFromDb.JobIdForUnusedTemplates)
|
||||
{
|
||||
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId);
|
||||
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId);
|
||||
await SyncUnusedTemplatesAsync(jobId, initiator);
|
||||
return;
|
||||
}
|
||||
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId);
|
||||
|
||||
// === Проверка: уже запущена? ===
|
||||
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
||||
if (existingStatus.DetailsJobs?.Any() == true)
|
||||
{
|
||||
@@ -111,7 +107,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
return;
|
||||
}
|
||||
|
||||
// === Устанавливаем статус "в процессе" ===
|
||||
var initialStatus = new MatchingStatusItemDto
|
||||
{
|
||||
DateStart = DateTimeOffset.UtcNow,
|
||||
@@ -119,169 +114,49 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
Comment = "Начало синхронизации"
|
||||
};
|
||||
await matchingStatusService.SetMatchingStatusAsync(
|
||||
jobId,
|
||||
SyncTaskEntityTypeEnum.Job,
|
||||
jobId, SyncTaskEntityTypeEnum.Job,
|
||||
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
|
||||
TimeSpan.FromMinutes(35)
|
||||
);
|
||||
TimeSpan.FromMinutes(35));
|
||||
|
||||
// Таймер запускается ПОСЛЕ инфраструктурных операций (статус, проверка блокировки)
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// === ЭТАП 1: Загрузка Job ===
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
var job = await jobService.Get()
|
||||
.AsNoTracking()
|
||||
.Include(j => j.AutoControl)
|
||||
.Include(j => j.Tnk)
|
||||
.Include(j => j.Group)
|
||||
.ThenInclude(g => g!.GroupType)
|
||||
.Include(j => j.UnitFilters)
|
||||
.ThenInclude(uf => uf.RelationshipFilters)
|
||||
.FirstOrDefaultAsync(j => j.Id == jobId);
|
||||
var context = new SimpleSyncContext { JobId = jobId, Initiator = initiator };
|
||||
|
||||
if (job == null)
|
||||
foreach (var stage in readStages)
|
||||
{
|
||||
logger.LogWarning("Job {JobId} не найден.", jobId);
|
||||
await UpdateMatchingStatusAsync(jobId, "Job не найден");
|
||||
return;
|
||||
}
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка Job | Время: {Ms} мс",
|
||||
jobId, stageSw.ElapsedMilliseconds);
|
||||
|
||||
// === ЭТАП 2: Фильтрация юнитов ===
|
||||
stageSw.Restart();
|
||||
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
|
||||
stageSw.Stop();
|
||||
var filterCount = filteredUnits?.Count() ?? 0;
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, filterCount);
|
||||
|
||||
var unitIds = filteredUnits?.Select(u => u.Id).ToHashSet() ?? new HashSet<Guid>();
|
||||
|
||||
// === ЭТАП 3: Загрузка существующих шаблонов ===
|
||||
stageSw.Restart();
|
||||
var existingTemplates = await templateService.Get()
|
||||
.Include(t => t.UnitsInTemplate)
|
||||
.Include(t => t.Job)
|
||||
.ThenInclude(t => t!.Group)
|
||||
.ThenInclude(t => t!.GroupType)
|
||||
.Include(t => t.Job)
|
||||
.ThenInclude(t => t!.Tnk)
|
||||
.Include(t => t.Unit)
|
||||
.Where(t => t.JobId == jobId)
|
||||
.ToListAsync();
|
||||
|
||||
var existingUsedTemplates = existingTemplates
|
||||
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
||||
.ToList();
|
||||
|
||||
var existingUnitIds = existingUsedTemplates.Select(t => t.UnitId).ToHashSet();
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка шаблонов | Время: {Ms} мс | Используется: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, existingUsedTemplates.Count);
|
||||
|
||||
// === ЭТАП 4: Расчёт диффа (создание / деактивация / переименование) ===
|
||||
stageSw.Restart();
|
||||
|
||||
var newUnitIds = unitIds.Except(existingUnitIds).ToList();
|
||||
var unusedTemplates = existingUsedTemplates
|
||||
.Where(t => !unitIds.Contains(t.UnitId))
|
||||
.ToList();
|
||||
|
||||
// Проверка имён существующих шаблонов
|
||||
var templatesToRename = new List<(Template Template, string ExpectedName)>();
|
||||
foreach (var template in existingUsedTemplates)
|
||||
{
|
||||
if (!unitIds.Contains(template.UnitId))
|
||||
continue;
|
||||
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
|
||||
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
templatesToRename.Add((template, expectedName));
|
||||
}
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Расчёт диффа | Время: {Ms} мс | Создать: {Create}, Деактивировать: {Deactivate}, Переименовать: {Rename}",
|
||||
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count, unusedTemplates.Count, templatesToRename.Count);
|
||||
|
||||
// === ЭТАП 5: Создание новых шаблонов ===
|
||||
stageSw.Restart();
|
||||
foreach (var unitId in newUnitIds)
|
||||
foreach (var stage in writeStages)
|
||||
{
|
||||
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState;
|
||||
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState;
|
||||
|
||||
var request = new TemplateAllocationRequest(
|
||||
TargetJob: job,
|
||||
TargetUnitId: unitId,
|
||||
TargetUnit: null,
|
||||
Index: null,
|
||||
UnitsInTemplate: new List<UnitInTemplateMessage>(),
|
||||
IsActiveTemplate: isActiveTemplate,
|
||||
IsActiveSchedule: isActiveSchedule,
|
||||
Initiator: initiator);
|
||||
|
||||
await templateAllocationService.AllocateAsync(request);
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Создание шаблонов | Время: {Ms} мс | Количество: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count);
|
||||
|
||||
// === ЭТАП 6: Обновление имён существующих шаблонов ===
|
||||
stageSw.Restart();
|
||||
foreach (var (template, expectedName) in templatesToRename)
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: '{OldName}' → '{NewName}'",
|
||||
template.Id, template.Name, expectedName);
|
||||
|
||||
var updateRequest = new TemplateUpdaterMessage
|
||||
{
|
||||
TemplateId = template.Id,
|
||||
JobId = jobId,
|
||||
UnitId = template.UnitId,
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = template.IsActiveTemplate,
|
||||
IsActiveSchedule = template.IsActiveSchedule,
|
||||
IsNew = false,
|
||||
Index = template.Index,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
Initiator = initiator,
|
||||
UnitsInTemplate = new List<UnitInTemplateMessage>()
|
||||
};
|
||||
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Обновление имён | Время: {Ms} мс | Количество: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, templatesToRename.Count);
|
||||
|
||||
// === ЭТАП 7: Деактивация лишних шаблонов ===
|
||||
stageSw.Restart();
|
||||
foreach (var unusedTemplate in unusedTemplates)
|
||||
{
|
||||
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).",
|
||||
unusedTemplate.Id, jobId, unusedTemplate.UnitId);
|
||||
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
|
||||
}
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Деактивация | Время: {Ms} мс | Количество: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, unusedTemplates.Count);
|
||||
|
||||
// === ИТОГО ===
|
||||
totalSw.Stop();
|
||||
logger.LogInformation("[Perf] Job {JobId} | ИТОГО: {TotalMs} мс", jobId, totalSw.ElapsedMilliseconds);
|
||||
logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс",
|
||||
context.JobName, jobId, totalSw.ElapsedMilliseconds);
|
||||
|
||||
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
|
||||
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
||||
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
|
||||
logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})",
|
||||
context.JobName, jobId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
totalSw.Stop();
|
||||
logger.LogError(ex, "Ошибка при синхронизации Job {JobId} через {ElapsedMs} мс", jobId, totalSw.ElapsedMilliseconds);
|
||||
logger.LogError(ex, "Ошибка при синхронизации Job '{JobName}' ({JobId}) через {ElapsedMs} мс",
|
||||
string.Empty, jobId, totalSw.ElapsedMilliseconds);
|
||||
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
@@ -293,6 +168,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
|
||||
@@ -444,30 +320,29 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
try
|
||||
{
|
||||
// 1. Находим ID нужных полей
|
||||
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(FieldNameResponsibilityArea);
|
||||
var tagField = await unitFieldService.GetByAihitNameAsync(FieldNameParrTag);
|
||||
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName);
|
||||
var tagField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName);
|
||||
|
||||
if (responsableAreaField == null || tagField == null)
|
||||
{
|
||||
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", FieldNameResponsibilityArea, FieldNameParrTag);
|
||||
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", UnusedTemplateConstants.ResponsibilityAreaFieldName, UnusedTemplateConstants.NotUsedTagValue);
|
||||
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
|
||||
return;
|
||||
}
|
||||
|
||||
var responsableAreaFieldId = responsableAreaField.Id;
|
||||
var tagFieldId = tagField.Id;
|
||||
const string targetTagValue = TagValueNotWorking;
|
||||
|
||||
// 2. Находим ValueId для тега "ПАРР-НЕИСП"
|
||||
var targetTagValueId = await unitInValueService.Get()
|
||||
.AsNoTracking()
|
||||
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == targetTagValue)
|
||||
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == UnusedTemplateConstants.NotUsedTagValue)
|
||||
.Select(uiv => uiv.ValueId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (targetTagValueId == Guid.Empty)
|
||||
{
|
||||
logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", targetTagValue, FieldNameParrTag);
|
||||
logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", UnusedTemplateConstants.NotUsedTagValue, UnusedTemplateConstants.ParrTagFieldName);
|
||||
}
|
||||
|
||||
var unusedJob = await jobService.Get().AsNoTracking()
|
||||
|
||||
296
PARR.TemplateMatcher/Services/Implementations/TemplateMatcher.cs
Normal file
296
PARR.TemplateMatcher/Services/Implementations/TemplateMatcher.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Core.Services.UnitFilterService.Models;
|
||||
using PARR.Domain.Entities.Job;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
|
||||
internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
|
||||
{
|
||||
private readonly ILogger<UnitInTemplateConflictMapper> logger;
|
||||
private readonly IUnitRepository unitRepository;
|
||||
private readonly IUnitKiiUnitRepository unitKiiUnitRepository;
|
||||
|
||||
|
||||
public UnitInTemplateConflictMapper(
|
||||
ILogger<UnitInTemplateConflictMapper> logger,
|
||||
IUnitRepository unitRepository,
|
||||
IUnitKiiUnitRepository unitKiiUnitRepository)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.unitRepository = unitRepository;
|
||||
this.unitKiiUnitRepository = unitKiiUnitRepository;
|
||||
}
|
||||
|
||||
|
||||
public async Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
||||
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
||||
Job maxJob,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger.LogDebug("Начало разрешения конфликтов и построения маппинга для юнитов в шаблонах.");
|
||||
|
||||
var unitFilterResultsList = unitFilterResults.ToList();
|
||||
if (!unitFilterResultsList.Any())
|
||||
return new Dictionary<Guid, List<Guid>>();
|
||||
|
||||
var potentialAssignments = new Dictionary<Guid, List<Guid>>();
|
||||
var allPotentialRelatedUnitIds = new HashSet<Guid>();
|
||||
var allUnitInTemplateIds = new HashSet<Guid>();
|
||||
|
||||
foreach (var dto in unitFilterResultsList)
|
||||
{
|
||||
var relatedUnitIds = maxJob.IsParentRelationships == true
|
||||
? dto.Children.Select(c => c.UnitId).ToList()
|
||||
: dto.Parents.Select(p => p.UnitId).ToList();
|
||||
|
||||
var unitInTemplateId = dto.Id;
|
||||
|
||||
allUnitInTemplateIds.Add(unitInTemplateId);
|
||||
allPotentialRelatedUnitIds.UnionWith(relatedUnitIds);
|
||||
|
||||
foreach (var relatedId in relatedUnitIds)
|
||||
{
|
||||
if (!potentialAssignments.TryGetValue(relatedId, out var list))
|
||||
{
|
||||
list = new List<Guid>();
|
||||
potentialAssignments[relatedId] = list;
|
||||
}
|
||||
list.Add(unitInTemplateId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allPotentialRelatedUnitIds.Any())
|
||||
return new Dictionary<Guid, List<Guid>>();
|
||||
|
||||
var relatedUnitNames = await unitRepository.Get()
|
||||
.AsNoTracking()
|
||||
.Where(u => allPotentialRelatedUnitIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString(), cancellationToken);
|
||||
|
||||
// Загружаем идентификаторы КИИ через ваш реальный репозиторий
|
||||
var kiiUnitIds = new HashSet<Guid>(await unitKiiUnitRepository.Get()
|
||||
.Select(k => k.UnitId)
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
logger.LogDebug("Загружено {KiiCount} идентификаторов КИИ юнитов для приоритезации.", kiiUnitIds.Count);
|
||||
|
||||
var unitInTemplateToBestRelatedUnit = new Dictionary<Guid, Guid>();
|
||||
|
||||
foreach (var unitInTemplateId in allUnitInTemplateIds)
|
||||
{
|
||||
var candidates = potentialAssignments
|
||||
.Where(kvp => kvp.Value.Contains(unitInTemplateId))
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0) continue;
|
||||
if (candidates.Count == 1)
|
||||
{
|
||||
unitInTemplateToBestRelatedUnit[unitInTemplateId] = candidates[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
var kiiCandidates = candidates.Where(id => kiiUnitIds.Contains(id)).ToList();
|
||||
var candidatesToConsider = kiiCandidates.Any() ? kiiCandidates : candidates;
|
||||
|
||||
var relationshipCounts = new Dictionary<Guid, int>();
|
||||
foreach (var candidateId in candidatesToConsider)
|
||||
{
|
||||
int totalRelationships = 0;
|
||||
foreach (var dto in unitFilterResultsList)
|
||||
{
|
||||
if (maxJob.IsParentRelationships == true && dto.Children.Any(c => c.UnitId == candidateId))
|
||||
totalRelationships += dto.Children.Count;
|
||||
else if (maxJob.IsParentRelationships == false && dto.Parents.Any(p => p.UnitId == candidateId))
|
||||
totalRelationships += dto.Parents.Count;
|
||||
}
|
||||
relationshipCounts[candidateId] = totalRelationships;
|
||||
}
|
||||
|
||||
var bestCandidate = candidatesToConsider[0];
|
||||
int bestCount = relationshipCounts[bestCandidate];
|
||||
|
||||
foreach (var candidateId in candidatesToConsider.Skip(1))
|
||||
{
|
||||
int currentCount = relationshipCounts[candidateId];
|
||||
if (currentCount > bestCount ||
|
||||
(currentCount == bestCount && string.Compare(
|
||||
relatedUnitNames.GetValueOrDefault(candidateId, candidateId.ToString()),
|
||||
relatedUnitNames.GetValueOrDefault(bestCandidate, bestCandidate.ToString()),
|
||||
StringComparison.OrdinalIgnoreCase) < 0))
|
||||
{
|
||||
bestCandidate = candidateId;
|
||||
bestCount = currentCount;
|
||||
}
|
||||
}
|
||||
|
||||
unitInTemplateToBestRelatedUnit[unitInTemplateId] = bestCandidate;
|
||||
}
|
||||
|
||||
logger.LogDebug("Разрешение конфликтов завершено. Найдено {Count} однозначных назначений.", unitInTemplateToBestRelatedUnit.Count);
|
||||
|
||||
var reverseMapping = new Dictionary<Guid, List<Guid>>();
|
||||
foreach (var kvp in unitInTemplateToBestRelatedUnit)
|
||||
{
|
||||
if (!reverseMapping.TryGetValue(kvp.Value, out var list))
|
||||
{
|
||||
list = new List<Guid>();
|
||||
reverseMapping[kvp.Value] = list;
|
||||
}
|
||||
list.Add(kvp.Key);
|
||||
}
|
||||
|
||||
return reverseMapping;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user