feat(templateMatcher): Рефакторинг, основной метод синхронизации GroupedTemplateSynchronizer разбит на отдельные классы; Неиспользуемые шаблоны теперь привязываются к ЭК КОСМПЛЕКСЫ-[ЗО].

This commit is contained in:
Mikhail Kuznetsov
2026-05-14 16:47:27 +10:00
parent f7a74d315a
commit 90e6352117
30 changed files with 1150 additions and 864 deletions

View File

@@ -981,7 +981,7 @@ internal class UnitFilterService : IUnitFilterService
// var cacheKey = cacheService.GetKey(new[] { "uf_ids", filter.UnitFilter }, isUseHash: true);
var cacheKey = cacheService.GetKey(new[] { "unit filter", "unit name mask" }, new[] { filter.UnitFilter });
var cachedData = await cacheService.GetCachedDataAsync<UnitFilterIds>(cacheKey);
var cachedData = await cacheService.GetCachedDataAsync<UnitFilterIds>(cacheKey, true);
if (cachedData != null)
{
return cachedData.Data.UnitIds;
@@ -998,7 +998,7 @@ internal class UnitFilterService : IUnitFilterService
Source = GetType().Name
};
await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1));
await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1), true);
return initialUnitIds;
}

View File

@@ -1,50 +0,0 @@
using PARR.Domain.Entities.Base.History;
namespace PARR.Domain.Common.Rabbit.Messages
{
/// <summary>
/// Модель в MQ, запрос на генерацию шаблона
/// </summary>
public class GeneratorTemplateMq
{
/// <summary>
/// Id работы
/// </summary>
public Guid WorkId { get; set; }
/// <summary>
/// Id приложения
/// </summary>
public Guid ApplicationId { get; set; }
/// <summary>
/// Действие: create, deactivate
/// </summary>
public required string Action { get; set; }
/// <summary>
/// ЭК, принимает регулярное выражение. Например: *, ВРТ-*-ДВС
/// </summary>
public required string Ek { get; set; }
/// <summary>
/// Статус ЭК. Допустимые значения: 0 - все статусы, 1,2,3,4,5,6,7,9
/// </summary>
public int StatusEk { get; set; }
/// <summary>
/// Активировать шаблон
/// </summary>
public bool? IsActiveTemplate { get; set; }
/// <summary>
/// Активировать расписание
/// </summary>
public bool? IsActiveSchedule { get; set; }
/// <summary>
/// Инициатор запроса к генератору
/// </summary>
public HistoryInitiator? HistoryInitiator { get; set; }
}
}

View File

@@ -1,46 +1,46 @@
using PARR.Domain.Entities.Base.History;
namespace PARR.Domain.Common.Rabbit.Messages
namespace PARR.Domain.Common.Rabbit.Messages.TemplateMatching
{
/// <summary>
/// Модель в MQ, простого создания Template
/// </summary>
public class TemplateGeneratorMq
public record TemplateGeneratorMessage
{
/// <summary>
/// Id регламентной работы
/// </summary>
public Guid JobId { get; set; }
public Guid JobId { get; init; }
/// <summary>
/// Id Юнита(единицы обслуживания)/ЭК
/// </summary>
public Guid UnitId { get; set; }
public Guid UnitId { get; init; }
/// <summary>
/// Актировать шаблон при инициализации
/// </summary>
public bool? IsActiveTemplate { get; set; }
public bool? IsActiveTemplate { get; init; }
/// <summary>
/// Актировать расписание при инициализации
/// </summary>
public bool? IsActiveSchedule { get; set; }
public bool? IsActiveSchedule { get; init; }
/// <summary>
/// Инициатор запроса к генератору
/// </summary>
public HistoryInitiator? HistoryInitiator { get; set; }
public HistoryInitiator? HistoryInitiator { get; init; }
/// <summary>
/// Связанные ЭК для сгруппированного типа JobGroup
/// </summary>
public required List<Guid> UnitsInTemplate { get; set; }
public required List<UnitInTemplateMessage> UnitsInTemplate { get; init; }
/// <summary>
/// Индекс, используется в групповых шаблонах
/// </summary>
public int? Index { get; set; }
public int? Index { get; init; }
}
}

View File

@@ -1,58 +1,60 @@
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums;
namespace PARR.Domain.Common.Rabbit.Messages
namespace PARR.Domain.Common.Rabbit.Messages.TemplateMatching
{
public class TemplateUpdaterMq
public record TemplateUpdaterMessage
{
public Guid TemplateId { get; set; }
public Guid TemplateId { get; init; }
/// <summary>
/// Id регламентной работы
/// </summary>
public Guid JobId { get; set; }
public Guid JobId { get; init; }
public required string Name { get; set; }
public required string Name { get; init; }
/// <summary>
/// Актировать шаблон при инициализации
/// </summary>
public bool IsActiveTemplate { get; set; }
public bool IsActiveTemplate { get; init; }
/// <summary>
/// Актировать расписание при инициализации
/// </summary>
public bool IsActiveSchedule { get; set; }
public bool IsActiveSchedule { get; init; }
//public DateTimeOffset? LastRun { get; set; }
//public DateTimeOffinit? LastRun { get; init; }
//public DateTimeOffset NextRun { get; set; }
//public DateTimeOffinit NextRun { get; init; }
/// <summary>
/// Это новый шаблон? (true - перемещаем существующий в новый Job) (false - шаблон остается в своем Job)
/// </summary>
public bool IsNew { get; set; }
public bool IsNew { get; init; }
/// <summary>
/// Id Юнита(единицы обслуживания)/ЭК
/// </summary>
public Guid UnitId { get; set; }
public Guid UnitId { get; init; }
/// <summary>
/// Индекс, используется в групповых шаблонах
/// </summary>
public int? Index { get; set; }
public int? Index { get; init; }
public TemplateStatusTypeEnum StatusTypeId { get; set; }
public TemplateStatusTypeEnum StatusTypeId { get; init; }
/// <summary>
/// Инициатор запроса к генератору
/// </summary>
public required HistoryInitiator Initiator { get; set; }
public required HistoryInitiator Initiator { get; init; }
/// <summary>
/// Связанные ЭК для сгруппированного типа JobGroup
/// </summary>
public required List<Guid> UnitsInTemplate { get; set; }
public required List<UnitInTemplateMessage> UnitsInTemplate { get; init; }
}
}

View File

@@ -0,0 +1,15 @@
namespace PARR.Domain.Common.Rabbit.Messages.TemplateMatching
{
public record UnitInTemplateMessage
{
/// <summary>
/// Id ЭК
/// </summary>
public Guid UnitId { get; init; }
/// <summary>
/// Id значения атрибута
/// </summary>
public Guid UnitFieldValueId { get; init; }
}
}

View File

@@ -1,13 +0,0 @@
namespace PARR.Domain.Common.Rabbit.Messages
{
/// <summary>
/// Модель в MQ, генератора заданий на генерацию шаблонов
/// </summary>
public class TemplateTaskGeneratorMq
{
/// <summary>
/// Id регламентной работы
/// </summary>
public Guid JobId { get; set; }
}
}

View File

