diff --git a/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs b/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs index 290dff6f..9beadac2 100644 --- a/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs +++ b/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs @@ -3,9 +3,20 @@ using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces.Job; using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Job; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace PARR.Core.Common.Helpers; +/// +/// Сервис для синхронизации свойств Job внутри JobGroup. +/// Эталоном считается Job с максимальным MaxValueRelationships. +/// Синхронизируются: WorkName, маски, IsParentRelationships, AutoControl, UnitFilters. +/// НЕ синхронизируются: Id, Name, MinValueRelationships, MaxValueRelationships, TnkId. +/// public class JobGroupUnitFilterSynchronizeHelper { private readonly IJobRepository jobRepository; @@ -25,12 +36,13 @@ public class JobGroupUnitFilterSynchronizeHelper .ThenInclude(uf => uf.FieldFilters) .Include(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) + .Include(j => j.AutoControl) .Where(j => j.GroupId == jobGroupId) .ToListAsync(ct); if (jobs == null || jobs.Count < 2) { - logger.LogDebug("JobGroup {JobGroupId} содержит менее двух Job. Синхронизация фильтров не требуется.", jobGroupId); + logger.LogDebug("JobGroup {JobGroupId} содержит менее двух Job. Синхронизация не требуется.", jobGroupId); return; } @@ -41,7 +53,7 @@ public class JobGroupUnitFilterSynchronizeHelper return; } - logger.LogInformation("Начало синхронизации фильтров для JobGroup {JobGroupId}. Эталонный Job: {ReferenceJobId}.", jobGroupId, referenceJob.Id); + logger.LogInformation("Начало синхронизации для JobGroup {JobGroupId}. Эталонный Job: {ReferenceJobId}.", jobGroupId, referenceJob.Id); foreach (var job in jobs) { @@ -49,31 +61,84 @@ public class JobGroupUnitFilterSynchronizeHelper if (job.Id == referenceJob.Id) continue; - if (!AreUnitFiltersIdentical(job.UnitFilters, referenceJob.UnitFilters)) + if (!AreJobsIdentical(job, referenceJob)) { - logger.LogDebug("Фильтры Job {JobId} отличаются от эталона. Запуск обновления.", job.Id); - await ApplyFilterCorrectionAsync(job.Id, referenceJob.UnitFilters ?? Enumerable.Empty(), initiator, ct); + logger.LogDebug("Свойства Job {JobId} отличаются от эталона. Запуск обновления.", job.Id); + await ApplyJobCorrectionAsync(job.Id, referenceJob, initiator, ct); } else { - logger.LogDebug("Фильтры Job {JobId} идентичны эталону. Пропуск.", job.Id); + logger.LogDebug("Свойства Job {JobId} идентичны эталону. Пропуск.", job.Id); } } - logger.LogInformation("Синхронизация фильтров для JobGroup {JobGroupId} завершена.", jobGroupId); + logger.LogInformation("Синхронизация для JobGroup {JobGroupId} завершена.", jobGroupId); } - private async Task ApplyFilterCorrectionAsync(Guid jobId, IEnumerable referenceFilters, HistoryInitiator initiator, CancellationToken ct) + private bool AreJobsIdentical(Job current, Job reference) + { + // Сравнение скалярных полей (кроме Id, Name, Min/MaxValueRelationships, TnkId) + if (current.WorkName != reference.WorkName) return false; + if (current.IsParentRelationships != reference.IsParentRelationships) return false; + if (current.TemplateNameMask != reference.TemplateNameMask) return false; + if (current.WorkGroupMask != reference.WorkGroupMask) return false; + if (current.ResponseAreaMask != reference.ResponseAreaMask) return false; + + // Сравнение AutoControl + if (current.AutoControl == null && reference.AutoControl != null) return false; + if (current.AutoControl != null && reference.AutoControl == null) return false; + if (current.AutoControl != null && reference.AutoControl != null) + { + if (current.AutoControl.InitUsedTemplateState != reference.AutoControl.InitUsedTemplateState) return false; + if (current.AutoControl.InitUsedScheduleState != reference.AutoControl.InitUsedScheduleState) return false; + } + + // Сравнение фильтров + if (!AreUnitFiltersIdentical(current.UnitFilters, reference.UnitFilters)) return false; + + return true; + } + + private async Task ApplyJobCorrectionAsync(Guid jobId, Job referenceJob, HistoryInitiator initiator, CancellationToken ct) { var job = await jobRepository.Get() .Include(j => j.UnitFilters) .ThenInclude(uf => uf.FieldFilters) .Include(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) + .Include(j => j.AutoControl) .FirstOrDefaultAsync(j => j.Id == jobId, ct); if (job == null) return; + // 1. Обновляем скалярные поля + job.WorkName = referenceJob.WorkName; + job.IsParentRelationships = referenceJob.IsParentRelationships; + job.TemplateNameMask = referenceJob.TemplateNameMask; + job.WorkGroupMask = referenceJob.WorkGroupMask; + job.ResponseAreaMask = referenceJob.ResponseAreaMask; + job.DateModified = DateTimeOffset.UtcNow; + + // 2. Обновляем AutoControl + if (referenceJob.AutoControl != null) + { + if (job.AutoControl == null) + { + job.AutoControl = new JobAutoControl + { + JobId = job.Id, + InitUsedTemplateState = referenceJob.AutoControl.InitUsedTemplateState, + InitUsedScheduleState = referenceJob.AutoControl.InitUsedScheduleState + }; + } + else + { + job.AutoControl.InitUsedTemplateState = referenceJob.AutoControl.InitUsedTemplateState; + job.AutoControl.InitUsedScheduleState = referenceJob.AutoControl.InitUsedScheduleState; + } + } + + // 3. Обновляем UnitFilters if (job.UnitFilters == null) { job.UnitFilters = new List(); @@ -83,14 +148,14 @@ public class JobGroupUnitFilterSynchronizeHelper job.UnitFilters.Clear(); } - var clonedFilters = CloneFilters(referenceFilters, job.Id); + var clonedFilters = CloneFilters(referenceJob.UnitFilters ?? Enumerable.Empty(), job.Id); foreach (var filter in clonedFilters) { job.UnitFilters!.Add(filter); } await jobRepository.CommitAsync(initiator); - logger.LogInformation("Фильтры Job {JobId} успешно обновлены.", jobId); + logger.LogInformation("Job {JobId} успешно синхронизирован с эталоном.", jobId); } private bool AreUnitFiltersIdentical(IEnumerable? current, IEnumerable? reference) diff --git a/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldRepository.cs b/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldRepository.cs index 2a9c0cf3..85050679 100644 --- a/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldRepository.cs +++ b/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldRepository.cs @@ -5,6 +5,7 @@ namespace PARR.Core.Repositories.Interfaces.Unit { public interface IUnitFieldRepository : IBaseRepository { - Task GetByAihitNameAsync(string name); + Task GetByCodeAsync(string code, CancellationToken ct = default); + Task GetByAihitNameAsync(string aihitName, CancellationToken ct = default); } } diff --git a/PARR.DAL/Repositories/Unit/UnitFieldRepository.cs b/PARR.DAL/Repositories/Unit/UnitFieldRepository.cs index 814136a8..03be73a1 100644 --- a/PARR.DAL/Repositories/Unit/UnitFieldRepository.cs +++ b/PARR.DAL/Repositories/Unit/UnitFieldRepository.cs @@ -10,10 +10,24 @@ namespace PARR.DAL.Repositories.Unit internal class UnitFieldRepository : BaseRepository, IUnitFieldRepository { public UnitFieldRepository(DataContext dataContext, ILogger logger) : base(logger, dataContext) { } - - public async Task GetByAihitNameAsync(string name) + public async Task GetByCodeAsync(string code, CancellationToken ct = default) { - return await EntitySet.FirstOrDefaultAsync(uf => uf.AihitName.ToLower().Trim() == name.ToLower().Trim()); + if (string.IsNullOrWhiteSpace(code)) + return null; + + return await Get() + .AsNoTracking() + .FirstOrDefaultAsync(f => f.Code != null && EF.Functions.ILike(f.Code, code), ct); + } + + public async Task GetByAihitNameAsync(string aihitName, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(aihitName)) + return null; + + return await Get() + .AsNoTracking() + .FirstOrDefaultAsync(f => EF.Functions.ILike(f.AihitName, aihitName), ct); } } } diff --git a/PARR.Domain/Constants/UnitFieldCodes.cs b/PARR.Domain/Constants/UnitFieldCodes.cs new file mode 100644 index 00000000..29a883b2 --- /dev/null +++ b/PARR.Domain/Constants/UnitFieldCodes.cs @@ -0,0 +1,15 @@ +namespace PARR.Domain.Constants +{ + public class UnitFieldCodes + { + public const string IP = "ip"; + public const string IsActive = "isActive"; + public const string ResponseArea = "responseArea"; + public const string WorkGroupResponseArea = "workGroupResponseArea"; + public const string OS = "os"; + public const string Responsible = "responsible"; + public const string Tag = "tag"; + public const string WorkGroup = "workGroup"; + public const string EkStatus = "ekStatus"; + } +} diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs index 32a869fc..fb1456fb 100644 --- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs +++ b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs @@ -1,6 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.Shortcodes; +using PARR.Domain.Constants; +using PARR.Domain.Entities; using PARR.Domain.Entities.Job; using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Services.Interfaces; @@ -12,42 +15,44 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder private readonly ILogger logger; private readonly IUnitInValueRepository unitInValueRepository; private readonly IUnitFieldRepository unitFieldRepository; + private readonly IShortcodesService shortcodesService; public GroupedTemplateBuilder( ILogger logger, IUnitInValueRepository unitInValueRepository, - IUnitFieldRepository unitFieldRepository) + IUnitFieldRepository unitFieldRepository, + IShortcodesService shortcodesService + ) { this.logger = logger; this.unitInValueRepository = unitInValueRepository; this.unitFieldRepository = unitFieldRepository; + this.shortcodesService = shortcodesService; } public async Task> BuildAsync( - Dictionary> initialReverseMapping, - JobGroup jobGroup, - Job maxJob, - CancellationToken ct = default) + Dictionary> initialReverseMapping, + JobGroup jobGroup, + Job maxJob, + CancellationToken ct = default) { logger.LogDebug("Начало построения структуры групп для JobGroup {JobGroupId}.", jobGroup.Id); if (!initialReverseMapping.Any()) return new List(); - // 1. Определяем поле для внутренней группировки - var isGroupByResponsible = jobGroup.IsGroupByResponsible == true; - var innerGroupingFieldName = isGroupByResponsible ? "ОТВЕТСТВЕННЫЙ_ЗА_ЭК" : "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК"; + // 1. Определяем стратегию внутренней группировки + // Если IsGroupByResponsible != true, используем WorkGroupMask через ShortcodesService + bool useWorkGroupMask = jobGroup.IsGroupByResponsible != true; - var innerGroupingField = await unitFieldRepository.GetByAihitNameAsync(innerGroupingFieldName) - ?? throw new InvalidOperationException($"Поле '{innerGroupingFieldName}' не найдено."); - - var innerGroupingFieldId = innerGroupingField.Id; - var groupingFieldId = jobGroup.GroupingUnitFieldId!.Value; + logger.LogDebug("Стратегия внутренней группировки: {Strategy}", + useWorkGroupMask ? "WorkGroupMask" : "Поле 'Ответственный за ЭК'"); // 2. Собираем все исходные UnitId var allSourceUnitIds = initialReverseMapping.Values.SelectMany(ids => ids).Distinct().ToList(); - // 3. Загружаем UnitInValue для трансформации + // 3. Загружаем UnitInValue для трансформации по полю группировки из настроек JobGroup + var groupingFieldId = jobGroup.GroupingUnitFieldId!.Value; var relevantUnitInValues = await unitInValueRepository.Get() .AsNoTracking() .Where(uiv => allSourceUnitIds.Contains(uiv.UnitId) && uiv.FieldId == groupingFieldId) @@ -58,9 +63,7 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder var uivLookup = relevantUnitInValues .GroupBy(x => x.UnitId) - .ToDictionary( - g => g.Key, - g => g.Select(x => x.ValueId).ToList()); + .ToDictionary(g => g.Key, g => g.Select(x => x.ValueId).ToList()); // 4. Трансформируем в reverseMapping с парами var reverseMapping = new Dictionary>(); @@ -83,7 +86,7 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder var uniqueEntries = entries .GroupBy(e => (e.UnitId, e.UnitFieldValueId)) .Select(g => g.First()) - .OrderBy(e => e.UnitId) // ИСПРАВЛЕНО: стабильная сортировка + .OrderBy(e => e.UnitId) .ThenBy(e => e.UnitFieldValueId) .ToList(); @@ -94,18 +97,62 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder if (!reverseMapping.Any()) return new List(); - // 5. Загружаем значения для внутренней группировки - var allUnitsInTemplatePairs = reverseMapping.Values.SelectMany(list => list).ToList(); - var allUnitIdsForInnerGrouping = allUnitsInTemplatePairs.Select(e => e.UnitId).Distinct().ToList(); + // 5. Определяем значения для внутренней группировки + Dictionary unitIdToGroupingValueMap; - var innerGroupingValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync( - allUnitIdsForInnerGrouping, - new HashSet { innerGroupingFieldId }, - ct); + if (useWorkGroupMask) + { + // Используем ShortcodesService для получения финальных значений WorkGroupMask + unitIdToGroupingValueMap = new Dictionary(); - var unitIdToInnerGroupingValueMap = innerGroupingValues - .Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value)) - .ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value); + foreach (var potentialUnitId in reverseMapping.Keys) + { + var relatedUnitIds = reverseMapping[potentialUnitId].Select(e => e.UnitId).Distinct().ToList(); + + foreach (var relatedUnitId in relatedUnitIds) + { + // Создаем временный Template для применения шорткодов + var tempTemplate = new Template + { + Id = Guid.NewGuid(), + Name = "temp", + JobId = maxJob.Id, + UnitId = potentialUnitId, // Родительский юнит шаблона + Job = maxJob, + UnitsInTemplate = new List + { + new UnitsInTemplate { UnitId = relatedUnitId, UnitFieldValueId = Guid.Empty } + } + }; + + var workGroupValue = await shortcodesService.ApplyShortcodesAsync( + maxJob.WorkGroupMask, + tempTemplate, + nameof(GroupedTemplateBuilder)); + + unitIdToGroupingValueMap[relatedUnitId] = workGroupValue; + } + } + } + else + { + // Логика разделения по ответственному за ЭК через служебный код поля + var innerGroupingField = await unitFieldRepository.GetByCodeAsync(UnitFieldCodes.Responsible, ct) + ?? throw new InvalidOperationException($"Поле с кодом '{UnitFieldCodes.Responsible}' не найдено в справочнике UnitField."); + + var innerGroupingFieldId = innerGroupingField.Id; + 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 { innerGroupingFieldId }, + ct); + + unitIdToGroupingValueMap = innerGroupingValues + .Where(uv => uv.Value != null && uv.Value.Value != null && !string.IsNullOrEmpty(uv.Value.Value)) + .ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value!); + } // 6. Формируем итоговую структуру var templateGroups = new List(); @@ -117,7 +164,7 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder var unitsInTemplateForThisPotentialUnitId = kvp.Value; var innerGroupedUnits = unitsInTemplateForThisPotentialUnitId - .GroupBy(entry => unitIdToInnerGroupingValueMap.GetValueOrDefault(entry.UnitId, "Нет данных")) + .GroupBy(entry => unitIdToGroupingValueMap.GetValueOrDefault(entry.UnitId, "Нет данных")) .OrderBy(g => g.Key, StringComparer.Ordinal) .ToList();