feat(templateMatcher): Убрана избыточная проверка связанных юнитов, теперь это выполняется в UnitFilter. Добавлена группировка по рабочей группе или ответственному за ЭК

This commit is contained in:
Mikhail Kuznetsov
2026-03-26 18:08:13 +10:00
parent 4864b9b343
commit 73d7f4b200
7 changed files with 368 additions and 429 deletions

View File

@@ -738,7 +738,8 @@ namespace PARR.DAL.DomainServices.Shortcodes
return NoContent; return NoContent;
} }
var cacheKey = $"gr_shcd_{jobGroupId:N}_{unitId:N}_{ComputeHash(fullShortcode)}"; //var cacheKey = $"gr_shcd_{jobGroupId:N}_{unitId:N}_{ComputeHash(fullShortcode)}";
var cacheKey = cacheService.GetKey(new[] { "grouped", "shortcode", $"{jobGroupId:N}", $"{unitId:N}" }, new[] { fullShortcode });
var cachedData = await cacheService.GetCachedDataAsync<CachedGroupedShortCode>(cacheKey).ConfigureAwait(false); var cachedData = await cacheService.GetCachedDataAsync<CachedGroupedShortCode>(cacheKey).ConfigureAwait(false);
if (cachedData != null) if (cachedData != null)
@@ -872,12 +873,5 @@ namespace PARR.DAL.DomainServices.Shortcodes
} }
return result.ToString(); return result.ToString();
} }
private static string ComputeHash(string input)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(hashedBytes).Replace('+', '-').Replace('/', '_').Substring(0, 16);
}
} }
} }

View File

@@ -566,6 +566,7 @@ internal class UnitFilterService : IUnitFilterService
return result; return result;
} }
/// <summary> /// <summary>
/// Формируем итоговый результат /// Формируем итоговый результат
/// </summary> /// </summary>
@@ -781,6 +782,7 @@ internal class UnitFilterService : IUnitFilterService
return result.ToList(); return result.ToList();
} }
#region вспомогательные методы #region вспомогательные методы
/// <summary> /// <summary>
/// Применяет фильтры к связям (родителям или детям) /// Применяет фильтры к связям (родителям или детям)
@@ -959,7 +961,6 @@ internal class UnitFilterService : IUnitFilterService
} }
private async Task<List<Guid>> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter, CancellationToken cancellationToken = default) private async Task<List<Guid>> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter, CancellationToken cancellationToken = default)
{ {
// var cacheKey = cacheService.GetKey(new[] { "uf_ids", filter.UnitFilter }, isUseHash: true); // var cacheKey = cacheService.GetKey(new[] { "uf_ids", filter.UnitFilter }, isUseHash: true);
@@ -991,7 +992,6 @@ internal class UnitFilterService : IUnitFilterService
} }
/// <summary> /// <summary>
/// Обрабатывает маску LIKE для корректной работы с SQL /// Обрабатывает маску LIKE для корректной работы с SQL
/// </summary> /// </summary>

View File