@@ -4,7 +4,7 @@ using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.NextRunServices;
using PARR.Core.Services.Shortcodes;
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
@@ -54,7 +54,7 @@ namespace PARR.TemplateGeneratorWorker
{
logger.LogInformation($"Получили запрос: {msg}");
var query = transformService.GetModelFromJson<TemplateGeneratorMq>(msg);
var query = transformService.GetModelFromJson<TemplateGeneratorMessage>(msg);
if (query == null)
{

View File

@@ -0,0 +1,10 @@
namespace PARR.TemplateMatcher.Models
{
/// <summary>
/// Группа шаблонов для одного PotentialUnitId (связанного юнита).
/// </summary>
public record GroupedTemplateGroup(
Guid PotentialUnitId,
List<GroupedTemplateSubGroup> SubGroups
);
}

View File

@@ -0,0 +1,11 @@
namespace PARR.TemplateMatcher.Models
{
/// <summary>
/// Итоговая подгруппа юнитов, готовая к созданию/обновлению одного шаблона.
/// </summary>
public record GroupedTemplateSubGroup(
List<(Guid UnitId, Guid UnitFieldValueId)> Entries,
string InnerGroupName,
int GlobalIndex
);
}

View File

@@ -0,0 +1,158 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
{
private readonly ILogger<GroupedTemplateBuilder> logger;
private readonly IUnitInValueRepository unitInValueRepository;
private readonly IUnitFieldRepository unitFieldRepository;
public GroupedTemplateBuilder(
ILogger<GroupedTemplateBuilder> logger,
IUnitInValueRepository unitInValueRepository,
IUnitFieldRepository unitFieldRepository)
{
this.logger = logger;
this.unitInValueRepository = unitInValueRepository;
this.unitFieldRepository = unitFieldRepository;
}
public async Task<List<GroupedTemplateGroup>> BuildAsync(
Dictionary<Guid, List<Guid>> initialReverseMapping,
JobGroup jobGroup,
Job maxJob,
CancellationToken ct = default)
{
logger.LogDebug("Начало построения структуры групп для JobGroup {JobGroupId}.", jobGroup.Id);
if (!initialReverseMapping.Any())
return new List<GroupedTemplateGroup>();
// 1. Определяем поле для внутренней группировки
var isGroupByResponsible = jobGroup.IsGroupByResponsible == true;
var innerGroupingFieldName = isGroupByResponsible ? "ОТВЕТСТВЕННЫЙ_ЗАК" : "РАБОЧАЯ_ГР_ОТВ_ЗАК";
var innerGroupingField = await unitFieldRepository.GetByAihitNameAsync(innerGroupingFieldName)
?? throw new InvalidOperationException($"Поле '{innerGroupingFieldName}' не найдено.");
var innerGroupingFieldId = innerGroupingField.Id;
var groupingFieldId = jobGroup.GroupingUnitFieldId!.Value;
// 2. Собираем все исходные UnitId
var allSourceUnitIds = initialReverseMapping.Values.SelectMany(ids => ids).Distinct().ToList();
// 3. Загружаем UnitInValue для трансформации
var relevantUnitInValues = await unitInValueRepository.Get()
.AsNoTracking()
.Where(uiv => allSourceUnitIds.Contains(uiv.UnitId) && uiv.FieldId == groupingFieldId)
.Select(uiv => new { uiv.UnitId, uiv.ValueId })
.ToListAsync(ct);
var uivLookup = relevantUnitInValues
.GroupBy(x => x.UnitId)
.ToDictionary(g => g.Key, g => g.Select(x => x.ValueId).ToList());
// 4. Трансформируем в reverseMapping с парами
var reverseMapping = new Dictionary<Guid, List<(Guid UnitId, Guid UnitFieldValueId)>>();
foreach (var kvp in initialReverseMapping)
{
var potentialUnitId = kvp.Key;
var sourceDtoIds = kvp.Value;
var entries = new List<(Guid UnitId, Guid UnitFieldValueId)>();
foreach (var dtoId in sourceDtoIds)
{
if (uivLookup.TryGetValue(dtoId, out var valueIds))
{
foreach (Guid valId in valueIds)
entries.Add((UnitId: dtoId, UnitFieldValueId: valId));
}
}
var uniqueEntries = entries
.GroupBy(e => (e.UnitId, e.UnitFieldValueId))
.Select(g => g.First())
.ToList();
if (uniqueEntries.Any())
reverseMapping[potentialUnitId] = uniqueEntries;
}
if (!reverseMapping.Any())
return new List<GroupedTemplateGroup>();
// 5. Загружаем значения для внутренней группировки
var allUnitsInTemplatePairs = reverseMapping.Values.SelectMany(list => list).ToList();
var allUnitIdsForInnerGrouping = allUnitsInTemplatePairs.Select(e => e.UnitId).Distinct().ToList();
var innerGroupingValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(
allUnitIdsForInnerGrouping,
(new HashSet<Guid> { innerGroupingFieldId }),
ct);
var unitIdToInnerGroupingValueMap = innerGroupingValues
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
.ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value);
// 6. Формируем итоговую структуру
var templateGroups = new List<GroupedTemplateGroup>();
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
foreach (var kvp in reverseMapping)
{
var potentialUnitId = kvp.Key;
var unitsInTemplateForThisPotentialUnitId = kvp.Value;
var innerGroupedUnits = unitsInTemplateForThisPotentialUnitId
.GroupBy(entry => unitIdToInnerGroupingValueMap.GetValueOrDefault(entry.UnitId, "Нет данных"))
.OrderBy(g => g.Key, StringComparer.Ordinal)
.ToList();
var subGroups = new List<GroupedTemplateSubGroup>();
int globalIndex = 1;
foreach (var innerGroup in innerGroupedUnits)
{
var innerGroupName = innerGroup.Key;
if (innerGroupName == null)
continue;
var unitsInInnerGroup = innerGroup.ToList();
var splitSubGroups = unitsInInnerGroup
.Select((entry, index) => new { entry, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.entry).ToList())
.ToList();
foreach (var subGroupEntries in splitSubGroups)
{
subGroups.Add(new GroupedTemplateSubGroup(
Entries: subGroupEntries,
InnerGroupName: innerGroupName,
GlobalIndex: globalIndex
));
globalIndex++;
}
}
if (subGroups.Any())
{
templateGroups.Add(new GroupedTemplateGroup(
PotentialUnitId: potentialUnitId,
SubGroups: subGroups
));
}
}
logger.LogDebug("Построено {Count} групп шаблонов.", templateGroups.Count);
return templateGroups;
}
}

View File

@@ -0,0 +1,316 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
{
private readonly ILogger<GroupedTemplateProcessor> logger;
private readonly ITemplateRepository templateRepository;
private readonly IUnitRepository unitRepository;
private readonly ITemplateReuser templateReuser;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly MqSettings mqSettings;
private readonly IRabbitService mqService;
public GroupedTemplateProcessor(
ILogger<GroupedTemplateProcessor> logger,
ITemplateRepository templateRepository,
IUnitRepository unitRepository,
ITemplateReuser templateReuser,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
MqSettings mqSettings,
IRabbitService mqService)
{
this.logger = logger;
this.templateRepository = templateRepository;
this.unitRepository = unitRepository;
this.templateReuser = templateReuser;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.mqSettings = mqSettings;
this.mqService = mqService;
}
public async Task<HashSet<(Guid JobId, Guid UnitId, int Index)>> ProcessAsync(
List<GroupedTemplateGroup> groups,
List<Job> jobsInGroup,
Job maxJob,
HistoryInitiator initiator,
CancellationToken ct = default)
{
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var group in groups)
{
var potentialUnitId = group.PotentialUnitId;
foreach (var subGroup in group.SubGroups)
{
var unitsInTemplateSubGroup = subGroup.Entries;
var globalIndex = subGroup.GlobalIndex;
var originatingInnerGroupName = subGroup.InnerGroupName;
logger.LogDebug("Обработка подгруппы {Index} ('{GroupingValue}') для UnitId {PotentialUnitId}, размер {Size}.",
globalIndex, originatingInnerGroupName, potentialUnitId, unitsInTemplateSubGroup.Count);
Job targetJob = SelectTargetJob(jobsInGroup, unitsInTemplateSubGroup.Count, maxJob);
expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, globalIndex));
// Поиск существующего шаблона
var existingTemplate = await templateRepository.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.Job).ThenInclude(t => t!.Tnk)
.Include(t => t.Job).ThenInclude(t => t!.Group).ThenInclude(t => t!.GroupType)
.Include(t => t.UnitsInTemplate).ThenInclude(uit => uit.Unit)
.Where(t => t.JobId == targetJob.Id &&
t.UnitId == potentialUnitId &&
t.Index == globalIndex &&
t.StatusTypeId == TemplateStatusTypeEnum.Used)
.FirstOrDefaultAsync(ct);
if (existingTemplate != null)
{
await HandleExistingTemplateAsync(existingTemplate, unitsInTemplateSubGroup, targetJob, globalIndex, initiator, ct);
}
else
{
await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator);
}
}
}
return expectedTemplateKeys;
}
private async Task HandleExistingTemplateAsync(
Template existingTemplate,
List<(Guid UnitId, Guid UnitFieldValueId)> proposedEntries,
Job targetJob,
int globalIndex,
HistoryInitiator initiator,
CancellationToken ct)
{
var currentEntries = existingTemplate.UnitsInTemplate
.Select(uit => (uit.UnitId, uit.UnitFieldValueId))
.ToList();
// Сравнение
var allUnitIdsForSort = currentEntries.Select(e => e.UnitId)
.Concat(proposedEntries.Select(e => e.UnitId))
.Distinct()
.ToList();
var unitNamesForSort = await unitRepository.Get()
.AsNoTracking()
.Where(u => allUnitIdsForSort.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString(), ct);
var sortedCurrent = currentEntries
.OrderBy(e => unitNamesForSort.GetValueOrDefault(e.UnitId, e.UnitId.ToString()))
.ThenBy(e => e.UnitFieldValueId)
.ToList();
var sortedProposed = proposedEntries
.OrderBy(e => unitNamesForSort.GetValueOrDefault(e.UnitId, e.UnitId.ToString()))
.ThenBy(e => e.UnitFieldValueId)
.ToList();
bool unitsAreEqual = sortedCurrent.SequenceEqual(sortedProposed);
if (unitsAreEqual)
{
logger.LogDebug("Шаблон {TemplateId} актуален по составу.", existingTemplate.Id);
// Проверка имени
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate);
if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = existingTemplate.Id,
JobId = targetJob.Id,
UnitId = existingTemplate.UnitId,
Name = expectedName,
IsActiveTemplate = existingTemplate.IsActiveTemplate,
IsActiveSchedule = existingTemplate.IsActiveSchedule,
IsNew = false,
Index = globalIndex,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = sortedProposed.Select(t => new UnitInTemplateMessage
{
UnitId = t.UnitId,
UnitFieldValueId = t.UnitFieldValueId
}).ToList()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
}
else
{
logger.LogDebug("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id);
await UpdateTemplateUnitsAsync(existingTemplate, sortedProposed, targetJob, globalIndex, initiator);
}
}
private async Task HandleNewOrReusableTemplateAsync(
Guid potentialUnitId,
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplateSubGroup,
Job targetJob,
int globalIndex,
HistoryInitiator initiator)
{
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator);
if (reusableTemplate != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId}.", reusableTemplate.Id);
var tempTemplateForName = new Template
{
Id = reusableTemplate.Id,
Name = reusableTemplate.Name,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Index = globalIndex,
Job = targetJob,
Unit = reusableTemplate.Unit,
UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitsInTemplate { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = reusableTemplate.Id,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Name = expectedName,
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false,
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
IsNew = true,
Index = globalIndex,
UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Создание нового шаблона.");
await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, globalIndex, initiator);
}
}
private async Task UpdateTemplateUnitsAsync(
Template template,
List<(Guid UnitId, Guid UnitFieldValueId)> newUnitEntries,
Job targetJob,
int newIndex,
HistoryInitiator initiator)
{
// 1. Устанавливаем статус и дату
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
// 2. КОММИТ В БАЗУ СРАЗУ
// Важно зафиксировать изменение статуса до отправки сообщения в очередь
if (!await templateRepository.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id);
return;
}
// 3. Формируем временный объект для генерации имени
var tempTemplateForName = new Template
{
Id = template.Id,
Name = template.Name,
JobId = targetJob.Id,
UnitId = template.UnitId,
Index = newIndex,
Job = targetJob,
Unit = template.Unit,
UnitsInTemplate = newUnitEntries.Select(e => new UnitsInTemplate { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
// 4. Отправляем сообщение в очередь
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = targetJob.Id,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
IsNew = false,
Index = newIndex,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = newUnitEntries.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
private async Task CreateGroupedTemplateAsync(
Guid jobId,
Guid relationshipUnitId,
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplate,
int index,
HistoryInitiator initiator)
{
logger.LogInformation("Создание нового группового шаблона.");
var mqRequest = new TemplateGeneratorMessage
{
JobId = jobId,
UnitId = relationshipUnitId,
UnitsInTemplate = unitsInTemplate.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList(),
Index = index,
HistoryInitiator = initiator
};
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания шаблона.");
}
private static Job SelectTargetJob(List<Job> jobsInGroup, int subGroupSize, Job maxJob)
{
Job? targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize)
.FirstOrDefault();
if (targetJob == null)
{
targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize)
.OrderBy(j => j.MaxValueRelationships!.Value)
.FirstOrDefault();
}
return targetJob ?? maxJob;
}
}

