880 lines
44 KiB
C#
880 lines
44 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.Cache.Models;
|
||
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.Text.Json;
|
||
|
||
namespace PARR.TemplateMatcher.Services.Implementations;
|
||
|
||
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||
{
|
||
#if DEBUG
|
||
private readonly Guid targetUnitId = Guid.Parse("87fc4c36-1ea8-4163-983f-1605fee1de99");
|
||
#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;
|
||
private readonly IMatchingStatusService matchingStatusService;
|
||
|
||
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,
|
||
IMatchingStatusService matchingStatusService
|
||
)
|
||
{
|
||
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;
|
||
this.matchingStatusService = matchingStatusService;
|
||
}
|
||
|
||
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||
{
|
||
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
|
||
}
|
||
|
||
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
|
||
{
|
||
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
|
||
|
||
// === Проверка: уже запущена? ===
|
||
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||
if (existingStatus.DetailsJobGroups?.Any() == true)
|
||
{
|
||
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
|
||
return;
|
||
}
|
||
|
||
// === Устанавливаем статус "в процессе" ===
|
||
var initialStatus = new MatchingStatusItemDto
|
||
{
|
||
DateStart = DateTimeOffset.UtcNow,
|
||
Action = TemplateMatcherActionEnum.Sync,
|
||
Comment = "Начало синхронизации"
|
||
};
|
||
await matchingStatusService.SetMatchingStatusAsync(
|
||
jobGroupId,
|
||
SyncTaskEntityTypeEnum.JobGroup,
|
||
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
|
||
TimeSpan.FromMinutes(35)
|
||
);
|
||
|
||
try
|
||
{
|
||
// 1. Получить JobGroup и связанные Job'ы
|
||
var jobGroup = await jobGroupService.Get()
|
||
.AsNoTracking()
|
||
.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);
|
||
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
|
||
return;
|
||
}
|
||
|
||
var jobsInGroup = jobGroup.Jobs.ToList();
|
||
|
||
// --- Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" ---
|
||
var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК");
|
||
if (workGroupField == null)
|
||
{
|
||
logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено в справочнике полей. Синхронизация прервана.");
|
||
await UpdateMatchingStatusAsync(jobGroupId, "Ошибка: поле 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' не найдено");
|
||
return;
|
||
}
|
||
var workGroupFieldId = workGroupField.Id;
|
||
|
||
var regionalGroupValueIds = regionalEkPtkGroupService.Get()
|
||
.Select(g => g.FieldValueId)
|
||
.ToList();
|
||
|
||
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);
|
||
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
|
||
return;
|
||
}
|
||
|
||
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);
|
||
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов");
|
||
return;
|
||
}
|
||
|
||
#if DEBUG
|
||
if (expectedUnitIds.Contains(targetUnitId))
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} найден в expectedUnitIds.", targetUnitId);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в expectedUnitIds.", targetUnitId);
|
||
}
|
||
#endif
|
||
|
||
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId
|
||
if (!jobGroup.GroupingUnitFieldId.HasValue)
|
||
{
|
||
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
|
||
await UpdateMatchingStatusAsync(jobGroupId, "Отсутствует GroupingUnitFieldId");
|
||
return;
|
||
}
|
||
|
||
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
|
||
|
||
var expectedUnitsWithGroupingField = await unitService.Get()
|
||
.AsNoTracking()
|
||
.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
|
||
if (unitIdsWithValidGroupingFieldSet.Contains(targetUnitId))
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidGroupingFieldSet.", targetUnitId);
|
||
}
|
||
#endif
|
||
|
||
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
|
||
|
||
if (!unitIdsWithValidGroupingFieldSet.Any())
|
||
{
|
||
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
|
||
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId");
|
||
return;
|
||
}
|
||
|
||
// --- Дополнительная фильтрация по "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК" ---
|
||
var unitIdsWithValidWorkGroupFieldSet = await FilterByWorkGroupFieldAsync(
|
||
expectedUnitsWithGroupingField,
|
||
unitIdsWithValidGroupingFieldSet,
|
||
workGroupFieldId,
|
||
regionalGroupValueIds
|
||
);
|
||
|
||
#if DEBUG
|
||
if (unitIdsWithValidWorkGroupFieldSet.Contains(targetUnitId))
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIdsWithValidWorkGroupFieldSet.", targetUnitId);
|
||
}
|
||
#endif
|
||
|
||
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count);
|
||
|
||
if (!unitIdsWithValidWorkGroupFieldSet.Any())
|
||
{
|
||
logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
|
||
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК'");
|
||
return;
|
||
}
|
||
|
||
unitIdsWithValidGroupingFieldSet = unitIdsWithValidWorkGroupFieldSet;
|
||
|
||
// 5. Получить RelationshipFilters из maxJob
|
||
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
|
||
|
||
logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию...");
|
||
var potentialUnitInUnitLinks = await unitInUnitService.Get()
|
||
.AsNoTracking()
|
||
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ParentUnitId) || unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
|
||
.ToListAsync();
|
||
|
||
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count);
|
||
|
||
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();
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count);
|
||
|
||
// --- Сгруппировать юниты ---
|
||
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);
|
||
|
||
// --- Разрешение конфликта ---
|
||
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);
|
||
}
|
||
}
|
||
|
||
var conflictedUnits = unitToKeys.Where(kvp => kvp.Value.Count > 1).ToList();
|
||
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;
|
||
}
|
||
}
|
||
|
||
foreach (var key in keysForUnit)
|
||
{
|
||
if (key != bestKey)
|
||
{
|
||
groupedRelationships[key].Remove(unitId);
|
||
}
|
||
}
|
||
}
|
||
|
||
var keysToRemove = groupedRelationships.Where(kvp => kvp.Value.Count == 0).Select(kvp => kvp.Key).ToList();
|
||
foreach (var key in keysToRemove)
|
||
{
|
||
groupedRelationships.Remove(key);
|
||
}
|
||
|
||
logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count);
|
||
|
||
// === Подсчёт операций ===
|
||
int toCreate = 0;
|
||
int toUpdate = 0;
|
||
int toDeactivate = 0;
|
||
|
||
foreach (var kvp in groupedRelationships)
|
||
{
|
||
var childUnitIds = kvp.Value;
|
||
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
|
||
var debugUnitIds = childUnitIds.Concat(new[] { relationshipUnitId }).Distinct().ToList();
|
||
var debugUnits = await unitService.Get()
|
||
.AsNoTracking()
|
||
.Where(u => debugUnitIds.Contains(u.Id))
|
||
.ToDictionaryAsync(u => u.Id, u => u.Name);
|
||
|
||
var childUnitNamesForDebug = childUnitIds.Select(id => debugUnits.GetValueOrDefault(id, id.ToString())).ToList();
|
||
var relationshipUnitName = debugUnits.GetValueOrDefault(relationshipUnitId, relationshipUnitId.ToString());
|
||
|
||
if (childUnitNamesForDebug.Contains("ВРТ-AOS-05-ДВС"))
|
||
{
|
||
logger.LogDebug("Группа с ключом {Key} (название: {Name}) содержит юнит 'ВРТ-AOS-05-ДВС' в childUnitIds: [{ChildUnitNames}]", relationshipUnitId, relationshipUnitName, string.Join(", ", childUnitNamesForDebug));
|
||
}
|
||
#endif
|
||
|
||
logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count);
|
||
|
||
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();
|
||
|
||
logger.LogDebug("Связанный юнит {RelationshipUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count);
|
||
|
||
for (int i = 0; i < childUnitGroups.Count; i++)
|
||
{
|
||
var subGroup = childUnitGroups[i];
|
||
var subGroupSize = subGroup.Count;
|
||
|
||
logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RelationshipUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize);
|
||
|
||
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
|
||
|
||
var existingTemplatesForRelationship = await templateService.Get()
|
||
.AsNoTracking()
|
||
.Include(t => t.Unit)
|
||
.Include(t => t.Job)
|
||
.ThenInclude(t => t!.Tnk)
|
||
.Include(t => t.Job)
|
||
.ThenInclude(t => t!.Group)
|
||
.ThenInclude(t => t!.GroupType)
|
||
.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} актуален по юнитам.", existingTemplateForSubGroup.Id);
|
||
|
||
// Обновляем UnitsInTemplate для генерации имени
|
||
existingTemplateForSubGroup.UnitsInTemplate = subGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList();
|
||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup);
|
||
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id);
|
||
|
||
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);
|
||
updated++;
|
||
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов.", existingTemplateForSubGroup.Id);
|
||
|
||
var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
|
||
if (newTargetJob.Id != existingTemplateForSubGroup.JobId)
|
||
{
|
||
logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id);
|
||
}
|
||
|
||
await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, newTargetJob, initiator);
|
||
updated++;
|
||
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, relationshipUnitId, initiator);
|
||
|
||
if (reusableTemplate != null)
|
||
{
|
||
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i);
|
||
|
||
var tempTemplateForName = new Template
|
||
{
|
||
Id = reusableTemplate.Id,
|
||
Name = reusableTemplate.Name,
|
||
JobId = targetJob.Id,
|
||
UnitId = relationshipUnitId,
|
||
Index = i,
|
||
Job = targetJob,
|
||
Unit = reusableTemplate.Unit,
|
||
UnitsInTemplate = subGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
|
||
};
|
||
|
||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||
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);
|
||
created++;
|
||
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count);
|
||
await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator);
|
||
created++;
|
||
await UpdateMatchingStatusAsync(jobGroupId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// === Деактивация ===
|
||
foreach (var existingTemplate in allExistingTemplatesInGroup)
|
||
{
|
||
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
|
||
if (!expectedTemplateKeys.Contains(key))
|
||
{
|
||
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
|
||
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);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId);
|
||
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||
{
|
||
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
|
||
return;
|
||
}
|
||
|
||
// --- Вспомогательные методы ---
|
||
|
||
private async Task<HashSet<Guid>> FilterByWorkGroupFieldAsync(
|
||
List<Unit> units,
|
||
HashSet<Guid> candidateUnitIds,
|
||
Guid workGroupFieldId,
|
||
List<Guid> regionalGroupValueIds)
|
||
{
|
||
return units
|
||
.Where(u => candidateUnitIds.Contains(u.Id) &&
|
||
u.UnitValues.Any(uv =>
|
||
uv.FieldId == workGroupFieldId &&
|
||
uv.Value != null &&
|
||
regionalGroupValueIds.Contains(uv.Value.Id)))
|
||
.Select(u => u.Id)
|
||
.ToHashSet();
|
||
}
|
||
|
||
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;
|
||
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 tempTemplateForName = new Template
|
||
{
|
||
Id = template.Id,
|
||
Name = template.Name,
|
||
JobId = targetJob.Id,
|
||
UnitId = template.UnitId,
|
||
Index = template.Index,
|
||
Job = targetJob,
|
||
Unit = template.Unit,
|
||
UnitsInTemplate = newUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
|
||
};
|
||
|
||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||
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 relationshipUnitId, List<Guid> unitIds, int index, HistoryInitiator initiator)
|
||
{
|
||
logger.LogInformation("Создание нового группового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", jobId, relationshipUnitId, index, unitIds.Count);
|
||
|
||
var mqRequest = new TemplateGeneratorMq
|
||
{
|
||
JobId = jobId,
|
||
UnitId = relationshipUnitId,
|
||
UnitsInTemplate = unitIds,
|
||
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}, связанного юнита {RelationshipUnitId}, Index {Index}.", jobId, relationshipUnitId, 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);
|
||
}
|
||
|
||
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)
|
||
);
|
||
}
|
||
} |