@@ -109,14 +109,16 @@ namespace PARR.DAL.Services.Implementations
} }
public async Task<Guid?> ReserveUnusedTemplateAsync(Guid newUnitId, HistoryInitiator initiator) public async Task<Guid?> ReserveUnusedTemplateAsync(Guid newUnitId, Guid newJobId, HistoryInitiator initiator)
{ {
logger.LogDebug("Резервирую неиспользуемый шаблон с проверкой конфигураций роботов для UnitId: {UnitId}", newUnitId); logger.LogDebug("Резервирую неиспользуемый шаблон с проверкой конфигураций роботов для UnitId: {UnitId}, JobId: {JobId}", newUnitId, newJobId);
var sql = @" var sql = @"
UPDATE ""Templates"" UPDATE ""Templates""
SET ""StatusTypeId"" = @NewStatus, SET ""StatusTypeId"" = @NewStatus,
""DateModified"" = @DateModified, ""DateModified"" = @DateModified,
""UnitId"" = @NewUnitId,
""JobId"" = @NewJobId,
""InitiatorIp"" = @InitiatorIp, ""InitiatorIp"" = @InitiatorIp,
""InitiatorParrComponentId"" = @InitiatorComponent, ""InitiatorParrComponentId"" = @InitiatorComponent,
""InitiatorComment"" = @InitiatorComment ""InitiatorComment"" = @InitiatorComment
@@ -150,6 +152,8 @@ namespace PARR.DAL.Services.Implementations
{ {
new NpgsqlParameter("@NewStatus", (int)TemplateStatusTypeEnum.Updating), new NpgsqlParameter("@NewStatus", (int)TemplateStatusTypeEnum.Updating),
new NpgsqlParameter("@DateModified", DateTimeOffset.UtcNow), new NpgsqlParameter("@DateModified", DateTimeOffset.UtcNow),
new NpgsqlParameter("@NewUnitId", newUnitId),
new NpgsqlParameter("@NewJobId", newJobId),
new NpgsqlParameter("@InitiatorIp", initiator.InitiatorIp ?? (object)DBNull.Value), new NpgsqlParameter("@InitiatorIp", initiator.InitiatorIp ?? (object)DBNull.Value),
new NpgsqlParameter("@InitiatorComponent", new NpgsqlParameter("@InitiatorComponent",
initiator.InitiatorParrComponentId.HasValue initiator.InitiatorParrComponentId.HasValue
@@ -157,12 +161,11 @@ namespace PARR.DAL.Services.Implementations
: DBNull.Value), : DBNull.Value),
new NpgsqlParameter("@InitiatorComment", initiator.InitiatorComment ?? (object)DBNull.Value), new NpgsqlParameter("@InitiatorComment", initiator.InitiatorComment ?? (object)DBNull.Value),
new NpgsqlParameter("@OldStatus", (int)TemplateStatusTypeEnum.Unused), new NpgsqlParameter("@OldStatus", (int)TemplateStatusTypeEnum.Unused),
new NpgsqlParameter("@NewUnitId", newUnitId),
// Параметры для проверки конфигураций роботов // Параметры для проверки конфигураций роботов
new NpgsqlParameter("@RobotCode1", 1), new NpgsqlParameter("@RobotCode1", (int)RobotsEnum.TemplateOrder),
new NpgsqlParameter("@RobotCode2", 2), new NpgsqlParameter("@RobotCode2", (int)RobotsEnum.ScheduleOrder),
new NpgsqlParameter("@TaskStatus", 30), new NpgsqlParameter("@TaskStatus", (int)TaskStatusEnum.Ok),
new NpgsqlParameter("@RobotStatus", 44) new NpgsqlParameter("@RobotStatus", (int)RobotStatusEnum.Complete)
}; };
try try
@@ -175,19 +178,19 @@ namespace PARR.DAL.Services.Implementations
if (reservedTemplateId != default(Guid)) if (reservedTemplateId != default(Guid))
{ {
logger.LogInformation("Успешно зарезервирован шаблон с ID: {TemplateId} для UnitId: {UnitId}", logger.LogInformation("Успешно зарезервирован шаблон с ID: {TemplateId} для UnitId: {UnitId}, JobId: {JobId}",
reservedTemplateId, newUnitId); reservedTemplateId, newUnitId, newJobId);
} }
else else
{ {
logger.LogDebug("Не удалось зарезервировать шаблон для UnitId: {UnitId} (не найдено подходящих конфигураций роботов)", newUnitId); logger.LogDebug("Не удалось зарезервировать шаблон для UnitId: {UnitId}, JobId: {JobId} (не найдено подходящих конфигураций роботов)", newUnitId, newJobId);
} }
return reservedTemplateId; return reservedTemplateId;
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Ошибка при резервировании шаблона для UnitId: {UnitId}", newUnitId); logger.LogError(ex, "Ошибка при резервировании шаблона для UnitId: {UnitId}, JobId: {JobId}", newUnitId, newJobId);
throw; throw;
} }
} }

View File

@@ -8,7 +8,18 @@ namespace PARR.DAL.Services.Interfaces
{ {
Task<Template?> GetTemplateByNameAsync(string name); Task<Template?> GetTemplateByNameAsync(string name);
Task<Guid?> ReserveUnusedTemplateAsync(Guid newUnitId, HistoryInitiator initiator); /// <summary>
/// Резервирует неиспользуемый шаблон для указанного ЭК и работы.
/// Для корректного расчета NextRun назначает захваченному шаблону ЭК и работу
/// </summary>
/// <param name="newUnitId">Id ЭК.</param>
/// <param name="newJobId">Id работы.</param>
/// <param name="initiator">Данные инициатора операции.</param>
/// <returns>
/// Id зарезервированного шаблона или <see langword="null"/>,
/// если подходящий шаблон не найден.
/// </returns>
Task<Guid?> ReserveUnusedTemplateAsync(Guid newUnitId, Guid newJobId, HistoryInitiator initiator);
IQueryable<Template> GetWithIncludes(); IQueryable<Template> GetWithIncludes();
} }

View File