View File

@@ -0,0 +1,243 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private readonly ILogger<GroupedTemplateSynchronizer> logger;
private readonly IJobGroupRepository jobGroupService;
private readonly IUnitFilterService unitFilterService;
private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter;
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
private readonly IGroupedTemplateBuilder groupedTemplateBuilder;
private readonly IGroupedTemplateProcessor groupedTemplateProcessor;
private readonly ITemplateRepository templateService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly IMatchingStatusService matchingStatusService;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
IJobGroupRepository jobGroupService,
IUnitFilterService unitFilterService,
IGroupedTemplateUnitFilter groupedTemplateUnitFilter,
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
IGroupedTemplateBuilder groupedTemplateBuilder,
IGroupedTemplateProcessor groupedTemplateProcessor,
ITemplateRepository templateService,
ITemplateDeactivator templateDeactivator,
IMatchingStatusService matchingStatusService)
{
this.logger = logger;
this.jobGroupService = jobGroupService;
this.unitFilterService = unitFilterService;
this.groupedTemplateUnitFilter = groupedTemplateUnitFilter;
this.unitInTemplateConflictMapper = unitInTemplateConflictMapper;
this.groupedTemplateBuilder = groupedTemplateBuilder;
this.groupedTemplateProcessor = groupedTemplateProcessor;
this.templateService = templateService;
this.templateDeactivator = templateDeactivator;
this.matchingStatusService = matchingStatusService;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Any() == true)
{
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
try
{
// 1. Загрузка JobGroup и связанных Job'ов
var jobGroup = await jobGroupService.Get()
.AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.ThenInclude(rf => rf.UnitField)
.Include(jg => jg.Jobs)
.ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return;
}
var jobsInGroup = jobGroup.Jobs.ToList();
// 2. Поиск Job с максимальным MaxValueRelationships
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
{
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
return;
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}).", maxJob.Id, maxJob.MaxValueRelationships);
// 3. Получение отфильтрованных юнитов через UnitFilterService
logger.LogDebug("Получение отфильтрованных юнитов через UnitFilterService для Job {JobId}.", maxJob.Id);
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
if (unitFilterResults == null || !unitFilterResults.Any())
{
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
return;
}
// 4. Применение специфичных правил фильтрации для групповых шаблонов
logger.LogDebug("Применение специфичных правил фильтрации для групповых шаблонов.");
var finalFilteredUnits = await groupedTemplateUnitFilter.FilterAsync(unitFilterResults, jobGroup);
if (!finalFilteredUnits.Any())
{
logger.LogInformation("После применения правил фильтрации в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
return;
}
// 5. Разрешение конфликтов связей и построение первичного маппинга
logger.LogDebug("Разрешение конфликтов связей и построение первичного маппинга.");
var initialReverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnits, maxJob);
if (!initialReverseMapping.Any())
{
logger.LogInformation("После разрешения конфликтов в JobGroup {JobGroupId} не осталось связей.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет связей после разрешения конфликтов");
return;
}
// 6. Построение структуры групп (трансформация, внутренняя группировка, разбиение)
logger.LogDebug("Построение структуры групп для шаблонов.");
var templateGroups = await groupedTemplateBuilder.BuildAsync(initialReverseMapping, jobGroup, maxJob);
if (!templateGroups.Any())
{
logger.LogInformation("После построения структуры групп в JobGroup {JobGroupId} не осталось данных.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет данных после построения групп");
return;
}
// 7. Обработка групп: сравнение, обновление, создание, отправка MQ
logger.LogDebug("Обработка групп шаблонов: сравнение, обновление и создание.");
var expectedTemplateKeys = await groupedTemplateProcessor.ProcessAsync(
templateGroups,
jobsInGroup,
maxJob,
initiator);
// 8. Деактивация лишних шаблонов
await DeactivateUnusedTemplatesAsync(expectedTemplateKeys, jobGroupId, jobsInGroup, initiator);
// 9. Успешное завершение
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
}
private async Task DeactivateUnusedTemplatesAsync(
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
Guid jobGroupId,
List<Job> jobsInGroup,
HistoryInitiator initiator)
{
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) &&
t.StatusTypeId == TemplateStatusTypeEnum.Used &&
t.Job!.GroupId == jobGroupId)
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Unit {UnitId}, Index {Index}).",
existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
}
}
}
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
{
var status = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
}
}

View File

