feat(templateMatcher): Рефакторинг, основной метод синхронизации GroupedTemplateSynchronizer разбит на отдельные классы; Неиспользуемые шаблоны теперь привязываются к ЭК КОСМПЛЕКСЫ-[ЗО].

This commit is contained in:
Mikhail Kuznetsov
2026-05-14 16:47:27 +10:00
parent f7a74d315a
commit 90e6352117
30 changed files with 1150 additions and 864 deletions

View File

@@ -0,0 +1,158 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Unit;
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;
public GroupedTemplateBuilder(
ILogger<GroupedTemplateBuilder> logger,
IUnitInValueRepository unitInValueRepository,
IUnitFieldRepository unitFieldRepository)
{
this.logger = logger;
this.unitInValueRepository = unitInValueRepository;
this.unitFieldRepository = unitFieldRepository;
}
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. Определяем поле для внутренней группировки
var isGroupByResponsible = jobGroup.IsGroupByResponsible == true;
var innerGroupingFieldName = isGroupByResponsible ? "ОТВЕТСТВЕННЫЙ_ЗАК" : "РАБОЧАЯ_ГР_ОТВ_ЗАК";
var innerGroupingField = await unitFieldRepository.GetByAihitNameAsync(innerGroupingFieldName)
?? throw new InvalidOperationException($"Поле '{innerGroupingFieldName}' не найдено.");
var innerGroupingFieldId = innerGroupingField.Id;
var groupingFieldId = jobGroup.GroupingUnitFieldId!.Value;
// 2. Собираем все исходные UnitId
var allSourceUnitIds = initialReverseMapping.Values.SelectMany(ids => ids).Distinct().ToList();
// 3. Загружаем UnitInValue для трансформации
var relevantUnitInValues = await unitInValueRepository.Get()
.AsNoTracking()
.Where(uiv => allSourceUnitIds.Contains(uiv.UnitId) && uiv.FieldId == groupingFieldId)
.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)
{
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())
.ToList();
if (uniqueEntries.Any())
reverseMapping[potentialUnitId] = uniqueEntries;
}
if (!reverseMapping.Any())
return new List<GroupedTemplateGroup>();
// 5. Загружаем значения для внутренней группировки
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);
var unitIdToInnerGroupingValueMap = innerGroupingValues
.Where(uv => uv.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)
{
var potentialUnitId = kvp.Key;
var unitsInTemplateForThisPotentialUnitId = kvp.Value;
var innerGroupedUnits = unitsInTemplateForThisPotentialUnitId
.GroupBy(entry => unitIdToInnerGroupingValueMap.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)
{
subGroups.Add(new GroupedTemplateSubGroup(
Entries: subGroupEntries,
InnerGroupName: innerGroupName,
GlobalIndex: globalIndex
));
globalIndex++;
}
}
if (subGroups.Any())
{
templateGroups.Add(new GroupedTemplateGroup(
PotentialUnitId: potentialUnitId,
SubGroups: subGroups
));
}
}
logger.LogDebug("Построено {Count} групп шаблонов.", templateGroups.Count);
return templateGroups;
}
}

View File

@@ -0,0 +1,316 @@
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 ITemplateReuser templateReuser;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly MqSettings mqSettings;
private readonly IRabbitService mqService;
public GroupedTemplateProcessor(
ILogger<GroupedTemplateProcessor> logger,
ITemplateRepository templateRepository,
IUnitRepository unitRepository,
ITemplateReuser templateReuser,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
MqSettings mqSettings,
IRabbitService mqService)
{
this.logger = logger;
this.templateRepository = templateRepository;
this.unitRepository = unitRepository;
this.templateReuser = templateReuser;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.mqSettings = mqSettings;
this.mqService = mqService;
}
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);
}
}
}
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 templateUpdaterMqSender.SendTemplateUpdateMessageAsync(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)
{
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator);
if (reusableTemplate != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId}.", reusableTemplate.Id);
var tempTemplateForName = new Template
{
Id = reusableTemplate.Id,
Name = reusableTemplate.Name,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Index = globalIndex,
Job = targetJob,
Unit = reusableTemplate.Unit,
UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitsInTemplate { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = reusableTemplate.Id,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Name = expectedName,
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false,
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
IsNew = true,
Index = globalIndex,
UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Создание нового шаблона.");
await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, globalIndex, initiator);
}
}
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 templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
private async Task CreateGroupedTemplateAsync(
Guid jobId,
Guid relationshipUnitId,
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplate,
int index,
HistoryInitiator initiator)
{
logger.LogInformation("Создание нового группового шаблона.");
var mqRequest = new TemplateGeneratorMessage
{
JobId = jobId,
UnitId = relationshipUnitId,
UnitsInTemplate = unitsInTemplate.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList(),
Index = index,
HistoryInitiator = initiator
};
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания шаблона.");
}
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;
}
}