@@ -5,13 +5,11 @@ using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain; using PARR.Common.Domain;
using PARR.Constants; using PARR.Constants;
using PARR.DAL.Cache.Models; using PARR.DAL.Cache.Models;
using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes;
using PARR.DAL.DomainServices.UnitFilterService; using PARR.DAL.DomainServices.UnitFilterService;
using PARR.DAL.Models; using PARR.DAL.Models;
using PARR.DAL.Models.Job; using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit;
using PARR.DAL.NextRunServices;
using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.Services.Interfaces.Unit;
@@ -28,7 +26,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
private readonly ILogger<GroupedTemplateSynchronizer> logger; private readonly ILogger<GroupedTemplateSynchronizer> logger;
private readonly IUnitFilterService unitFilterService; private readonly IUnitFilterService unitFilterService;
private readonly IUnitInUnitService unitInUnitService;
private readonly IUnitInValueService unitInValueService; private readonly IUnitInValueService unitInValueService;
private readonly IUnitService unitService; private readonly IUnitService unitService;
private readonly MqSettings mqSettings; private readonly MqSettings mqSettings;
@@ -42,12 +39,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly IMatchingStatusService matchingStatusService; private readonly IMatchingStatusService matchingStatusService;
private readonly IShortcodesService shortcodesService;
//private readonly INextRunService nextRunService; //private readonly INextRunService nextRunService;
public GroupedTemplateSynchronizer( public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger, ILogger<GroupedTemplateSynchronizer> logger,
IUnitFilterService unitFilterService, IUnitFilterService unitFilterService,
IUnitInUnitService unitInUnitService,
IUnitInValueService unitInValueService, IUnitInValueService unitInValueService,
IUnitService unitService, IUnitService unitService,
MqSettings mqSettings, MqSettings mqSettings,
@@ -60,13 +58,13 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
ITemplateDeactivator templateDeactivator, ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer, ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender, ITemplateUpdaterMqSender templateUpdaterMqSender,
IMatchingStatusService matchingStatusService IMatchingStatusService matchingStatusService,
IShortcodesService shortcodesService
//INextRunService nextRunService //INextRunService nextRunService
) )
{ {
this.logger = logger; this.logger = logger;
this.unitFilterService = unitFilterService; this.unitFilterService = unitFilterService;
this.unitInUnitService = unitInUnitService;
this.unitInValueService = unitInValueService; this.unitInValueService = unitInValueService;
this.unitService = unitService; this.unitService = unitService;
this.mqSettings = mqSettings; this.mqSettings = mqSettings;
@@ -80,6 +78,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
this.templateNameNormalizer = templateNameNormalizer; this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender; this.templateUpdaterMqSender = templateUpdaterMqSender;
this.matchingStatusService = matchingStatusService; this.matchingStatusService = matchingStatusService;
this.shortcodesService = shortcodesService;
//this.nextRunService = nextRunService; //this.nextRunService = nextRunService;
} }
@@ -87,7 +86,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{ {
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
} }
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{ {
logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); logger.LogDebug("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
@@ -119,12 +117,14 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
// 1. Получить JobGroup и связанные Job'ы // 1. Получить JobGroup и связанные Job'ы
var jobGroup = await jobGroupService.Get() var jobGroup = await jobGroupService.Get()
.AsNoTracking() .AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType) .Include(jg => jg.GroupType)
.Include(jg => jg.Jobs) .Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl) .ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs) .Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters) .ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters) .ThenInclude(uf => uf.RelationshipFilters)
.ThenInclude(rf => rf.UnitField) // Подгрузим поля для фильтрации
.Include(jg => jg.Jobs) .Include(jg => jg.Jobs)
.ThenInclude(jg => jg.Tnk) .ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId); .FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
@@ -138,20 +138,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
var jobsInGroup = jobGroup.Jobs.ToList(); var jobsInGroup = jobGroup.Jobs.ToList();
// --- Получение FieldId и разрешённых значений для "РАБОЧАЯ_ГР_ОТВ_ЗАК" ---
var workGroupField = await unitFieldService.GetByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
if (workGroupField == null)
{
logger.LogError("Поле 'РАБОЧАЯ_ГР_ОТВ_ЗАК' не найдено в справочнике полей. Синхронизация прервана.");
await UpdateMatchingStatusAsync(jobGroupId, "Ошибка: поле 'РАБОЧАЯ_ГР_ОТВ_ЗАК' не найдено");
return;
}
var workGroupFieldId = workGroupField.Id;
var relationshipGroupValueIds = regionalEkPtkGroupService.Get()
.Select(g => g.FieldValueId)
.ToList();
logger.LogDebug("Найдено {Count} значений из UnitRegionalEkPtkGroup для проверки поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК'.", relationshipGroupValueIds.Count);
// 2. Найти Job с максимальным MaxValueRelationships // 2. Найти Job с максимальным MaxValueRelationships
var maxJob = jobsInGroup var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue) .Where(j => j.MaxValueRelationships.HasValue)
@@ -165,26 +151,25 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
return; return;
} }
if (maxJob.UnitFilters == null)
{
logger.LogWarning("Job {JobId} не содержит UnitFilters.", maxJob.Id);
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships); logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}) для фильтрации.", maxJob.Id, maxJob.MaxValueRelationships);
// 3. Использовать фильтры maxJob для получения отфильтрованных юнитов // 3. Использовать unitFilterService для получения отфильтрованных юнитов с их связями
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id); // Это включает в себя все фильтры: UnitFilter, FieldFilter, RelationshipFilter, UmbrellaFilter
if (filteredUnits == null || !filteredUnits.Any()) 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); logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов"); await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
return; return;
} }
// Извлекаем ID юнитов для последующих операций logger.LogDebug("Получено {Count} юнитов с подходящими связями через UnitFilterService.", unitFilterResults.Count());
var expectedUnitIds = filteredUnits.Select(u => u.Id).ToList();
// 4. Отфильтровать expectedUnitIds по GroupingUnitFieldId // --- ФИЛЬТРАЦИЯ unitFilterResults (dto.Id) ---
// 4. Фильтрация unitFilterResults по GroupingUnitFieldId (проверяем dto.Id)
if (!jobGroup.GroupingUnitFieldId.HasValue) if (!jobGroup.GroupingUnitFieldId.HasValue)
{ {
logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId); logger.LogError("JobGroup {JobGroupId} не имеет GroupingUnitFieldId, необходимого для группировки.", jobGroupId);
@@ -193,249 +178,166 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
} }
var groupingFieldId = jobGroup.GroupingUnitFieldId.Value; var groupingFieldId = jobGroup.GroupingUnitFieldId.Value;
logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по GroupingUnitFieldId (FieldId={FieldId}).", groupingFieldId);
// --- ОПТИМИЗАЦИЯ: Загрузка значений поля GroupingUnitFieldId отдельно --- // Загрузим значения поля GroupingUnitFieldId для всех Id из unitFilterResults
logger.LogDebug("Загружаем значения поля GroupingUnitFieldId (FieldId={FieldId}) для {Count} юнитов.", groupingFieldId, expectedUnitIds.Count); var allUnitFilterResultIds = unitFilterResults.Select(dto => dto.Id).ToList();
var groupingUnitValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitFilterResultIds, new HashSet<Guid> { groupingFieldId });
var groupingUnitValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(expectedUnitIds, new HashSet<Guid> { groupingFieldId }); // Найдем Id юнитов, у которых есть значение в GroupingUnitFieldId
var validUnitFilterResultIds = groupingUnitValues
var unitIdsWithValidGroupingFieldSet = groupingUnitValues
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value)) .Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
.Select(uv => uv.UnitId) .Select(uv => uv.UnitId)
.ToHashSet();
// Оставляем только те UnitFilterResultDto, чей Id проходит фильтр
var filteredUnitFilterResultsByGrouping = unitFilterResults
.Where(dto => validUnitFilterResultIds.Contains(dto.Id))
.ToList(); .ToList();
logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} юнитов.", unitIdsWithValidGroupingFieldSet.Count); logger.LogDebug("После фильтрации по GroupingUnitFieldId осталось {Count} UnitFilterResultDto.", filteredUnitFilterResultsByGrouping.Count);
if (!unitIdsWithValidGroupingFieldSet.Any()) if (!filteredUnitFilterResultsByGrouping.Any())
{ {
logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); logger.LogInformation("После фильтрации по GroupingUnitFieldId в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId"); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по GroupingUnitFieldId");
return; return;
} }
// --- Аналогично для фильтрации по "РАБОЧАЯ_ГР_ОТВ_ЗАК" --- // 5. Фильтрация unitFilterResults по РАБОЧАЯ_ГР_ОТВ_ЗАК (проверяем dto.Id)
// var workGroupFieldId = workGroupField.Id; // УЖЕ ОПРЕДЕЛЕНО РАНЕЕ (строка 157) var workGroupFieldId = await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
// var relationshipGroupValueIds = regionalEkPtkGroupService.Get() // УДАЛИТЬ - УЖЕ ОПРЕДЕЛЕНО (строка 161)
logger.LogDebug("Загружаем значения поля 'РАБОЧАЯ_ГР_ОТВ_ЗАК' (FieldId={FieldId}) для {Count} юнитов.", workGroupFieldId, unitIdsWithValidGroupingFieldSet.Count); logger.LogDebug("Фильтрация юнитов (UnitFilterResultDto.Id) по полю 'РАБОЧАЯ_ГР_ОТВ_ЗАК' (FieldId={FieldId}).", workGroupFieldId);
var workGroupValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(unitIdsWithValidGroupingFieldSet, new HashSet<Guid> { workGroupFieldId }); // Загрузим значения поля РАБОЧАЯ_ГР_ОТВ_ЗАК для Id из filteredUnitFilterResultsByGrouping
var allFilteredUnitFilterResultIds = filteredUnitFilterResultsByGrouping.Select(dto => dto.Id).ToList();
var workGroupValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allFilteredUnitFilterResultIds, new HashSet<Guid> { workGroupFieldId });
var unitIdsWithValidWorkGroupFieldSet = workGroupValues // Получим разрешенные значения из regionalEkPtkGroupService
.Where(uv => uv.Value != null && relationshipGroupValueIds.Contains(uv.Value.Id)) 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) .Select(uv => uv.UnitId)
.ToHashSet();
// Оставляем только те UnitFilterResultDto, чей Id проходит фильтр
var finalFilteredUnitFilterResults = filteredUnitFilterResultsByGrouping
.Where(dto => validUnitFilterResultIdsForWorkGroup.Contains(dto.Id))
.ToList(); .ToList();
logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' осталось {Count} юнитов.", unitIdsWithValidWorkGroupFieldSet.Count); logger.LogDebug("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' осталось {Count} UnitFilterResultDto.", finalFilteredUnitFilterResults.Count);
if (!unitIdsWithValidWorkGroupFieldSet.Any()) if (!finalFilteredUnitFilterResults.Any())
{ {
logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); logger.LogInformation("После фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК' в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК'"); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации по 'РАБОЧАЯ_ГР_ОТВ_ЗАК'");
return; return;
} }
// --- ИСПОЛЬЗУЕМ ПОСЛЕДНИЙ РЕЗУЛЬТАТ ДАЛЬШЕ --- // --- ПОСТРОЕНИЕ ОБРАТНОГО ОТОБРАЖЕНИЯ (после фильтрации) ---
var finalUnitIds = unitIdsWithValidWorkGroupFieldSet; // Более понятное имя logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
var reverseMapping = new Dictionary<Guid, List<Guid>>();
// 5. Получить RelationshipFilters из maxJob foreach (var dto in finalFilteredUnitFilterResults)
var relationshipFilters = maxJob.UnitFilters?.SelectMany(uf => uf.RelationshipFilters).ToList() ?? new List<JobRelationshipFilter>();
logger.LogDebug("Получение связей UnitInUnit для юнитов, прошедших фильтрацию...");
var potentialUnitInUnitLinks = await unitInUnitService.Get()
.AsNoTracking()
.Where(link => finalUnitIds.Contains(link.ParentUnitId) || finalUnitIds.Contains(link.ChildUnitId))
.ToListAsync();
logger.LogDebug("Найдено {Count} потенциальных связей UnitInUnit.", potentialUnitInUnitLinks.Count);
var allParentIds = potentialUnitInUnitLinks.Select(l => l.ParentUnitId).ToHashSet();
var allChildIds = potentialUnitInUnitLinks.Select(l => l.ChildUnitId).ToHashSet();
// --- Оптимизация: Загрузка всех значений за ОДИН запрос ---
var allRelevantUnitIds = allParentIds.Concat(allChildIds).ToHashSet();
var allUnitValues = await unitInValueService.Get()
.AsNoTracking()
.Include(uv => uv.Field)
.Include(uv => uv.Value)
.Where(uv => allRelevantUnitIds.Contains(uv.UnitId))
.ToListAsync();
// --- Создание карт значений из одного списка ---
var parentValuesMap = allUnitValues
.Where(uv => allParentIds.Contains(uv.UnitId))
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
var childValuesMap = allUnitValues
.Where(uv => allChildIds.Contains(uv.UnitId))
.GroupBy(uv => uv.UnitId)
.ToDictionary(g => g.Key, g => g.ToList());
logger.LogDebug("Применение {Count} RelationshipFilters к найденным связям.", relationshipFilters.Count);
var filteredUnitInUnitLinks = new List<UnitInUnit>();
foreach (var link in potentialUnitInUnitLinks)
{ {
bool linkMatchesAllFilters = true; List<Guid> relatedUnitIds;
foreach (var rf in relationshipFilters) if (maxJob.IsParentRelationships == true)
{ {
var valuesToCheck = rf.IsParent ? parentValuesMap.GetValueOrDefault(link.ParentUnitId, new List<UnitInValue>()) : childValuesMap.GetValueOrDefault(link.ChildUnitId, new List<UnitInValue>()); // dto.Id - это ParentUnitId, связанные - ChildUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate
// relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона
bool filterMatch = valuesToCheck.Any(uv => relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); // <-- Исправлено
uv.FieldId == rf.FieldId && }
uv.Value != null && else
uv.Value.Value != null &&
uv.Value.Value.Contains(rf.ValueMask ?? "", StringComparison.OrdinalIgnoreCase)
);
if (rf.IsInverse)
filterMatch = !filterMatch;
if (!filterMatch)
{ {
linkMatchesAllFilters = false; // dto.Id - это ChildUnitId, связанные - ParentUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate
break; // relatedUnitIds - это ParentUnitIds, которые станут UnitId шаблона
relatedUnitIds = dto.Parents.Select(p => p.UnitId).ToList();
}
// dto.Id - это юнит, который прошел фильтры, он будет в UnitsInTemplate
var unitInTemplateId = dto.Id;
foreach (var relatedUnitId in relatedUnitIds)
{
// relatedUnitId уже прошел все фильтры, т.к. dto.Id (его связанный юнит) прошел фильтры
if (!reverseMapping.ContainsKey(relatedUnitId))
{
reverseMapping[relatedUnitId] = new List<Guid>();
}
reverseMapping[relatedUnitId].Add(unitInTemplateId);
} }
} }
if (linkMatchesAllFilters) logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
if (!reverseMapping.Any())
{ {
filteredUnitInUnitLinks.Add(link); logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
} await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения");
return;
} }
logger.LogDebug("После применения RelationshipFilters осталось {Count} связей UnitInUnit.", filteredUnitInUnitLinks.Count); // 6. Внутренняя группировка по ОТВЕТСТВЕННЫЙ_ЗАК / РАБОЧАЯ_ГР_ОТВ_ЗАК (для юнитов в UnitsInTemplate)
logger.LogDebug("Внутренняя группировка по полю (IsGroupByResponsible={IsGroupByResponsible}).", jobGroup.IsGroupByResponsible);
// --- Сгруппировать юниты --- var groupingFieldIdForInnerGrouping = jobGroup.IsGroupByResponsible == true
var groupedRelationships = new Dictionary<Guid, List<Guid>>(); ? await GetFieldIdByAihitNameAsync("ОТВЕТСТВЕННЫЙ_ЗАК")
foreach (var link in filteredUnitInUnitLinks) : await GetFieldIdByAihitNameAsync("РАБОЧАЯ_ГР_ОТВ_ЗАК");
{
var parentUnitId = link.ParentUnitId;
var childUnitId = link.ChildUnitId;
if (finalUnitIds.Contains(parentUnitId)) // Загрузим значения поля для *всех* юнитов, которые могут быть в UnitsInTemplate
{ // Это все юниты из всех списков в reverseMapping.Values
if (!groupedRelationships.ContainsKey(childUnitId)) var allUnitsInTemplate = reverseMapping.Values.SelectMany(list => list).Distinct().ToList();
{ var innerGroupingValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitsInTemplate, new HashSet<Guid> { groupingFieldIdForInnerGrouping });
groupedRelationships[childUnitId] = new List<Guid>();
}
groupedRelationships[childUnitId].Add(parentUnitId);
}
else if (finalUnitIds.Contains(childUnitId))
{
if (!groupedRelationships.ContainsKey(parentUnitId))
{
groupedRelationships[parentUnitId] = new List<Guid>();
}
groupedRelationships[parentUnitId].Add(childUnitId);
}
}
logger.LogDebug("Сформировано {Count} групп по связанным юнитам до разрешения конфликтов.", groupedRelationships.Count); // Создадим маппинг 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 unitToKeys = new Dictionary<Guid, List<Guid>>(); foreach (var kvpOuter in reverseMapping)
foreach (var kvp in groupedRelationships)
{ {
var key = kvp.Key; var potentialUnitId = kvpOuter.Key;
var units = kvp.Value; var unitsInTemplateForThisPotentialUnitId = kvpOuter.Value;
foreach (var unitId in units)
{
if (!unitToKeys.ContainsKey(unitId))
{
unitToKeys[unitId] = new List<Guid>();
}
unitToKeys[unitId].Add(key);
}
}
// Получаем имена всех конфликтующих ключей logger.LogDebug("Обработка потенциального шаблона для UnitId {PotentialUnitId} с {Count} юнитами в UnitsInTemplate до внутренней группировки.", potentialUnitId, unitsInTemplateForThisPotentialUnitId.Count);
var conflictKeys = unitToKeys
.Where(kvp => kvp.Value.Count > 1) // --- ВНУТРЕННЯЯ ГРУППИРОВКА ---
.SelectMany(kvp => kvp.Value) // Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
.Distinct() var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
.GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных")) // Используем маппинг
.ToList(); .ToList();
var unitNamesMap = await unitService.Get() logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate.", potentialUnitId, innerGroupedUnitsInTemplate.Count);
.Where(u => conflictKeys.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name ?? string.Empty);
foreach (var conflictedUnitEntry in unitToKeys.Where(kvp => kvp.Value.Count > 1)) // 8. Цикл по внутренним группам UnitsInTemplate
foreach (var innerGroup in innerGroupedUnitsInTemplate)
{ {
var unitId = conflictedUnitEntry.Key; var groupingValueName = innerGroup.Key; // Значение поля
var keysForUnit = conflictedUnitEntry.Value; var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Список юнитов (UnitId), связанных с potentialUnitId и имеющих одно и то же значение поля
// Сортируем по: 1) кол-во юнитов (убывание), 2) имя ключа (возрастание) logger.LogDebug("Обработка внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId} с {Count} юнитами.", groupingValueName, potentialUnitId, unitsInTemplateInInnerGroup.Count);
var sortedKeys = keysForUnit
.Select(key => (
key,
count: groupedRelationships[key].Count,
name: unitNamesMap.GetValueOrDefault(key, "")
))
.OrderByDescending(x => x.count)
.ThenBy(x => x.name)
.ToList();
var bestKey = sortedKeys.First().key;
// Удаляем юнит из ВСЕХ групп, кроме лучшей
foreach (var key in keysForUnit)
{
if (key != bestKey && groupedRelationships.ContainsKey(key))
{
groupedRelationships[key].Remove(unitId);
}
}
}
// Удаляем пустые группы
var emptyKeys = groupedRelationships
.Where(kvp => !kvp.Value.Any())
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in emptyKeys)
{
groupedRelationships.Remove(key);
}
logger.LogDebug("Сформировано {Count} групп по связанным юнитам после разрешения конфликтов.", groupedRelationships.Count);
// === Основной цикл обработки ===
foreach (var kvp in groupedRelationships)
{
var relationshipUnitId = kvp.Key;
var childUnitIds = kvp.Value;
if (childUnitIds.Count == 0) continue;
logger.LogDebug("Обработка связанного юнита {RelationshipUnitId} с {Count} юнитами из списка.", relationshipUnitId, childUnitIds.Count);
var childUnitNameMap = await unitService.Get()
.AsNoTracking()
.Where(u => childUnitIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Name);
var sortedChildUnitIds = childUnitIds
.OrderBy(id => childUnitNameMap.GetValueOrDefault(id, id.ToString()))
.ToList();
// Разбиваем юниты из *этой* внутренней группы на подгруппы по maxJob.MaxValueRelationships
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value; int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
var childUnitGroups = sortedChildUnitIds var unitsInTemplateSubGroups = unitsInTemplateInInnerGroup
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex) .GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList()) .Select(g => g.Select(x => x.id).ToList())
.ToList(); .ToList();
logger.LogDebug("Связанный юнит {RelationshipUnitId}: разбит на {GroupCount} подгрупп.", relationshipUnitId, childUnitGroups.Count); logger.LogDebug("Внутренняя группа '{GroupingValue}' для UnitId {PotentialUnitId}: разбит на {GroupCount} подгрупп UnitsInTemplate.", groupingValueName, potentialUnitId, unitsInTemplateSubGroups.Count);
// Индекс начинается с 1 // 9. Цикл по подгруппам UnitsInTemplate для создания/обновления шаблонов
for (int i = 1; i <= childUnitGroups.Count; i++) for (int i = 1; i <= unitsInTemplateSubGroups.Count; i++) // Индекс начинается с 1
{ {
var subGroup = childUnitGroups[i - 1]; // корректируем индекс для доступа к коллекции var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции
var subGroupSize = subGroup.Count; var subGroupSize = unitsInTemplateSubGroup.Count;
logger.LogDebug("Обработка подгруппы {Index} связанного юнита {RelationshipUnitId}, размер {Size}.", i, relationshipUnitId, subGroupSize); logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", i, groupingValueName, potentialUnitId, subGroupSize);
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
@@ -449,7 +351,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
.ThenInclude(t => t!.GroupType) .ThenInclude(t => t!.GroupType)
.Include(t => t.UnitsInTemplate) .Include(t => t.UnitsInTemplate)
.ThenInclude(uit => uit.Unit) .ThenInclude(uit => uit.Unit)
.Where(t => t.JobId == targetJob.Id && t.UnitId == relationshipUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used) .Where(t => t.JobId == targetJob.Id && t.UnitId == potentialUnitId && t.Index == i && t.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToListAsync(); .ToListAsync();
var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault(); var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault();
@@ -458,7 +360,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{ {
// === 1. Получаем текущие и новые ID юнитов === // === 1. Получаем текущие и новые ID юнитов ===
var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList(); var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
var proposedUnitIds = subGroup.ToList(); var proposedUnitIds = unitsInTemplateSubGroup.ToList();
// === 2. Сравниваем детерминированно с сортировкой по имени === // === 2. Сравниваем детерминированно с сортировкой по имени ===
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList(); var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
@@ -486,17 +388,14 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id); logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", existingTemplateForSubGroup.Id);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(existingTemplateForSubGroup.Id, false);
var updateRequest = new TemplateUpdaterMq var updateRequest = new TemplateUpdaterMq
{ {
TemplateId = existingTemplateForSubGroup.Id, TemplateId = existingTemplateForSubGroup.Id,
JobId = targetJob.Id, JobId = targetJob.Id,
UnitId = relationshipUnitId, UnitId = potentialUnitId,
Name = expectedName, Name = expectedName,
IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate, IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate,
IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule, IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule,
//LastRun = existingTemplateForSubGroup.LastRun,
//NextRun = nextRun,
IsNew = false, IsNew = false,
Index = i, Index = i,
StatusTypeId = TemplateStatusTypeEnum.Used, StatusTypeId = TemplateStatusTypeEnum.Used,
@@ -513,7 +412,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
else else
{ {
logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов или их порядка (после сортировки).", existingTemplateForSubGroup.Id); logger.LogDebug("Шаблон {TemplateId} требует обновления юнитов или их порядка (после сортировки).", existingTemplateForSubGroup.Id);
var newTargetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); var newTargetJob = SelectTargetJob(jobsInGroup, unitsInTemplateSubGroup.Count, maxJob); // Размер - из подмножества
if (newTargetJob.Id != existingTemplateForSubGroup.JobId) if (newTargetJob.Id != existingTemplateForSubGroup.JobId)
{ {
logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id); logger.LogDebug("Job для шаблона {TemplateId} изменился.", existingTemplateForSubGroup.Id);
@@ -523,83 +422,91 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
} }
else else
{ {
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, relationshipUnitId, initiator); var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator);
if (reusableTemplate != null) if (reusableTemplate != null)
{ {
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, relationshipUnitId, i); logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}.", reusableTemplate.Id, targetJob.Id, potentialUnitId, i);
var tempTemplateForName = new Template var tempTemplateForName = new Template
{ {
Id = reusableTemplate.Id, Id = reusableTemplate.Id,
Name = reusableTemplate.Name, Name = reusableTemplate.Name,
JobId = targetJob.Id, JobId = targetJob.Id,
UnitId = relationshipUnitId, UnitId = potentialUnitId,
Index = i, Index = i,
Job = targetJob, Job = targetJob,
Unit = reusableTemplate.Unit, Unit = reusableTemplate.Unit,
UnitsInTemplate = subGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList() UnitsInTemplate = unitsInTemplateSubGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
}; };
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true);
var updateRequest = new TemplateUpdaterMq var updateRequest = new TemplateUpdaterMq
{ {
TemplateId = reusableTemplate.Id, TemplateId = reusableTemplate.Id,
JobId = targetJob.Id, JobId = targetJob.Id,
UnitId = relationshipUnitId, UnitId = potentialUnitId,
Name = expectedName, Name = expectedName,
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used, StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator, Initiator = initiator,
//NextRun = nextRun,
IsNew = true, IsNew = true,
Index = i, Index = i,
UnitsInTemplate = subGroup UnitsInTemplate = unitsInTemplateSubGroup
}; };
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
} }
else else
{ {
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipUnitId}, Index {Index}, с {Count} юнитами.", targetJob.Id, relationshipUnitId, i, subGroup.Count); logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}, с {Count} юнитами.", targetJob.Id, potentialUnitId, i, unitsInTemplateSubGroup.Count);
await CreateGroupedTemplateAsync(targetJob.Id, relationshipUnitId, subGroup, i, initiator); await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, i, initiator);
}
} }
} }
} }
} }
// === Деактивация === // === Деактивация ===
// Собираем ожидаемые ключи шаблонов на основе внутренне сгруппированных результатов
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
foreach (var kvp in groupedRelationships) foreach (var kvpOuter in reverseMapping)
{ {
var relationshipUnitId = kvp.Key; var potentialUnitId = kvpOuter.Key;
var childUnitIds = kvp.Value; var unitsInTemplateForThisPotentialUnitId = kvpOuter.Value;
var childUnitNameMapForDeactivate = await unitService.Get()
.AsNoTracking() // Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
.Where(u => childUnitIds.Contains(u.Id)) var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
.ToDictionaryAsync(u => u.Id, u => u.Name); .GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных"))
var sortedChildUnitIdsForDeactivate = childUnitIds
.OrderBy(id => childUnitNameMapForDeactivate.GetValueOrDefault(id, id.ToString()))
.ToList(); .ToList();
foreach (var innerGroup in innerGroupedUnitsInTemplate)
{
var groupingValueName = innerGroup.Key;
var unitsInTemplateInInnerGroup = innerGroup.ToList();
// Разбиваем юниты из *этой* внутренней группы на подгруппы по maxJob.MaxValueRelationships
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value; int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
var childUnitGroups = sortedChildUnitIdsForDeactivate var unitsInTemplateSubGroups = unitsInTemplateInInnerGroup
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting }) .Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex) .GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.id).ToList()) .Select(g => g.Select(x => x.id).ToList())
.ToList(); .ToList();
// Индекс начинается с 1
for (int i = 1; i <= childUnitGroups.Count; i++) // Для каждой подгруппы UnitsInTemplate
for (int i = 1; i <= unitsInTemplateSubGroups.Count; i++) // Индекс начинается с 1
{ {
var subGroup = childUnitGroups[i - 1]; // корректируем индекс для доступа к коллекции var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции
var subGroupSize = subGroup.Count; var subGroupSize = unitsInTemplateSubGroup.Count;
Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob); Job? targetJobForExpectedKey = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
expectedTemplateKeys.Add((targetJobForExpectedKey.Id, relationshipUnitId, i));
expectedTemplateKeys.Add((targetJobForExpectedKey.Id, potentialUnitId, i)); // potentialUnitId - это UnitId шаблона
}
} }
} }
// Получаем ВСЕ шаблоны для JobGroup (не только для текущих relationshipUnitIds) // Получаем ВСЕ шаблоны для JobGroup (не только для текущих potentialUnitIds)
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get() var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking() .AsNoTracking()
@@ -724,8 +631,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
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)
@@ -749,4 +654,17 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
TimeSpan.FromMinutes(30) TimeSpan.FromMinutes(30)
); );
} }
#region Вспомогательные методы
private async Task<Guid> GetFieldIdByAihitNameAsync(string fieldName)
{
var field = await unitFieldService.GetByAihitNameAsync(fieldName);
if (field == null)
{
logger.LogError("Поле '{FieldName}' не найдено в справочнике полей.", fieldName);
throw new InvalidOperationException($"Поле '{fieldName}' не найдено в справочнике полей.");
}
return field.Id;
}
#endregion
} }