@@ -0,0 +1,100 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateUnitFilter : IGroupedTemplateUnitFilter
{
private readonly ILogger<GroupedTemplateUnitFilter> logger;
private readonly IUnitInValueRepository unitInValueRepository;
private readonly IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupRepository;
private readonly IUnitFieldRepository unitFieldRepository;
public GroupedTemplateUnitFilter(
ILogger<GroupedTemplateUnitFilter> logger,
IUnitInValueRepository unitInValueRepository,
IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupRepository,
IUnitFieldRepository unitFieldRepository)
{
this.logger = logger;
this.unitInValueRepository = unitInValueRepository;
this.regionalEkPtkGroupRepository = regionalEkPtkGroupRepository;
this.unitFieldRepository = unitFieldRepository;
}
public async Task<List<UnitFilterResultDto>> FilterAsync(
IEnumerable<UnitFilterResultDto> initialUnits,
JobGroup jobGroup,
CancellationToken ct = default)
{
var unitsList = initialUnits.ToList();
if (!unitsList.Any())
{
logger.LogDebug("Входной список юнитов пуст. Фильтрация пропущена.");
return unitsList;
}
logger.LogDebug("Начало фильтрации юнитов для JobGroup {JobGroupId}.", jobGroup.Id);
// 1. Проверка наличия GroupingUnitFieldId
if (!jobGroup.GroupingUnitFieldId.HasValue)
{
throw new InvalidOperationException($"JobGroup {jobGroup.Id} не имеет GroupingUnitFieldId, необходимого для группировки.");
}
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
logger.LogDebug("Фильтрация по GroupingUnitFieldId (FieldId={FieldId}).", groupingFieldId);
// 2. Фильтрация по GroupingUnitFieldId
var allUnitIds = unitsList.Select(u => u.Id).ToList();
var groupingValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(allUnitIds, new HashSet<Guid> { groupingFieldId });
var validUnitIdsAfterGrouping = groupingValues
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
.Select(uv => uv.UnitId)
.ToHashSet();
var filteredByGrouping = unitsList
.Where(u => validUnitIdsAfterGrouping.Contains(u.Id))
.ToList();
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", filteredByGrouping.Count);
if (!filteredByGrouping.Any())
{
return filteredByGrouping;
}
// 3. Фильтрация по РАБОЧАЯ_ГР_ОТВ_ЗАК
var workGroupField = await unitFieldRepository.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК")
?? throw new InvalidOperationException("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗАК' не найдено в справочнике полей.");
var workGroupFieldId = workGroupField.Id;
logger.LogDebug("Фильтрация по полю 'РАБОЧАЯ_ГР_ОТВ_ЗАК' (FieldId={FieldId}).", workGroupFieldId);
var filteredUnitIds = filteredByGrouping.Select(u => u.Id).ToList();
var workGroupValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(filteredUnitIds, new HashSet<Guid> { workGroupFieldId });
var allowedValueIds = regionalEkPtkGroupRepository.Get()
.Select(g => g.FieldValueId)
.ToHashSet();
logger.LogDebug("Найдено {Count} разрешенных значений для поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.", allowedValueIds.Count);
var validUnitIdsAfterWorkGroup = workGroupValues
.Where(uv => uv.Value != null && allowedValueIds.Contains(uv.Value.Id))
.Select(uv => uv.UnitId)
.ToHashSet();
var finalFiltered = filteredByGrouping
.Where(u => validUnitIdsAfterWorkGroup.Contains(u.Id))
.ToList();
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' осталось {Count} юнитов.", finalFiltered.Count);
return finalFiltered;
}
}

View File

