795 lines
41 KiB
C#
795 lines
41 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using PARR.BLL.Domain.Mq;
|
||
using PARR.BLL.Services.Interfaces;
|
||
using PARR.Common.Domain;
|
||
using PARR.Constants;
|
||
using PARR.DAL.Contracts;
|
||
using PARR.DAL.DomainServices.Interfaces;
|
||
using PARR.DAL.Models;
|
||
using PARR.DAL.Models.Job;
|
||
using PARR.DAL.Models.Unit;
|
||
using PARR.DAL.Services.Interfaces;
|
||
using PARR.DAL.Services.Interfaces.Job;
|
||
using PARR.DAL.Services.Interfaces.Unit;
|
||
using PARR.DAL.TransformServices;
|
||
using PARR.TemplateMatcher.Services.Interfaces;
|
||
using PARR.TemplateMatcher.Settings;
|
||
using System.Configuration;
|
||
using System.Text.Json;
|
||
|
||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||
|
||
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||
{
|
||
#if DEBUG
|
||
private readonly Guid targetUnitId = Guid.Parse("d4322a08-246b-4380-8953-8ce4a8446235");
|
||
#endif
|
||
|
||
private const bool DefaultUsedTemplateState = false;
|
||
private const bool DefaultUsedScheduleState = false;
|
||
|
||
private readonly ILogger<GroupedTemplateSynchronizer> logger;
|
||
private readonly IUnitFilterService unitFilterService;
|
||
private readonly IUnitInUnitService unitInUnitService;
|
||
private readonly IUnitInValueService unitInValueService;
|
||
private readonly IUnitService unitService;
|
||
private readonly MqSettings mqSettings;
|
||
private readonly IMqService mqService;
|
||
private readonly ITemplateService templateService;
|
||
private readonly IJobGroupService jobGroupService;
|
||
private readonly ITemplateReuser templateReuser;
|
||
private readonly IEsppScheduleTransformService esppScheduleTransformService;
|
||
private readonly IUnitRegionalEkPtkGroupService regionalEkPtkGroupService;
|
||
private readonly IUnitFieldService unitFieldService;
|
||
private readonly ITemplateDeactivator templateDeactivator;
|
||
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
||
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
|
||
|
||
public GroupedTemplateSynchronizer(
|
||
ILogger<GroupedTemplateSynchronizer> logger,
|
||
IUnitFilterService unitFilterService,
|
||
IUnitInUnitService unitInUnitService,
|
||
IUnitInValueService unitInValueService,
|
||
IUnitService unitService,
|
||
MqSettings mqSettings,
|
||
IMqService mqService,
|
||
ITemplateService templateService,
|
||
IJobGroupService jobGroupService,
|
||
ITemplateReuser templateReuser,
|
||
IEsppScheduleTransformService esppScheduleTransformService,
|
||
IUnitRegionalEkPtkGroupService regionalEkPtkGroupService,
|
||
IUnitFieldService unitFieldService,
|
||
ITemplateDeactivator templateDeactivator,
|
||
ITemplateNameNormalizer templateNameNormalizer,
|
||
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
||
SettingsFromDb settingsFromDb
|
||
)
|
||
{
|
||
this.logger = logger;
|
||
this.unitFilterService = unitFilterService;
|
||
this.unitInUnitService = unitInUnitService;
|
||
this.unitInValueService = unitInValueService;
|
||
this.unitService = unitService;
|
||
this.mqSettings = mqSettings;
|
||
this.mqService = mqService;
|
||
this.templateService = templateService;
|
||
this.jobGroupService = jobGroupService;
|
||
this.templateReuser = templateReuser;
|
||
this.esppScheduleTransformService = esppScheduleTransformService;
|
||
this.regionalEkPtkGroupService = regionalEkPtkGroupService;
|
||
this.unitFieldService = unitFieldService;
|
||
this.templateDeactivator = templateDeactivator;
|
||
this.templateNameNormalizer = templateNameNormalizer;
|
||
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
||
}
|
||
|
||
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);
|
||
|
||
// 1. Получить JobGroup и связанные Job'ы
|
||
var jobGroup = await jobGroupService.Get()
|
||
.AsNoTracking()
|
||
.Include(jg => jg.GroupType)
|
||
.Include(jg => jg.Jobs)
|
||
.ThenInclude(j => j.AutoControl)
|
||
.Include(jg => jg.Jobs)
|
||
.ThenInclude(j => j.UnitFilters)
|
||
.ThenInclude(uf => uf.RelationshipFilters)
|
||
.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);
|
||
return;
|
||
}
|
||
|
||
var jobsInGroup = jobGroup.Jobs.ToList();
|
||
|
||
// --- НОВАЯ ЛОГИКА: Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" ---
|
||
var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК");
|
||
if (workGroupField == null)
|
||
{
|
||
logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено в справочнике полей. Синхронизация прервана.");
|
||
return;
|
||
}
|
||
var workGroupFieldId = workGroupField.Id;
|
||
|
||
var regionalGroupValueIds = regionalEkPtkGroupService.Get()
|
||
.Select(g => g.FieldValueId)
|
||
.ToList(); // Получаем список UnitFieldValue.Id
|
||
|
||
logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.", regionalGroupValueIds.Count);
|
||
|
||
// 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);
|
||
return;
|
||
}
|
||
|
||
// Проверяем, что UnitFilters и RelationshipFilters загружены (если используется для выбора targetJob)
|
||
if (maxJob.UnitFilters == null)
|
||
{
|
||
logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id);
|
||
}
|
||
|
||
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
|
||
|
||
// 3. Использовать фильтры maxJob для получения expectedUnitIds
|
||
var expectedUnitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(maxJob.Id);
|
||
if (expectedUnitIds == null || !expectedUnitIds.Any())
|
||
{
|
||
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов.", jobGroupId);
|
||
return;
|
||
}
|
||
|
||
#if DEBUG
|
||
// Отладка: проверить, есть ли юнит в expectedUnitIds
|
||
if (expectedUnitIds.Contains(targetUnitId))
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в expectedUnitIds.", targetUnitId);
|
||
//return; // ❌ юнит отсеялся на этом этапе
|
||
}
|
||
#endif
|
||
|
||
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId (дополнительный фильтр)
|
||
if (!jobGroup.GroupingUnitFieldId.HasValue)
|
||
{
|
||
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
|
||
return;
|
||
}
|
||
|
||
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
|
||
|
||
// Загрузить UnitValues для юнитов из expectedUnitIds, чтобы проверить GroupingUnitFieldId
|
||
var expectedUnitsWithGroupingField = await unitService.Get()
|
||
.AsNoTracking()
|
||
.AsSplitQuery() // Для Unit -> UnitValues
|
||
.Include(u => u.UnitValues)
|
||
.ThenInclude(uv => uv.Value)
|
||
.Where(u => expectedUnitIds.Contains(u.Id))
|
||
.ToListAsync();
|
||
|
||
var unitIdsWithValidGroupingFieldSet = expectedUnitsWithGroupingField
|
||
.Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value)))
|
||
.Select(u => u.Id)
|
||
.ToHashSet();
|
||
|
||
#if DEBUG
|
||
// Отладка: проверить, есть ли юнит в unitIdsWithValidGroupingFieldSet
|
||
if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId))
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
|
||
//return; // ❌ юнит отсеялся на этом этапе
|
||
}
|
||
#endif
|
||
|
||
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
|
||
|
||
if (!unitIdsWithValidGroupingFieldSet.Any())
|
||
{
|
||
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
|
||
return;
|
||
}
|
||
|
||
// --- Дополнительная фильтрация по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" ---
|
||
var unitIdsWithValidWorkGroupFieldSet = expectedUnitsWithGroupingField
|
||
.Where(u => unitIdsWithValidGroupingFieldSet.Contains(u.Id) && // Убедимся, что юнит уже прошёл фильтр по GroupingFieldId
|
||
u.UnitValues.Any(uv =>
|
||
uv.FieldId == workGroupFieldId && // Поле "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"
|
||
uv.Value != null && // Значение существует
|
||
regionalGroupValueIds.Contains(uv.Value.Id) // Значение в списке разрешённых
|
||
))
|
||
.Select(u => u.Id) // Выбираем Id юнита
|
||
.ToHashSet(); // И снова в HashSet
|
||
|
||
#if DEBUG
|
||
// Отладка: проверить, есть ли юнит в unitIdsWithValidWorkGroupFieldSet
|
||
if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId))
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
|
||
//return; // ❌ юнит отсеялся на этом этапе
|
||
}
|
||
#endif
|
||
|
||
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count);
|
||
|
||
if (!unitIdsWithValidWorkGroupFieldSet.Any())
|
||
{
|
||
logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
|
||
return;
|
||
}
|
||
|
||
// Обновляем список юнитов, прошедших оба фильтра
|
||
unitIdsWithValidGroupingFieldSet = unitIdsWithValidWorkGroupFieldSet;
|
||
|
||
// 5. Получить RelationshipFilters из maxJob
|
||
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
|
||
|
||
// --- Найти и отфильтровать UnitInUnit связи ---
|
||
logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию по GroupingUnitFieldId и 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'.");
|
||
var potentialUnitInUnitLinks = await unitInUnitService.Get()
|
||
.AsNoTracking()
|
||
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
|
||
.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);
|
||
|
||
// Загрузить UnitInValue для всех ParentUnitId и ChildUnitId из potentialUnitInUnitLinks
|
||
var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet();
|
||
var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet();
|
||
|
||
var parentUnitValues = await unitInValueService.Get()
|
||
.AsNoTracking()
|
||
.Include(uv => uv.Field)
|
||
.Include(uv => uv.Value)
|
||
.Where(uv => allParentIds.Contains(uv.UnitId))
|
||
.ToListAsync();
|
||
|
||
var childUnitValues = await unitInValueService.Get()
|
||
.AsNoTracking()
|
||
.Include(uv => uv.Field)
|
||
.Include(uv => uv.Value)
|
||
.Where(uv => allChildIds.Contains(uv.UnitId))
|
||
.ToListAsync();
|
||
|
||
// Сгруппировать значения по UnitId для быстрого доступа
|
||
var parentValuesMap = parentUnitValues
|
||
.GroupBy(uv => uv.UnitId)
|
||
.ToDictionary(g => g.Key, g => g.ToList());
|
||
|
||
var childValuesMap = childUnitValues
|
||
.GroupBy(uv => uv.UnitId)
|
||
.ToDictionary(g => g.Key, g => g.ToList());
|
||
|
||
// Применить фильтры к связям
|
||
logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count);
|
||
var filteredUnitInUnitLinks = new List<UnitInUnit>();
|
||
foreach (var link in potentialUnitInUnitLinks)
|
||
{
|
||
bool linkMatchesAllFilters = true;
|
||
|
||
foreach (var rf in relationshipFilters)
|
||
{
|
||
var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List<UnitInValue>()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List<UnitInValue>());
|
||
|
||
bool filterMatch = valuesToCheck.Any(uv =>
|
||
uv.FieldId == rf.FieldId &&
|
||
uv.Value != null &&
|
||
uv.Value.Value != null &&
|
||
uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase)
|
||
);
|
||
|
||
if (rf.IsInverse)
|
||
filterMatch = !filterMatch;
|
||
|
||
if (!filterMatch)
|
||
{
|
||
linkMatchesAllFilters = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (linkMatchesAllFilters)
|
||
{
|
||
filteredUnitInUnitLinks.Add(link);
|
||
}
|
||
}
|
||
|
||
#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);
|
||
|
||
// --- Сгруппировать юниты из unitIdsWithValidGroupingFieldSet по связанному юниту ---
|
||
var groupedRelationships = new Dictionary<Guid, List<Guid>>();
|
||
foreach (var link in filteredUnitInUnitLinks)
|
||
{
|
||
var parentUnitId = link.ParentUnitId;
|
||
var childUnitId = link.ChildUnitId;
|
||
|
||
if (unitIdsWithValidGroupingFieldSet.Contains(parentUnitId))
|
||
{
|
||
if (!groupedRelationships.ContainsKey(childUnitId))
|
||
{
|
||
groupedRelationships[childUnitId] = new List<Guid>();
|
||
}
|
||
groupedRelationships[childUnitId].Add(parentUnitId);
|
||
}
|
||
else if (unitIdsWithValidGroupingFieldSet.Contains(childUnitId))
|
||
{
|
||
if (!groupedRelationships.ContainsKey(parentUnitId))
|
||
{
|
||
groupedRelationships[parentUnitId] = new List<Guid>();
|
||
}
|
||
groupedRelationships[parentUnitId].Add(childUnitId);
|
||
}
|
||
}
|
||
|
||
logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count);
|
||
|
||
#if DEBUG
|
||
// Отладка: проверить, юнит есть в groupedRelationships.Values
|
||
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)
|
||
{
|
||
var key = kvp.Key;
|
||
var units = kvp.Value;
|
||
foreach (var unitId in units)
|
||
{
|
||
if (!unitToKeys.ContainsKey(unitId))
|
||
{
|
||
unitToKeys[unitId] = new List<Guid>();
|
||
}
|
||
unitToKeys[unitId].Add(key);
|
||
}
|
||
}
|
||
|
||
#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();
|
||
|
||
#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)
|
||
{
|
||
var unitId = conflictedUnitEntry.Key;
|
||
var keysForUnit = conflictedUnitEntry.Value;
|
||
|
||
Guid bestKey = keysForUnit[0]; // Инициализируем первым ключом
|
||
int maxCount = groupedRelationships[bestKey].Count;
|
||
|
||
for (int i = 1; i < keysForUnit.Count; i++)
|
||
{
|
||
var currentKey = keysForUnit[i];
|
||
var currentCount = groupedRelationships[currentKey].Count;
|
||
if (currentCount > maxCount)
|
||
{
|
||
bestKey = currentKey;
|
||
maxCount = currentCount;
|
||
}
|
||
}
|
||
|
||
// Удалить юнит из списков всех ключей, кроме bestKey
|
||
foreach (var key in keysForUnit)
|
||
{
|
||
if (key != bestKey)
|
||
{
|
||
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();
|
||
foreach (var key in keysToRemove)
|
||
{
|
||
groupedRelationships.Remove(key);
|
||
logger.LogDebug("Ключ {Key} удалён, так как его список юнитов стал пустым после разрешения конфликтов.", key);
|
||
}
|
||
|
||
logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count);
|
||
|
||
#if DEBUG
|
||
logger.LogDebug("Содержимое groupedRelationships после разрешения конфликтов: [{Groups}]", string.Join(", ", groupedRelationships.Select(kvp => $"Key: {kvp.Key}, Values: [{string.Join(", ", kvp.Value)}]")));
|
||
#endif
|
||
|
||
// 7. Разбить каждую группу и сопоставить с Job
|
||
foreach (var kvp in groupedRelationships)
|
||
{
|
||
var relationshipUnitId = kvp.Key; // Связанный юнит (не из unitIdsWithValidGroupingFieldSet)
|
||
var childUnitIds = kvp.Value; // Юниты из unitIdsWithValidGroupingFieldSet, связанные с regionalUnitId
|
||
if (childUnitIds.Count == 0)
|
||
continue;
|
||
#if DEBUG
|
||
// Отладка: проверить, есть ли ВРТ-AOS-05-ДВС в childUnitIds
|
||
var childUnitNames = childUnitIds.Select(id => unitService.Get().AsNoTracking().Where(u => u.Id == id).Select(u => u.Name).FirstOrDefaultAsync().GetAwaiter().GetResult() ?? id.ToString()).ToList();
|
||
if (childUnitNames.Contains("ВРТ-AOS-05-ДВС"))
|
||
{
|
||
var relationshipUnitName = await unitService.Get().AsNoTracking().Where(u => u.Id == relationshipUnitId).Select(u => u.Name).FirstOrDefaultAsync();
|
||
logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юнит 'ВРТ-AOS-05-ДВС' в childUnitIds: [{ChildUnitNames}]", relationshipUnitId, relationshipUnitName, string.Join(", ", childUnitNames));
|
||
}
|
||
#endif
|
||
|
||
logger.LogDebug("Обработка связанного юнита {RegionalUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count);
|
||
|
||
// Применяем ограничение MaxValueRelationships maxJob
|
||
if (!maxJob.MaxValueRelationships.HasValue)
|
||
{
|
||
logger.LogWarning("Job {JobId} не заполнено MaxValueRelationships.", maxJob.Id);
|
||
return;
|
||
}
|
||
int maxValueForSplitting = maxJob.MaxValueRelationships.Value;
|
||
var childUnitGroups = childUnitIds
|
||
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
|
||
.GroupBy(x => x.groupIndex)
|
||
.Select(g => g.Select(x => x.id).ToList())
|
||
.ToList();
|
||
|
||
logger.LogDebug("Связанный юнит {RegionalUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count);
|
||
|
||
// Для каждой подгруппы:
|
||
for (int i = 0; i < childUnitGroups.Count; i++)
|
||
{
|
||
var subGroup = childUnitGroups[i];
|
||
var subGroupSize = subGroup.Count;
|
||
|
||
logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RegionalUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize);
|
||
|
||
// Выбираем targetJob для подгруппы
|
||
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
|
||
|
||
// 9. Загрузить существующие шаблоны для targetJob, связанные с relationshipUnitId
|
||
var existingTemplatesForRelationship = await templateService.Get()
|
||
.AsNoTracking()
|
||
.Include(t => t.UnitsInTemplate)
|
||
.Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
||
.ToListAsync();
|
||
|
||
var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault();
|
||
|
||
if (existingTemplateForSubGroup != null)
|
||
{
|
||
// Проверить, изменились ли юниты
|
||
var existingUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToHashSet();
|
||
var newUnitIds = subGroup.ToHashSet();
|
||
|
||
if (existingUnitIds.SetEquals(newUnitIds))
|
||
{
|
||
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) актуален по юнитам.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i);
|
||
|
||
// Проверить, изменилось ли имя шаблона (например, из-за %МАКС:...% или %ТНК-КРАТКО%)
|
||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup);
|
||
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.Name, expectedName);
|
||
|
||
var nextRun = await GetNextRunAsync(targetJob, existingTemplateForSubGroup.NextRun);
|
||
|
||
var updateRequest = new TemplateUpdaterMq
|
||
{
|
||
TemplateId = existingTemplateForSubGroup.Id,
|
||
JobId = targetJob.Id,
|
||
UnitId = relationshipUnitId,
|
||
Name = expectedName,
|
||
IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate,
|
||
IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule,
|
||
LastRun = existingTemplateForSubGroup.LastRun,
|
||
NextRun = nextRun,
|
||
Index = i,
|
||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||
Initiator = initiator,
|
||
UnitsInTemplate = subGroup
|
||
};
|
||
|
||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Шаблон {TemplateId} полностью актуален (юниты и имя).", existingTemplateForSubGroup.Id);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i);
|
||
|
||
// Пересчитать targetJob для нового размера subGroup
|
||
var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
|
||
|
||
// Если Job изменился — нужно обновить шаблон с новым JobId
|
||
if (newTargetJob.Id != existingTemplateForSubGroup.JobId)
|
||
{
|
||
logger.LogDebug("Job для шаблона {TemplateId} изменился: старый {OldJobId}, новый {NewJobId}.", existingTemplateForSubGroup.Id, existingTemplateForSubGroup.JobId, newTargetJob.Id);
|
||
}
|
||
|
||
await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, newTargetJob, initiator);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, relationshipUnitId, initiator);
|
||
|
||
if (reusableTemplate != null)
|
||
{
|
||
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i);
|
||
|
||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, relationshipUnitId, i, subGroup);
|
||
var nextRun = await GetNextRunAsync(targetJob);
|
||
|
||
var updateRequest = new TemplateUpdaterMq
|
||
{
|
||
TemplateId = reusableTemplate.Id,
|
||
JobId = targetJob.Id,
|
||
UnitId = relationshipUnitId,
|
||
Name = expectedName,
|
||
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
|
||
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
|
||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||
Initiator = initiator,
|
||
NextRun = nextRun,
|
||
Index = i,
|
||
UnitsInTemplate = subGroup
|
||
};
|
||
|
||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count);
|
||
await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе (логика без изменений)
|
||
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
|
||
foreach (var kvp in groupedRelationships)
|
||
{
|
||
var regionalUnitId = kvp.Key;
|
||
var childUnitIds = kvp.Value;
|
||
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
|
||
var childUnitGroups = childUnitIds
|
||
.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, regionalUnitId, i));
|
||
}
|
||
}
|
||
|
||
var allRegionalUnitIds = 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) && allRegionalUnitIds.Contains(t.UnitId))
|
||
.ToListAsync();
|
||
|
||
foreach (var existingTemplate in allExistingTemplatesInGroup)
|
||
{
|
||
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
|
||
if (!expectedTemplateKeys.Contains(key))
|
||
{
|
||
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Regional {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
|
||
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
|
||
}
|
||
}
|
||
|
||
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
|
||
}
|
||
|
||
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||
{
|
||
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
|
||
return;
|
||
}
|
||
|
||
// --- Вспомогательные методы ---
|
||
|
||
/// <summary>
|
||
/// Выбирает Job, соответствующий размеру подгруппы
|
||
/// </summary>
|
||
/// <param name="jobsInGroup">Список Job'ов в группе</param>
|
||
/// <param name="subGroupSize">Размер подгруппы</param>
|
||
/// <param name="maxJob">Job с максимальным MaxValueRelationships</param>
|
||
/// <returns>Найденный Job или maxJob, если не найден подходящий</returns>
|
||
private 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();
|
||
}
|
||
|
||
if (targetJob == null)
|
||
{
|
||
targetJob = maxJob; // maxJob уже проверен на null ранее
|
||
logger.LogDebug("Для подгруппы размером {Size} не найден подходящий Job, используем maxJob {MaxJobId}.", subGroupSize, maxJob.Id);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Для подгруппы размером {Size} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", subGroupSize, targetJob.Id, targetJob.MaxValueRelationships);
|
||
}
|
||
|
||
return targetJob;
|
||
}
|
||
|
||
private async Task UpdateTemplateUnitsAsync(Template template, List<Guid> newUnitIds, Job targetJob, HistoryInitiator initiator)
|
||
{
|
||
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
|
||
template.DateModified = DateTimeOffset.UtcNow;
|
||
|
||
if (!await templateService.CommitAsync(initiator))
|
||
{
|
||
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id);
|
||
return;
|
||
}
|
||
|
||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(targetJob, template.UnitId, template.Index, newUnitIds);
|
||
var nextRun = await GetNextRunAsync(targetJob, template.NextRun);
|
||
|
||
var updateRequest = new TemplateUpdaterMq
|
||
{
|
||
TemplateId = template.Id,
|
||
JobId = targetJob.Id,
|
||
UnitId = template.UnitId,
|
||
Name = expectedName,
|
||
IsActiveTemplate = template.IsActiveTemplate,
|
||
IsActiveSchedule = template.IsActiveSchedule,
|
||
LastRun = template.LastRun,
|
||
NextRun = nextRun,
|
||
Index = template.Index,
|
||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||
Initiator = initiator,
|
||
UnitsInTemplate = newUnitIds
|
||
};
|
||
|
||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||
}
|
||
|
||
private async Task CreateGroupedTemplateAsync(Guid jobId, Guid regionalUnitId, List<Guid> unitIds, int index, HistoryInitiator initiator)
|
||
{
|
||
logger.LogInformation("Создание нового группового шаблона для Job {JobId}, связанного юнита {RegionalUnitId}, Index {Index}, с {Count} юнитами.", jobId, regionalUnitId, index, unitIds.Count);
|
||
|
||
var mqRequest = new TemplateGeneratorMq
|
||
{
|
||
JobId = jobId,
|
||
UnitId = regionalUnitId, // UnitId шаблона
|
||
UnitsInTemplate = unitIds, // Юниты для UnitsInTemplate
|
||
Index = index, // Индекс шаблона
|
||
HistoryInitiator = initiator
|
||
};
|
||
|
||
var msg = JsonSerializer.Serialize(mqRequest);
|
||
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg });
|
||
|
||
if (!result.IsSuccess)
|
||
logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, связанного юнита {RegionalUnitId}, Index {Index}.", jobId, regionalUnitId, index);
|
||
}
|
||
|
||
private async Task<DateTimeOffset> GetNextRunAsync(Job targetJob, DateTimeOffset? currentNextRun = null)
|
||
{
|
||
var referenceDate = targetJob.Group?.ReferenceDate ?? DateTimeOffset.UtcNow;
|
||
return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate);
|
||
}
|
||
} |