711 lines
35 KiB
C#
711 lines
35 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.DomainServices.Interfaces;
|
||
using PARR.DAL.DomainServices.Shortcodes;
|
||
using PARR.DAL.DomainServices.Shortcodes.Models;
|
||
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.Implemetaions;
|
||
|
||
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
||
{
|
||
private const bool DefaultUnusedTemplateState = false;
|
||
private const bool DefaultUnusedScheduleState = false;
|
||
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 IShortcodesService shortcodesService;
|
||
private readonly IEsppScheduleTransformService esppScheduleTransformService;
|
||
private readonly IUnitRegionalEkPtkGroupService regionalEkPtkGroupService;
|
||
private readonly IUnitFieldService unitFieldService;
|
||
|
||
public GroupedTemplateSynchronizer(
|
||
ILogger<GroupedTemplateSynchronizer> logger,
|
||
IUnitFilterService unitFilterService,
|
||
IUnitInUnitService unitInUnitService,
|
||
IUnitInValueService unitInValueService,
|
||
IUnitService unitService,
|
||
MqSettings mqSettings,
|
||
IMqService mqService,
|
||
ITemplateService templateService,
|
||
IJobGroupService jobGroupService,
|
||
ITemplateReuser templateReuser,
|
||
IShortcodesService shortcodesService,
|
||
IEsppScheduleTransformService esppScheduleTransformService,
|
||
IUnitRegionalEkPtkGroupService regionalEkPtkGroupService,
|
||
IUnitFieldService unitFieldService
|
||
)
|
||
{
|
||
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.shortcodesService = shortcodesService;
|
||
this.esppScheduleTransformService = esppScheduleTransformService;
|
||
this.regionalEkPtkGroupService = regionalEkPtkGroupService;
|
||
this.unitFieldService = unitFieldService;
|
||
}
|
||
|
||
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)
|
||
.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;
|
||
}
|
||
|
||
// 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();
|
||
|
||
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
|
||
|
||
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();
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
// --- НОВАЯ ЛОГИКА: Разрешение конфликта - один юнит из 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);
|
||
}
|
||
}
|
||
|
||
// Найти юниты, которые находятся в нескольких списках
|
||
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;
|
||
}
|
||
}
|
||
|
||
// Удалить юнит из списков всех ключей, кроме bestKey
|
||
foreach (var key in keysForUnit)
|
||
{
|
||
if (key != bestKey)
|
||
{
|
||
groupedRelationships[key].Remove(unitId);
|
||
logger.LogDebug("Юнит {UnitId} перемещён из группы {OldKey} в группу {BestKey} (по кол-ву).", unitId, key, bestKey);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Удаляем ключи, у которых список стал пустым после разрешения конфликтов
|
||
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);
|
||
|
||
// 7. Разбить каждую группу и сопоставить с Job
|
||
foreach (var kvp in groupedRelationships)
|
||
{
|
||
var relationshipUnitId = kvp.Key; // Связанный юнит (не из unitIdsWithValidGroupingFieldSet)
|
||
var childUnitIds = kvp.Value; // Юниты из unitIdsWithValidGroupingFieldSet, связанные с regionalUnitId
|
||
|
||
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);
|
||
|
||
// 8. Найти подходящий Job для подгруппы (логика без изменений)
|
||
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("Для подгруппы {Index} связанного юнита {RegionalUnitId} не найден подходящий Job, используем maxJob {MaxJobId}.", i, relationshipUnitId, maxJob.Id);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Для подгруппы {Index} связанного юнита {RegionalUnitId} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", i, relationshipUnitId, targetJob.Id, targetJob.MaxValueRelationships);
|
||
}
|
||
|
||
// 9. Загрузить существующие шаблоны для targetJob, связанные с regionalUnitId
|
||
var existingTemplatesForRelationship = await templateService.Get()
|
||
.AsNoTracking()
|
||
.Include(t => t.UnitsInTemplate)
|
||
.Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i)
|
||
.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);
|
||
}
|
||
else
|
||
{
|
||
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, relationshipUnitId, i);
|
||
await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, targetJob, 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 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 SendTemplateUpdateMessage(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 = jobsInGroup
|
||
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize)
|
||
.FirstOrDefault();
|
||
|
||
if (targetJobForExpectedKey == null)
|
||
{
|
||
targetJobForExpectedKey = jobsInGroup
|
||
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize)
|
||
.OrderBy(j => j.MaxValueRelationships!.Value)
|
||
.FirstOrDefault();
|
||
}
|
||
|
||
if (targetJobForExpectedKey == null) targetJobForExpectedKey = maxJob; // maxJob уже проверен на null
|
||
|
||
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 DeactivateTemplateAsync(existingTemplate, existingTemplate.JobId, initiator);
|
||
}
|
||
}
|
||
|
||
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
|
||
}
|
||
|
||
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||
{
|
||
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
|
||
return;
|
||
}
|
||
|
||
// --- Вспомогательные методы ---
|
||
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 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 SendTemplateUpdateMessage(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<bool> DeactivateTemplateAsync(
|
||
Template template,
|
||
Guid jobId,
|
||
HistoryInitiator initiator)
|
||
{
|
||
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
|
||
return true; // уже в обработке
|
||
|
||
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.",
|
||
template.Id, template.UnitId);
|
||
|
||
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
|
||
template.DateModified = DateTimeOffset.UtcNow;
|
||
|
||
if (!await templateService.CommitAsync(initiator))
|
||
{
|
||
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
|
||
return false;
|
||
}
|
||
|
||
var updateRequest = new TemplateUpdaterMq
|
||
{
|
||
TemplateId = template.Id,
|
||
JobId = jobId,
|
||
UnitId = template.UnitId,
|
||
Name = GetTemplateNameForUnused(template.Name),
|
||
IsActiveTemplate = DefaultUnusedTemplateState,
|
||
IsActiveSchedule = DefaultUnusedScheduleState,
|
||
LastRun = template.LastRun,
|
||
NextRun = template.NextRun,
|
||
Index = template.Index,
|
||
StatusTypeId = TemplateStatusTypeEnum.Unused,
|
||
Initiator = initiator,
|
||
UnitsInTemplate = new List<Guid>()
|
||
};
|
||
|
||
await SendTemplateUpdateMessage(updateRequest);
|
||
return true;
|
||
}
|
||
|
||
private async Task SendTemplateUpdateMessage(TemplateUpdaterMq updateRequest)
|
||
{
|
||
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
|
||
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
|
||
|
||
var msg = JsonSerializer.Serialize(updateRequest);
|
||
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new[] { msg });
|
||
|
||
if (result.IsSuccess)
|
||
{
|
||
logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId);
|
||
}
|
||
else
|
||
{
|
||
logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.",
|
||
updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName);
|
||
}
|
||
}
|
||
|
||
private string GetTemplateNameForUnused(string templateName)
|
||
{
|
||
return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||
}
|
||
|
||
// Изменённая сигнатура: добавлен templateUnitIds
|
||
private async Task<string> GetNormalizedTemplateNameAsync(Job targetJob, Guid unitId, int? index = null, List<Guid>? templateUnitIds = null)
|
||
{
|
||
// Подготовка объекта TemplateForShortcodes для передачи в ShortcodesService
|
||
var templateForShortcodes = new TemplateForShortcodes
|
||
{
|
||
Id = Guid.Empty, // Не используется в подстановке, но нужен для структуры
|
||
Index = index,
|
||
JobId = targetJob.Id,
|
||
UnitId = unitId,
|
||
Job = new JobForShortcodes
|
||
{
|
||
Group = targetJob.Group != null ? new JobGroupForShortcodes
|
||
{
|
||
Id = targetJob.Group.Id,
|
||
GroupingUnitFieldId = targetJob.Group.GroupingUnitFieldId,
|
||
GroupType = targetJob.Group.GroupType != null ? new JobGroupTypeForShortcodes
|
||
{
|
||
Code = targetJob.Group.GroupType.Code
|
||
} : null,
|
||
GroupName = targetJob.Group.GroupName
|
||
} : null,
|
||
Tnk = targetJob.Tnk != null ? new TnkForShortcodes
|
||
{
|
||
Name = targetJob.Tnk.Name,
|
||
ShortName = targetJob.Tnk.ShortName ?? ""
|
||
} : null,
|
||
WorkName = targetJob.WorkName,
|
||
Name = targetJob.Name
|
||
},
|
||
// Преобразование List<Guid> в List<UnitInTemplateForShortcodes>
|
||
UnitsInTemplate = templateUnitIds?.Select(id => new UnitInTemplateForShortcodes { UnitId = id }).ToList() ?? new List<UnitInTemplateForShortcodes>()
|
||
};
|
||
|
||
var rawName = await shortcodesService.ApplyShortcodesAsync(targetJob.TemplateNameMask, templateForShortcodes);
|
||
return rawName.ToUpper();
|
||
}
|
||
|
||
private async Task<DateTimeOffset> GetNextRunAsync(Job targetJob, DateTimeOffset? currentNextRun = null)
|
||
{
|
||
var now = DateTimeOffset.UtcNow;
|
||
if (currentNextRun.HasValue && currentNextRun.Value > now)
|
||
{
|
||
return currentNextRun.Value;
|
||
}
|
||
|
||
var referenceDate = targetJob.Group?.ReferenceDate ?? now;
|
||
return await esppScheduleTransformService.GetNextDateAsync(targetJob.GroupId, referenceDate);
|
||
}
|
||
} |