View File

@@ -31,7 +31,7 @@ internal class TemplateReuser : ITemplateReuser
try try
{ {
// Атомарно резервируем один шаблон через DAL // Атомарно резервируем один шаблон через DAL
var templateId = await templateService.ReserveUnusedTemplateAsync(unitId, initiator); var templateId = await templateService.ReserveUnusedTemplateAsync(unitId, jobId, initiator);
if (templateId == null) if (templateId == null)
{ {

View File

@@ -5,6 +5,19 @@ namespace PARR.TemplateMatcher.Services.Interfaces
{ {
public interface ITemplateReuser public interface ITemplateReuser
{ {
/// <summary>
/// Пытается переиспользовать неиспользуемый шаблон для указанной работы и ЭК.
/// При резервировании шаблона для правильного расчета NextRun Id работы
/// и ЭК устанавливается при захвате.
/// </summary>
/// <param name="jobId">Id работы.</param>
/// <param name="unitId">Id ЭК.</param>
/// <param name="initiator">Данные инициатора операции.</param>
/// <param name="maxAttempts">Максимальное количество попыток (по умолчанию 3).</param>
/// <returns>
/// Зарезервированный шаблон или <see langword="null"/>,
/// если не удалось получить шаблон после всех попыток.
/// </returns>
Task<Template?> TryReuseOneUnusedTemplateAsync( Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId, Guid jobId,
Guid unitId, Guid unitId,