View File

@@ -0,0 +1,243 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private readonly ILogger<GroupedTemplateSynchronizer> logger;
private readonly IJobGroupRepository jobGroupService;
private readonly IUnitFilterService unitFilterService;
private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter;
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
private readonly IGroupedTemplateBuilder groupedTemplateBuilder;
private readonly IGroupedTemplateProcessor groupedTemplateProcessor;
private readonly ITemplateRepository templateService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly IMatchingStatusService matchingStatusService;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
IJobGroupRepository jobGroupService,
IUnitFilterService unitFilterService,
IGroupedTemplateUnitFilter groupedTemplateUnitFilter,
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
IGroupedTemplateBuilder groupedTemplateBuilder,
IGroupedTemplateProcessor groupedTemplateProcessor,
ITemplateRepository templateService,
ITemplateDeactivator templateDeactivator,
IMatchingStatusService matchingStatusService)
{
this.logger = logger;
this.jobGroupService = jobGroupService;
this.unitFilterService = unitFilterService;
this.groupedTemplateUnitFilter = groupedTemplateUnitFilter;
this.unitInTemplateConflictMapper = unitInTemplateConflictMapper;
this.groupedTemplateBuilder = groupedTemplateBuilder;
this.groupedTemplateProcessor = groupedTemplateProcessor;
this.templateService = templateService;
this.templateDeactivator = templateDeactivator;
this.matchingStatusService = matchingStatusService;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Any() == true)
{
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
try
{
// 1. Загрузка JobGroup и связанных Job'ов
var jobGroup = await jobGroupService.Get()
.AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.ThenInclude(rf => rf.UnitField)
.Include(jg => jg.Jobs)
.ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return;
}
var jobsInGroup = jobGroup.Jobs.ToList();
// 2. Поиск Job с максимальным MaxValueRelationships
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
{
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
return;
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}).", maxJob.Id, maxJob.MaxValueRelationships);
// 3. Получение отфильтрованных юнитов через UnitFilterService
logger.LogDebug("Получение отфильтрованных юнитов через UnitFilterService для Job {JobId}.", maxJob.Id);
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
if (unitFilterResults == null || !unitFilterResults.Any())
{
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
return;
}
// 4. Применение специфичных правил фильтрации для групповых шаблонов
logger.LogDebug("Применение специфичных правил фильтрации для групповых шаблонов.");
var finalFilteredUnits = await groupedTemplateUnitFilter.FilterAsync(unitFilterResults, jobGroup);
if (!finalFilteredUnits.Any())
{
logger.LogInformation("После применения правил фильтрации в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
return;
}
// 5. Разрешение конфликтов связей и построение первичного маппинга
logger.LogDebug("Разрешение конфликтов связей и построение первичного маппинга.");
var initialReverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnits, maxJob);
if (!initialReverseMapping.Any())
{
logger.LogInformation("После разрешения конфликтов в JobGroup {JobGroupId} не осталось связей.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет связей после разрешения конфликтов");
return;
}
// 6. Построение структуры групп (трансформация, внутренняя группировка, разбиение)
logger.LogDebug("Построение структуры групп для шаблонов.");
var templateGroups = await groupedTemplateBuilder.BuildAsync(initialReverseMapping, jobGroup, maxJob);
if (!templateGroups.Any())
{
logger.LogInformation("После построения структуры групп в JobGroup {JobGroupId} не осталось данных.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет данных после построения групп");
return;
}
// 7. Обработка групп: сравнение, обновление, создание, отправка MQ
logger.LogDebug("Обработка групп шаблонов: сравнение, обновление и создание.");
var expectedTemplateKeys = await groupedTemplateProcessor.ProcessAsync(
templateGroups,
jobsInGroup,
maxJob,
initiator);
// 8. Деактивация лишних шаблонов
await DeactivateUnusedTemplatesAsync(expectedTemplateKeys, jobGroupId, jobsInGroup, initiator);
// 9. Успешное завершение
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
}
private async Task DeactivateUnusedTemplatesAsync(
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
Guid jobGroupId,
List<Job> jobsInGroup,
HistoryInitiator initiator)
{
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) &&
t.StatusTypeId == TemplateStatusTypeEnum.Used &&
t.Job!.GroupId == jobGroupId)
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Unit {UnitId}, Index {Index}).",
existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
}
}
}
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
{
var status = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
}
}