@@ -6,13 +6,13 @@ using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.Shortcodes;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models;
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums;
using PARR.Domain.Settings;
using PARR.TemplateMatcher.Services.Interfaces;
@@ -41,30 +41,28 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService;
private readonly SettingsFromDb settingsFromDb;
//private readonly INextRunService nextRunService;
private readonly IOptions<TemplateSettings> templateSettings;
private readonly IUnitFieldRepository unitFieldService;
private readonly IUnitInValueRepository unitInValueService;
private readonly IUnitRepository unitRepository;
public SimpleTemplateSynchronizer(
ILogger<SimpleTemplateSynchronizer> logger,
IUnitFilterService unitFilterService,
IUnitInUnitRepository unitInUnitService,
IUnitInValueRepository unitInValueService,
IUnitRepository unitService,
MqSettings mqSettings,
IRabbitService mqService,
ITemplateRepository templateService,
IJobRepository jobService,
ITemplateReuser templateReuser,
IShortcodesService shortcodesService,
IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupService,
IUnitFieldRepository unitFieldService,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService,
SettingsFromDb settingsFromDb,
//INextRunService nextRunService,
IOptions<TemplateSettings> templateSettings
IOptions<TemplateSettings> templateSettings,
IUnitFieldRepository unitFieldService,
IUnitInValueRepository unitInValueService,
IUnitRepository unitRepository
)
{
this.logger = logger;
@@ -79,8 +77,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService;
this.settingsFromDb = settingsFromDb;
//this.nextRunService = nextRunService;
this.templateSettings = templateSettings;
this.unitFieldService = unitFieldService;
this.unitInValueService = unitInValueService;
this.unitRepository = unitRepository;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
@@ -182,7 +182,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
.Include(t => t.UnitsInTemplate)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t => t.GroupType)
.ThenInclude(t => t!.GroupType)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Include(t => t.Unit)
@@ -225,7 +225,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true);
var updateRequest = new TemplateUpdaterMq
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = reusableTemplate.Id,
JobId = jobId,
@@ -237,7 +237,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Initiator = initiator,
//NextRun = nextRun,
IsNew = true,
UnitsInTemplate = new List<Guid>() // для простого шаблона
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
@@ -259,9 +259,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
var updateRequest = new TemplateUpdaterMq
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
@@ -269,13 +268,11 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
//LastRun = template.LastRun,
//NextRun = nextRun,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = new List<Guid>() // для простого шаблона
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
@@ -400,7 +397,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
var updateRequest = new TemplateUpdaterMq
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
@@ -414,7 +411,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = new List<Guid>() // для простого шаблона
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
@@ -434,9 +431,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
}
}
private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator)
{
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
if (existingStatus.DetailsJobs?.Any() == true)
{
@@ -444,7 +441,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
@@ -460,7 +456,33 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
try
{
// Загружаем Job неиспользуемых шаблонов с Group
// 1. Находим ID нужных полей
var zoRgField = await unitFieldService.GetByAihitNameAsync("ЗОНА_ОТВЕТСТВЕННОСТИ");
var tagField = await unitFieldService.GetByAihitNameAsync("ПАРР тег");
if (zoRgField == null || tagField == null)
{
logger.LogError("Не найдены поля 'ЗОНА_ОТВЕТСТВЕННОСТИ' или 'Тег'. Синхронизация прервана.");
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
return;
}
var zoRgFieldId = zoRgField.Id;
var tagFieldId = tagField.Id;
const string targetTagValue = "ПАРР-НЕИСП";
// 2. Находим ValueId для тега "ПАРР-НЕИСП"
var targetTagValueId = await unitInValueService.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == targetTagValue)
.Select(uiv => uiv.ValueId)
.FirstOrDefaultAsync();
if (targetTagValueId == Guid.Empty)
{
logger.LogWarning("Значение '{TagValue}' для поля 'Тег' не найдено в справочнике UnitFieldValue.", targetTagValue);
}
var unusedJob = await jobService.Get()
.AsNoTracking()
.Include(j => j.Group)
@@ -473,9 +495,13 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
return;
}
// Находим все шаблоны со статусом Unused
var unusedTemplates = await templateService.Get()
.Include(t => t.Unit)
.Include(t => t.Job)
.ThenInclude(t => t.Group)
.ThenInclude(t => t.GroupType)
.Include(t => t.Job)
.ThenInclude(t => t.Tnk)
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
.ToListAsync();
@@ -490,47 +516,96 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
await UpdateMatchingStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки");
int processed = 0;
var allTemplateUnitIds = unusedTemplates.Select(t => t.UnitId).Distinct().ToList();
// Получаем значения ЗОНА_ОТВЕТСТВЕННОСТИ для всех юнитов шаблонов
var unitZoRgValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allTemplateUnitIds, new List<Guid> { zoRgFieldId });
var unitToZoRgValueMap = unitZoRgValues
.Where(uiv => uiv.ValueId != Guid.Empty)
.ToDictionary(uiv => uiv.UnitId, uiv => uiv.ValueId);
foreach (var template in unusedTemplates)
{
// Генерируем ожидаемое имя один раз
var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob);
// Извлекаем "базовые" имена
var currentBaseName = ExtractBaseName(template.Name);
var expectedBaseName = ExtractBaseName(expectedName);
// Проверяем, нужно ли обновление
bool needsUpdate = template.JobId != unusedJobId ||
currentBaseName != expectedBaseName;
if (!needsUpdate)
try
{
logger.LogDebug("Шаблон {TemplateId} уже имеет актуальное имя и JobId. Пропускаем.", template.Id);
continue;
if (template.Unit == null)
{
logger.LogWarning("У шаблона {TemplateId} отсутствует Unit. Пропускаем.", template.Id);
processed++;
continue;
}
if (!unitToZoRgValueMap.TryGetValue(template.UnitId, out var currentZoRgValueId))
{
logger.LogDebug("У юнита {UnitId} шаблона {TemplateId} нет значения поля ЗОНА_ОТВЕТСТВЕННОСТИ. Пропускаем замену UnitId.", template.UnitId, template.Id);
// Исправлено: используем другое имя или просто вызываем метод
var nameForUpdate = await GenerateUnusedTemplateNameAsync(template, unusedJob, template.Unit);
await SendUpdateRequest(template, unusedJobId, nameForUpdate, initiator, template.UnitId);
processed++;
continue;
}
Guid? targetUnitId = null;
if (targetTagValueId != Guid.Empty)
{
var candidatesWithZoRg = await unitInValueService.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == zoRgFieldId && uiv.ValueId == currentZoRgValueId)
.Select(uiv => uiv.UnitId)
.Distinct()
.ToListAsync();
if (candidatesWithZoRg.Any())
{
// Сначала проверим, есть ли вообще такие юниты
var foundUnitId = await unitInValueService.Get()
.AsNoTracking()
.Where(uiv => uiv.FieldId == tagFieldId && uiv.ValueId == targetTagValueId && candidatesWithZoRg.Contains(uiv.UnitId))
.Select(uiv => uiv.UnitId)
.FirstOrDefaultAsync();
// Если нашли реальный Guid (не Empty), то используем его
if (foundUnitId != Guid.Empty)
{
targetUnitId = foundUnitId;
}
}
}
Guid finalUnitId = targetUnitId ?? template.UnitId;
Unit finalUnit = template.Unit;
if (targetUnitId.HasValue && targetUnitId.Value != template.UnitId)
{
logger.LogInformation("Для шаблона {TemplateId} найден новый UnitId {NewUnitId} (был {OldUnitId}).",
template.Id, targetUnitId.Value, template.UnitId);
// Загружаем новый юнит для генерации имени
var newUnit = await unitRepository.Get().AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == targetUnitId.Value);
if (newUnit != null)
finalUnit = newUnit;
else
{
logger.LogWarning("Не удалось загрузить новый юнит {UnitId}. Используем старый.", targetUnitId.Value);
finalUnitId = template.UnitId;
}
}
// Здесь expectedName объявляется впервые в этой итерации цикла, конфликта нет
var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob, finalUnit);
await SendUpdateRequest(template, unusedJobId, expectedName, initiator, finalUnitId);
processed++;
await UpdateMatchingStatusAsync(unusedJobId, $"Обработано: {processed}/{unusedTemplates.Count}");
}
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
var updateRequest = new TemplateUpdaterMq
catch (Exception ex)
{
TemplateId = template.Id,
JobId = unusedJobId,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = false,
IsActiveSchedule = false,
//LastRun = template.LastRun,
//NextRun = nextRun,
IsNew = false,
Index = null,
StatusTypeId = template.StatusTypeId,
Initiator = initiator,
UnitsInTemplate = new List<Guid>()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
processed++;
await UpdateMatchingStatusAsync(unusedJobId, $"Обработано: {processed}/{unusedTemplates.Count}");
logger.LogError(ex, "Ошибка при обработке шаблона {TemplateId}", template.Id);
}
}
await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена");
@@ -545,8 +620,28 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
}
}
private async Task SendUpdateRequest(Template template, Guid jobId, string name, HistoryInitiator initiator, Guid unitId)
{
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = jobId,
UnitId = unitId,
Name = name,
IsActiveTemplate = false,
IsActiveSchedule = false,
IsNew = false,
Index = null,
StatusTypeId = template.StatusTypeId,
Initiator = initiator,
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
private async Task<string> GenerateUnusedTemplateNameAsync(Template template, Job unusedJob)
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
private async Task<string> GenerateUnusedTemplateNameAsync(Template template, Job unusedJob, Unit unit)
{
var tempJob = new Job
{
@@ -573,10 +668,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
Id = template.Id,
Name = template.Name,
JobId = unusedJob.Id,
UnitId = template.UnitId,
UnitId = unit.Id,
Index = null,
Job = tempJob,
Unit = template.Unit,
Unit = unit,
UnitsInTemplate = new List<UnitsInTemplate>()
};
@@ -588,16 +683,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
{
logger.LogInformation("Создание нового простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
var mqRequest = new TemplateGeneratorMq
var mqRequest = new TemplateGeneratorMessage
{
JobId = jobId,
UnitId = unitId,
UnitsInTemplate = new List<Guid>(), // для простого шаблона
UnitsInTemplate = new List<UnitInTemplateMessage>(), // для простого шаблона
HistoryInitiator = initiator
};
//var msg = JsonSerializer.Serialize(mqRequest);
//var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg });
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
if (!result.IsSuccess)
@@ -620,29 +713,4 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
TimeSpan.FromMinutes(30)
);
}
private static string ExtractBaseName(string name)
{
var lastUnderscoreIndex = name.LastIndexOf('_');
if (lastUnderscoreIndex > 0)
{
var suffix = name.Substring(lastUnderscoreIndex + 1);
if (long.TryParse(suffix, out long unixTimestampMs))
{
try
{
// Проверяем, является ли это валидным временем
var dto = DateTimeOffset.FromUnixTimeMilliseconds(unixTimestampMs);
return name.Substring(0, lastUnderscoreIndex);
}
catch (ArgumentOutOfRangeException)
{
// Значение вне диапазона для DateTimeOffset
return name; // Не трогаем имя, если суффикс невалиден
}
}
}
return name;
}
}

View File

@@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
@@ -100,7 +100,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
var expectedName = await namenormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
var updateRequest = new TemplateUpdaterMq
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = unusedJob.Id,
@@ -108,13 +108,11 @@ internal class TemplateDeactivator : ITemplateDeactivator
Name = expectedName,
IsActiveTemplate = DefaultUnusedTemplateState,
IsActiveSchedule = DefaultUnusedScheduleState,
//LastRun = template.LastRun,
//NextRun = template.NextRun,
IsNew = false,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator,
UnitsInTemplate = new List<Guid>()
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await sender.SendTemplateUpdateMessageAsync(updateRequest);

View File

@@ -14,6 +14,7 @@ internal class TemplateNameNormalizer : ITemplateNameNormalizer
this.shortcodesService = shortcodesService;
}
public async Task<string> GetNormalizedTemplateNameAsync(Template template, [CallerMemberName] string? caller = null)
{
var callerName = caller ?? "Unknown";

View File

@@ -20,6 +20,7 @@ internal class TemplateReuser : ITemplateReuser
this.templateService = templateService;
}
public async Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId,
Guid unitId,
@@ -42,12 +43,12 @@ internal class TemplateReuser : ITemplateReuser
// Загружаем зарезервированный шаблон
var template = await templateService.Get()
.AsNoTracking()
.Include(t=>t.Unit)
.Include(t=>t.Job)
.ThenInclude(t=>t!.Tnk)
.Include(t => t.Unit)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t=>t!.GroupType)
.ThenInclude(t => t!.GroupType)
.FirstOrDefaultAsync(t => t.Id == templateId);
if (template == null)

View File

