Files
parr_api/PARR.TemplateMatcher/TemplateMatcher.cs

960 lines
49 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.Settings;
using System.Text.Json;
namespace PARR.TemplateMatcher
{
internal class TemplateMatcher : ITemplateMatcher
{
private const int UnusedCandidateBatchSize = 10;
private const bool DefaultUnusedTemplateState = false;
private const bool DefaultUnusedScheduleState = false;
private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false;
private readonly ILogger<TemplateMatcher> 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 IJobService jobService;
private readonly IJobGroupService jobGroupService;
private readonly IShortcodesService shortcodesService;
private readonly IEsppScheduleTransformService esppScheduleTransformService;
public TemplateMatcher(
ILogger<TemplateMatcher> logger,
IUnitFilterService unitFilterService,
IUnitInUnitService unitInUnitService,
IUnitInValueService unitInValueService,
IUnitService unitService,
MqSettings mqSettings,
IMqService mqService,
ITemplateService templateService,
IJobService jobService,
IJobGroupService jobGroupService,
IShortcodesService shortcodesService,
IEsppScheduleTransformService esppScheduleTransformService)
{
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.jobService = jobService;
this.jobGroupService = jobGroupService;
this.shortcodesService = shortcodesService;
this.esppScheduleTransformService = esppScheduleTransformService;
}
public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId);
var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet<Guid>();
logger.LogDebug("JobId {JobId}: найдено {Count} UnitId по фильтрам.", jobId, expectedUnitIds.Count);
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null)
{
logger.LogError("Job с Id {JobId} не найден.", jobId);
return;
}
// Проверяем, является ли Job "групповым"
bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group;
if (isGroupJob && job.Group.GroupingUnitFieldId.HasValue)
{
logger.LogInformation("Job {JobId} является групповым. Используйте SyncTemplatesForJobGroup для синхронизации.", jobId);
return; // Ничего не делаем для группового Job
}
else
{
await SyncSimpleTemplatesAsync(job, expectedUnitIds, initiator);
}
logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId);
}
public async Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// 1. Получить JobGroup и связанные Job'ы
var jobGroup = await jobGroupService.Get()
.AsNoTracking() // Добавлено
.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();
// 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);
// Возможно, нужно обработать случай, когда MaxValueRelationships не установлено ни у одного Job.
// Пока просто выйдем.
return;
}
// Проверяем, что UnitFilters и RelationshipFilters загружены
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);
// Деактивировать все шаблоны для всех Job в группе?
// Пока просто выйдем.
return;
}
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId
if (!jobGroup.GroupingUnitFieldId.HasValue)
{
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
return;
}
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
// --- ИСПРАВЛЕНИЕ: Разбиваем запрос на части ---
// Загрузить UnitValues для отфильтрованных юнитов, чтобы проверить GroupingUnitFieldId
var filteredUnits = await unitService.Get()
.AsNoTracking() // Добавлено
.AsSplitQuery() // Добавлено
.Include(t => t.UnitValues) // Добавлено
.ThenInclude(t => t.Value) // Добавлено
.Where(u => expectedUnitIds.Contains(u.Id))
.ToListAsync(); // Сначала загружаем Unit'ы
// Затем фильтруем их UnitValues и собираем UnitId
var unitIdsWithValidGroupingFieldSet = filteredUnits
.Where(u => u.UnitValues.Any(uv => uv.FieldId == groupingFieldId && uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value)))
.Select(u => u.Id)
.ToHashSet(); // Создаем HashSet
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count);
if (!unitIdsWithValidGroupingFieldSet.Any())
{
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
return;
}
// --- НОВАЯ ЛОГИКА: Применение RelationshipFilters ---
// Найти связи UnitInUnit для unitIdsWithValidGroupingFieldSet
var potentialUnitInUnitLinks = await unitInUnitService.Get()
.AsNoTracking() // Добавлено
.Where(link => unitIdsWithValidGroupingFieldSet.Contains(link.ChildUnitId))
.ToListAsync();
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit до применения RelationshipFilters.", potentialUnitInUnitLinks.Count);
// Получить RelationshipFilters из maxJob
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
if (relationshipFilters.Any())
{
// Загрузить 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());
// Применить фильтры к связям
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);
}
}
potentialUnitInUnitLinks = filteredUnitInUnitLinks; // Заменяем на отфильтрованные
}
logger.LogDebug("Осталось {Count} связей UnitInUnit после применения RelationshipFilters.", potentialUnitInUnitLinks.Count);
// 6. Сгруппировать ChildUnitId по ParentUnitId (региональный ЭК) из ОТФИЛЬТРОВАННЫХ связей
var groupedByRegional = potentialUnitInUnitLinks
.GroupBy(link => link.ParentUnitId)
.ToDictionary(g => g.Key, g => g.Select(l => l.ChildUnitId).ToList());
logger.LogDebug("Сформировано {Count} групп по региональным юнитам.", groupedByRegional.Count);
// 7. Разбить каждую группу и сопоставить с Job
// Для каждого регионального юнита и его дочерних юнитов:
foreach (var kvp in groupedByRegional)
{
var regionalUnitId = kvp.Key;
var childUnitIds = kvp.Value;
logger.LogDebug("Обработка регионального юнита {RegionalUnitId} с {Count} дочерними юнитами.", regionalUnitId, childUnitIds.Count);
// Применяем ограничение MaxValueRelationships maxJob
int maxValueForSplitting = maxJob.MaxValueRelationships.Value; // Уже проверили, что не null
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} подгрупп.", regionalUnitId, childUnitGroups.Count);
// Для каждой подгруппы:
for (int i = 0; i < childUnitGroups.Count; i++)
{
var subGroup = childUnitGroups[i];
var subGroupSize = subGroup.Count;
logger.LogDebug("Обработка подгруппы {Index} регионального юнита {RegionalUnitId}, размер {Size}.", i, regionalUnitId, subGroupSize);
// 8. Найти подходящий Job для подгруппы
// Попробовать найти Job с MaxValueRelationships, равным размеру подгруппы
var targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize)
.FirstOrDefault();
if (targetJob == null)
{
// Найти Job с MaxValueRelationships >= размеру подгруппы, но минимально подходящее
targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize)
.OrderBy(j => j.MaxValueRelationships.Value)
.FirstOrDefault();
}
if (targetJob == null)
{
// Если подходящий Job не найден, используем maxJob
targetJob = maxJob;
logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} не найден подходящий Job, используем maxJob {MaxJobId}.", i, regionalUnitId, maxJob.Id);
}
else
{
logger.LogDebug("Для подгруппы {Index} регионального юнита {RegionalUnitId} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", i, regionalUnitId, targetJob.Id, targetJob.MaxValueRelationships);
}
// 9. Загрузить существующие шаблоны для targetJob, связанные с regionalUnitId
var existingTemplatesForRegional = await templateService.Get()
.AsNoTracking() // Добавлено
.Include(t => t.UnitsInTemplate)
.Where(t => t.JobId == targetJob.Id && t.UnitId == regionalUnitId && t.Index == i)
.ToListAsync();
Template existingTemplateForSubGroup = existingTemplatesForRegional.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, regionalUnitId, i);
// Возможно, нужно обновить имя или статус, если изменились фильтры или AutoControl
// Пока оставим как есть, если структура не изменилась.
}
else
{
logger.LogDebug("Шаблон {TemplateId} (Job {JobId}, Regional {RegionalId}, Index {Index}) требует обновления юнитов.", existingTemplateForSubGroup.Id, targetJob.Id, regionalUnitId, i);
// Обновляем существующий шаблон
await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, subGroup, targetJob, initiator);
}
}
else
{
// --- ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ МЕТОД TryReuseOneUnusedTemplateAsync ---
var reusableTemplate = await TryReuseOneUnusedTemplateAsync(targetJob.Id, regionalUnitId, initiator); // передаём regionalUnitId как unitId для старого метода
if (reusableTemplate != null) // если захват успешен
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, регионального юнита {RegionalId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, regionalUnitId, i);
// Подготовить сообщение для TemplateUpdater с новыми параметрами
var expectedName = await GetNormalizedTemplateNameAsync(targetJob, regionalUnitId, i);
var nextRun = await GetNextRunAsync(targetJob); // всегда пересчитываем для нового назначения
var updateRequest = new TemplateUpdaterMq
{
TemplateId = reusableTemplate.Id, // ID захваченного шаблона
JobId = targetJob.Id, // Новый JobId
UnitId = regionalUnitId, // Новый UnitId (региональный)
Name = expectedName,
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
NextRun = nextRun,
Index = i, // Новый Index
UnitsInTemplate = subGroup // Новые UnitsInTemplate
};
await SendTemplateUpdateMessage(updateRequest);
}
else
{
// Создать новый шаблон
logger.LogDebug("Создание нового шаблона для Job {JobId}, Regional {RegionalId}, Index {Index}, с {Count} юнитами.", targetJob.Id, regionalUnitId, i, subGroup.Count);
await CreateGroupedTemplateAsync(targetJob.Id, regionalUnitId, subGroup, i, initiator);
}
}
}
}
// 10. Деактивировать шаблоны, которые больше не соответствуют ни одной подгруппе
// Это требует сбора всех ожидаемых (JobId, UnitId, Index) и сравнения с существующими.
// Соберем ожидаемые комбинации
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvp in groupedByRegional)
{
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;
var 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;
// Исправлено: используем конкретные типы для кортежа
expectedTemplateKeys.Add((targetJob.Id, regionalUnitId, i));
}
}
// Загрузить *все* шаблоны для всех Job в группе, связанные с региональными юнитами из групп
var allRegionalUnitIds = groupedByRegional.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);
}
private async Task SyncSimpleTemplatesAsync(Job job, HashSet<Guid> expectedUnitIds, HistoryInitiator initiator)
{
logger.LogDebug("Синхронизация обычных шаблонов для JobId {JobId}", job.Id);
var existingTemplates = await templateService.Get()
.AsNoTracking() // Добавлено
.Where(t => t.JobId == job.Id)
.ToListAsync();
logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.",
job.Id, expectedUnitIds.Count, existingTemplates.Count);
// Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны
if (!expectedUnitIds.Any())
{
if (existingTemplates.Any())
{
logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.",
job.Id, existingTemplates.Count);
foreach (var template in existingTemplates)
{
await DeactivateTemplateAsync(template, job.Id, initiator);
}
}
else
{
logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", job.Id);
}
logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", job.Id);
return;
}
// Деактивация шаблонов, которые вышли из фильтра
var templatesToDeactivate = existingTemplates
.Where(t => !expectedUnitIds.Contains(t.UnitId))
.ToList();
foreach (var template in templatesToDeactivate)
{
await DeactivateTemplateAsync(template, job.Id, initiator);
}
// Перечитываем шаблоны после деактивации
existingTemplates = await templateService.Get()
.AsNoTracking() // Добавлено
.Where(t => t.JobId == job.Id)
.ToListAsync();
var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t);
// UnitId без шаблона → попытка переиспользования или создание
var unitIdsMissingTemplates = expectedUnitIds
.Where(unitId => !unitToTemplate.ContainsKey(unitId))
.ToList();
var unitIdsToCreateFresh = new List<Guid>();
foreach (var unitId in unitIdsMissingTemplates)
{
var reused = await TryReuseOneUnusedTemplateAsync(job.Id, unitId, initiator);
if (reused != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId);
var expectedName = await GetNormalizedTemplateNameAsync(job, unitId);
var nextRun = await GetNextRunAsync(job); // всегда пересчитываем для нового назначения
var updateRequest = new TemplateUpdaterMq
{
TemplateId = reused.Id,
JobId = job.Id,
UnitId = unitId,
Name = expectedName,
IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
NextRun = nextRun,
UnitsInTemplate = new List<Guid>()
};
await SendTemplateUpdateMessage(updateRequest);
}
else
{
logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId);
unitIdsToCreateFresh.Add(unitId);
}
}
// Обновление/реактивация шаблонов, оставшихся в фильтре
var templatesInFilter = existingTemplates
.Where(t => expectedUnitIds.Contains(t.UnitId))
.ToList();
foreach (var template in templatesInFilter)
{
var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId);
await ReactivateOrRenameTemplateAsync(template, job, expectedName, initiator);
}
// Создание новых шаблонов
foreach (var unitId in unitIdsToCreateFresh)
{
await SendTemplateGeneratorMessageAsync(job.Id, unitId, initiator);
}
}
public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{
logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId);
var existingTemplates = await templateService.Get()
.AsNoTracking() // Добавлено
.Where(t => t.JobId == jobId)
.ToListAsync();
if (!existingTemplates.Any()) return;
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null) return;
var currentUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet<Guid>();
logger.LogDebug("JobId {JobId}: {Count} UnitId по текущему фильтру.", jobId, currentUnitIds.Count);
foreach (var template in existingTemplates)
{
logger.LogDebug("Обработка шаблона {TemplateId} (UnitId {UnitId}).", template.Id, template.UnitId);
bool unitStillInFilter = currentUnitIds.Contains(template.UnitId);
var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId);
TemplateStatusTypeEnum targetStatus;
bool targetIsActiveTemplate;
bool targetIsActiveSchedule;
if (unitStillInFilter)
{
targetStatus = TemplateStatusTypeEnum.Used;
targetIsActiveTemplate = template.IsActiveTemplate;
targetIsActiveSchedule = template.IsActiveSchedule;
}
else
{
targetStatus = TemplateStatusTypeEnum.Unused;
targetIsActiveTemplate = DefaultUnusedTemplateState;
targetIsActiveSchedule = DefaultUnusedScheduleState;
expectedName = GetTemplateNameForUnused(expectedName);
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId);
}
bool needsUpdate = template.StatusTypeId != TemplateStatusTypeEnum.Updating &&
(
// 1. Статус изменился (например, Used → Unused или Unused → Used)
template.StatusTypeId != targetStatus ||
// 2. Для Used — имя должно соответствовать шаблону
(targetStatus == TemplateStatusTypeEnum.Used && template.Name != expectedName) ||
// 3. Флаги изменились (редко, но возможно через AutoControl изменение)
template.IsActiveTemplate != targetIsActiveTemplate ||
template.IsActiveSchedule != targetIsActiveSchedule
);
if (!needsUpdate) continue;
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
continue;
}
var nextRun = await GetNextRunAsync(job, template.NextRun);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = jobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = targetIsActiveTemplate,
IsActiveSchedule = targetIsActiveSchedule,
LastRun = template.LastRun,
NextRun = nextRun,
Index = template.Index,
StatusTypeId = targetStatus,
Initiator = initiator,
UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList()
};
await SendTemplateUpdateMessage(updateRequest);
}
logger.LogInformation("Обновление шаблонов завершено для JobId {JobId}.", jobId);
}
private async Task UpdateTemplateUnitsAsync(Template template, List<Guid> newUnitIds, Job job, HistoryInitiator initiator)
{
// Обновляем шаблон как "Updating"
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id);
return;
}
// Здесь нужно обновить UnitsInTemplate.
// Это может быть сделано через TemplateUpdaterMq, если он поддерживает передачу нового списка юнитов.
// Или напрямую в сервисе шаблонов, если логика обновления простая.
// Пока отправим сообщение в TemplateUpdater.
var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId);
var nextRun = await GetNextRunAsync(job, template.NextRun);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = job.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);
// Предполагаем, что TemplateGeneratorMq может обрабатывать UnitsInTemplate и Index
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<bool> ReactivateOrRenameTemplateAsync(
Template template,
Job job,
string expectedName,
HistoryInitiator initiator)
{
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
return true;
bool needsUpdate = template.Name != expectedName
|| template.StatusTypeId != TemplateStatusTypeEnum.Used;
if (!needsUpdate) return true;
logger.LogInformation("Шаблон {TemplateId}: требуется обновление имени или реактивация.", template.Id);
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
return false;
}
var nextRun = await GetNextRunAsync(job, template.NextRun);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = job.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 = template.UnitsInTemplate.Select(t => t.UnitId).ToList()
};
await SendTemplateUpdateMessage(updateRequest);
return true;
}
private async Task<bool> SendTemplateGeneratorMessageAsync(
Guid jobId,
Guid unitId,
HistoryInitiator initiator)
{
logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId);
var mqRequest = new TemplateGeneratorMq
{
JobId = jobId,
UnitId = unitId,
HistoryInitiator = initiator,
UnitsInTemplate = new List<Guid>()
};
var msg = JsonSerializer.Serialize(mqRequest);
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания шаблона для UnitId {UnitId}.", unitId);
return result.IsSuccess;
}
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 async Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync)
Guid unitId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync)
HistoryInitiator initiator,
int maxAttempts = 3)
{
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
var unusedCandidates = await templateService.Get()
.AsNoTracking() // Добавлено
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
.OrderBy(t => t.DateModified ?? t.DateCreated)
.Take(UnusedCandidateBatchSize)
.ToListAsync();
if (!unusedCandidates.Any())
{
logger.LogDebug("Нет Unused-шаблонов (попытка {Attempt}).", attempt);
return null;
}
foreach (var candidate in unusedCandidates)
{
var originalStatus = candidate.StatusTypeId;
var originalModified = candidate.DateModified;
try
{
candidate.StatusTypeId = TemplateStatusTypeEnum.Updating;
candidate.DateModified = DateTimeOffset.UtcNow;
if (await templateService.CommitAsync(initiator))
{
logger.LogInformation("Успешно захвачен шаблон {TemplateId} для переиспользования (попытка {Attempt}).",
candidate.Id, attempt);
return candidate; // Возвращаем захваченный шаблон
}
// Откат при неудаче
candidate.StatusTypeId = originalStatus;
candidate.DateModified = originalModified;
}
catch (Exception ex) when (
ex is DbUpdateException ||
ex.InnerException?.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase) == true ||
ex.InnerException?.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) == true)
{
logger.LogWarning(ex, "Конфликт при захвате шаблона {TemplateId} (попытка {Attempt}).", candidate.Id, attempt);
candidate.StatusTypeId = originalStatus;
candidate.DateModified = originalModified;
}
}
if (attempt < maxAttempts)
await Task.Delay(Random.Shared.Next(5, 15) * attempt);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка в попытке захвата (попытка {Attempt}).", attempt);
if (attempt == maxAttempts) throw;
}
}
return null;
}
private string GetTemplateNameForUnused(string templateName)
{
return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
{
return await jobService.Get()
.AsNoTracking() // Добавлено
.Include(j => j.Group)
.ThenInclude(j => j.GroupType)
.Include(j => j.AutoControl)
.FirstOrDefaultAsync(j => j.Id == jobId);
}
private async Task<HashSet<Guid>> GetExpectedUnitIdsAsync(Guid jobId)
{
var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId);
return units?.ToHashSet() ?? new HashSet<Guid>();
}
private async Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId, int? index = null)
{
var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id, index);
return rawName.ToUpper();
}
private async Task<DateTimeOffset> GetNextRunAsync(Job job, DateTimeOffset? currentNextRun = null)
{
var now = DateTimeOffset.UtcNow;
if (currentNextRun.HasValue && currentNextRun.Value > now)
{
return currentNextRun.Value;
}
var referenceDate = job.Group?.ReferenceDate ?? now;
return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate);
}
}
}