View File

@@ -0,0 +1,100 @@
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;
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions
{
internal class JobGroupValidatorService : IJobGroupValidatorService
{
private readonly ILogger<IJobValidatorService> logger;
private readonly IJobGroupRepository jobGroupService;
public JobGroupValidatorService(
ILogger<IJobValidatorService> logger,
IJobGroupRepository jobGroupService
)
{
this.logger = logger;
this.jobGroupService = jobGroupService;
}
public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId)
{
var isExist = await jobGroupService.GetAsync(jobGroupId);
if (isExist == null)
{
logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions
{
internal class JobValidatorService : IJobValidatorService
{
private readonly ILogger<IJobValidatorService> logger;
private readonly IJobRepository jobService;
public JobValidatorService(
ILogger<IJobValidatorService> logger,
IJobRepository jobService
)
{
this.logger = logger;
this.jobService = jobService;
}
public async Task<bool> IsValidJobAsync(Guid jobId)
{
var isExist = await jobService.GetAsync(jobId);
if (isExist == null)
{
logger.LogError($"Не найдена регалментная работа {nameof(jobId)}: {jobId}");
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,716 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models;
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.Entities.Unit;
using PARR.Domain.Enums;
using PARR.Domain.Settings;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{
#if DEBUG
private readonly Guid targetUnitId = Guid.Parse("358437ac-1eeb-4c00-840c-998326f657ac");
#endif
private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false;
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 ITemplateReuser templateReuser;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService;
private readonly SettingsFromDb settingsFromDb;
private readonly IOptions<TemplateSettings> templateSettings;
private readonly IUnitFieldRepository unitFieldService;
private readonly IUnitInValueRepository unitInValueService;
private readonly IUnitRepository unitRepository;
public SimpleTemplateSynchronizer(
ILogger<SimpleTemplateSynchronizer> logger,
IUnitFilterService unitFilterService,
MqSettings mqSettings,
IRabbitService mqService,
ITemplateRepository templateService,
IJobRepository jobService,
ITemplateReuser templateReuser,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService,
SettingsFromDb settingsFromDb,
IOptions<TemplateSettings> templateSettings,
IUnitFieldRepository unitFieldService,
IUnitInValueRepository unitInValueService,
IUnitRepository unitRepository
)
{
this.logger = logger;
this.unitFilterService = unitFilterService;
this.mqSettings = mqSettings;
this.mqService = mqService;
this.templateService = templateService;
this.jobService = jobService;
this.templateReuser = templateReuser;
this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService;
this.settingsFromDb = settingsFromDb;
this.templateSettings = templateSettings;
this.unitFieldService = unitFieldService;
this.unitInValueService = unitInValueService;
this.unitRepository = unitRepository;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
// === Специальная обработка для Job неиспользуемых шаблонов ===
if (jobId == settingsFromDb.JobIdForUnusedTemplates)
{
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId);
await SyncUnusedTemplatesAsync(jobId, initiator);
return;
}
// === Обычная логика для всех остальных Job ===
logger.LogDebug("Начало синхронизации шаблонов для Job {JobId}", jobId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Any() == true)
{
logger.LogWarning("Синхронизация для Job {JobId} уже запущена. Пропускаем.", jobId);
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobId,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
try
{
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);
if (job == null)
{
logger.LogWarning("Job {JobId} не найден.", jobId);
await UpdateMatchingStatusAsync(jobId, "Job не найден");
return;
}
// === Получение отфильтрованных юнитов с полной информацией ===
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
if (filteredUnits == null || !filteredUnits.Any())
{
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
var existingTemplatesForDeactivation = await templateService.Get()
.AsNoTracking()
.Include(t => t.UnitsInTemplate)
.Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync();
await UpdateMatchingStatusAsync(jobId, $"Нет Unit'ов. Деактивация {existingTemplatesForDeactivation.Count} шаблонов...");
foreach (var unusedTemplate in existingTemplatesForDeactivation)
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
}
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена: нет Unit'ов");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
return;
}
// Извлекаем ID юнитов для последующих операций
var unitIds = filteredUnits.Select(u => u.Id).ToList();
#if DEBUG
// Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
}
#endif
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();
var newUnitIds = unitIds.Except(existingUnitIds).ToList();
var unusedTemplates = existingUsedTemplates
.Where(t => !unitIds.Contains(t.UnitId))
.ToList();
foreach (var unitId in newUnitIds)
{
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
if (reusableTemplate != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId);
// === Создаём временный Template для нормализации имени ===
var tempTemplateForName = new Template
{
Id = reusableTemplate.Id,
Name = reusableTemplate.Name,
JobId = jobId,
UnitId = unitId,
Index = reusableTemplate.Index,
Job = job, // загруженный job
Unit = reusableTemplate.Unit, // может быть null — нормально
UnitsInTemplate = new List<UnitsInTemplate>() // для простого шаблона
};
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = reusableTemplate.Id,
JobId = jobId,
UnitId = unitId,
Name = expectedName,
IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
//NextRun = nextRun,
IsNew = true,
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
await CreateSimpleTemplateAsync(jobId, unitId, initiator);
}
}
// === Обработка существующих шаблонов (проверка имени) ===
foreach (var template in existingUsedTemplates)
{
if (unitIds.Contains(template.UnitId))
{
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
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 templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
}
}
// === Деактивация лишних шаблонов ===
foreach (var unusedTemplate in unusedTemplates)
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
}
// === Успешное завершение ===
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при синхронизации Job {JobId}", jobId);
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Any() == true)
{
logger.LogWarning("Обновление для Job {JobId} уже запущено. Пропускаем.", jobId);
return;
}
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Update,
Comment = "Начало обновления имён"
};
await matchingStatusService.SetMatchingStatusAsync(
jobId,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
try
{
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);
if (job == null)
{
logger.LogWarning("Job {JobId} не найден.", jobId);
await UpdateMatchingStatusAsync(jobId, "Job не найден");
return;
}
// === Получение отфильтрованных юнитов с полной информацией ===
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
if (filteredUnits == null || !filteredUnits.Any())
{
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
return;
}
// Извлекаем ID юнитов для последующих операций
var unitIds = filteredUnits.Select(u => u.Id).ToList();
#if DEBUG
// Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
}
#endif
var existingTemplates = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t => t!.GroupType)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync();
foreach (var template in existingTemplates)
{
if (unitIds.Contains(template.UnitId))
{
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
//LastRun = template.LastRun,
//NextRun = nextRun,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
}
}
await UpdateMatchingStatusAsync(jobId, "Обновление завершено");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при обновлении Job {JobId}", jobId);
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
throw;
}
}
private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator)
{
var existingStatus = await matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Any() == true)
{
logger.LogWarning("Синхронизация для Job неиспользуемых шаблонов {JobId} уже запущена. Пропускаем.", unusedJobId);
return;
}
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Синхронизация неиспользуемых шаблонов"
};
await matchingStatusService.SetMatchingStatusAsync(
unusedJobId,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
try
{
// 1. Находим ID нужных полей
var zoRgField = await unitFieldService.GetByAihitNameAsync("ЗОНА_ОТВЕТСТВЕННОСТИ");
var tagField = await unitFieldService.GetByAihitNameAsync("ПАРР тег");
if (zoRgField == null || tagField == null)
{
logger.LogError("Не найдены поля 'ЗОНА_ОТВЕТСТВЕННОСТИ' или 'Тег'. Синхронизация прервана.");
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
return;
}
var zoRgFieldId = zoRgField.Id;
var tagFieldId = tagField.Id;
const string targetTagValue = "ПАРР-НЕИСП";
// 2. Находим ValueId для тега "ПАРР-НЕИСП"
var targetTagValueId = await unitInValueService.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == targetTagValue)
.Select(uiv => uiv.ValueId)
.FirstOrDefaultAsync();
if (targetTagValueId == Guid.Empty)
{
logger.LogWarning("Значение '{TagValue}' для поля 'Тег' не найдено в справочнике UnitFieldValue.", targetTagValue);
}
var unusedJob = await jobService.Get()
.AsNoTracking()
.Include(j => j.Group)
.FirstOrDefaultAsync(j => j.Id == unusedJobId);
if (unusedJob == null)
{
logger.LogError("Job неиспользуемых шаблонов {JobId} не найден.", unusedJobId);
await UpdateMatchingStatusAsync(unusedJobId, "Job не найден");
return;
}
var unusedTemplates = await templateService.Get()
.Include(t => t.Unit)
.Include(t => t.Job)
.ThenInclude(t => t.Group)
.ThenInclude(t => t.GroupType)
.Include(t => t.Job)
.ThenInclude(t => t.Tnk)
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
.ToListAsync();
if (!unusedTemplates.Any())
{
logger.LogInformation("Не найдено шаблонов со статусом Unused.");
await UpdateMatchingStatusAsync(unusedJobId, "Нет шаблонов для обработки");
await matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
return;
}
await UpdateMatchingStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки");
int processed = 0;
var allTemplateUnitIds = unusedTemplates.Select(t => t.UnitId).Distinct().ToList();
// Получаем значения ЗОНА_ОТВЕТСТВЕННОСТИ для всех юнитов шаблонов
var unitZoRgValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allTemplateUnitIds, new List<Guid> { zoRgFieldId });
var unitToZoRgValueMap = unitZoRgValues
.Where(uiv => uiv.ValueId != Guid.Empty)
.ToDictionary(uiv => uiv.UnitId, uiv => uiv.ValueId);
foreach (var template in unusedTemplates)
{
try
{
if (template.Unit == null)
{
logger.LogWarning("У шаблона {TemplateId} отсутствует Unit. Пропускаем.", template.Id);
processed++;
continue;
}
if (!unitToZoRgValueMap.TryGetValue(template.UnitId, out var currentZoRgValueId))
{
logger.LogDebug("У юнита {UnitId} шаблона {TemplateId} нет значения поля ЗОНА_ОТВЕТСТВЕННОСТИ. Пропускаем замену UnitId.", template.UnitId, template.Id);
// Исправлено: используем другое имя или просто вызываем метод
var nameForUpdate = await GenerateUnusedTemplateNameAsync(template, unusedJob, template.Unit);
await SendUpdateRequest(template, unusedJobId, nameForUpdate, initiator, template.UnitId);
processed++;
continue;
}
Guid? targetUnitId = null;
if (targetTagValueId != Guid.Empty)
{
var candidatesWithZoRg = await unitInValueService.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == zoRgFieldId && uiv.ValueId == currentZoRgValueId)
.Select(uiv => uiv.UnitId)
.Distinct()
.ToListAsync();
if (candidatesWithZoRg.Any())
{
// Сначала проверим, есть ли вообще такие юниты
var foundUnitId = await unitInValueService.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == tagFieldId && uiv.ValueId == targetTagValueId && candidatesWithZoRg.Contains(uiv.UnitId))
.Select(uiv => uiv.UnitId)
.FirstOrDefaultAsync();
// Если нашли реальный Guid (не Empty), то используем его
if (foundUnitId != Guid.Empty)
{
targetUnitId = foundUnitId;
}
}
}
Guid finalUnitId = targetUnitId ?? template.UnitId;
Unit finalUnit = template.Unit;
if (targetUnitId.HasValue && targetUnitId.Value != template.UnitId)
{
logger.LogInformation("Для шаблона {TemplateId} найден новый UnitId {NewUnitId} (был {OldUnitId}).",
template.Id, targetUnitId.Value, template.UnitId);
// Загружаем новый юнит для генерации имени
var newUnit = await unitRepository.Get().AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == targetUnitId.Value);
if (newUnit != null)
finalUnit = newUnit;
else
{
logger.LogWarning("Не удалось загрузить новый юнит {UnitId}. Используем старый.", targetUnitId.Value);
finalUnitId = template.UnitId;
}
}
// Здесь expectedName объявляется впервые в этой итерации цикла, конфликта нет
var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob, finalUnit);
await SendUpdateRequest(template, unusedJobId, expectedName, initiator, finalUnitId);
processed++;
await UpdateMatchingStatusAsync(unusedJobId, $"Обработано: {processed}/{unusedTemplates.Count}");
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при обработке шаблона {TemplateId}", template.Id);
}
}
await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена");
await matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Синхронизация неиспользуемых шаблонов завершена. Обработано {Count} шаблонов.", unusedTemplates.Count);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при синхронизации неиспользуемых шаблонов для Job {JobId}", unusedJobId);
await UpdateMatchingStatusAsync(unusedJobId, $"Ошибка: {ex.Message}");
throw;
}
}
private async Task SendUpdateRequest(Template template, Guid jobId, string name, HistoryInitiator initiator, Guid unitId)
{
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
UnitId = unitId,
Name = name,
IsActiveTemplate = false,
IsActiveSchedule = false,
IsNew = false,
Index = null,
StatusTypeId = template.StatusTypeId,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
private async Task<string> GenerateUnusedTemplateNameAsync(Template template, Job unusedJob, Unit unit)
{
var tempJob = 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 tempTemplateForName = new Template
{
Id = template.Id,
Name = template.Name,
JobId = unusedJob.Id,
UnitId = unit.Id,
Index = null,
Job = tempJob,
Unit = unit,
UnitsInTemplate = new List<UnitsInTemplate>()
};
return await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
}
private async Task CreateSimpleTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator)
{
logger.LogInformation("Создание нового простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
var mqRequest = new TemplateGeneratorMessage
{
JobId = jobId,
UnitId = unitId,
UnitsInTemplate = new List<UnitInTemplateMessage>(), // для простого шаблона
HistoryInitiator = initiator
};
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
}
private async Task UpdateMatchingStatusAsync(Guid jobId, string comment)
{
var status = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await matchingStatusService.SetMatchingStatusAsync(
jobId,
SyncTaskEntityTypeEnum.Job,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
}
}

View File

@@ -0,0 +1,121 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
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.Domain.Settings;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
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 templateService;
private readonly IJobRepository jobService;
private readonly ITemplateNameNormalizer namenormalizer;
private readonly ITemplateUpdaterMqSender sender;
private readonly SettingsFromDb settingsFromDb;
private readonly IOptions<TemplateSettings> templateSettings;
public TemplateDeactivator(
ILogger<TemplateDeactivator> logger,
ITemplateRepository templateService,
IJobRepository jobService,
ITemplateNameNormalizer namenormalizer,
ITemplateUpdaterMqSender sender,
SettingsFromDb settingsFromDb,
IOptions<TemplateSettings> templateSettings
)
{
this.logger = logger;
this.templateService = templateService;
this.jobService = jobService;
this.namenormalizer = namenormalizer;
this.sender = sender;
this.settingsFromDb = settingsFromDb;
this.templateSettings = templateSettings;
}
public async Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator)
{
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
return true;
if (template.StatusTypeId == TemplateStatusTypeEnum.Unused)
{
logger.LogDebug("Шаблон {TemplateId} уже неактивен (Unused), пропускаем деактивацию.", template.Id);
return true;
}
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.",
template.Id, template.UnitId);
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
return false;
}
var unusedJob = await jobService.Get()
.AsNoTracking()
.Include(j => j.Tnk)
.Include(j => j.Group)
.ThenInclude(g => g!.GroupType)
.FirstOrDefaultAsync(j => j.Id == settingsFromDb.JobIdForUnusedTemplates);
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 sender.SendTemplateUpdateMessageAsync(updateRequest);
return true;
}
}

