feat(tempateMatcher): в матчеры добавлено взаимодействие с MatchingStatusService, для отображения статуса операции

This commit is contained in:
Mikhail Kuznetsov
2026-01-16 19:45:38 +10:00
parent d91877453a
commit 5eb0ab81b9
2 changed files with 936 additions and 781 deletions

View File

@@ -4,6 +4,7 @@ using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces; using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain; using PARR.Common.Domain;
using PARR.Constants; using PARR.Constants;
using PARR.DAL.Cache.Models;
using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.Models; using PARR.DAL.Models;
using PARR.DAL.Models.Job; using PARR.DAL.Models.Job;
@@ -43,6 +44,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
private readonly ITemplateDeactivator templateDeactivator; private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService;
public GroupedTemplateSynchronizer( public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger, ILogger<GroupedTemplateSynchronizer> logger,
@@ -60,7 +62,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
IUnitFieldService unitFieldService, IUnitFieldService unitFieldService,
ITemplateDeactivator templateDeactivator, ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer, ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService
) )
{ {
this.logger = logger; this.logger = logger;
@@ -79,6 +82,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
this.templateDeactivator = templateDeactivator; this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer; this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender; this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService;
} }
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
@@ -90,6 +94,30 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{ {
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); 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'ы // 1. Получить JobGroup и связанные Job'ы
var jobGroup = await jobGroupService.Get() var jobGroup = await jobGroupService.Get()
.AsNoTracking() .AsNoTracking()
@@ -106,6 +134,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
{ {
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return; return;
} }
@@ -116,13 +145,14 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (workGroupField == null) if (workGroupField == null)
{ {
logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗАК' не найдено в справочнике полей. Синхронизация прервана."); logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗАК' не найдено в справочнике полей. Синхронизация прервана.");
await UpdateMatchingStatusAsync(jobGroupId, "Ошибка: поле 'РАБОЧАЯ_ГР_ОТВ_ЗАК' не найдено");
return; return;
} }
var workGroupFieldId = workGroupField.Id; var workGroupFieldId = workGroupField.Id;
var regionalGroupValueIds = regionalEkPtkGroupService.Get() var regionalGroupValueIds = regionalEkPtkGroupService.Get()
.Select(g => g.FieldValueId) .Select(g => g.FieldValueId)
.ToList(); // Получаем список UnitFieldValue.Id .ToList();
logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.", regionalGroupValueIds.Count); logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.", regionalGroupValueIds.Count);
@@ -135,10 +165,10 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (maxJob == null) if (maxJob == null)
{ {
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
return; return;
} }
// Проверяем, что UnitFilters и RelationshipFilters загружены (если используется для выбора targetJob)
if (maxJob.UnitFilters == null) if (maxJob.UnitFilters == null)
{ {
logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id); logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id);
@@ -151,11 +181,11 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (expectedUnitIds == null || !expectedUnitIds.Any()) if (expectedUnitIds == null || !expectedUnitIds.Any())
{ {
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId); logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов");
return; return;
} }
#if DEBUG #if DEBUG
// Отладка: проверить, есть ли юнит в expectedUnitIds
if (expectedUnitIds.Contains(targetUnitId)) if (expectedUnitIds.Contains(targetUnitId))
{ {
logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId); logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId);
@@ -166,16 +196,16 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
} }
#endif #endif
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId (дополнительный фильтр) // 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId
if (!jobGroup.GroupingUnitFieldId.HasValue) if (!jobGroup.GroupingUnitFieldId.HasValue)
{ {
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Отсутствует GroupingUnitFieldId");
return; return;
} }
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
// Загрузить UnitValues для юнитов из expectedUnitIds, чтобы проверить GroupingUnitFieldId
var expectedUnitsWithGroupingField = await unitService.Get() var expectedUnitsWithGroupingField = await unitService.Get()
.AsNoTracking() .AsNoTracking()
.Include(u => u.UnitValues) .Include(u => u.UnitValues)
@@ -189,7 +219,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.ToHashSet(); .ToHashSet();
#if DEBUG #if DEBUG
// Отладка: проверить, есть ли юнит в unitIdsWithValidGroupingFieldSet
if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId)) if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId))
{ {
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId); logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
@@ -205,6 +234,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (!unitIdsWithValidGroupingFieldSet.Any()) if (!unitIdsWithValidGroupingFieldSet.Any())
{ {
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId");
return; return;
} }
@@ -217,7 +247,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
); );
#if DEBUG #if DEBUG
// Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet
if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId)) if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId))
{ {
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId); logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
@@ -233,38 +262,23 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (!unitIdsWithValidWorkGroupFieldSet.Any()) if (!unitIdsWithValidWorkGroupFieldSet.Any())
{ {
logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК'");
return; return;
} }
// Обновляем список юнитов, прошедших оба фильтра
unitIdsWithValidGroupingFieldSet = unitIdsWithValidWorkGroupFieldSet; unitIdsWithValidGroupingFieldSet = unitIdsWithValidWorkGroupFieldSet;
// 5. Получить RelationshipFilters из maxJob // 5. Получить RelationshipFilters из maxJob
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>(); var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
// --- Найти и отфильтровать UnitInUnit связи --- logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию...");
logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию по GroupingUnitFieldId и 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.");
var potentialUnitInUnitLinks = await unitInUnitService.Get() var potentialUnitInUnitLinks = await unitInUnitService.Get()
.AsNoTracking() .AsNoTracking()
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId)) .Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
.ToListAsync(); .ToListAsync();
#if DEBUG
// Отладка: проверить, юнит участвует в potentialUnitInUnitLinks
var potentialLinksContainingTarget = potentialUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList();
if (potentialLinksContainingTarget.Any())
{
logger.LogDebug("Юнит {TargetUnitId} участвует в {Count} потенциальных связях UnitInUnit.", targetUnitId, potentialLinksContainingTarget.Count);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ участвует в потенциальных связях UnitInUnit.", targetUnitId);
}
#endif
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count); logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count);
// Загрузить UnitInValue для всех ParentUnitId и ChildUnitId из potentialUnitInUnitLinks
var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet(); var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet();
var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet(); var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet();
@@ -282,7 +296,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.Where(uv => allChildIds.Contains(uv.UnitId)) .Where(uv => allChildIds.Contains(uv.UnitId))
.ToListAsync(); .ToListAsync();
// Сгруппировать значения по UnitId для быстрого доступа
var parentValuesMap = parentUnitValues var parentValuesMap = parentUnitValues
.GroupBy(uv => uv.UnitId) .GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList()); .ToDictionary(g => g.Key, g => g.ToList());
@@ -291,7 +304,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.GroupBy(uv => uv.UnitId) .GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList()); .ToDictionary(g => g.Key, g => g.ToList());
// Применить фильтры к связям
logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count); logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count);
var filteredUnitInUnitLinks = new List<UnitInUnit>(); var filteredUnitInUnitLinks = new List<UnitInUnit>();
foreach (var link in potentialUnitInUnitLinks) foreach (var link in potentialUnitInUnitLinks)
@@ -325,22 +337,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
} }
} }
#if DEBUG
// Отладка: проверить, юнит участвует в filteredUnitInUnitLinks
var filteredLinksContainingTarget = filteredUnitInUnitLinks.Where(l => l.ParentUnitId == targetUnitId || l.ChildUnitId == targetUnitId).ToList();
if (filteredLinksContainingTarget.Any())
{
logger.LogDebug("Юнит {TargetUnitId} участвует в {Count} отфильтрованных связях UnitInUnit.", targetUnitId, filteredLinksContainingTarget.Count);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ участвует в отфильтрованных связях UnitInUnit.", targetUnitId);
}
#endif
logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count); logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count);
// --- Сгруппировать юниты из unitIdsWithValidGroupingFieldSet по связанному юниту --- // --- Сгруппировать юниты ---
var groupedRelationships = new Dictionary<Guid, List<Guid>>(); var groupedRelationships = new Dictionary<Guid, List<Guid>>();
foreach (var link in filteredUnitInUnitLinks) foreach (var link in filteredUnitInUnitLinks)
{ {
@@ -367,24 +366,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count); logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count);
#if DEBUG // --- Разрешение конфликта ---
// Отладка: проверить, юнит есть в groupedRelationships.Values var unitToKeys = new Dictionary<Guid, List<Guid>>();
var allUnitsInGroups = groupedRelationships.Values.SelectMany(x => x).ToList();
if (allUnitsInGroups.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в groupedRelationships.Values до разрешения конфликтов.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в groupedRelationships.Values до разрешения конфликтов.", targetUnitId);
}
logger.LogDebug("Содержимое groupedRelationships до разрешения конфликтов: [{Groups}]", string.Join(", ", groupedRelationships.Select(kvp => $"Key: {kvp.Key}, Values: [{string.Join(", ", kvp.Value)}]")));
#endif
// --- Разрешение конфликта - один юнит из unitIdsWithValidGroupingFieldSet только в одном списке значений ---
var unitToKeys = new Dictionary<Guid, List<Guid>>(); // Карта: юнит из списка -> список ключей, где он встречается
foreach (var kvp in groupedRelationships) foreach (var kvp in groupedRelationships)
{ {
var key = kvp.Key; var key = kvp.Key;
@@ -399,40 +382,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
} }
} }
#if DEBUG
// Отладка: проверить, юнит есть в unitToKeys до поиска конфликтов
if (unitToKeys.ContainsKey(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в unitToKeys до поиска конфликтов, находится в {Count} группах: [{Groups}]", targetUnitId, unitToKeys[targetUnitId].Count, string.Join(", ", unitToKeys[targetUnitId]));
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitToKeys до поиска конфликтов.", targetUnitId);
}
#endif
// Найти юниты, которые находятся в нескольких списках
var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList(); var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList();
#if DEBUG
// Отладка: проверить, юнит в conflictedUnits
var targetConflictedEntry = conflictedUnits.FirstOrDefault(c => c.Key == targetUnitId);
if (targetConflictedEntry.Key != default)
{
logger.LogDebug("Юнит {TargetUnitId} находится в {Count} группах (конфликт).", targetUnitId, targetConflictedEntry.Value.Count);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ находится в конфликте (не в нескольких группах).", targetUnitId);
}
#endif
foreach (var conflictedUnitEntry in conflictedUnits) foreach (var conflictedUnitEntry in conflictedUnits)
{ {
var unitId = conflictedUnitEntry.Key; var unitId = conflictedUnitEntry.Key;
var keysForUnit = conflictedUnitEntry.Value; var keysForUnit = conflictedUnitEntry.Value;
Guid bestKey = keysForUnit[0]; // Инициализируем первым ключом Guid bestKey = keysForUnit[0];
int maxCount = groupedRelationships[bestKey].Count; int maxCount = groupedRelationships[bestKey].Count;
for (int i = 1; i < keysForUnit.Count; i++) for (int i = 1; i < keysForUnit.Count; i++)
@@ -446,54 +402,150 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
} }
} }
// Удалить юнит из списков всех ключей, кроме bestKey
foreach (var key in keysForUnit) foreach (var key in keysForUnit)
{ {
if (key != bestKey) if (key != bestKey)
{ {
groupedRelationships[key].Remove(unitId); groupedRelationships[key].Remove(unitId);
logger.LogDebug("Юнит {UnitId} перемещён из группы {OldKey} в группу {BestKey} (по кол-ву).", unitId, key, bestKey);
} }
} }
} }
#if DEBUG
// Отладка: проверить, юнит есть в groupedRelationships.Values после разрешения конфликтов
var allUnitsInGroupsAfterConflictResolution = groupedRelationships.Values.SelectMany(x => x).ToList();
if (allUnitsInGroupsAfterConflictResolution.Contains(targetUnitId))
{
logger.LogDebug("Юнит {TargetUnitId} найден в groupedRelationships.Values после разрешения конфликтов.", targetUnitId);
}
else
{
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в groupedRelationships.Values после разрешения конфликтов.", targetUnitId);
}
#endif
// Удаляем ключи, у которых список стал пустым после разрешения конфликтов
var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList(); var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList();
foreach (var key in keysToRemove) foreach (var key in keysToRemove)
{ {
groupedRelationships.Remove(key); groupedRelationships.Remove(key);
logger.LogDebug("Ключ {Key} удалён, так как его список юнитов стал пустым после разрешения конфликтов.", key);
} }
logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count); logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count);
#if DEBUG // === Подсчёт операций ===
logger.LogDebug("Содержимое groupedRelationships после разрешения конфликтов: [{Groups}]", string.Join(", ", groupedRelationships.Select(kvp => $"Key: {kvp.Key}, Values: [{string.Join(", ", kvp.Value)}]"))); int toCreate = 0;
#endif int toUpdate = 0;
int toDeactivate = 0;
// 7. Разбить каждую группу и сопоставить с Job
foreach (var kvp in groupedRelationships) foreach (var kvp in groupedRelationships)
{ {
var relationshipUnitId = kvp.Key; // Связанный юнит (не из unitIdsWithValidGroupingFieldSet) var childUnitIds = kvp.Value;
var childUnitIds = kvp.Value; // Юниты из unitIdsWithValidGroupingFieldSet, связанные с relationshipUnitId if (childUnitIds.Count == 0) continue;
if (childUnitIds.Count == 0)
continue; var childUnitNameMap = await unitService.Get()
.AsNoTracking()
.Where(u => childUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name);
var sortedChildUnitIds = childUnitIds
.OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString()))
.ToList();
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
var childUnitGroups = sortedChildUnitIds
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
for (int i = 0; i < childUnitGroups.Count; i++)
{
var subGroup = childUnitGroups[i];
var subGroupSize = subGroup.Count;
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
var existingTemplatesForRelationship = await templateService.Get()
.AsNoTracking()
.Include(t=>t.Unit)
.Include(t=>t.Job)
.ThenInclude(t=>t.Group)
.ThenInclude(t=>t.GroupType)
.Include(t => t.Job)
.ThenInclude(t => t.Tnk)
.Include(t => t.UnitsInTemplate)
.Where(t => t.JobId == targetJob.Id && t.UnitId == kvp.Key && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync();
if (existingTemplatesForRelationship.Any())
{
var existingTemplate = existingTemplatesForRelationship.First();
var existingUnitIds = existingTemplate.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet();
var newUnitIds = subGroup.ToHashSet();
if (!existingUnitIds.SetEquals(newUnitIds))
toUpdate++;
else
{
// Обновляем UnitsInTemplate для корректной генерации имени
existingTemplate.UnitsInTemplate = subGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList();
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate);
if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase))
toUpdate++;
}
}
else
{
toCreate++;
}
}
}
// Подсчёт деактивации
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvp in groupedRelationships)
{
var relationshipUnitId = kvp.Key;
var childUnitIds = kvp.Value;
var childUnitNameMapForDeactivate = await unitService.Get()
.AsNoTracking()
.Where(u => childUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name);
var sortedChildUnitIdsForDeactivate = childUnitIds
.OrderBy(id => childUnitNameMapForDeactivate.GetValueOrDefault(id, id.ToString()))
.ToList();
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
var childUnitGroups = sortedChildUnitIdsForDeactivate
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
for (int i = 0; i < childUnitGroups.Count; i++)
{
var subGroup = childUnitGroups[i];
var subGroupSize = subGroup.Count;
Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
expectedTemplateKeys.Add((targetJobForExpectedKey.Id, relationshipUnitId, i));
}
}
var allRelationshipUnitIds = groupedRelationships.Keys.ToHashSet();
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking()
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) && allRelationshipUnitIds.Contains(t.UnitId))
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedTemplateKeys.Contains(key))
toDeactivate++;
}
await UpdateMatchingStatusAsync(jobGroupId, $"Осталось: создать={toCreate}, обновить={toUpdate}, деактивировать={toDeactivate}");
// === Основной цикл обработки ===
int created = 0, updated = 0, deactivated = 0;
foreach (var kvp in groupedRelationships)
{
var relationshipUnitId = kvp.Key;
var childUnitIds = kvp.Value;
if (childUnitIds.Count == 0) continue;
#if DEBUG #if DEBUG
// === Безопасная асинхронная загрузка имён для отладки ===
var debugUnitIds = childUnitIds.Concat(new[] { relationshipUnitId }).Distinct().ToList(); var debugUnitIds = childUnitIds.Concat(new[] { relationshipUnitId }).Distinct().ToList();
var debugUnits = await unitService.Get() var debugUnits = await unitService.Get()
.AsNoTracking() .AsNoTracking()
@@ -511,7 +563,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count); logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count);
// --- СОРТИРОВКА childUnitIds ПО ИМЕНИ ---
var childUnitNameMap = await unitService.Get() var childUnitNameMap = await unitService.Get()
.AsNoTracking() .AsNoTracking()
.Where(u => childUnitIds.Contains(u.Id)) .Where(u => childUnitIds.Contains(u.Id))
@@ -521,22 +572,15 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString())) .OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString()))
.ToList(); .ToList();
// Применяем ограничение MaxValueRelationships maxJob int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
if (!maxJob.MaxValueRelationships.HasValue)
{
logger.LogWarning("Job {JobId} не заполнено MaxValueRelationships.", maxJob.Id);
return;
}
int maxValueForSplitting = maxJob.MaxValueRelationships.Value;
var childUnitGroups = sortedChildUnitIds var childUnitGroups = sortedChildUnitIds
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex) .GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList()) .Select(g => g.Select(x => x.id).ToList())
.ToList(); .ToList();
logger.LogDebug("Связанный юнит {RelationshipUnitId}: разбит на {GroupCount} подгрупп (после сортировки по имени).", relationshipUnitId, childUnitGroups.Count); logger.LogDebug("Связанный юнит {RelationshipUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count);
// Для каждой подгруппы:
for (int i = 0; i < childUnitGroups.Count; i++) for (int i = 0; i < childUnitGroups.Count; i++)
{ {
var subGroup = childUnitGroups[i]; var subGroup = childUnitGroups[i];
@@ -544,19 +588,16 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RelationshipUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize); logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RelationshipUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize);
// Выбираем targetJob для подгруппы
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
// 9. Загрузить существующие шаблоны для targetJob, связанные с relationshipUnitId
var existingTemplatesForRelationship = await templateService.Get() var existingTemplatesForRelationship = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit) .Include(t => t.Unit)
.Include(t => t.Job) .Include(t => t.Job)
.ThenInclude(t => t!.Tnk) .ThenInclude(t => t!.Tnk)
.Include(t => t.Job) .Include(t => t.Job)
.ThenInclude(t => t.Group) .ThenInclude(t => t!.Group)
.ThenInclude(t => t.GroupType) .ThenInclude(t => t!.GroupType)
.AsNoTracking()
.Include(t => t.UnitsInTemplate)
.Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) .Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync(); .ToListAsync();
@@ -564,19 +605,19 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (existingTemplateForSubGroup != null) if (existingTemplateForSubGroup != null)
{ {
// Проверить, изменились ли юниты
var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet(); var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet();
var newUnitIds = subGroup.ToHashSet(); var newUnitIds = subGroup.ToHashSet();
if (existingUnitIds.SetEquals(newUnitIds)) if (existingUnitIds.SetEquals(newUnitIds))
{ {
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Relationship {RelationshipId}, Index {Index}) актуален по юнитам.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i); logger.LogDebug("Шаблон {TemplateId} актуален по юнитам.", existingTemplateForSubGroup.Id);
// Проверить, изменилось ли имя шаблона // Обновляем UnitsInTemplate для генерации имени
existingTemplateForSubGroup.UnitsInTemplate = subGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList();
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup);
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.Name, expectedName); logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id);
var nextRun = await GetNextRunAsync(targetJob, existingTemplateForSubGroup.NextRun); var nextRun = await GetNextRunAsync(targetJob, existingTemplateForSubGroup.NextRun);
@@ -597,26 +638,27 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}; };
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
updated++;
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
} }
else else
{ {
logger.LogDebug("Шаблон {TemplateId} полностью актуален (юниты и имя).", existingTemplateForSubGroup.Id); logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id);
} }
} }
else else
{ {
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Relationship {RelationshipId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i); logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов.", existingTemplateForSubGroup.Id);
// Пересчитать targetJob для нового размера subGroup
var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
// Если Job изменился — нужно обновить шаблон с новым JobId
if (newTargetJob.Id != existingTemplateForSubGroup.JobId) if (newTargetJob.Id != existingTemplateForSubGroup.JobId)
{ {
logger.LogDebug("Job для шаблона {TemplateId} изменился: старый {OldJobId}, новый {NewJobId}.", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.JobId, newTargetJob.Id); logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id);
} }
await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, newTargetJob, initiator); await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, newTargetJob, initiator);
updated++;
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
} }
} }
else else
@@ -627,7 +669,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{ {
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i); logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i);
// === Создаём временный Template для нормализации имени ===
var tempTemplateForName = new Template var tempTemplateForName = new Template
{ {
Id = reusableTemplate.Id, Id = reusableTemplate.Id,
@@ -659,60 +700,21 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}; };
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
created++;
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
} }
else else
{ {
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count); logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count);
await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator); await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator);
created++;
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
} }
} }
} }
} }
// 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе // === Деактивация ===
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvp in groupedRelationships)
{
var relationshipUnitId = kvp.Key;
var childUnitIds = kvp.Value;
// --- СОРТИРОВКА ПЕРЕД ФОРМИРОВАНИЕМ expectedTemplateKeys ---
var childUnitNameMapForDeactivate = await unitService.Get()
.AsNoTracking()
.Where(u => childUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name);
var sortedChildUnitIdsForDeactivate = childUnitIds
.OrderBy(id => childUnitNameMapForDeactivate.GetValueOrDefault(id, id.ToString()))
.ToList();
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
var childUnitGroups = sortedChildUnitIdsForDeactivate
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
for (int i = 0; i < childUnitGroups.Count; i++)
{
var subGroup = childUnitGroups[i];
var subGroupSize = subGroup.Count;
Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
expectedTemplateKeys.Add((targetJobForExpectedKey.Id, relationshipUnitId, i));
}
}
var allRelationshipUnitIds = groupedRelationships.Keys.ToHashSet();
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking()
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) && allRelationshipUnitIds.Contains(t.UnitId))
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup) foreach (var existingTemplate in allExistingTemplatesInGroup)
{ {
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
@@ -720,11 +722,23 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{ {
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index); logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator); await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
deactivated++;
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
} }
} }
// === Успешное завершение ===
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); 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) public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{ {
@@ -734,9 +748,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
// --- Вспомогательные методы --- // --- Вспомогательные методы ---
/// <summary>
/// Фильтрует юниты по полю "РАБОЧАЯ_ГР_ОТВ_ЗАК"
/// </summary>
private async Task<HashSet<Guid>> FilterByWorkGroupFieldAsync( private async Task<HashSet<Guid>> FilterByWorkGroupFieldAsync(
List<Unit> units, List<Unit> units,
HashSet<Guid> candidateUnitIds, HashSet<Guid> candidateUnitIds,
@@ -753,9 +764,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.ToHashSet(); .ToHashSet();
} }
/// <summary>
/// Выбирает Job, соответствующий размеру подгруппы
/// </summary>
private Job SelectTargetJob(List<Job> jobsInGroup, int subGroupSize, Job maxJob) private Job SelectTargetJob(List<Job> jobsInGroup, int subGroupSize, Job maxJob)
{ {
Job? targetJob = jobsInGroup Job? targetJob = jobsInGroup
@@ -794,7 +802,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
return; return;
} }
// === Создаём временный Template для нормализации имени ===
var tempTemplateForName = new Template var tempTemplateForName = new Template
{ {
Id = template.Id, Id = template.Id,
@@ -854,4 +861,20 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
var referenceDate = targetJob.Group?.ReferenceDate ?? DateTimeOffset.UtcNow; var referenceDate = targetJob.Group?.ReferenceDate ?? DateTimeOffset.UtcNow;
return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate); return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate);
} }
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

