Files
parr_api/PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateBuilder.cs

347 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.JobEntities;
using PARR.Domain.Entities.JobGroupEntities;
using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.GroupedSync;
internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
{
private readonly ILogger<GroupedTemplateBuilder> logger;
private readonly IUnitInValueRepository unitInValueRepository;
private readonly IUnitFieldRepository unitFieldRepository;
private readonly IShortcodesService shortcodesService;
private readonly IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupRepository;
public GroupedTemplateBuilder(
ILogger<GroupedTemplateBuilder> logger,
IUnitInValueRepository unitInValueRepository,
IUnitFieldRepository unitFieldRepository,
IShortcodesService shortcodesService,
IUnitRegionalEkPtkGroupRepository regionalEkPtkGroupRepository
)
{
this.logger = logger;
this.unitInValueRepository = unitInValueRepository;
this.unitFieldRepository = unitFieldRepository;
this.shortcodesService = shortcodesService;
this.regionalEkPtkGroupRepository = regionalEkPtkGroupRepository;
}
public async Task<List<GroupedTemplateGroup>> BuildAsync(
Dictionary<Guid, List<Guid>> initialReverseMapping,
JobGroup jobGroup,
Job maxJob,
Dictionary<Guid, string> unitNames,
CancellationToken ct = default)
{
logger.LogDebug("Начало построения структуры групп для JobGroup {JobGroupId}.", jobGroup.Id);
if (!initialReverseMapping.Any())
return new List<GroupedTemplateGroup>();
// 1. Определяем стратегию внутренней группировки
bool useWorkGroupMask = jobGroup.IsGroupByResponsible != true;
logger.LogDebug("Стратегия внутренней группировки: {Strategy}",
useWorkGroupMask ? "WorkGroupMask" : "Поле 'Ответственный за ЭК'");
// 2. Собираем все исходные UnitId
var allSourceUnitIds = initialReverseMapping.Values.SelectMany(ids => ids).Distinct().ToList();
// 3. Загружаем UnitInValue для трансформации по полю группировки из настроек JobGroup
var groupingFieldId = jobGroup.GroupingUnitFieldId!.Value;
var relevantUnitInValues = await unitInValueRepository.Get()
.AsNoTracking()
.Where(uiv => allSourceUnitIds.Contains(uiv.UnitId) && uiv.FieldId == groupingFieldId)
.OrderBy(uiv => uiv.UnitId)
.ThenBy(uiv => uiv.ValueId)
.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.OrderBy(k => k.Key))
{
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())
.OrderBy(e => e.UnitId)
.ThenBy(e => e.UnitFieldValueId)
.ToList();
if (uniqueEntries.Any())
reverseMapping[potentialUnitId] = uniqueEntries;
}
if (!reverseMapping.Any())
return new List<GroupedTemplateGroup>();
// 5. Определяем значения для внутренней группировки
Dictionary<Guid, string> unitIdToGroupingValueMap;
// Загружаем список разрешённых региональных ПТК (если не нужно пропускать фильтрацию)
bool skipRegionalFilter = maxJob.WorkGroupMask.Contains("_в_ЗО_РГ", StringComparison.OrdinalIgnoreCase);
HashSet<string>? allowedWorkGroupSet = null;
if (!skipRegionalFilter)
{
var allowedWorkGroupValues = await regionalEkPtkGroupRepository.Get()
.AsNoTracking()
.Include(g => g.FieldValue)
.Where(g => g.FieldValue != null && g.FieldValue.Value != null)
.Select(g => g.FieldValue!.Value!)
.ToListAsync(ct);
allowedWorkGroupSet = new HashSet<string>(allowedWorkGroupValues, StringComparer.OrdinalIgnoreCase);
logger.LogDebug("Загружено {Count} разрешённых значений рабочих групп из RegionalEkPtkGroup.", allowedWorkGroupSet.Count);
}
else
{
logger.LogDebug("Маска содержит тег с '_в_ЗО_РГ'. Фильтрация по списку рабочих групп ПТК пропущена.");
}
if (useWorkGroupMask)
{
// Применяем WorkGroupMask для каждого юнита отдельно
unitIdToGroupingValueMap = new Dictionary<Guid, string>();
foreach (var potentialUnitId in reverseMapping.Keys)
{
var relatedUnitIds = reverseMapping[potentialUnitId]
.Select(e => e.UnitId)
.Distinct()
.ToList();
foreach (var relatedUnitId in relatedUnitIds)
{
// Формируем шаблон с одним связанным юнитом
var tempTemplate = new Template
{
Id = Guid.NewGuid(),
Name = "temp",
JobId = maxJob.Id,
UnitId = potentialUnitId,
Job = maxJob,
UnitsInTemplate = new List<UnitsInTemplate>
{
new UnitsInTemplate { UnitId = relatedUnitId, UnitFieldValueId = Guid.Empty }
}
};
var workGroupValue = await shortcodesService.ApplyShortcodesAsync(
maxJob.WorkGroupMask,
tempTemplate,
nameof(GroupedTemplateBuilder));
if (string.IsNullOrEmpty(workGroupValue))
{
logger.LogDebug(
"Юнит {UnitId} исключён: маска вернула пустую строку.",
relatedUnitId);
continue;
}
// Фильтрация по списку рабочих групп ПТК
if (!skipRegionalFilter && !allowedWorkGroupSet!.Contains(workGroupValue))
{
logger.LogDebug(
"[FILTER] Юнит {UnitId} ИСКЛЮЧЁН: рабочая группа '{WorkGroup}' не найдена в списке разрешённых ({AllowedCount} значений).",
relatedUnitId, workGroupValue, allowedWorkGroupSet!.Count);
continue;
}
logger.LogDebug(
"[FILTER] Юнит {UnitId} ПРОШЁЛ: рабочая группа '{WorkGroup}' для PotentialUnit {PotentialUnitId}.",
relatedUnitId, workGroupValue, potentialUnitId);
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<Guid> { innerGroupingFieldId },
ct);
// Собираем значения поля "Ответственный" для группировки
var responsibleValueByUnit = innerGroupingValues
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
.ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value!);
unitIdToGroupingValueMap = new Dictionary<Guid, string>();
// Для каждого юнита дополнительно применяем WorkGroupMask и фильтруем по RegionalEkPtkGroup
foreach (var potentialUnitId in reverseMapping.Keys)
{
var relatedUnitIds = reverseMapping[potentialUnitId]
.Select(e => e.UnitId)
.Distinct()
.ToList();
foreach (var relatedUnitId in relatedUnitIds)
{
// Юнит исключается, если у него нет значения поля "Ответственный"
if (!responsibleValueByUnit.TryGetValue(relatedUnitId, out var responsibleValue))
{
logger.LogDebug(
"Юнит {UnitId} исключён: отсутствует значение поля 'Ответственный за ЭК'.",
relatedUnitId);
continue;
}
// Фильтрация по списку региональных ПТК выполняется только если не используется тег с "_в_ЗО_РГ"
if (!skipRegionalFilter)
{
// Формируем временный шаблон с одним связанным юнитом для применения WorkGroupMask
var tempTemplate = new Template
{
Id = Guid.NewGuid(),
Name = "temp",
JobId = maxJob.Id,
UnitId = potentialUnitId,
Job = maxJob,
UnitsInTemplate = new List<UnitsInTemplate>
{
new UnitsInTemplate { UnitId = relatedUnitId, UnitFieldValueId = Guid.Empty }
}
};
var workGroupValue = await shortcodesService.ApplyShortcodesAsync(
maxJob.WorkGroupMask,
tempTemplate,
nameof(GroupedTemplateBuilder));
if (string.IsNullOrEmpty(workGroupValue))
{
logger.LogDebug(
"Юнит {UnitId} исключён: WorkGroupMask вернула пустую строку.",
relatedUnitId);
continue;
}
if (!allowedWorkGroupSet!.Contains(workGroupValue))
{
logger.LogDebug(
"[FILTER] Юнит {UnitId} ИСКЛЮЧЁН: рабочая группа '{WorkGroup}' не найдена в списке разрешённых ({AllowedCount} значений).",
relatedUnitId, workGroupValue, allowedWorkGroupSet!.Count);
continue;
}
logger.LogDebug(
"[FILTER] Юнит {UnitId} ПРОШЁЛ: рабочая группа '{WorkGroup}' для PotentialUnit {PotentialUnitId}.",
relatedUnitId, workGroupValue, potentialUnitId);
}
// В маппинг группировки пишем значение поля "Ответственный" — по нему идёт группировка в этапе 6
unitIdToGroupingValueMap[relatedUnitId] = responsibleValue;
}
}
}
// 6. Формируем итоговую структуру
var templateGroups = new List<GroupedTemplateGroup>();
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
foreach (var kvp in reverseMapping.OrderBy(k => k.Key))
{
var potentialUnitId = kvp.Key;
var unitsInTemplateForThisPotentialUnitId = kvp.Value;
// Фильтруем записи: оставляем только те, которые прошли проверку
var filteredUnits = unitsInTemplateForThisPotentialUnitId
.Where(entry => unitIdToGroupingValueMap.ContainsKey(entry.UnitId))
.ToList();
if (!filteredUnits.Any())
{
logger.LogDebug("PotentialUnitId {PotentialUnitId} исключён: нет юнитов после фильтрации.", potentialUnitId);
continue;
}
var innerGroupedUnits = filteredUnits
.GroupBy(entry => unitIdToGroupingValueMap.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();
// Сортируем по имени юнита, потом по UnitFieldValueId
var sortedUnitsInInnerGroup = unitsInInnerGroup
.OrderBy(e => unitNames.GetValueOrDefault(e.UnitId, e.UnitId.ToString()))
.ThenBy(e => e.UnitFieldValueId)
.ToList();
var splitSubGroups = sortedUnitsInInnerGroup
.Select((entry, index) => new { entry, groupIndex = index / maxValueForSplitting })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.entry).ToList())
.ToList();
// ВАЖНО: добавляем подгруппы в subGroups
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;
}
}