feat(templateMatcher): Рефакторинг, основной метод синхронизации GroupedTemplateSynchronizer разбит на отдельные классы; Неиспользуемые шаблоны теперь привязываются к ЭК КОСМПЛЕКСЫ-[ЗО].
This commit is contained in:
@@ -981,7 +981,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
// var cacheKey = cacheService.GetKey(new[] { "uf_ids", filter.UnitFilter }, isUseHash: true);
|
// 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 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)
|
if (cachedData != null)
|
||||||
{
|
{
|
||||||
return cachedData.Data.UnitIds;
|
return cachedData.Data.UnitIds;
|
||||||
@@ -998,7 +998,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
Source = GetType().Name
|
Source = GetType().Name
|
||||||
};
|
};
|
||||||
|
|
||||||
await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1));
|
await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1), true);
|
||||||
|
|
||||||
return initialUnitIds;
|
return initialUnitIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
using PARR.Domain.Entities.Base.History;
|
using PARR.Domain.Entities.Base.History;
|
||||||
|
|
||||||
namespace PARR.Domain.Common.Rabbit.Messages
|
namespace PARR.Domain.Common.Rabbit.Messages.TemplateMatching
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Модель в MQ, простого создания Template
|
/// Модель в MQ, простого создания Template
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TemplateGeneratorMq
|
public record TemplateGeneratorMessage
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id регламентной работы
|
/// Id регламентной работы
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid JobId { get; set; }
|
public Guid JobId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id Юнита(единицы обслуживания)/ЭК
|
/// Id Юнита(единицы обслуживания)/ЭК
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid UnitId { get; set; }
|
public Guid UnitId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Актировать шаблон при инициализации
|
/// Актировать шаблон при инициализации
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsActiveTemplate { get; set; }
|
public bool? IsActiveTemplate { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Актировать расписание при инициализации
|
/// Актировать расписание при инициализации
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsActiveSchedule { get; set; }
|
public bool? IsActiveSchedule { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициатор запроса к генератору
|
/// Инициатор запроса к генератору
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HistoryInitiator? HistoryInitiator { get; set; }
|
public HistoryInitiator? HistoryInitiator { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Связанные ЭК для сгруппированного типа JobGroup
|
/// Связанные ЭК для сгруппированного типа JobGroup
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required List<Guid> UnitsInTemplate { get; set; }
|
public required List<UnitInTemplateMessage> UnitsInTemplate { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Индекс, используется в групповых шаблонах
|
/// Индекс, используется в групповых шаблонах
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Index { get; set; }
|
public int? Index { get; init; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,58 +1,60 @@
|
|||||||
using PARR.Domain.Entities.Base.History;
|
using PARR.Domain.Entities.Base.History;
|
||||||
using PARR.Domain.Enums;
|
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>
|
/// <summary>
|
||||||
/// Id регламентной работы
|
/// Id регламентной работы
|
||||||
/// </summary>
|
/// </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>
|
||||||
/// Актировать шаблон при инициализации
|
/// Актировать шаблон при инициализации
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsActiveTemplate { get; set; }
|
public bool IsActiveTemplate { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Актировать расписание при инициализации
|
/// Актировать расписание при инициализации
|
||||||
/// </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>
|
/// <summary>
|
||||||
/// Это новый шаблон? (true - перемещаем существующий в новый Job) (false - шаблон остается в своем Job)
|
/// Это новый шаблон? (true - перемещаем существующий в новый Job) (false - шаблон остается в своем Job)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsNew { get; set; }
|
public bool IsNew { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id Юнита(единицы обслуживания)/ЭК
|
/// Id Юнита(единицы обслуживания)/ЭК
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid UnitId { get; set; }
|
public Guid UnitId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Индекс, используется в групповых шаблонах
|
/// Индекс, используется в групповых шаблонах
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Index { get; set; }
|
public int? Index { get; init; }
|
||||||
|
|
||||||
public TemplateStatusTypeEnum StatusTypeId { get; set; }
|
public TemplateStatusTypeEnum StatusTypeId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициатор запроса к генератору
|
/// Инициатор запроса к генератору
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required HistoryInitiator Initiator { get; set; }
|
public required HistoryInitiator Initiator { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Связанные ЭК для сгруппированного типа JobGroup
|
/// Связанные ЭК для сгруппированного типа JobGroup
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required List<Guid> UnitsInTemplate { get; set; }
|
public required List<UnitInTemplateMessage> UnitsInTemplate { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace PARR.Domain.Common.Rabbit.Messages
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Модель в MQ, генератора заданий на генерацию шаблонов
|
|
||||||
/// </summary>
|
|
||||||
public class TemplateTaskGeneratorMq
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id регламентной работы
|
|
||||||
/// </summary>
|
|
||||||
public Guid JobId { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ using PARR.Core.Repositories.Interfaces;
|
|||||||
using PARR.Core.Repositories.Interfaces.Job;
|
using PARR.Core.Repositories.Interfaces.Job;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
using PARR.Core.Services.Shortcodes;
|
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;
|
||||||
using PARR.Domain.Entities.Base.History;
|
using PARR.Domain.Entities.Base.History;
|
||||||
using PARR.Domain.Entities.Job;
|
using PARR.Domain.Entities.Job;
|
||||||
@@ -54,7 +54,7 @@ namespace PARR.TemplateGeneratorWorker
|
|||||||
{
|
{
|
||||||
logger.LogInformation($"Получили запрос: {msg}");
|
logger.LogInformation($"Получили запрос: {msg}");
|
||||||
|
|
||||||
var query = transformService.GetModelFromJson<TemplateGeneratorMq>(msg);
|
var query = transformService.GetModelFromJson<TemplateGeneratorMessage>(msg);
|
||||||
|
|
||||||
if (query == null)
|
if (query == null)
|
||||||
{
|
{
|
||||||
|
|||||||
10
PARR.TemplateMatcher/Models/GroupedTemplateGroup.cs
Normal file
10
PARR.TemplateMatcher/Models/GroupedTemplateGroup.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace PARR.TemplateMatcher.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Группа шаблонов для одного PotentialUnitId (связанного юнита).
|
||||||
|
/// </summary>
|
||||||
|
public record GroupedTemplateGroup(
|
||||||
|
Guid PotentialUnitId,
|
||||||
|
List<GroupedTemplateSubGroup> SubGroups
|
||||||
|
);
|
||||||
|
}
|
||||||
11
PARR.TemplateMatcher/Models/GroupedTemplateSubGroup.cs
Normal file
11
PARR.TemplateMatcher/Models/GroupedTemplateSubGroup.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PARR.TemplateMatcher.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Итоговая подгруппа юнитов, готовая к созданию/обновлению одного шаблона.
|
||||||
|
/// </summary>
|
||||||
|
public record GroupedTemplateSubGroup(
|
||||||
|
List<(Guid UnitId, Guid UnitFieldValueId)> Entries,
|
||||||
|
string InnerGroupName,
|
||||||
|
int GlobalIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ using PARR.Core.Repositories.Interfaces;
|
|||||||
using PARR.Core.Repositories.Interfaces.Job;
|
using PARR.Core.Repositories.Interfaces.Job;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
using PARR.Core.Services.MatchingStatusService;
|
using PARR.Core.Services.MatchingStatusService;
|
||||||
using PARR.Core.Services.Shortcodes;
|
|
||||||
using PARR.Core.Services.UnitFilterService;
|
using PARR.Core.Services.UnitFilterService;
|
||||||
using PARR.Domain.Cache.Models;
|
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;
|
||||||
using PARR.Domain.Entities.Base.History;
|
using PARR.Domain.Entities.Base.History;
|
||||||
using PARR.Domain.Entities.Job;
|
using PARR.Domain.Entities.Job;
|
||||||
|
using PARR.Domain.Entities.Unit;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
using PARR.TemplateMatcher.Services.Interfaces;
|
using PARR.TemplateMatcher.Services.Interfaces;
|
||||||
@@ -41,30 +41,28 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
|
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
|
||||||
private readonly IMatchingStatusService matchingStatusService;
|
private readonly IMatchingStatusService matchingStatusService;
|
||||||
private readonly SettingsFromDb settingsFromDb;
|
private readonly SettingsFromDb settingsFromDb;
|
||||||
//private readonly INextRunService nextRunService;
|
|
||||||
private readonly IOptions<TemplateSettings> templateSettings;
|
private readonly IOptions<TemplateSettings> templateSettings;
|
||||||
|
private readonly IUnitFieldRepository unitFieldService;
|
||||||
|
private readonly IUnitInValueRepository unitInValueService;
|
||||||
|
private readonly IUnitRepository unitRepository;
|
||||||
|
|
||||||
public SimpleTemplateSynchronizer(
|
public SimpleTemplateSynchronizer(
|
||||||
ILogger<SimpleTemplateSynchronizer> logger,
|
ILogger<SimpleTemplateSynchronizer> logger,
|
||||||
IUnitFilterService unitFilterService,
|
IUnitFilterService unitFilterService,
|
||||||
IUnitInUnitRepository unitInUnitService,
|
|
||||||
IUnitInValueRepository unitInValueService,
|
|
||||||
IUnitRepository unitService,
|
|
||||||
MqSettings mqSettings,
|
MqSettings mqSettings,
|
||||||
IRabbitService mqService,
|
IRabbitService mqService,
|
||||||
ITemplateRepository templateService,
|
ITemplateRepository templateService,
|
||||||
IJobRepository jobService,
|
IJobRepository jobService,
|
||||||
ITemplateReuser templateReuser,
|
ITemplateReuser templateReuser,
|
||||||
IShortcodesService shortcodesService,
|
|
||||||
IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupService,
|
|
||||||
IUnitFieldRepository unitFieldService,
|
|
||||||
ITemplateDeactivator templateDeactivator,
|
ITemplateDeactivator templateDeactivator,
|
||||||
ITemplateNameNormalizer templateNameNormalizer,
|
ITemplateNameNormalizer templateNameNormalizer,
|
||||||
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
||||||
IMatchingStatusService matchingStatusService,
|
IMatchingStatusService matchingStatusService,
|
||||||
SettingsFromDb settingsFromDb,
|
SettingsFromDb settingsFromDb,
|
||||||
//INextRunService nextRunService,
|
IOptions<TemplateSettings> templateSettings,
|
||||||
IOptions<TemplateSettings> templateSettings
|
IUnitFieldRepository unitFieldService,
|
||||||
|
IUnitInValueRepository unitInValueService,
|
||||||
|
IUnitRepository unitRepository
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
@@ -79,8 +77,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
||||||
this.matchingStatusService = matchingStatusService;
|
this.matchingStatusService = matchingStatusService;
|
||||||
this.settingsFromDb = settingsFromDb;
|
this.settingsFromDb = settingsFromDb;
|
||||||
//this.nextRunService = nextRunService;
|
|
||||||
this.templateSettings = templateSettings;
|
this.templateSettings = templateSettings;
|
||||||
|
this.unitFieldService = unitFieldService;
|
||||||
|
this.unitInValueService = unitInValueService;
|
||||||
|
this.unitRepository = unitRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||||
@@ -182,7 +182,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
.Include(t => t.UnitsInTemplate)
|
.Include(t => t.UnitsInTemplate)
|
||||||
.Include(t => t.Job)
|
.Include(t => t.Job)
|
||||||
.ThenInclude(t => t!.Group)
|
.ThenInclude(t => t!.Group)
|
||||||
.ThenInclude(t => t.GroupType)
|
.ThenInclude(t => t!.GroupType)
|
||||||
.Include(t => t.Job)
|
.Include(t => t.Job)
|
||||||
.ThenInclude(t => t!.Tnk)
|
.ThenInclude(t => t!.Tnk)
|
||||||
.Include(t => t.Unit)
|
.Include(t => t.Unit)
|
||||||
@@ -225,7 +225,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||||||
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true);
|
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true);
|
||||||
|
|
||||||
var updateRequest = new TemplateUpdaterMq
|
var updateRequest = new TemplateUpdaterMessage
|
||||||
{
|
{
|
||||||
TemplateId = reusableTemplate.Id,
|
TemplateId = reusableTemplate.Id,
|
||||||
JobId = jobId,
|
JobId = jobId,
|
||||||
@@ -237,7 +237,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
Initiator = initiator,
|
Initiator = initiator,
|
||||||
//NextRun = nextRun,
|
//NextRun = nextRun,
|
||||||
IsNew = true,
|
IsNew = true,
|
||||||
UnitsInTemplate = new List<Guid>() // для простого шаблона
|
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
|
||||||
};
|
};
|
||||||
|
|
||||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||||
@@ -259,9 +259,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
{
|
{
|
||||||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName);
|
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,
|
TemplateId = template.Id,
|
||||||
JobId = jobId,
|
JobId = jobId,
|
||||||
@@ -269,13 +268,11 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
Name = expectedName,
|
Name = expectedName,
|
||||||
IsActiveTemplate = template.IsActiveTemplate,
|
IsActiveTemplate = template.IsActiveTemplate,
|
||||||
IsActiveSchedule = template.IsActiveSchedule,
|
IsActiveSchedule = template.IsActiveSchedule,
|
||||||
//LastRun = template.LastRun,
|
|
||||||
//NextRun = nextRun,
|
|
||||||
IsNew = false,
|
IsNew = false,
|
||||||
Index = template.Index,
|
Index = template.Index,
|
||||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||||
Initiator = initiator,
|
Initiator = initiator,
|
||||||
UnitsInTemplate = new List<Guid>() // для простого шаблона
|
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
|
||||||
};
|
};
|
||||||
|
|
||||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||||
@@ -400,7 +397,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
|
|
||||||
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
||||||
|
|
||||||
var updateRequest = new TemplateUpdaterMq
|
var updateRequest = new TemplateUpdaterMessage
|
||||||
{
|
{
|
||||||
TemplateId = template.Id,
|
TemplateId = template.Id,
|
||||||
JobId = jobId,
|
JobId = jobId,
|
||||||
@@ -414,7 +411,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
Index = template.Index,
|
Index = template.Index,
|
||||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||||
Initiator = initiator,
|
Initiator = initiator,
|
||||||
UnitsInTemplate = new List<Guid>() // для простого шаблона
|
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
|
||||||
};
|
};
|
||||||
|
|
||||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||||
@@ -434,9 +431,9 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator)
|
private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator)
|
||||||
{
|
{
|
||||||
// === Проверка: уже запущена? ===
|
|
||||||
var existingStatus = await matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
|
var existingStatus = await matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
|
||||||
if (existingStatus.DetailsJobs?.Any() == true)
|
if (existingStatus.DetailsJobs?.Any() == true)
|
||||||
{
|
{
|
||||||
@@ -444,7 +441,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Устанавливаем статус "в процессе" ===
|
|
||||||
var initialStatus = new MatchingStatusItemDto
|
var initialStatus = new MatchingStatusItemDto
|
||||||
{
|
{
|
||||||
DateStart = DateTimeOffset.UtcNow,
|
DateStart = DateTimeOffset.UtcNow,
|
||||||
@@ -460,7 +456,33 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
|
|
||||||
try
|
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()
|
var unusedJob = await jobService.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(j => j.Group)
|
.Include(j => j.Group)
|
||||||
@@ -473,9 +495,13 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Находим все шаблоны со статусом Unused
|
|
||||||
var unusedTemplates = await templateService.Get()
|
var unusedTemplates = await templateService.Get()
|
||||||
.Include(t => t.Unit)
|
.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)
|
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -490,47 +516,96 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
await UpdateMatchingStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки");
|
await UpdateMatchingStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки");
|
||||||
|
|
||||||
int processed = 0;
|
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)
|
foreach (var template in unusedTemplates)
|
||||||
{
|
{
|
||||||
// Генерируем ожидаемое имя один раз
|
try
|
||||||
var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob);
|
|
||||||
|
|
||||||
// Извлекаем "базовые" имена
|
|
||||||
var currentBaseName = ExtractBaseName(template.Name);
|
|
||||||
var expectedBaseName = ExtractBaseName(expectedName);
|
|
||||||
|
|
||||||
// Проверяем, нужно ли обновление
|
|
||||||
bool needsUpdate = template.JobId != unusedJobId ||
|
|
||||||
currentBaseName != expectedBaseName;
|
|
||||||
|
|
||||||
if (!needsUpdate)
|
|
||||||
{
|
{
|
||||||
logger.LogDebug("Шаблон {TemplateId} уже имеет актуальное имя и JobId. Пропускаем.", template.Id);
|
if (template.Unit == null)
|
||||||
continue;
|
{
|
||||||
|
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}");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
|
||||||
|
|
||||||
var updateRequest = new TemplateUpdaterMq
|
|
||||||
{
|
{
|
||||||
TemplateId = template.Id,
|
logger.LogError(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}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена");
|
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
|
var tempJob = new Job
|
||||||
{
|
{
|
||||||
@@ -573,10 +668,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
Id = template.Id,
|
Id = template.Id,
|
||||||
Name = template.Name,
|
Name = template.Name,
|
||||||
JobId = unusedJob.Id,
|
JobId = unusedJob.Id,
|
||||||
UnitId = template.UnitId,
|
UnitId = unit.Id,
|
||||||
Index = null,
|
Index = null,
|
||||||
Job = tempJob,
|
Job = tempJob,
|
||||||
Unit = template.Unit,
|
Unit = unit,
|
||||||
UnitsInTemplate = new List<UnitsInTemplate>()
|
UnitsInTemplate = new List<UnitsInTemplate>()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -588,16 +683,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
{
|
{
|
||||||
logger.LogInformation("Создание нового простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
|
logger.LogInformation("Создание нового простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
|
||||||
|
|
||||||
var mqRequest = new TemplateGeneratorMq
|
var mqRequest = new TemplateGeneratorMessage
|
||||||
{
|
{
|
||||||
JobId = jobId,
|
JobId = jobId,
|
||||||
UnitId = unitId,
|
UnitId = unitId,
|
||||||
UnitsInTemplate = new List<Guid>(), // для простого шаблона
|
UnitsInTemplate = new List<UnitInTemplateMessage>(), // для простого шаблона
|
||||||
HistoryInitiator = initiator
|
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 });
|
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
|
||||||
|
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
@@ -620,29 +713,4 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
TimeSpan.FromMinutes(30)
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces.Job;
|
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;
|
||||||
using PARR.Domain.Entities.Base.History;
|
using PARR.Domain.Entities.Base.History;
|
||||||
using PARR.Domain.Entities.Job;
|
using PARR.Domain.Entities.Job;
|
||||||
@@ -100,7 +100,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
|||||||
|
|
||||||
var expectedName = await namenormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
var expectedName = await namenormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||||||
|
|
||||||
var updateRequest = new TemplateUpdaterMq
|
var updateRequest = new TemplateUpdaterMessage
|
||||||
{
|
{
|
||||||
TemplateId = template.Id,
|
TemplateId = template.Id,
|
||||||
JobId = unusedJob.Id,
|
JobId = unusedJob.Id,
|
||||||
@@ -108,13 +108,11 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
|||||||
Name = expectedName,
|
Name = expectedName,
|
||||||
IsActiveTemplate = DefaultUnusedTemplateState,
|
IsActiveTemplate = DefaultUnusedTemplateState,
|
||||||
IsActiveSchedule = DefaultUnusedScheduleState,
|
IsActiveSchedule = DefaultUnusedScheduleState,
|
||||||
//LastRun = template.LastRun,
|
|
||||||
//NextRun = template.NextRun,
|
|
||||||
IsNew = false,
|
IsNew = false,
|
||||||
Index = template.Index,
|
Index = template.Index,
|
||||||
StatusTypeId = TemplateStatusTypeEnum.Unused,
|
StatusTypeId = TemplateStatusTypeEnum.Unused,
|
||||||
Initiator = initiator,
|
Initiator = initiator,
|
||||||
UnitsInTemplate = new List<Guid>()
|
UnitsInTemplate = new List<UnitInTemplateMessage>()
|
||||||
};
|
};
|
||||||
|
|
||||||
await sender.SendTemplateUpdateMessageAsync(updateRequest);
|
await sender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||||
@@ -14,6 +14,7 @@ internal class TemplateNameNormalizer : ITemplateNameNormalizer
|
|||||||
this.shortcodesService = shortcodesService;
|
this.shortcodesService = shortcodesService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<string> GetNormalizedTemplateNameAsync(Template template, [CallerMemberName] string? caller = null)
|
public async Task<string> GetNormalizedTemplateNameAsync(Template template, [CallerMemberName] string? caller = null)
|
||||||
{
|
{
|
||||||
var callerName = caller ?? "Unknown";
|
var callerName = caller ?? "Unknown";
|
||||||
@@ -20,6 +20,7 @@ internal class TemplateReuser : ITemplateReuser
|
|||||||
this.templateService = templateService;
|
this.templateService = templateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<Template?> TryReuseOneUnusedTemplateAsync(
|
public async Task<Template?> TryReuseOneUnusedTemplateAsync(
|
||||||
Guid jobId,
|
Guid jobId,
|
||||||
Guid unitId,
|
Guid unitId,
|
||||||
@@ -42,12 +43,12 @@ internal class TemplateReuser : ITemplateReuser
|
|||||||
// Загружаем зарезервированный шаблон
|
// Загружаем зарезервированный шаблон
|
||||||
var template = await templateService.Get()
|
var template = await templateService.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(t=>t.Unit)
|
.Include(t => t.Unit)
|
||||||
.Include(t=>t.Job)
|
.Include(t => t.Job)
|
||||||
.ThenInclude(t=>t!.Tnk)
|
.ThenInclude(t => t!.Tnk)
|
||||||
.Include(t => t.Job)
|
.Include(t => t.Job)
|
||||||
.ThenInclude(t => t!.Group)
|
.ThenInclude(t => t!.Group)
|
||||||
.ThenInclude(t=>t!.GroupType)
|
.ThenInclude(t => t!.GroupType)
|
||||||
.FirstOrDefaultAsync(t => t.Id == templateId);
|
.FirstOrDefaultAsync(t => t.Id == templateId);
|
||||||
|
|
||||||
if (template == null)
|
if (template == null)
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using PARR.Core.Common.Interfaces.RabbitServices;
|
using PARR.Core.Common.Interfaces.RabbitServices;
|
||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||||
using PARR.Domain.Common.Rabbit.Messages;
|
|
||||||
using PARR.TemplateMatcher.Services.Interfaces;
|
using PARR.TemplateMatcher.Services.Interfaces;
|
||||||
using PARR.TemplateMatcher.Settings;
|
using PARR.TemplateMatcher.Settings;
|
||||||
|
|
||||||
@@ -13,35 +11,23 @@ internal class TemplateUpdaterMqSender : ITemplateUpdaterMqSender
|
|||||||
private readonly ILogger<TemplateUpdaterMqSender> logger;
|
private readonly ILogger<TemplateUpdaterMqSender> logger;
|
||||||
private readonly IRabbitService mqService;
|
private readonly IRabbitService mqService;
|
||||||
private readonly MqSettings mqSettings;
|
private readonly MqSettings mqSettings;
|
||||||
private readonly ITemplateRepository templateService;
|
|
||||||
|
|
||||||
public TemplateUpdaterMqSender(
|
public TemplateUpdaterMqSender(
|
||||||
ILogger<TemplateUpdaterMqSender> logger,
|
ILogger<TemplateUpdaterMqSender> logger,
|
||||||
IRabbitService mqService,
|
IRabbitService mqService,
|
||||||
MqSettings mqSettings,
|
MqSettings mqSettings
|
||||||
ITemplateRepository templateService
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.mqService = mqService;
|
this.mqService = mqService;
|
||||||
this.mqSettings = mqSettings;
|
this.mqSettings = mqSettings;
|
||||||
this.templateService = templateService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendTemplateUpdateMessageAsync(TemplateUpdaterMq updateRequest)
|
public async Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
|
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
|
||||||
mqSettings.TemplateUpdater.QueueName, updateRequest.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 });
|
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new List<object> { updateRequest });
|
||||||
|
|
||||||
if (result.IsSuccess)
|
if (result.IsSuccess)
|
||||||
@@ -11,6 +11,7 @@ internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
|
|||||||
private readonly IUnitRepository unitRepository;
|
private readonly IUnitRepository unitRepository;
|
||||||
private readonly IUnitKiiUnitRepository unitKiiUnitRepository;
|
private readonly IUnitKiiUnitRepository unitKiiUnitRepository;
|
||||||
|
|
||||||
|
|
||||||
public UnitInTemplateConflictMapper(
|
public UnitInTemplateConflictMapper(
|
||||||
ILogger<UnitInTemplateConflictMapper> logger,
|
ILogger<UnitInTemplateConflictMapper> logger,
|
||||||
IUnitRepository unitRepository,
|
IUnitRepository unitRepository,
|
||||||
@@ -21,6 +22,7 @@ internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
|
|||||||
this.unitKiiUnitRepository = unitKiiUnitRepository;
|
this.unitKiiUnitRepository = unitKiiUnitRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
public async Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
||||||
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
||||||
Job maxJob,
|
Job maxJob,
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using PARR.Domain.Common.Rabbit.Messages;
|
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||||
|
|
||||||
namespace PARR.TemplateMatcher.Services.Interfaces
|
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||||
{
|
{
|
||||||
public interface ITemplateUpdaterMqSender
|
public interface ITemplateUpdaterMqSender
|
||||||
{
|
{
|
||||||
Task SendTemplateUpdateMessageAsync(TemplateUpdaterMq updateRequest);
|
Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ namespace PARR.TemplateMatcher
|
|||||||
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
|
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
|
||||||
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
|
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
|
||||||
services.AddTransient<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
|
services.AddTransient<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
|
||||||
|
services.AddTransient<IGroupedTemplateUnitFilter, GroupedTemplateUnitFilter>();
|
||||||
|
services.AddTransient<IGroupedTemplateBuilder, GroupedTemplateBuilder>();
|
||||||
|
services.AddTransient<IGroupedTemplateProcessor, GroupedTemplateProcessor>();
|
||||||
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
||||||
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using PARR.Domain.Common.Rabbit.Messages;
|
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||||
|
|
||||||
namespace PARR.TemplateUpdater.Services
|
namespace PARR.TemplateUpdater.Services
|
||||||
{
|
{
|
||||||
internal interface ITemplateUpdaterService
|
internal interface ITemplateUpdaterService
|
||||||
{
|
{
|
||||||
Task UpdateTemplateAsync(TemplateUpdaterMq query);
|
Task UpdateTemplateAsync(TemplateUpdaterMessage query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ using PARR.Core.Repositories.Interfaces;
|
|||||||
using PARR.Core.Repositories.Interfaces.Job;
|
using PARR.Core.Repositories.Interfaces.Job;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
using PARR.Domain.Common.Rabbit.Messages;
|
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||||
using PARR.Domain.Entities;
|
|
||||||
using PARR.Domain.Entities.Job;
|
using PARR.Domain.Entities.Job;
|
||||||
using PARR.Domain.Enums;
|
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);
|
var isValid = await IsValidAsync(query);
|
||||||
if (!isValid)
|
if (!isValid)
|
||||||
@@ -95,52 +94,55 @@ namespace PARR.TemplateUpdater.Services
|
|||||||
scheduleIsChanged = true;
|
scheduleIsChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
//просто обнволяем. не влияет ни на шаблон, ни на расписание
|
|
||||||
//template.LastRun = query.LastRun;
|
|
||||||
template.Index = query.Index;
|
template.Index = query.Index;
|
||||||
template.StatusTypeId = query.StatusTypeId;
|
template.StatusTypeId = query.StatusTypeId;
|
||||||
|
|
||||||
// === Обработка изменения состава UnitsInTemplate ===
|
// === Обработка изменения состава UnitsInTemplate ===
|
||||||
var currentUnitIds = template.UnitsInTemplate.Select(u => u.UnitId).ToHashSet();
|
// Формируем HashSet пар (UnitId, UnitFieldValueId) для быстрого сравнения
|
||||||
var newUnitIds = query.UnitsInTemplate.ToHashSet();
|
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
|
var toRemove = template.UnitsInTemplate
|
||||||
.Where(u => !newUnitIds.Contains(u.UnitId))
|
.Where(u => !newEntries.Contains(new UnitInTemplateMessage { UnitId = u.UnitId, UnitFieldValueId = u.UnitFieldValueId }))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var item in toRemove)
|
foreach (var item in toRemove)
|
||||||
template.UnitsInTemplate.Remove(item);
|
template.UnitsInTemplate.Remove(item);
|
||||||
|
|
||||||
// Добавляем новые связи
|
// Добавляем новые связи
|
||||||
var toAdd = newUnitIds.Except(currentUnitIds);
|
var toAdd = newEntries.Except(currentEntries);
|
||||||
foreach (var unitId in toAdd)
|
foreach (var entry in toAdd)
|
||||||
{
|
{
|
||||||
template.UnitsInTemplate.Add(new UnitsInTemplate
|
template.UnitsInTemplate.Add(new UnitsInTemplate
|
||||||
{
|
{
|
||||||
TemplateId = template.Id,
|
TemplateId = template.Id,
|
||||||
UnitId = unitId,
|
UnitId = entry.UnitId,
|
||||||
|
UnitFieldValueId = entry.UnitFieldValueId,
|
||||||
DateCreated = DateTimeOffset.UtcNow
|
DateCreated = DateTimeOffset.UtcNow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Поскольку коллекция изменилась — шаблон считается изменённым
|
|
||||||
templateIsChanged = true;
|
templateIsChanged = true;
|
||||||
}
|
}
|
||||||
|
// === Конец обработки UnitsInTemplate ===
|
||||||
|
|
||||||
//Для правильного расчета shortcod'ов в БД должны быть сохранены данные UnitInTemplates, JobId, UnitId и т.д.
|
// Для правильного расчета shortcod'ов в БД должны быть сохранены данные UnitsInTemplate, JobId, UnitId и т.д.
|
||||||
if (templateIsChanged && !await templateService.CommitAsync(query.Initiator))
|
if (templateIsChanged && !await templateService.CommitAsync(query.Initiator))
|
||||||
{
|
{
|
||||||
logger.LogError("Не удалось сохранить изменения в БД. {QueryType}: {Query}", nameof(TemplateUpdaterMq), query.ToJson());
|
logger.LogError("Не удалось сохранить изменения в БД. {QueryType}: {Query}", nameof(TemplateUpdaterMessage), query.ToJson());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, query.IsNew);
|
var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, query.IsNew);
|
||||||
if (!nextRun.HasValue)
|
if (!nextRun.HasValue)
|
||||||
{
|
{
|
||||||
logger.LogError("Ошибка при расчете нового nextRun (вернулся null) для шаблона {templateId}, {templateName}", template.Id, template.Name);
|
logger.LogError("Ошибка при расчете нового nextRun (вернулся null) для шаблона {TemplateId}, {TemplateName}", template.Id, template.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,32 +153,23 @@ namespace PARR.TemplateUpdater.Services
|
|||||||
scheduleIsChanged = true;
|
scheduleIsChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === конец обработки UnitsInTemplate ===
|
|
||||||
|
|
||||||
if (templateIsChanged)
|
if (templateIsChanged)
|
||||||
{
|
{
|
||||||
// ставим задачу на обновление шаблона
|
|
||||||
var config = robotConfigurationService.GetFromTemplateByRobotCode(RobotsEnum.TemplateOrder, template);
|
var config = robotConfigurationService.GetFromTemplateByRobotCode(RobotsEnum.TemplateOrder, template);
|
||||||
//robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Updating, config);
|
|
||||||
robotConfigurationService.SetUpdateTaskStatusIfAllow(config);
|
robotConfigurationService.SetUpdateTaskStatusIfAllow(config);
|
||||||
|
|
||||||
logger.LogDebug("Для шаблона {TemplateId} устанавливаю статус {Status}", template.Id, RobotStatusEnum.Wait.ToString());
|
logger.LogDebug("Для шаблона {TemplateId} устанавливаю статус {Status}", template.Id, RobotStatusEnum.Wait.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scheduleIsChanged)
|
if (scheduleIsChanged)
|
||||||
{
|
{
|
||||||
// ставим задачу на обновление расписания
|
|
||||||
var config = robotConfigurationService.GetFromTemplateByRobotCode(RobotsEnum.ScheduleOrder, template);
|
var config = robotConfigurationService.GetFromTemplateByRobotCode(RobotsEnum.ScheduleOrder, template);
|
||||||
//robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Updating, config);
|
|
||||||
robotConfigurationService.SetUpdateTaskStatusIfAllow(config);
|
robotConfigurationService.SetUpdateTaskStatusIfAllow(config);
|
||||||
|
|
||||||
logger.LogDebug("Для расписания {TemplateId} устанавливаю статус {Status}", template.Id, RobotStatusEnum.Wait.ToString());
|
logger.LogDebug("Для расписания {TemplateId} устанавливаю статус {Status}", template.Id, RobotStatusEnum.Wait.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!await templateService.CommitAsync(query.Initiator))
|
if (!await templateService.CommitAsync(query.Initiator))
|
||||||
{
|
{
|
||||||
logger.LogError("Не удалось сохранить изменения в БД. {QueryType}: {Query}", nameof(TemplateUpdaterMq), query.ToJson());
|
logger.LogError("Не удалось сохранить изменения в БД. {QueryType}: {Query}", nameof(TemplateUpdaterMessage), query.ToJson());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +182,7 @@ namespace PARR.TemplateUpdater.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query"></param>
|
/// <param name="query"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task<bool> IsValidAsync(TemplateUpdaterMq query)
|
private async Task<bool> IsValidAsync(TemplateUpdaterMessage query)
|
||||||
{
|
{
|
||||||
var template = await templateService.Get().AsNoTracking()
|
var template = await templateService.Get().AsNoTracking()
|
||||||
.Include(t => t.RobotConfigurations)
|
.Include(t => t.RobotConfigurations)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PARR.Core.Common.Interfaces;
|
using PARR.Core.Common.Interfaces;
|
||||||
using PARR.Core.Common.Interfaces.RabbitServices;
|
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.Services;
|
||||||
using PARR.TemplateUpdater.Settings;
|
using PARR.TemplateUpdater.Settings;
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ namespace PARR.TemplateUpdater
|
|||||||
{
|
{
|
||||||
logger.LogInformation("Получили запрос: {msg}", msg);
|
logger.LogInformation("Получили запрос: {msg}", msg);
|
||||||
|
|
||||||
var query = transformService.GetModelFromJson<TemplateUpdaterMq>(msg);
|
var query = transformService.GetModelFromJson<TemplateUpdaterMessage>(msg);
|
||||||
if (query == null)
|
if (query == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user