View File

@@ -0,0 +1,29 @@
using PARR.Core.Services.Shortcodes;
using PARR.Domain.Entities;
using PARR.TemplateMatcher.Services.Interfaces;
using System.Runtime.CompilerServices;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateNameNormalizer : ITemplateNameNormalizer
{
private readonly IShortcodesService shortcodesService;
public TemplateNameNormalizer(IShortcodesService shortcodesService)
{
this.shortcodesService = shortcodesService;
}
public async Task<string> GetNormalizedTemplateNameAsync(Template template, [CallerMemberName] string? caller = null)
{
var callerName = caller ?? "Unknown";
if (template.Job == null)
throw new ArgumentNullException(nameof(template.Job));
var rawName = await shortcodesService.ApplyShortcodesAsync(template.Job.TemplateNameMask, template, callerName);
return rawName.ToUpper();
}
}

View File

@@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateReuser : ITemplateReuser
{
private readonly ILogger<TemplateReuser> logger;
private readonly ITemplateRepository templateService;
public TemplateReuser(
ILogger<TemplateReuser> logger,
ITemplateRepository templateService)
{
this.logger = logger;
this.templateService = templateService;
}
public async Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId,
Guid unitId,
HistoryInitiator initiator,
int maxAttempts = 3)
{
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
// Атомарно резервируем один шаблон через DAL
var templateId = await templateService.ReserveUnusedTemplateAsync(unitId, initiator);
if (templateId == null)
{
logger.LogDebug("Нет доступных Unused-шаблонов для переиспользования (попытка {Attempt}).", attempt);
return null;
}
// Загружаем зарезервированный шаблон
var template = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t => t!.GroupType)
.FirstOrDefaultAsync(t => t.Id == templateId);
if (template == null)
{
logger.LogWarning("Зарезервированный шаблон {TemplateId} не найден при загрузке.", templateId);
continue;
}
logger.LogInformation(
"Успешно захвачен шаблон {TemplateId} (старый Job {OldJobId}) для нового Job {NewJobId}, Unit {UnitId} (попытка {Attempt}).",
template.Id, template.JobId, jobId, unitId, attempt);
return template;
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при попытке захвата шаблона (попытка {Attempt}).", attempt);
if (attempt == maxAttempts)
throw;
// Небольшая задержка перед повтором
await Task.Delay(Random.Shared.Next(10, 50));
}
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateUpdaterMqSender : ITemplateUpdaterMqSender
{
private readonly ILogger<TemplateUpdaterMqSender> logger;
private readonly IRabbitService mqService;
private readonly MqSettings mqSettings;
public TemplateUpdaterMqSender(
ILogger<TemplateUpdaterMqSender> logger,
IRabbitService mqService,
MqSettings mqSettings
)
{
this.logger = logger;
this.mqService = mqService;
this.mqSettings = mqSettings;
}
public async Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest)
{
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new List<object> { updateRequest });
if (result.IsSuccess)
{
logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId);
}
else
{
logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.",
updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName);
}
}
}

View File

@@ -0,0 +1,146 @@
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;
}
}