Files
parr_api/PARR.TemplateMatcher/Services/GroupedSync/GroupedTemplateBuilder.cs
Mikhail Kuznetsov a438f18480 refactor(templateMatcher): Переход на Pipeline-архитектуру для SimpleSync и GroupedSync.
- SimpleTemplateSynchronizer и GroupedTemplateSynchronizer переведены на паттерн Pipeline с разделением на Read/Write этапы
- Выделены контракты этапов (ISimpleSyncStage, IGroupedSyncStage) и контексты (SimpleSyncContext, GroupedSyncContext)
- Read-этапы безопасны для тестов (не пишут в БД/MQ), Write-этапы изолированы через отдельные интерфейсы
- Добавлено [Perf]-логирование каждого этапа с метриками времени выполнения
- Логи приведены к человекочитаемому формату 'Имя' (ID) для Job, JobGroup и Unit
- Устранено дублирование данных в контекстах (FilteredUnits перезаписывается, TemplateGroups строго типизирован)
- Константы неиспользуемых шаблонов вынесены в UnusedTemplateConstants
- Структура проекта реорганизована: SimpleSync, GroupedSync, Implementations, Interfaces
2026-06-11 15:09:46 +10:00

215 lines
9.2 KiB
C#
Raw 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.Job;
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;
public GroupedTemplateBuilder(
ILogger<GroupedTemplateBuilder> logger,
IUnitInValueRepository unitInValueRepository,
IUnitFieldRepository unitFieldRepository,
IShortcodesService shortcodesService
)
{
this.logger = logger;
this.unitInValueRepository = unitInValueRepository;
this.unitFieldRepository = unitFieldRepository;
this.shortcodesService = shortcodesService;
}
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. Определяем стратегию внутренней группировки
// Если IsGroupByResponsible != true, используем WorkGroupMask через ShortcodesService
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;
if (useWorkGroupMask)
{
// Используем ShortcodesService для получения финальных значений 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)
{
// Создаем временный Template для применения шорткодов
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));
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);
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<GroupedTemplateGroup>();
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
foreach (var kvp in reverseMapping.OrderBy(k => k.Key))
{
var potentialUnitId = kvp.Key;
var unitsInTemplateForThisPotentialUnitId = kvp.Value;
var innerGroupedUnits = unitsInTemplateForThisPotentialUnitId
.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();
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)
{
var sortedEntries = subGroupEntries
.OrderBy(e => e.UnitId)
.ThenBy(e => e.UnitFieldValueId)
.ToList();
subGroups.Add(new GroupedTemplateSubGroup(
Entries: sortedEntries,
InnerGroupName: innerGroupName,
GlobalIndex: globalIndex
));
globalIndex++;
}
}
if (subGroups.Any())
{
templateGroups.Add(new GroupedTemplateGroup(
PotentialUnitId: potentialUnitId,
SubGroups: subGroups
));
}
}
logger.LogDebug("Построено {Count} групп шаблонов.", templateGroups.Count);
return templateGroups;
}
}