@@ -1,8 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
@@ -13,35 +11,23 @@ internal class TemplateUpdaterMqSender : ITemplateUpdaterMqSender
private readonly ILogger<TemplateUpdaterMqSender> logger;
private readonly IRabbitService mqService;
private readonly MqSettings mqSettings;
private readonly ITemplateRepository templateService;
public TemplateUpdaterMqSender(
ILogger<TemplateUpdaterMqSender> logger,
IRabbitService mqService,
MqSettings mqSettings,
ITemplateRepository templateService
MqSettings mqSettings
)
{
this.logger = logger;
this.mqService = mqService;
this.mqSettings = mqSettings;
this.templateService = templateService;
}
public async Task SendTemplateUpdateMessageAsync(TemplateUpdaterMq updateRequest)
public async Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest)
{
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
//var existingTemplateByName = await templateService.Get().AsNoTracking()
// .FirstOrDefaultAsync(t => EF.Functions.ILike(t.Name, updateRequest.Name) && updateRequest.TemplateId != t.Id);
//if (existingTemplateByName != null)
//{
// logger.LogError("Шаблон с именем {TemplateName} уже существует в базе данных. Текущий Id:{TemplateId}", existingTemplateByName.Name, existingTemplateByName.Id);
// return;
//}
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new List<object> { updateRequest });
if (result.IsSuccess)

View File

@@ -11,6 +11,7 @@ internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
private readonly IUnitRepository unitRepository;
private readonly IUnitKiiUnitRepository unitKiiUnitRepository;
public UnitInTemplateConflictMapper(
ILogger<UnitInTemplateConflictMapper> logger,
IUnitRepository unitRepository,
@@ -21,6 +22,7 @@ internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
this.unitKiiUnitRepository = unitKiiUnitRepository;
}
public async Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
IEnumerable<UnitFilterResultDto> unitFilterResults,
Job maxJob,

View File

