feat(templateMatcher,dal): Первая реализация SyncTemplatesForJobGroup для сгруппированных типов групп регламентных работ.

This commit is contained in:
Mikhail Kuznetsov
2025-12-19 19:15:58 +10:00
parent cc104c30b1
commit 7ca536ce52
20 changed files with 687 additions and 71 deletions

View File

@@ -2,9 +2,10 @@
namespace PARR.TemplateMatcher
{
internal interface ITemplateMatcher
public interface ITemplateMatcher
{
Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator);
Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator);
Task SyncTemplatesForJobGroup(Guid jobGroupId, HistoryInitiator initiator);
}
}

View File

@@ -59,8 +59,8 @@ namespace PARR.TemplateMatcher
switch (query.EntityType)
{
case SyncTaskEntityTypeEnum.Job:
var validatorService = GetServiceInScope<IJobValidatorService>(scope);
if (!await validatorService.IsValidAsync(query.Id))
var jobValidatorService = GetServiceInScope<IJobValidatorService>(scope);
if (!await jobValidatorService.IsValidJobAsync(query.Id))
{
logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id);
return;
@@ -83,9 +83,29 @@ namespace PARR.TemplateMatcher
break;
case SyncTaskEntityTypeEnum.JobGroup:
logger.LogWarning("Обработка EntityType JobGroup не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
break;
var jobGroupValidatorService = GetServiceInScope<IJobGroupValidatorService>(scope);
if (!await jobGroupValidatorService.IsValidJobGroupAsync(query.Id))
{
logger.LogWarning("Сущность {EntityType} с Id {Id} не прошла валидацию", query.EntityType, query.Id);
return;
}
switch (query.Action)
{
case TemplateMatcherActionEnum.Sync:
// Создать недостающие шаблоны, привязать к нужному Job, включить/выключить по фильтрам
await templateMatcherService.SyncTemplatesForJobGroup(query.Id, query.Initiator);
break;
case TemplateMatcherActionEnum.Update:
// Обновить существующие шаблоны: имя, привязка к Job, вкл/выкл по фильтрам
//await templateMatcherService.UpdateTemplatesForJob(query.Id, query.Initiator);
logger.LogWarning("Обработка EntityType JobGroup и TemplateMatcherActionEnum.Update не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
break;
default:
logger.LogWarning("Неизвестное действие для {EntityType}: {Action}", query.EntityType, query.Action);
break;
}
break;
case SyncTaskEntityTypeEnum.Template:
logger.LogWarning("Обработка EntityType Template не реализована. Id: {Id}, Action: {Action}", query.Id, query.Action);
break;

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using PARR.DAL.Services.Interfaces.Job;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implemetaions
{
internal class JobGroupValidatorService : IJobGroupValidatorService
{
private readonly ILogger<IJobValidatorService> logger;
private readonly IJobGroupService jobGroupService;
public JobGroupValidatorService(
ILogger<IJobValidatorService> logger,
IJobGroupService jobGroupService
)
{
this.logger = logger;
this.jobGroupService = jobGroupService;
}
public async Task<bool> IsValidJobGroupAsync(Guid jobGroupId)
{
var isExist = await jobGroupService.GetAsync(jobGroupId);
if (isExist == null)
{
logger.LogError($"Не найдена регалментная работа {nameof(jobGroupId)}: {jobGroupId}");
return false;
}
return true;
}
}
}

View File

@@ -17,7 +17,7 @@ namespace PARR.TemplateMatcher.Services.Implemetaions
this.logger = logger;
this.jobService = jobService;
}
public async Task<bool> IsValidAsync(Guid jobId)
public async Task<bool> IsValidJobAsync(Guid jobId)
{
var isExist = await jobService.GetAsync(jobId);

View File

@@ -0,0 +1,12 @@
namespace PARR.TemplateMatcher.Services.Interfaces
{
internal interface IJobGroupValidatorService
{
/// <summary>
/// Проверяет существование группы регламентной работы
/// </summary>
/// <param name="jobId"></param>
/// <returns></returns>
Task<bool> IsValidJobGroupAsync(Guid jobGroupId);
}
}

View File

@@ -7,6 +7,6 @@
/// </summary>
/// <param name="jobId"></param>
/// <returns></returns>
Task<bool> IsValidAsync(Guid jobId);
Task<bool> IsValidJobAsync(Guid jobId);
}
}

View File

@@ -4,11 +4,14 @@ 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;
@@ -25,29 +28,41 @@ namespace PARR.TemplateMatcher
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;
}
@@ -66,12 +81,393 @@ namespace PARR.TemplateMatcher
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()
.Where(t => t.JobId == jobId)
.AsNoTracking() // Добавлено
.Where(t => t.JobId == job.Id)
.ToListAsync();
logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.",
jobId, expectedUnitIds.Count, existingTemplates.Count);
job.Id, expectedUnitIds.Count, existingTemplates.Count);
// Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны
if (!expectedUnitIds.Any())
@@ -79,19 +475,19 @@ namespace PARR.TemplateMatcher
if (existingTemplates.Any())
{
logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.",
jobId, existingTemplates.Count);
job.Id, existingTemplates.Count);
foreach (var template in existingTemplates)
{
await DeactivateTemplateAsync(template, jobId, initiator);
await DeactivateTemplateAsync(template, job.Id, initiator);
}
}
else
{
logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", jobId);
logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", job.Id);
}
logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", jobId);
logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", job.Id);
return;
}
@@ -102,12 +498,13 @@ namespace PARR.TemplateMatcher
foreach (var template in templatesToDeactivate)
{
await DeactivateTemplateAsync(template, jobId, initiator);
await DeactivateTemplateAsync(template, job.Id, initiator);
}
// Перечитываем шаблоны после деактивации
existingTemplates = await templateService.Get()
.Where(t => t.JobId == jobId)
.AsNoTracking() // Добавлено
.Where(t => t.JobId == job.Id)
.ToListAsync();
var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t);
@@ -121,7 +518,7 @@ namespace PARR.TemplateMatcher
foreach (var unitId in unitIdsMissingTemplates)
{
var reused = await TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
var reused = await TryReuseOneUnusedTemplateAsync(job.Id, unitId, initiator);
if (reused != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId);
@@ -132,14 +529,15 @@ namespace PARR.TemplateMatcher
var updateRequest = new TemplateUpdaterMq
{
TemplateId = reused.Id,
JobId = jobId,
JobId = job.Id,
UnitId = unitId,
Name = expectedName,
IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
NextRun = nextRun
NextRun = nextRun,
UnitsInTemplate = new List<Guid>()
};
await SendTemplateUpdateMessage(updateRequest);
@@ -165,17 +563,17 @@ namespace PARR.TemplateMatcher
// Создание новых шаблонов
foreach (var unitId in unitIdsToCreateFresh)
{
await SendTemplateGeneratorMessageAsync(jobId, unitId, initiator);
await SendTemplateGeneratorMessageAsync(job.Id, unitId, initiator);
}
logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId);
}
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();
@@ -251,7 +649,8 @@ namespace PARR.TemplateMatcher
NextRun = nextRun,
Index = template.Index,
StatusTypeId = targetStatus,
Initiator = initiator
Initiator = initiator,
UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList()
};
await SendTemplateUpdateMessage(updateRequest);
@@ -261,6 +660,66 @@ namespace PARR.TemplateMatcher
}
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,
@@ -293,7 +752,8 @@ namespace PARR.TemplateMatcher
NextRun = template.NextRun,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator
Initiator = initiator,
UnitsInTemplate = new List<Guid>()
};
await SendTemplateUpdateMessage(updateRequest);
@@ -339,7 +799,8 @@ namespace PARR.TemplateMatcher
NextRun = nextRun,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator
Initiator = initiator,
UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList()
};
await SendTemplateUpdateMessage(updateRequest);
@@ -352,11 +813,12 @@ namespace PARR.TemplateMatcher
HistoryInitiator initiator)
{
logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId);
var mqRequest = new TemplateGeneratorWorkerMq
var mqRequest = new TemplateGeneratorMq
{
JobId = jobId,
UnitId = unitId,
HistoryInitiator = initiator
HistoryInitiator = initiator,
UnitsInTemplate = new List<Guid>()
};
var msg = JsonSerializer.Serialize(mqRequest);
@@ -387,9 +849,10 @@ namespace PARR.TemplateMatcher
}
}
// --- ИЗМЕНЕННЫЙ МЕТОД: Теперь используется как для простых, так и для групповых шаблонов ---
private async Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId,
Guid unitId,
Guid jobId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync)
Guid unitId, // Используется для логики внутри метода (например, подготовка updateRequest в SyncSimpleTemplatesAsync)
HistoryInitiator initiator,
int maxAttempts = 3)
{
@@ -398,6 +861,7 @@ namespace PARR.TemplateMatcher
try
{
var unusedCandidates = await templateService.Get()
.AsNoTracking() // Добавлено
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
.OrderBy(t => t.DateModified ?? t.DateCreated)
.Take(UnusedCandidateBatchSize)
@@ -421,9 +885,9 @@ namespace PARR.TemplateMatcher
if (await templateService.CommitAsync(initiator))
{
logger.LogInformation("Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}).",
candidate.Id, unitId, attempt);
return candidate;
logger.LogInformation("Успешно захвачен шаблон {TemplateId} для переиспользования (попытка {Attempt}).",
candidate.Id, attempt);
return candidate; // Возвращаем захваченный шаблон
}
// Откат при неудаче
@@ -462,7 +926,9 @@ namespace PARR.TemplateMatcher
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);
}
@@ -473,9 +939,9 @@ namespace PARR.TemplateMatcher
return units?.ToHashSet() ?? new HashSet<Guid>();
}
private async Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId)
private async Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId, int? index = null)
{
var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id);
var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id, index);
return rawName.ToUpper();
}

View File

@@ -21,6 +21,7 @@ namespace PARR.TemplateMatcher
services.AddTransient<IMqTemplateMatcher, MqTemplateMatcher>();
services.AddTransient<IJobValidatorService, JobValidatorService>();
services.AddTransient<IJobGroupValidatorService, JobGroupValidatorService>();
services.AddTransient<ITemplateMatcher, TemplateMatcher>();
}