@@ -4,6 +4,7 @@ using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces; using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain; using PARR.Common.Domain;
using PARR.Constants; using PARR.Constants;
using PARR.DAL.Cache.Models;
using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.Models; using PARR.DAL.Models;
@@ -38,6 +39,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private readonly ITemplateDeactivator templateDeactivator; private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService;
public SimpleTemplateSynchronizer( public SimpleTemplateSynchronizer(
ILogger<SimpleTemplateSynchronizer> logger, ILogger<SimpleTemplateSynchronizer> logger,
@@ -56,7 +58,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
IUnitFieldService unitFieldService, IUnitFieldService unitFieldService,
ITemplateDeactivator templateDeactivator, ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer, ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService
) )
{ {
this.logger = logger; this.logger = logger;
@@ -70,12 +73,37 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
this.templateDeactivator = templateDeactivator; this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer; this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender; this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService;
} }
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало синхронизации шаблонов для Job {JobId}", jobId); 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() var job = await jobService.Get()
.AsNoTracking() .AsNoTracking()
.Include(j => j.AutoControl) .Include(j => j.AutoControl)
@@ -89,33 +117,37 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
if (job == null) if (job == null)
{ {
logger.LogWarning("Job {JobId} не найден.", jobId); logger.LogWarning("Job {JobId} не найден.", jobId);
await UpdateMatchingStatusAsync(jobId, "Job не найден");
return; return;
} }
var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId);
{
if (unitIds == null || !unitIds.Any()) if (unitIds == null || !unitIds.Any())
{ {
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
// Деактивировать все существующие шаблоны для этого Job
var existingTemplatesForDeactivation = await templateService.Get() var existingTemplatesForDeactivation = await templateService.Get()
.AsNoTracking() .AsNoTracking()
.Include(t => t.UnitsInTemplate) .Include(t => t.UnitsInTemplate)
.Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync(); .ToListAsync();
await UpdateMatchingStatusAsync(jobId, $"Нет Unit'ов. Деактивация {existingTemplatesForDeactivation.Count} шаблонов...");
foreach (var unusedTemplate in existingTemplatesForDeactivation) foreach (var unusedTemplate in existingTemplatesForDeactivation)
{ {
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
} }
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена: нет Unit'ов");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
return; return;
} }
}
#if DEBUG #if DEBUG
// Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(targetUnitId)) if (unitIds.Contains(targetUnitId))
{ {
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
@@ -150,7 +182,26 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
.Where(t => !unitIds.Contains(t.UnitId)) .Where(t => !unitIds.Contains(t.UnitId))
.ToList(); .ToList();
// Обработка новых юнитов // === Подсчёт операций ===
int toCreate = newUnitIds.Count;
int toUpdate = 0;
int toDeactivate = unusedTemplates.Count;
// Подсчёт обновлений по имени
foreach (var template in existingUsedTemplates)
{
if (unitIds.Contains(template.UnitId))
{
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
toUpdate++;
}
}
await UpdateMatchingStatusAsync(jobId, $"Осталось: создать={toCreate}, обновить={toUpdate}, деактивировать={toDeactivate}");
// === Обработка новых юнитов ===
int created = 0, updated = 0, deactivated = 0;
foreach (var unitId in newUnitIds) foreach (var unitId in newUnitIds)
{ {
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
@@ -158,7 +209,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{ {
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId); logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId);
// === Создаём временный Template для нормализации имени ===
var tempTemplateForName = new Template var tempTemplateForName = new Template
{ {
Id = reusableTemplate.Id, Id = reusableTemplate.Id,
@@ -166,9 +216,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
JobId = jobId, JobId = jobId,
UnitId = unitId, UnitId = unitId,
Index = reusableTemplate.Index, Index = reusableTemplate.Index,
Job = job, // загруженный job Job = job,
Unit = reusableTemplate.Unit, // может быть null — нормально Unit = reusableTemplate.Unit,
UnitsInTemplate = new List<UnitsInTemplate>() // для простого шаблона UnitsInTemplate = new List<UnitsInTemplate>()
}; };
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
@@ -185,7 +235,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
StatusTypeId = TemplateStatusTypeEnum.Used, StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator, Initiator = initiator,
NextRun = nextRun, NextRun = nextRun,
UnitsInTemplate = new List<Guid>() // для простого шаблона UnitsInTemplate = new List<Guid>()
}; };
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
@@ -195,9 +245,11 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId); logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
await CreateSimpleTemplateAsync(jobId, unitId, initiator); await CreateSimpleTemplateAsync(jobId, unitId, initiator);
} }
created++;
await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
} }
// Обработка существующих шаблонов (проверка имени) // === Обработка существующих шаблонов (проверка имени) ===
foreach (var template in existingUsedTemplates) foreach (var template in existingUsedTemplates)
{ {
if (unitIds.Contains(template.UnitId)) if (unitIds.Contains(template.UnitId))
@@ -205,7 +257,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", template.Id);
var nextRun = await GetNextRunAsync(job, template.NextRun); var nextRun = await GetNextRunAsync(job, template.NextRun);
@@ -222,23 +274,37 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Index = template.Index, Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used, StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator, Initiator = initiator,
UnitsInTemplate = new List<Guid>() // для простого шаблона UnitsInTemplate = new List<Guid>()
}; };
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
updated++;
await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
} }
} }
} }
// Деактивация лишних шаблонов // === Деактивация лишних шаблонов ===
foreach (var unusedTemplate in unusedTemplates) foreach (var unusedTemplate in unusedTemplates)
{ {
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
deactivated++;
await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
} }
// === Успешное завершение ===
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); 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) public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{ {
@@ -249,6 +315,29 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{ {
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId); 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() var job = await jobService.Get()
.AsNoTracking() .AsNoTracking()
.Include(j => j.AutoControl) .Include(j => j.AutoControl)
@@ -262,6 +351,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
if (job == null) if (job == null)
{ {
logger.LogWarning("Job {JobId} не найден.", jobId); logger.LogWarning("Job {JobId} не найден.", jobId);
await UpdateMatchingStatusAsync(jobId, "Job не найден");
return; return;
} }
@@ -269,11 +359,12 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
if (unitIds == null || !unitIds.Any()) if (unitIds == null || !unitIds.Any())
{ {
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
return; return;
} }
#if DEBUG #if DEBUG
// Отладка: проверить, есть ли юнит в unitIds
if (unitIds.Contains(targetUnitId)) if (unitIds.Contains(targetUnitId))
{ {
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
@@ -296,6 +387,20 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
.Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync(); .ToListAsync();
int toUpdate = 0;
foreach (var template in existingTemplates)
{
if (unitIds.Contains(template.UnitId))
{
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
toUpdate++;
}
}
await UpdateMatchingStatusAsync(jobId, $"Обновление имён: {toUpdate} шаблонов");
int updated = 0;
foreach (var template in existingTemplates) foreach (var template in existingTemplates)
{ {
if (unitIds.Contains(template.UnitId)) if (unitIds.Contains(template.UnitId))
@@ -303,7 +408,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", template.Id);
var nextRun = await GetNextRunAsync(job, template.NextRun); var nextRun = await GetNextRunAsync(job, template.NextRun);
@@ -320,16 +425,27 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Index = template.Index, Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used, StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator, Initiator = initiator,
UnitsInTemplate = new List<Guid>() // для простого шаблона UnitsInTemplate = new List<Guid>()
}; };
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
updated++;
await UpdateMatchingStatusAsync(jobId, $"Обновлено: {updated}/{toUpdate}");
} }
} }
} }
await UpdateMatchingStatusAsync(jobId, "Обновление завершено");
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId); logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId);
} }
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при обновлении Job {JobId}", jobId);
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
throw;
}
}
private async Task CreateSimpleTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator) private async Task CreateSimpleTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator)
{ {
@@ -339,7 +455,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{ {
JobId = jobId, JobId = jobId,
UnitId = unitId, UnitId = unitId,
UnitsInTemplate = new List<Guid>(), // для простого шаблона UnitsInTemplate = new List<Guid>(),
HistoryInitiator = initiator HistoryInitiator = initiator
}; };
@@ -355,4 +471,20 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
var referenceDate = job.Group?.ReferenceDate ?? DateTimeOffset.UtcNow; var referenceDate = job.Group?.ReferenceDate ?? DateTimeOffset.UtcNow;
return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate);
} }
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)
);
}
} }