@@ -1,616 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models;
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false;
private readonly ILogger<GroupedTemplateSynchronizer> logger;
private readonly IUnitFilterService unitFilterService;
private readonly IUnitInValueRepository unitInValueService;
private readonly IUnitRepository unitService;
private readonly MqSettings mqSettings;
private readonly IRabbitService mqService;
private readonly ITemplateRepository templateService;
private readonly IJobGroupRepository jobGroupService;
private readonly ITemplateReuser templateReuser;
private readonly IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupService;
private readonly IUnitFieldRepository unitFieldService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService;
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
IUnitFilterService unitFilterService,
IUnitInValueRepository unitInValueService,
IUnitRepository unitService,
MqSettings mqSettings,
IRabbitService mqService,
ITemplateRepository templateService,
IJobGroupRepository jobGroupService,
ITemplateReuser templateReuser,
IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupService,
IUnitFieldRepository unitFieldService,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService,
IUnitInTemplateConflictMapper unitInTemplateConflictMapper
)
{
this.logger = logger;
this.unitFilterService = unitFilterService;
this.unitInValueService = unitInValueService;
this.unitService = unitService;
this.mqSettings = mqSettings;
this.mqService = mqService;
this.templateService = templateService;
this.jobGroupService = jobGroupService;
this.templateReuser = templateReuser;
this.regionalEkPtkGroupService = regionalEkPtkGroupService;
this.unitFieldService = unitFieldService;
this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService;
this.unitInTemplateConflictMapper = unitInTemplateConflictMapper;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Any() == true)
{
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
try
{
// 1. Получить JobGroup и связанные Job'ы
var jobGroup = await jobGroupService.Get()
.AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.ThenInclude(rf => rf.UnitField) // Подгрузим поля для фильтрации
.Include(jg => jg.Jobs)
.ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return;
}
var jobsInGroup = jobGroup.Jobs.ToList();
// 2. Найти Job с максимальным MaxValueRelationships
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
{
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
return;
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
// 3. Использовать unitFilterService для получения отфильтрованных юнитов с их связями
// Это включает в себя все фильтры: UnitFilter, FieldFilter, RelationshipFilter, UmbrellaFilter
logger.LogDebug("Получение отфильтрованных юнитов с их связями через UnitFilterService для Job {JobId}.", maxJob.Id);
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
if (unitFilterResults == null || !unitFilterResults.Any())
{
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
return;
}
logger.LogDebug("Получено {Count} юнитов с подходящими связями через UnitFilterService.", unitFilterResults.Count());
// --- ФИЛЬТРАЦИЯ unitFilterResults (dto.Id) ---
// 4. Фильтрация unitFilterResults по GroupingUnitFieldId (проверяем dto.Id)
if (!jobGroup.GroupingUnitFieldId.HasValue)
{
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Отсутствует GroupingUnitFieldId");
return;
}
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по GroupingUnitFieldId (FieldId={FieldId}).", groupingFieldId);
// Загрузим значения поля GroupingUnitFieldId для всех Id из unitFilterResults
var allUnitFilterResultIds = unitFilterResults.Select(dto => dto.Id).ToList();
var groupingUnitValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitFilterResultIds, new HashSet<Guid> { groupingFieldId });
// Найдем Id юнитов, у которых есть значение в GroupingUnitFieldId
var validUnitFilterResultIds = groupingUnitValues
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
.Select(uv => uv.UnitId)
.ToHashSet();
// Оставляем только те UnitFilterResultDto, чей Id проходит фильтр
var filteredUnitFilterResultsByGrouping = unitFilterResults
.Where(dto => validUnitFilterResultIds.Contains(dto.Id))
.ToList();
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} UnitFilterResultDto.", filteredUnitFilterResultsByGrouping.Count);
if (!filteredUnitFilterResultsByGrouping.Any())
{
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId");
return;
}
// 5. Фильтрация unitFilterResults по РАБОЧАЯ_ГР_ОТВ_ЗАК (проверяем dto.Id)
var workGroupFieldId = await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по полю 'РАБОЧАЯ_ГР_ОТВ_ЗАК' (FieldId={FieldId}).", workGroupFieldId);
// Загрузим значения поля РАБОЧАЯ_ГР_ОТВ_ЗАК для Id из filteredUnitFilterResultsByGrouping
var allFilteredUnitFilterResultIds = filteredUnitFilterResultsByGrouping.Select(dto => dto.Id).ToList();
var workGroupValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allFilteredUnitFilterResultIds, new HashSet<Guid> { workGroupFieldId });
// Получим разрешенные значения из regionalEkPtkGroupService
var allowedValueIds = regionalEkPtkGroupService.Get()
.Select(g => g.FieldValueId)
.ToHashSet();
logger.LogDebug("Найдено {Count} разрешенных значений для поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.", allowedValueIds.Count);
// Найдем Id юнитов, у которых значение в РАБОЧАЯ_ГР_ОТВ_ЗАК разрешено
var validUnitFilterResultIdsForWorkGroup = workGroupValues
.Where(uv => uv.Value != null && allowedValueIds.Contains(uv.Value.Id))
.Select(uv => uv.UnitId)
.ToHashSet();
// Оставляем только те UnitFilterResultDto, чей Id проходит фильтр
var finalFilteredUnitFilterResults = filteredUnitFilterResultsByGrouping
.Where(dto => validUnitFilterResultIdsForWorkGroup.Contains(dto.Id))
.ToList();
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' осталось {Count} UnitFilterResultDto.", finalFilteredUnitFilterResults.Count);
if (!finalFilteredUnitFilterResults.Any())
{
logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК'");
return;
}
logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
var reverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnitFilterResults, maxJob);
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
if (!reverseMapping.Any())
{
logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения");
return;
}
// 6. Внутренняя группировка по ОТВЕТСТВЕННЫЙ_ЗАК / РАБОЧАЯ_ГР_ОТВ_ЗАК (для юнитов в UnitsInTemplate)
logger.LogDebug("Внутренняя группировка по полю (IsGroupByResponsible={IsGroupByResponsible}).", jobGroup.IsGroupByResponsible);
var groupingFieldIdForInnerGrouping = jobGroup.IsGroupByResponsible == true
? await GetFieldIdByAihitNameAsync("ОТВЕТСТВЕННЫЙ_ЗАК")
: await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
// Загрузим значения поля для *всех* юнитов, которые могут быть в UnitsInTemplate
// Это все юниты из всех списков в reverseMapping.Values
var allUnitsInTemplate = reverseMapping.Values.SelectMany(list => list).Distinct().ToList();
var innerGroupingValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitsInTemplate, new HashSet<Guid> { groupingFieldIdForInnerGrouping });
// Создадим маппинг UnitId (из UnitsInTemplate) -> значение поля для внутренней группировки
var unitInTemplateToInnerGroupingValueMap = innerGroupingValues
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
.ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value);
// 7. Основной цикл обработки: итерируемся по potentialUnitIds (UnitId шаблонов)
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvpOuter in reverseMapping)
{
var potentialUnitId = kvpOuter.Key;
var unitsInTemplateForThisPotentialUnitId = kvpOuter.Value;
logger.LogDebug("Обработка потенциального шаблона для UnitId {PotentialUnitId} с {Count} юнитами в UnitsInTemplate до внутренней группировки.", potentialUnitId, unitsInTemplateForThisPotentialUnitId.Count);
// --- ВНУТРЕННЯЯ ГРУППИРОВКА ---
// Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
.GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных"))
.OrderBy(g => g.Key, StringComparer.Ordinal) // <-- Сортировка по имени внутренней группы
.ToList();
logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate (отсортировано).", potentialUnitId, innerGroupedUnitsInTemplate.Count);
// --- СОБЕРЕМ ВСЕ ИТОГОВЫЕ ПОДГРУППЫ ДЛЯ ЭТОГО potentialUnitId ---
var allFinalSubGroups = new List<(List<Guid> UnitsInTemplateSubGroup, string InnerGroupName, int SubGroupSizeWithinInnerGroup)>(); // (UnitsInTemplate, Имя_внутренней_группы, размер_подгруппы_внутри_внутренней_группы)
foreach (var innerGroup in innerGroupedUnitsInTemplate) // Теперь проходит в отсортированном порядке по groupingValueName
{
var groupingValueName = innerGroup.Key;
var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Список юнитов (UnitId), связанных с potentialUnitId и имеющих одно и то же значение поля
logger.LogDebug("Обработка внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId} с {Count} юнитами.", groupingValueName, potentialUnitId, unitsInTemplateInInnerGroup.Count);
// Разбиваем юниты из *этой* внутренней группы на подгруппы по maxJob.MaxValueRelationships
// Это нужно делать для каждой внутренней группы отдельно
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
var unitsInTemplateSubGroups = unitsInTemplateInInnerGroup
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList())
.ToList();
logger.LogDebug("Внутренняя группа '{GroupingValue}' для UnitId {PotentialUnitId}: разбит на {GroupCount} подгрупп UnitsInTemplate.", groupingValueName, potentialUnitId, unitsInTemplateSubGroups.Count);
// Добавим каждую *итоговую* подгруппу в общий список
foreach (var subGroup in unitsInTemplateSubGroups)
allFinalSubGroups.Add((subGroup, groupingValueName!, subGroup.Count));
}
int globalIndexForThisUnitId = 1;
foreach (var finalSubGroupData in allFinalSubGroups)
{
var unitsInTemplateSubGroup = finalSubGroupData.UnitsInTemplateSubGroup;
var subGroupSize = finalSubGroupData.SubGroupSizeWithinInnerGroup;
var originatingInnerGroupName = finalSubGroupData.InnerGroupName;
logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", globalIndexForThisUnitId, originatingInnerGroupName, potentialUnitId, subGroupSize);
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
// --- Добавляем УНИКАЛЬНЫЙ ключ в список ожидаемых ---
expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, globalIndexForThisUnitId));
var existingTemplatesForRelationship = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t => t!.GroupType)
.Include(t => t.UnitsInTemplate)
.ThenInclude(uit => uit.Unit)
.Where(t => t.JobId == targetJob.Id && t.UnitId == potentialUnitId && t.Index == globalIndexForThisUnitId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync();
var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault();
if (existingTemplateForSubGroup != null)
{
// === 1. Получаем текущие и новые ID юнитов ===
var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
var proposedUnitIds = unitsInTemplateSubGroup.ToList();
// === 2. Сравниваем детерминированно с сортировкой по имени ===
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
var unitNamesForSort = await unitService.Get()
.AsNoTracking()
.Where(u => allUnitIdsForSort.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString());
var sortedCurrentUnitIds = currentUnitIds
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
.ToList();
var sortedProposedUnitIds = proposedUnitIds
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
.ToList();
bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds);
if (unitsAreEqual)
{
logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id);
existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList();
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup);
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = existingTemplateForSubGroup.Id,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Name = expectedName,
IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate,
IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule,
IsNew = false,
Index = globalIndexForThisUnitId,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = sortedProposedUnitIds
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id);
}
}
else
{
logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов или их порядка (после сортировки).", existingTemplateForSubGroup.Id);
var newTargetJob = SelectTargetJob(jobsInGroup, unitsInTemplateSubGroup.Count, maxJob); // Размер - из подмножества
if (newTargetJob.Id != existingTemplateForSubGroup.JobId)
{
logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id);
}
await UpdateTemplateUnitsAsync(existingTemplateForSubGroup, sortedProposedUnitIds, newTargetJob, initiator, globalIndexForThisUnitId);
}
}
else
{
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator);
if (reusableTemplate != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, potentialUnitId, globalIndexForThisUnitId);
var tempTemplateForName = new Template
{
Id = reusableTemplate.Id,
Name = reusableTemplate.Name,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Index = globalIndexForThisUnitId,
Job = targetJob,
Unit = reusableTemplate.Unit,
UnitsInTemplate = unitsInTemplateSubGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
};
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = reusableTemplate.Id,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Name = expectedName,
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
IsNew = true,
Index = globalIndexForThisUnitId,
UnitsInTemplate = unitsInTemplateSubGroup
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}, с {Count} юнитами.", targetJob.Id, potentialUnitId, globalIndexForThisUnitId, unitsInTemplateSubGroup.Count);
await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, globalIndexForThisUnitId, initiator);
}
}
globalIndexForThisUnitId++; // Увеличиваем индекс для следующей итоговой подгруппы
}
}
// === Деактивация ===
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) &&
t.StatusTypeId == TemplateStatusTypeEnum.Used &&
t.Job!.GroupId == jobGroupId)
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedTemplateKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Relationship {UnitId}, Index {Index}).",
existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
}
}
// === Успешное завершение ===
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
return;
}
private Job SelectTargetJob(List<Job> jobsInGroup, int subGroupSize, Job maxJob)
{
Job? targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize)
.FirstOrDefault();
if (targetJob == null)
{
targetJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize)
.OrderBy(j => j.MaxValueRelationships!.Value)
.FirstOrDefault();
}
if (targetJob == null)
{
targetJob = maxJob;
logger.LogDebug("Для подгруппы размером {Size} не найден подходящий Job, используем maxJob {MaxJobId}.", subGroupSize, maxJob.Id);
}
else
{
logger.LogDebug("Для подгруппы размером {Size} выбран Job {TargetJobId} с MaxValueRelationships {MaxValue}.", subGroupSize, targetJob.Id, targetJob.MaxValueRelationships);
}
return targetJob;
}
private async Task UpdateTemplateUnitsAsync(Template template, List<Guid> newUnitIds, Job targetJob, HistoryInitiator initiator, int newIndex)
{
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для обновления юнитов.", template.Id);
return;
}
var tempTemplateForName = new Template
{
Id = template.Id,
Name = template.Name,
JobId = targetJob.Id,
UnitId = template.UnitId,
Index = newIndex,
Job = targetJob,
Unit = template.Unit,
UnitsInTemplate = newUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
};
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = targetJob.Id,
UnitId = template.UnitId,
Name = expectedName,
IsActiveTemplate = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
IsNew = false,
Index = newIndex,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
UnitsInTemplate = newUnitIds
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
private async Task CreateGroupedTemplateAsync(Guid jobId, Guid relationshipUnitId, List<Guid> unitIds, int index, HistoryInitiator initiator)
{
logger.LogInformation("Создание нового группового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", jobId, relationshipUnitId, index, unitIds.Count);
var mqRequest = new TemplateGeneratorMq
{
JobId = jobId,
UnitId = relationshipUnitId,
UnitsInTemplate = unitIds,
Index = index,
HistoryInitiator = initiator
};
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания группового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}.", jobId, relationshipUnitId, index);
}
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
{
var status = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
}
#region Вспомогательные методы
private async Task<Guid> GetFieldIdByAihitNameAsync(string fieldName)
{
var field = await unitFieldService.GetByAihitNameAsync(fieldName);
if (field == null)
{
logger.LogError("Поле '{FieldName}' не найдено в справочнике полей.", fieldName);
throw new InvalidOperationException($"Поле '{fieldName}' не найдено в справочнике полей.");
}
return field.Id;
}
#endregion
}

View File

@@ -0,0 +1,18 @@
using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Interfaces;
public interface IGroupedTemplateBuilder
{
/// <summary>
/// Строит структуру групп для создания групповых шаблонов.
/// Трансформирует связи в пары (UnitId, UnitFieldValueId),
/// выполняет внутреннюю группировку и разбиение на подгруппы.
/// </summary>
Task<List<GroupedTemplateGroup>> BuildAsync(
Dictionary<Guid, List<Guid>> initialReverseMapping,
JobGroup jobGroup,
Job maxJob,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,22 @@
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Interfaces;
public interface IGroupedTemplateProcessor
{
/// <summary>
/// Обрабатывает построенные группы шаблонов:
/// 1. Ищет существующие шаблоны.
/// 2. Сравнивает состав юнитов.
/// 3. Обновляет, переиспользует или создает новые шаблоны.
/// 4. Возвращает набор ключей ожидаемых шаблонов для последующей деактивации лишних.
/// </summary>
Task<HashSet<(Guid JobId, Guid UnitId, int Index)>> ProcessAsync(
List<GroupedTemplateGroup> groups,
List<Job> jobsInGroup,
Job maxJob,
HistoryInitiator initiator,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,18 @@
using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Entities.Job;
namespace PARR.TemplateMatcher.Services.Interfaces
{
public interface IGroupedTemplateUnitFilter
{
/// <summary>
/// Применяет специфичные правила фильтрации для групповых шаблонов.
/// 1. Отбирает юниты, имеющие заполненное значение в GroupingUnitFieldId.
/// 2. Оставляет только юниты, значение которых в поле РАБОЧАЯ_ГР_ОТВ_ЗАК разрешено.
/// </summary>
Task<List<UnitFilterResultDto>> FilterAsync(
IEnumerable<UnitFilterResultDto> initialUnits,
JobGroup jobGroup,
CancellationToken ct = default);
}
}

View File

@@ -1,9 +1,9 @@
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
namespace PARR.TemplateMatcher.Services.Interfaces
{
public interface ITemplateUpdaterMqSender
{
Task SendTemplateUpdateMessageAsync(TemplateUpdaterMq updateRequest);
Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest);
}
}

View File

@@ -35,6 +35,9 @@ namespace PARR.TemplateMatcher
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
services.AddTransient<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
services.AddTransient<IGroupedTemplateUnitFilter, GroupedTemplateUnitFilter>();
services.AddTransient<IGroupedTemplateBuilder, GroupedTemplateBuilder>();
services.AddTransient<IGroupedTemplateProcessor, GroupedTemplateProcessor>();
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
}

View File

@@ -1,9 +1,9 @@
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
namespace PARR.TemplateUpdater.Services
{
internal interface ITemplateUpdaterService
{
Task UpdateTemplateAsync(TemplateUpdaterMq query);
Task UpdateTemplateAsync(TemplateUpdaterMessage query);
}
}

View File

@@ -5,8 +5,7 @@ using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.NextRunServices;
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Entities;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
@@ -39,7 +38,7 @@ namespace PARR.TemplateUpdater.Services
}
public async Task UpdateTemplateAsync(TemplateUpdaterMq query)
public async Task UpdateTemplateAsync(TemplateUpdaterMessage query)
{
var isValid = await IsValidAsync(query);
if (!isValid)
@@ -95,52 +94,55 @@ namespace PARR.TemplateUpdater.Services
scheduleIsChanged = true;
}
//просто обнволяем. не влияет ни на шаблон, ни на расписание
//template.LastRun = query.LastRun;
template.Index = query.Index;
template.StatusTypeId = query.StatusTypeId;
// === Обработка изменения состава UnitsInTemplate ===
var currentUnitIds = template.UnitsInTemplate.Select(u => u.UnitId).ToHashSet();
var newUnitIds = query.UnitsInTemplate.ToHashSet();
// Формируем HashSet пар (UnitId, UnitFieldValueId) для быстрого сравнения
var currentEntries = template.UnitsInTemplate
.Select(u => new UnitInTemplateMessage { UnitId = u.UnitId, UnitFieldValueId = u.UnitFieldValueId })
.ToHashSet();
if (!currentUnitIds.SetEquals(newUnitIds))
var newEntries = query.UnitsInTemplate.ToHashSet();
if (!currentEntries.SetEquals(newEntries))
{
// Удаляем старые связи
// Удаляем связи, которых нет в новом наборе
var toRemove = template.UnitsInTemplate
.Where(u => !newUnitIds.Contains(u.UnitId))
.Where(u => !newEntries.Contains(new UnitInTemplateMessage { UnitId = u.UnitId, UnitFieldValueId = u.UnitFieldValueId }))
.ToList();
foreach (var item in toRemove)
template.UnitsInTemplate.Remove(item);
// Добавляем новые связи
var toAdd = newUnitIds.Except(currentUnitIds);
foreach (var unitId in toAdd)
var toAdd = newEntries.Except(currentEntries);
foreach (var entry in toAdd)
{
template.UnitsInTemplate.Add(new UnitsInTemplate
{
TemplateId = template.Id,
UnitId = unitId,
UnitId = entry.UnitId,
UnitFieldValueId = entry.UnitFieldValueId,
DateCreated = DateTimeOffset.UtcNow
});
}
// Поскольку коллекция изменилась — шаблон считается изменённым
templateIsChanged = true;
}
// === Конец обработки UnitsInTemplate ===
//Для правильного расчета shortcod'ов в БД должны быть сохранены данные UnitInTemplates, JobId, UnitId и т.д.
// Для правильного расчета shortcod'ов в БД должны быть сохранены данные UnitsInTemplate, JobId, UnitId и т.д.
if (templateIsChanged && !await templateService.CommitAsync(query.Initiator))
{
logger.LogError("Не удалось сохранить изменения в БД. {QueryType}: {Query}", nameof(TemplateUpdaterMq), query.ToJson());
logger.LogError("Не удалось сохранить изменения в БД. {QueryType}: {Query}", nameof(TemplateUpdaterMessage), query.ToJson());
return;
}
var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, query.IsNew);
if (!nextRun.HasValue)
{
logger.LogError("Ошибка при расчете нового nextRun (вернулся null) для шаблона {templateId}, {templateName}", template.Id, template.Name);
logger.LogError("Ошибка при расчете нового nextRun (вернулся null) для шаблона {TemplateId}, {TemplateName}", template.Id, template.Name);
return;
}
@@ -151,32 +153,23 @@ namespace PARR.TemplateUpdater.Services
scheduleIsChanged = true;
}
// === конец обработки UnitsInTemplate ===
if (templateIsChanged)
{
// ставим задачу на обновление шаблона
var config = robotConfigurationService.GetFromTemplateByRobotCode(RobotsEnum.TemplateOrder, template);
//robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Updating, config);
robotConfigurationService.SetUpdateTaskStatusIfAllow(config);
logger.LogDebug("Для шаблона {TemplateId} устанавливаю статус {Status}", template.Id, RobotStatusEnum.Wait.ToString());
}
if (scheduleIsChanged)
{
// ставим задачу на обновление расписания
var config = robotConfigurationService.GetFromTemplateByRobotCode(RobotsEnum.ScheduleOrder, template);
//robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Updating, config);
robotConfigurationService.SetUpdateTaskStatusIfAllow(config);
logger.LogDebug("Для расписания {TemplateId} устанавливаю статус {Status}", template.Id, RobotStatusEnum.Wait.ToString());
}
if (!await templateService.CommitAsync(query.Initiator))
{
logger.LogError("Не удалось сохранить изменения в БД. {QueryType}: {Query}", nameof(TemplateUpdaterMq), query.ToJson());
logger.LogError("Не удалось сохранить изменения в БД. {QueryType}: {Query}", nameof(TemplateUpdaterMessage), query.ToJson());
return;
}
@@ -189,7 +182,7 @@ namespace PARR.TemplateUpdater.Services
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
private async Task<bool> IsValidAsync(TemplateUpdaterMq query)
private async Task<bool> IsValidAsync(TemplateUpdaterMessage query)
{
var template = await templateService.Get().AsNoTracking()
.Include(t => t.RobotConfigurations)

View File

@@ -2,7 +2,7 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateUpdater.Services;
using PARR.TemplateUpdater.Settings;
@@ -51,7 +51,7 @@ namespace PARR.TemplateUpdater
{
logger.LogInformation("Получили запрос: {msg}", msg);
var query = transformService.GetModelFromJson<TemplateUpdaterMq>(msg);
var query = transformService.GetModelFromJson<TemplateUpdaterMessage>(msg);
if (query == null)
return;