Merge branch 'dev' of gitlab.dvgd.oao.rzd:devptk/parr/parr_api into dev
This commit is contained in:
@@ -197,7 +197,7 @@ namespace PARR.AIHITMainSyncer.Services
|
|||||||
// Для тегов нормализуем разделители до пробела
|
// Для тегов нормализуем разделители до пробела
|
||||||
var normalizedValue = multiValueProperty.Value
|
var normalizedValue = multiValueProperty.Value
|
||||||
.Replace(';', ' ')
|
.Replace(';', ' ')
|
||||||
.Replace('.', ' ')
|
.Replace('#', ' ')
|
||||||
.Replace(',', ' ');
|
.Replace(',', ' ');
|
||||||
|
|
||||||
splitValues = normalizedValue.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
splitValues = normalizedValue.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|||||||
@@ -163,7 +163,8 @@ namespace PARR.Core.Services.Shortcodes
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Сохранение в кэш (только если были изменения)
|
// Сохранение в кэш (только если были изменения)
|
||||||
if (!string.IsNullOrEmpty(result) && result != str)
|
// Нельзя писать template.Id == Guid.Empty, так как id для новых шаблонов 0000-000-000000 и будет не верно реализован шорт-код
|
||||||
|
if (!string.IsNullOrEmpty(result) && result != str && template.Id != Guid.Empty)
|
||||||
{
|
{
|
||||||
var toCache = new ShortcodesResult
|
var toCache = new ShortcodesResult
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace PARR.Core.Services.UnitFilterService.Models
|
namespace PARR.Core.Services.UnitFilterService.Models
|
||||||
{
|
{
|
||||||
internal class FilteredUnitContext
|
internal class UnitFilterMatchResult
|
||||||
{
|
{
|
||||||
public Guid UnitId { get; set; }
|
public Guid UnitId { get; set; }
|
||||||
public HashSet<Guid> ValidParentIds { get; set; } = new();
|
public HashSet<Guid> ValidParentIds { get; set; } = new();
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
{
|
{
|
||||||
internal class UnitFilterServiceOptions
|
internal class UnitFilterServiceOptions
|
||||||
{
|
{
|
||||||
public int LoadBatchSize { get; set; } = 50;
|
public int LoadBatchSize { get; set; } = 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace PARR.Core.Services.UnitFilterService;
|
|||||||
internal class UnitFilterService : IUnitFilterService
|
internal class UnitFilterService : IUnitFilterService
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private readonly Guid targetUnitId = Guid.Parse("9d88fff2-a861-487f-b73d-bce1f0218e9f");
|
private readonly Guid debugTargetUnitId = Guid.Parse("f2017292-193c-48e9-b333-3a00e737c6fc");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private const int DebugMaxUnitsToLog = 10;
|
private const int DebugMaxUnitsToLog = 10;
|
||||||
@@ -94,7 +94,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count);
|
logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count);
|
||||||
|
|
||||||
// Собираем все контексты юнитов, прошедших фильтрацию
|
// Собираем все контексты юнитов, прошедших фильтрацию
|
||||||
var allFilteredContexts = new List<FilteredUnitContext>();
|
var allFilteredContexts = new List<UnitFilterMatchResult>();
|
||||||
|
|
||||||
// Преобразуем в список для индексации
|
// Преобразуем в список для индексации
|
||||||
var unitFiltersList = job.UnitFilters.ToList();
|
var unitFiltersList = job.UnitFilters.ToList();
|
||||||
@@ -118,9 +118,9 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (initialUnitIds.Contains(targetUnitId))
|
if (initialUnitIds.Contains(debugTargetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", targetUnitId, i + 1);
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", debugTargetUnitId, i + 1);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -136,13 +136,13 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (initialUnitIds.Contains(targetUnitId) && !fieldFilteredIds.Contains(targetUnitId))
|
if (initialUnitIds.Contains(debugTargetUnitId) && !fieldFilteredIds.Contains(debugTargetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", targetUnitId, i + 1);
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", debugTargetUnitId, i + 1);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ FilteredUnitContext
|
// 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ UnitFilterMatchResult
|
||||||
var relStopwatch = Stopwatch.StartNew();
|
var relStopwatch = Stopwatch.StartNew();
|
||||||
var relationshipFilteredContexts = await ProcessRelationshipFiltersAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken);
|
var relationshipFilteredContexts = await ProcessRelationshipFiltersAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken);
|
||||||
relStopwatch.Stop();
|
relStopwatch.Stop();
|
||||||
@@ -154,11 +154,11 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == targetUnitId);
|
var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||||
if (targetContext != null)
|
if (targetContext != null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} прошёл фильтр #{Index}. Родителей: {ParentCount}, Детей: {ChildCount}",
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} прошёл фильтр #{Index}. Родителей: {ParentCount}, Детей: {ChildCount}",
|
||||||
targetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count);
|
debugTargetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
// Убираем дубликаты по UnitId, объединяя связи
|
// Убираем дубликаты по UnitId, объединяя связи
|
||||||
var mergedContexts = allFilteredContexts
|
var mergedContexts = allFilteredContexts
|
||||||
.GroupBy(c => c.UnitId)
|
.GroupBy(c => c.UnitId)
|
||||||
.Select(g => new FilteredUnitContext
|
.Select(g => new UnitFilterMatchResult
|
||||||
{
|
{
|
||||||
UnitId = g.Key,
|
UnitId = g.Key,
|
||||||
ValidParentIds = new HashSet<Guid>(g.SelectMany(c => c.ValidParentIds)),
|
ValidParentIds = new HashSet<Guid>(g.SelectMany(c => c.ValidParentIds)),
|
||||||
@@ -204,7 +204,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
job.MinValueRelationships, job.MaxValueRelationships, job.IsParentRelationships);
|
job.MinValueRelationships, job.MaxValueRelationships, job.IsParentRelationships);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var targetBeforeUmbrella = mergedContexts.FirstOrDefault(c => c.UnitId == targetUnitId);
|
var targetBeforeUmbrella = mergedContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||||
if (targetBeforeUmbrella != null)
|
if (targetBeforeUmbrella != null)
|
||||||
{
|
{
|
||||||
var count = job.IsParentRelationships == true
|
var count = job.IsParentRelationships == true
|
||||||
@@ -213,7 +213,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
var passes = count >= (job.MinValueRelationships ?? 0) && count <= (job.MaxValueRelationships ?? int.MaxValue);
|
var passes = count >= (job.MinValueRelationships ?? 0) && count <= (job.MaxValueRelationships ?? int.MaxValue);
|
||||||
|
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} перед Umbrella: Count={Count}, Min={Min}, Max={Max}, Passes={Passes}",
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} перед Umbrella: Count={Count}, Min={Min}, Max={Max}, Passes={Passes}",
|
||||||
targetUnitId, count, job.MinValueRelationships, job.MaxValueRelationships, passes);
|
debugTargetUnitId, count, job.MinValueRelationships, job.MaxValueRelationships, passes);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -222,10 +222,10 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
logger.LogInformation("Этап фильтрации по числу связей завершён: после Umbrella-фильтра осталось {UnitCount} юнитов", umbrellaFilteredContexts.Count);
|
logger.LogInformation("Этап фильтрации по числу связей завершён: после Umbrella-фильтра осталось {UnitCount} юнитов", umbrellaFilteredContexts.Count);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var targetAfterUmbrella = umbrellaFilteredContexts.FirstOrDefault(c => c.UnitId == targetUnitId);
|
var targetAfterUmbrella = umbrellaFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||||
if (targetBeforeUmbrella != null && targetAfterUmbrella == null)
|
if (targetBeforeUmbrella != null && targetAfterUmbrella == null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", targetUnitId);
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", debugTargetUnitId);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,6 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
|
private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await jobService
|
return await jobService
|
||||||
@@ -294,39 +293,26 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
logger.LogDebug(" FieldFilter #{Index}: Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}",
|
logger.LogDebug(" FieldFilter #{Index}: Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}",
|
||||||
filterIndex, fieldName, dbValueMask, fieldFilter.IsInverse);
|
filterIndex, fieldName, dbValueMask, fieldFilter.IsInverse);
|
||||||
|
|
||||||
//if (fieldFilter.IsInverse)
|
|
||||||
//{
|
|
||||||
// query = query.Where(u => !u.UnitValues.Any(v =>
|
|
||||||
// v.FieldId == fieldFilter.FieldId &&
|
|
||||||
// EF.Functions.ILike(v.Value.Value, dbValueMask)));
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
//{
|
|
||||||
// query = query.Where(u => u.UnitValues.Any(v =>
|
|
||||||
// v.FieldId == fieldFilter.FieldId &&
|
|
||||||
// EF.Functions.ILike(v.Value.Value, dbValueMask)));
|
|
||||||
//}
|
|
||||||
|
|
||||||
query = unitService.GetUnitByFieldAndValue(query, fieldFilter.FieldId, fieldFilter.ValueMask!, fieldFilter.IsInverse);
|
query = unitService.GetUnitByFieldAndValue(query, fieldFilter.FieldId, fieldFilter.ValueMask!, fieldFilter.IsInverse);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (unitIds.Contains(targetUnitId))
|
if (unitIds.Contains(debugTargetUnitId))
|
||||||
{
|
{
|
||||||
var unitHasField = await unitInValueService.Get()
|
var unitHasField = await unitInValueService.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AnyAsync(uiv => uiv.UnitId == targetUnitId && uiv.FieldId == fieldFilter.FieldId, cancellationToken);
|
.AnyAsync(uiv => uiv.UnitId == debugTargetUnitId && uiv.FieldId == fieldFilter.FieldId, cancellationToken);
|
||||||
|
|
||||||
var unitValue = await unitInValueService.Get()
|
var unitValue = await unitInValueService.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(uiv => uiv.UnitId == targetUnitId && uiv.FieldId == fieldFilter.FieldId)
|
.Where(uiv => uiv.UnitId == debugTargetUnitId && uiv.FieldId == fieldFilter.FieldId)
|
||||||
.Select(uiv => uiv.Value.Value)
|
.Select(uiv => uiv.Value.Value)
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
var intermediateResult = await query.Select(u => u.Id).ToListAsync(cancellationToken);
|
var intermediateResult = await query.Select(u => u.Id).ToListAsync(cancellationToken);
|
||||||
var passes = intermediateResult.Contains(targetUnitId);
|
var passes = intermediateResult.Contains(debugTargetUnitId);
|
||||||
|
|
||||||
logger.LogDebug(" DEBUG: Юнит {TargetUnitId}: Поле={FieldName}, Значение={Value}, Маска={Mask}, HasField={HasField}, Проходит={Passes}",
|
logger.LogDebug(" DEBUG: Юнит {TargetUnitId}: Поле={FieldName}, Значение={Value}, Маска={Mask}, HasField={HasField}, Проходит={Passes}",
|
||||||
targetUnitId, fieldName, unitValue ?? "null", dbValueMask, unitHasField, passes);
|
debugTargetUnitId, fieldName, unitValue ?? "null", dbValueMask, unitHasField, passes);
|
||||||
|
|
||||||
query = unitService.Get().AsNoTracking()
|
query = unitService.Get().AsNoTracking()
|
||||||
.Where(u => intermediateResult.Contains(u.Id));
|
.Where(u => intermediateResult.Contains(u.Id));
|
||||||
@@ -344,7 +330,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Применяем фильтры аттрибутов у связанных ЭК
|
/// Применяем фильтры аттрибутов у связанных ЭК
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<List<FilteredUnitContext>> ProcessRelationshipFiltersAsync(
|
private async Task<List<UnitFilterMatchResult>> ProcessRelationshipFiltersAsync(
|
||||||
List<Guid> unitIds,
|
List<Guid> unitIds,
|
||||||
IEnumerable<JobRelationshipFilter> relationshipFilters,
|
IEnumerable<JobRelationshipFilter> relationshipFilters,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -353,29 +339,29 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
{
|
{
|
||||||
logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений",
|
logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений",
|
||||||
unitIds.Count);
|
unitIds.Count);
|
||||||
return unitIds.Select(id => new FilteredUnitContext { UnitId = id }).ToList();
|
return unitIds.Select(id => new UnitFilterMatchResult { UnitId = id }).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}",
|
logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}",
|
||||||
unitIds.Count, relationshipFilters.Count());
|
unitIds.Count, relationshipFilters.Count());
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (unitIds.Contains(targetUnitId))
|
if (unitIds.Contains(debugTargetUnitId))
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден во входных данных ProcessRelationshipFiltersAsync", targetUnitId);
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден во входных данных ProcessRelationshipFiltersAsync", debugTargetUnitId);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id });
|
var preFilteredUnitsById = unitIds.ToDictionary(id => id, id => new UnitFilterMatchResult { UnitId = id });
|
||||||
|
|
||||||
var parentRelFilters = relationshipFilters.Where(rf => rf.IsParent).ToList();
|
var parentRelFilters = relationshipFilters.Where(rf => rf.IsParent).ToList();
|
||||||
var childRelFilters = relationshipFilters.Where(rf => !rf.IsParent).ToList();
|
var childRelFilters = relationshipFilters.Where(rf => !rf.IsParent).ToList();
|
||||||
|
|
||||||
if (parentRelFilters.Any())
|
if (parentRelFilters.Any())
|
||||||
{
|
{
|
||||||
await ApplyRelationshipFiltersOnDbAsync(
|
await ApplyRelationshipFiltersToUnitsAsync(
|
||||||
unitIds,
|
unitIds,
|
||||||
resultContexts,
|
preFilteredUnitsById,
|
||||||
parentRelFilters,
|
parentRelFilters,
|
||||||
isParentDirection: true,
|
isParentDirection: true,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
@@ -383,48 +369,48 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
|
|
||||||
if (childRelFilters.Any())
|
if (childRelFilters.Any())
|
||||||
{
|
{
|
||||||
await ApplyRelationshipFiltersOnDbAsync(
|
await ApplyRelationshipFiltersToUnitsAsync(
|
||||||
unitIds,
|
unitIds,
|
||||||
resultContexts,
|
preFilteredUnitsById,
|
||||||
childRelFilters,
|
childRelFilters,
|
||||||
isParentDirection: false,
|
isParentDirection: false,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var unitsWithAnyConnections = resultContexts.Values.Count(c => c.ValidParentIds.Any() || c.ValidChildIds.Any());
|
var unitsWithAnyConnections = preFilteredUnitsById.Values.Count(c => c.ValidParentIds.Any() || c.ValidChildIds.Any());
|
||||||
var unitsWithoutConnections = resultContexts.Values.Count(c => !c.ValidParentIds.Any() && !c.ValidChildIds.Any());
|
var unitsWithoutConnections = preFilteredUnitsById.Values.Count(c => !c.ValidParentIds.Any() && !c.ValidChildIds.Any());
|
||||||
logger.LogDebug("DEBUG: После ApplyRelationshipFiltersOnDbAsync: {WithConnections} юнитов со связями, {WithoutConnections} без связей",
|
logger.LogDebug("DEBUG: После ApplyRelationshipFiltersToUnitsAsync: {WithConnections} юнитов со связями, {WithoutConnections} без связей",
|
||||||
unitsWithAnyConnections, unitsWithoutConnections);
|
unitsWithAnyConnections, unitsWithoutConnections);
|
||||||
|
|
||||||
var targetContext = resultContexts.Values.FirstOrDefault(c => c.UnitId == targetUnitId);
|
var targetContext = preFilteredUnitsById.Values.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||||
if (targetContext != null)
|
if (targetContext != null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId}: Родителей={ParentCount}, Детей={ChildCount}, PassesFilter={Passes}",
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId}: Родителей={ParentCount}, Детей={ChildCount}, PassesFilter={Passes}",
|
||||||
targetUnitId,
|
debugTargetUnitId,
|
||||||
targetContext.ValidParentIds.Count,
|
targetContext.ValidParentIds.Count,
|
||||||
targetContext.ValidChildIds.Count,
|
targetContext.ValidChildIds.Count,
|
||||||
targetContext.ValidParentIds.Any() || targetContext.ValidChildIds.Any());
|
targetContext.ValidParentIds.Any() || targetContext.ValidChildIds.Any());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var result = resultContexts.Values.ToList();
|
var result = preFilteredUnitsById.Values.ToList();
|
||||||
|
|
||||||
logger.LogDebug("ProcessRelationshipFiltersAsync: выход {ContextCount} контекстов (из {InitialCount})",
|
logger.LogDebug("ProcessRelationshipFiltersAsync: выход {ContextCount} контекстов (из {InitialCount})",
|
||||||
result.Count, resultContexts.Count);
|
result.Count, preFilteredUnitsById.Count);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (unitIds.Contains(targetUnitId))
|
if (unitIds.Contains(debugTargetUnitId))
|
||||||
{
|
{
|
||||||
var targetContextInResult = result.FirstOrDefault(c => c.UnitId == targetUnitId);
|
var targetContextInResult = result.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||||
if (targetContextInResult != null)
|
if (targetContextInResult != null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} в результатах ProcessRelationshipFiltersAsync. Родителей: {ParentCount}, Детей: {ChildCount}",
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} в результатах ProcessRelationshipFiltersAsync. Родителей: {ParentCount}, Детей: {ChildCount}",
|
||||||
targetUnitId, targetContextInResult.ValidParentIds.Count, targetContextInResult.ValidChildIds.Count);
|
debugTargetUnitId, targetContextInResult.ValidParentIds.Count, targetContextInResult.ValidChildIds.Count);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ в результатах ProcessRelationshipFiltersAsync (удалён)", targetUnitId);
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ в результатах ProcessRelationshipFiltersAsync (удалён)", debugTargetUnitId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +522,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
/// <param name="contexts"></param>
|
/// <param name="contexts"></param>
|
||||||
/// <param name="job"></param>
|
/// <param name="job"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private List<FilteredUnitContext> ApplyUmbrellaFilterInMemory(List<FilteredUnitContext> contexts, Job job)
|
private List<UnitFilterMatchResult> ApplyUmbrellaFilterInMemory(List<UnitFilterMatchResult> contexts, Job job)
|
||||||
{
|
{
|
||||||
var min = job.MinValueRelationships ?? 0;
|
var min = job.MinValueRelationships ?? 0;
|
||||||
var max = job.MaxValueRelationships ?? int.MaxValue;
|
var max = job.MaxValueRelationships ?? int.MaxValue;
|
||||||
@@ -545,17 +531,17 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
if (min == 0 && max == int.MaxValue)
|
if (min == 0 && max == int.MaxValue)
|
||||||
return contexts;
|
return contexts;
|
||||||
|
|
||||||
var result = new List<FilteredUnitContext>();
|
var result = new List<UnitFilterMatchResult>();
|
||||||
foreach (var context in contexts)
|
foreach (var context in contexts)
|
||||||
{
|
{
|
||||||
int count = isParentDirection ? context.ValidParentIds.Count : context.ValidChildIds.Count;
|
int count = isParentDirection ? context.ValidParentIds.Count : context.ValidChildIds.Count;
|
||||||
bool passes = count >= min && count <= max;
|
bool passes = count >= min && count <= max;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (context.UnitId == targetUnitId)
|
if (context.UnitId == debugTargetUnitId)
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG Umbrella: Юнит {TargetUnitId}, Count={Count}, Min={Min}, Max={Max}, Passes={Passes}",
|
logger.LogDebug("DEBUG Umbrella: Юнит {TargetUnitId}, Count={Count}, Min={Min}, Max={Max}, Passes={Passes}",
|
||||||
targetUnitId, count, min, max, passes);
|
debugTargetUnitId, count, min, max, passes);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -574,20 +560,20 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="contexts"></param>
|
/// <param name="contexts"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task<List<UnitFilterResultDto>> LoadFinalResultAsync(List<FilteredUnitContext> contexts, CancellationToken cancellationToken = default)
|
private async Task<List<UnitFilterResultDto>> LoadFinalResultAsync(List<UnitFilterMatchResult> contexts, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var allResults = new List<UnitFilterResultDto>();
|
var allResults = new List<UnitFilterResultDto>();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var targetInContexts = contexts.FirstOrDefault(c => c.UnitId == targetUnitId);
|
var targetInContexts = contexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||||
if (targetInContexts != null)
|
if (targetInContexts != null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} передан в LoadFinalResultAsync. Родителей: {ParentCount}, Детей: {ChildCount}",
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} передан в LoadFinalResultAsync. Родителей: {ParentCount}, Детей: {ChildCount}",
|
||||||
targetUnitId, targetInContexts.ValidParentIds.Count, targetInContexts.ValidChildIds.Count);
|
debugTargetUnitId, targetInContexts.ValidParentIds.Count, targetInContexts.ValidChildIds.Count);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", targetUnitId);
|
logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", debugTargetUnitId);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -607,7 +593,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="batch"></param>
|
/// <param name="batch"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task<List<UnitFilterResultDto>> LoadBatchAsync(List<FilteredUnitContext> batch, CancellationToken cancellationToken = default)
|
private async Task<List<UnitFilterResultDto>> LoadBatchAsync(List<UnitFilterMatchResult> batch, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var unitIds = batch.Select(c => c.UnitId).ToList();
|
var unitIds = batch.Select(c => c.UnitId).ToList();
|
||||||
var allParentIds = batch.SelectMany(c => c.ValidParentIds).Distinct().ToList();
|
var allParentIds = batch.SelectMany(c => c.ValidParentIds).Distinct().ToList();
|
||||||
@@ -786,12 +772,14 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
|
|
||||||
|
|
||||||
#region вспомогательные методы
|
#region вспомогательные методы
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Применяет фильтры к связям (родителям или детям)
|
/// Применяет фильтры к связям (родителям или детям)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ApplyRelationshipFiltersOnDbAsync(
|
private async Task ApplyRelationshipFiltersToUnitsAsync(
|
||||||
List<Guid> unitIds,
|
List<Guid> unitIds,
|
||||||
Dictionary<Guid, FilteredUnitContext> resultContexts,
|
Dictionary<Guid, UnitFilterMatchResult> preFilteredUnitsById,
|
||||||
List<JobRelationshipFilter> relFilters,
|
List<JobRelationshipFilter> relFilters,
|
||||||
bool isParentDirection,
|
bool isParentDirection,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -802,6 +790,7 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
var directionName = isParentDirection ? "Родительские" : "Дочерние";
|
var directionName = isParentDirection ? "Родительские" : "Дочерние";
|
||||||
logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count);
|
logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count);
|
||||||
|
|
||||||
|
// 1. Получить все связи (unitInUnit) для юнитов из unitIds
|
||||||
var allLinks = await unitInUnitService.Get()
|
var allLinks = await unitInUnitService.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(link => isParentDirection
|
.Where(link => isParentDirection
|
||||||
@@ -814,16 +803,18 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var allTargetIds = allLinks.Select(l => l.TargetId).Distinct().ToList();
|
var allRelatedUnitIds = allLinks.Select(l => l.TargetId).Distinct().ToList();
|
||||||
logger.LogDebug(" Найдено {TargetCount} уникальных {TargetType} для {LinkCount} связей",
|
logger.LogDebug(" Найдено {TargetCount} уникальных {TargetType} для {LinkCount} связей",
|
||||||
allTargetIds.Count,
|
allRelatedUnitIds.Count,
|
||||||
isParentDirection ? "родителей" : "детей",
|
isParentDirection ? "родителей" : "детей",
|
||||||
allLinks.Count);
|
allLinks.Count);
|
||||||
|
|
||||||
var linksBySource = allLinks.GroupBy(l => l.SourceId)
|
// 2. Сгруппировать связи по SourceId (ID юнита из unitIds)
|
||||||
|
var relationshipsByUnitId = allLinks.GroupBy(l => l.SourceId)
|
||||||
.ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList());
|
.ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList());
|
||||||
|
|
||||||
var targetFilterPassCount = new Dictionary<Guid, int>();
|
// 3. Для каждого фильтра в группе найдём TargetId, которые ему соответствуют (с учётом IsInverse)
|
||||||
|
var matchedTargetIdsByFilter = new Dictionary<JobRelationshipFilter, HashSet<Guid>>();
|
||||||
|
|
||||||
foreach (var relFilter in relFilters)
|
foreach (var relFilter in relFilters)
|
||||||
{
|
{
|
||||||
@@ -832,137 +823,156 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
string dbValueMask = NormalizeLikeMask(valueMask);
|
string dbValueMask = NormalizeLikeMask(valueMask);
|
||||||
|
|
||||||
var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}";
|
var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}";
|
||||||
|
|
||||||
logger.LogDebug(" RelationshipFilter ({Direction}): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}",
|
logger.LogDebug(" RelationshipFilter ({Direction}): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}",
|
||||||
isParentDirection ? "Parent" : "Child",
|
isParentDirection ? "Parent" : "Child",
|
||||||
fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);
|
fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);
|
||||||
|
|
||||||
//var matchingTargetIds = await unitInValueService.Get()
|
|
||||||
// .AsNoTracking()
|
|
||||||
// .Where(uiv => uiv.FieldId == relFilter.FieldId)
|
|
||||||
// .Where(uiv => EF.Functions.ILike(uiv.Value.Value, dbValueMask))
|
|
||||||
// .Select(uiv => uiv.UnitId)
|
|
||||||
// .Distinct()
|
|
||||||
// .ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var matchingTargetIds = await unitInValueService.GetMatchingTargetIds(relFilter.FieldId, dbValueMask)
|
var matchingTargetIds = await unitInValueService.GetMatchingTargetIds(relFilter.FieldId, dbValueMask)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Ограничиваем найденные ID только теми, которые действительно связаны с юнитами из unitIds
|
||||||
var filteredMatchingTargetIds = matchingTargetIds
|
var filteredMatchingTargetIds = matchingTargetIds
|
||||||
.Intersect(allTargetIds)
|
.Intersect(allRelatedUnitIds)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске",
|
logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске",
|
||||||
filteredMatchingTargetIds.Count,
|
filteredMatchingTargetIds.Count,
|
||||||
isParentDirection ? "родителей" : "детей");
|
isParentDirection ? "родителей" : "детей");
|
||||||
|
|
||||||
|
HashSet<Guid> targetIdsThatPassThisFilter;
|
||||||
|
|
||||||
if (relFilter.IsInverse)
|
if (relFilter.IsInverse)
|
||||||
{
|
{
|
||||||
foreach (var targetId in allTargetIds)
|
// Юнит проходит фильтр, если его значение НЕ соответствует маске
|
||||||
{
|
// Это означает, что юниты, НЕ входящие в filteredMatchingTargetIds, проходят фильтр
|
||||||
if (!filteredMatchingTargetIds.Contains(targetId))
|
targetIdsThatPassThisFilter = new HashSet<Guid>(allRelatedUnitIds.Except(filteredMatchingTargetIds));
|
||||||
{
|
logger.LogDebug(" (IsInverse) Юниты, прошедшие фильтр: {Count}", targetIdsThatPassThisFilter.Count);
|
||||||
if (!targetFilterPassCount.ContainsKey(targetId))
|
|
||||||
targetFilterPassCount[targetId] = 0;
|
|
||||||
targetFilterPassCount[targetId]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogDebug(" Для IsInverse: {NonMatchCount} {TargetType} НЕ соответствуют маске",
|
|
||||||
allTargetIds.Count - filteredMatchingTargetIds.Count,
|
|
||||||
isParentDirection ? "родителей" : "детей");
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var targetId in filteredMatchingTargetIds)
|
// Юнит проходит фильтр, если его значение соответствует маске
|
||||||
{
|
targetIdsThatPassThisFilter = new HashSet<Guid>(filteredMatchingTargetIds);
|
||||||
if (!targetFilterPassCount.ContainsKey(targetId))
|
logger.LogDebug(" (Direct) Юниты, прошедшие фильтр: {Count}", targetIdsThatPassThisFilter.Count);
|
||||||
targetFilterPassCount[targetId] = 0;
|
|
||||||
targetFilterPassCount[targetId]++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сохраняем результат для этого конкретного фильтра
|
||||||
|
matchedTargetIdsByFilter[relFilter] = targetIdsThatPassThisFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim()));
|
// 4. Определим, какие юниты (context.UnitId) проходят ВСЕ фильтры с учётом IsFullMatch
|
||||||
var validTargetIds = targetFilterPassCount
|
var unitIdsToRemove = new HashSet<Guid>();
|
||||||
.Where(kvp => kvp.Value == requiredFiltersCount)
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
logger.LogDebug(" Найдено {ValidCount} {TargetType}, прошедших ВСЕ {RequiredCount} фильтров",
|
foreach (var context in preFilteredUnitsById.Values)
|
||||||
validTargetIds.Count,
|
|
||||||
isParentDirection ? "родителей" : "детей",
|
|
||||||
requiredFiltersCount);
|
|
||||||
|
|
||||||
var unitsToRemove = new HashSet<Guid>();
|
|
||||||
|
|
||||||
foreach (var context in resultContexts.Values)
|
|
||||||
{
|
{
|
||||||
if (unitsToRemove.Contains(context.UnitId))
|
// Получаем связанные юниты для конкретного context.UnitId
|
||||||
continue;
|
var relatedUnitIds = relationshipsByUnitId.GetValueOrDefault(context.UnitId, new List<Guid>()).ToHashSet();
|
||||||
|
bool hasTargets = relatedUnitIds.Count > 0;
|
||||||
var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List<Guid>());
|
|
||||||
bool hasTargets = unitTargets.Any();
|
|
||||||
|
|
||||||
var validTargets = unitTargets.Intersect(validTargetIds).ToList();
|
|
||||||
bool hasValidTargets = validTargets.Any();
|
|
||||||
|
|
||||||
bool isFullMatch = relFilters.All(rf => rf.IsFullMatch);
|
|
||||||
bool isInverse = relFilters.Any(rf => rf.IsInverse);
|
|
||||||
|
|
||||||
bool passesFilter;
|
|
||||||
|
|
||||||
if (!hasTargets)
|
if (!hasTargets)
|
||||||
{
|
{
|
||||||
passesFilter = isInverse;
|
// Нет связей - проверяем, есть ли фильтры, которые требуют наличия связей
|
||||||
|
// Если есть хотя бы один фильтр с маской, и связей нет - юнит не проходит.
|
||||||
|
var hasFiltersWithMask = relFilters.Any(rf => !string.IsNullOrWhiteSpace(rf.ValueMask?.Trim()));
|
||||||
|
if (hasFiltersWithMask)
|
||||||
|
{
|
||||||
|
unitIdsToRemove.Add(context.UnitId);
|
||||||
|
continue; // Переходим к следующему юниту
|
||||||
|
}
|
||||||
|
// Если фильтров с маской нет, юнит остаётся.
|
||||||
}
|
}
|
||||||
else if (isFullMatch)
|
|
||||||
|
bool unitPassesCurrentFilterGroup = true;
|
||||||
|
|
||||||
|
// Проверяем, проходит ли юнит все фильтры в группе
|
||||||
|
foreach (var relFilter in relFilters)
|
||||||
{
|
{
|
||||||
passesFilter = validTargets.Count == unitTargets.Count;
|
var valueMask = relFilter.ValueMask?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(valueMask))
|
||||||
|
{
|
||||||
|
continue; // Пустой фильтр пропускаем
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем юниты, прошедшие конкретный фильтр relFilter
|
||||||
|
var targetIdsThatPassThisFilter = matchedTargetIdsByFilter[relFilter];
|
||||||
|
|
||||||
|
if (relFilter.IsFullMatch)
|
||||||
|
{
|
||||||
|
// ВСЕ relatedUnitIds должны пройти этот конкретный фильтр relFilter
|
||||||
|
// Это означает, что каждый relatedUnitId должен быть в targetIdsThatPassThisFilter
|
||||||
|
bool allTargetsPassThisFilter = relatedUnitIds.All(targetId => targetIdsThatPassThisFilter.Contains(targetId));
|
||||||
|
if (!allTargetsPassThisFilter)
|
||||||
|
{
|
||||||
|
unitPassesCurrentFilterGroup = false;
|
||||||
|
break; // Не нужно проверять остальные фильтры, юнит заведомо не проходит
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // IsFullMatch = false
|
||||||
|
{
|
||||||
|
// Хоть один relatedUnitId должен пройти этот конкретный фильтр relFilter
|
||||||
|
// Это означает, что хотя бы один relatedUnitId должен быть в targetIdsThatPassThisFilter
|
||||||
|
bool anyTargetPassesThisFilter = relatedUnitIds.Any(targetId => targetIdsThatPassThisFilter.Contains(targetId));
|
||||||
|
if (!anyTargetPassesThisFilter)
|
||||||
|
{
|
||||||
|
unitPassesCurrentFilterGroup = false;
|
||||||
|
break; // Не нужно проверять остальные фильтры, юнит заведимо не проходит
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unitPassesCurrentFilterGroup)
|
||||||
|
{
|
||||||
|
unitIdsToRemove.Add(context.UnitId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
passesFilter = hasValidTargets;
|
// Юнит прошёл все фильтры.
|
||||||
}
|
// Теперь определим, какие из его связей (relatedUnitIds) идут в ValidParentIds/ValidChildIds.
|
||||||
|
// Это должны быть связи, прошедшие *все* фильтры (т.е. быть в пересечении результатов для каждого фильтра).
|
||||||
|
// Соберём пересечение всех targetId, прошедших каждый фильтр для этого конкретного юнита.
|
||||||
|
// Начинаем с множества всех его связей.
|
||||||
|
var validRelatedIds = new HashSet<Guid>(relatedUnitIds);
|
||||||
|
|
||||||
if (passesFilter)
|
foreach (var relFilter in relFilters)
|
||||||
{
|
|
||||||
if (isFullMatch)
|
|
||||||
{
|
{
|
||||||
if (isParentDirection)
|
var valueMask = relFilter.ValueMask?.Trim();
|
||||||
context.ValidParentIds.UnionWith(unitTargets);
|
if (string.IsNullOrWhiteSpace(valueMask))
|
||||||
else
|
continue;
|
||||||
context.ValidChildIds.UnionWith(unitTargets);
|
|
||||||
|
var targetIdsThatPassThisFilter = matchedTargetIdsByFilter[relFilter];
|
||||||
|
// Пересекаем текущий список validRelatedIds с результатами для этого фильтра
|
||||||
|
validRelatedIds.IntersectWith(targetIdsThatPassThisFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем полученные валидные связи в контекст юнита
|
||||||
|
if (isParentDirection)
|
||||||
|
{
|
||||||
|
context.ValidParentIds.UnionWith(validRelatedIds);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (isParentDirection)
|
context.ValidChildIds.UnionWith(validRelatedIds);
|
||||||
context.ValidParentIds.UnionWith(validTargets);
|
|
||||||
else
|
|
||||||
context.ValidChildIds.UnionWith(validTargets);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
unitsToRemove.Add(context.UnitId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var unitId in unitsToRemove)
|
// Удаляем юниты, которые не прошли проверку
|
||||||
|
foreach (var unitId in unitIdsToRemove)
|
||||||
{
|
{
|
||||||
resultContexts.Remove(unitId);
|
preFilteredUnitsById.Remove(unitId);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug(" Удалено {RemovedCount} юнитов, не прошедших фильтры", unitsToRemove.Count);
|
logger.LogDebug(" Удалено {RemovedCount} юнитов, не прошедших фильтры (новая логика с IsFullMatch/IsInverse для каждого фильтра)",
|
||||||
|
unitIdsToRemove.Count);
|
||||||
|
|
||||||
var totalAdded = resultContexts.Values.Sum(c =>
|
var totalAdded = preFilteredUnitsById.Values.Sum(c =>
|
||||||
isParentDirection ? c.ValidParentIds.Count : c.ValidChildIds.Count);
|
isParentDirection ? c.ValidParentIds.Count : c.ValidChildIds.Count);
|
||||||
|
|
||||||
logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов",
|
logger.LogDebug(" Добавлено {Total} {TargetType} связей для {UnitCount} юнитов (новая логика)",
|
||||||
totalAdded,
|
totalAdded,
|
||||||
isParentDirection ? "родительских" : "дочерних",
|
isParentDirection ? "родительских" : "дочерних",
|
||||||
resultContexts.Count);
|
preFilteredUnitsById.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -979,11 +989,6 @@ internal class UnitFilterService : IUnitFilterService
|
|||||||
|
|
||||||
var dbValueMask = NormalizeLikeMask(filter.UnitFilter);
|
var dbValueMask = NormalizeLikeMask(filter.UnitFilter);
|
||||||
|
|
||||||
//var initialUnitIds = await unitService.Get().AsNoTracking()
|
|
||||||
// .Where(unit => EF.Functions.ILike(unit.Name, dbValueMask))
|
|
||||||
// .Select(u => u.Id)
|
|
||||||
// .ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var initialUnitIds = await unitService.GetInitialUnitIds(dbValueMask).ToListAsync(cancellationToken);
|
var initialUnitIds = await unitService.GetInitialUnitIds(dbValueMask).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var toCache = new UnitFilterIds
|
var toCache = new UnitFilterIds
|
||||||
|
|||||||
@@ -161,17 +161,17 @@ namespace PARR.DAL.Repositories
|
|||||||
|
|
||||||
var reservedTemplateId = result.FirstOrDefault();
|
var reservedTemplateId = result.FirstOrDefault();
|
||||||
|
|
||||||
if (reservedTemplateId != default(Guid))
|
if (reservedTemplateId != Guid.Empty)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Успешно зарезервирован шаблон с ID: {TemplateId} для UnitId: {UnitId}",
|
logger.LogInformation("Успешно зарезервирован шаблон с ID: {TemplateId} для UnitId: {UnitId}",
|
||||||
reservedTemplateId, newUnitId);
|
reservedTemplateId, newUnitId);
|
||||||
|
return reservedTemplateId;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Не удалось зарезервировать шаблон для UnitId: {UnitId} (не найдено подходящих конфигураций роботов)", newUnitId);
|
logger.LogDebug("Не удалось зарезервировать шаблон для UnitId: {UnitId} (не найдено подходящих конфигураций роботов)", newUnitId);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return reservedTemplateId;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,15 +8,8 @@ namespace PARR.Domain.Entities.Job
|
|||||||
[Table("RelationshipFilters", Schema = DatabaseSchemas.Job)]
|
[Table("RelationshipFilters", Schema = DatabaseSchemas.Job)]
|
||||||
[Comment("Таблица фильтров связей ЭК")]
|
[Comment("Таблица фильтров связей ЭК")]
|
||||||
[PrimaryKey(nameof(UnitFilterId), nameof(FieldId))]
|
[PrimaryKey(nameof(UnitFilterId), nameof(FieldId))]
|
||||||
public class JobRelationshipFilter //: IBase
|
public class JobRelationshipFilter
|
||||||
{
|
{
|
||||||
//[Key]
|
|
||||||
//public Guid Id { get; set; }
|
|
||||||
|
|
||||||
//public DateTimeOffset DateCreated { get; set; }
|
|
||||||
|
|
||||||
//public DateTimeOffset? DateModified { get; set; }
|
|
||||||
|
|
||||||
public Guid UnitFilterId { get; set; }
|
public Guid UnitFilterId { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Родительская связь - true,
|
/// Родительская связь - true,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ 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 IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
|
||||||
|
|
||||||
public GroupedTemplateSynchronizer(
|
public GroupedTemplateSynchronizer(
|
||||||
ILogger<GroupedTemplateSynchronizer> logger,
|
ILogger<GroupedTemplateSynchronizer> logger,
|
||||||
@@ -54,7 +54,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
ITemplateDeactivator templateDeactivator,
|
ITemplateDeactivator templateDeactivator,
|
||||||
ITemplateNameNormalizer templateNameNormalizer,
|
ITemplateNameNormalizer templateNameNormalizer,
|
||||||
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
||||||
IMatchingStatusService matchingStatusService
|
IMatchingStatusService matchingStatusService,
|
||||||
|
IUnitInTemplateConflictMapper unitInTemplateConflictMapper
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
@@ -72,12 +73,14 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
this.templateNameNormalizer = templateNameNormalizer;
|
this.templateNameNormalizer = templateNameNormalizer;
|
||||||
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
||||||
this.matchingStatusService = matchingStatusService;
|
this.matchingStatusService = matchingStatusService;
|
||||||
|
this.unitInTemplateConflictMapper = unitInTemplateConflictMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||||
{
|
{
|
||||||
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);
|
||||||
@@ -154,6 +157,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
{
|
{
|
||||||
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
|
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
|
||||||
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
|
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
|
||||||
|
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
|
||||||
|
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,38 +236,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ПОСТРОЕНИЕ ОБРАТНОГО ОТОБРАЖЕНИЯ (после фильтрации) ---
|
|
||||||
logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
|
logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
|
||||||
var reverseMapping = new Dictionary<Guid, List<Guid>>();
|
var reverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnitFilterResults, maxJob);
|
||||||
foreach (var dto in finalFilteredUnitFilterResults)
|
|
||||||
{
|
|
||||||
List<Guid> relatedUnitIds;
|
|
||||||
if (maxJob.IsParentRelationships == true)
|
|
||||||
{
|
|
||||||
// dto.Id - это ParentUnitId, связанные - ChildUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate
|
|
||||||
// relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона
|
|
||||||
relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// dto.Id - это ChildUnitId, связанные - ParentUnitIds (expectedUnitIds) -> dto.Id идет в UnitsInTemplate
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
|
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
|
||||||
|
|
||||||
@@ -288,7 +263,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
// Создадим маппинг UnitId (из UnitsInTemplate) -> значение поля для внутренней группировки
|
// Создадим маппинг UnitId (из UnitsInTemplate) -> значение поля для внутренней группировки
|
||||||
var unitInTemplateToInnerGroupingValueMap = innerGroupingValues
|
var unitInTemplateToInnerGroupingValueMap = innerGroupingValues
|
||||||
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
|
.Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
|
||||||
.ToDictionary(uv => uv.UnitId, uv => uv.Value.Value);
|
.ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value);
|
||||||
|
|
||||||
// 7. Основной цикл обработки: итерируемся по potentialUnitIds (UnitId шаблонов)
|
// 7. Основной цикл обработки: итерируемся по potentialUnitIds (UnitId шаблонов)
|
||||||
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
|
var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>();
|
||||||
@@ -304,20 +279,23 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
// Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
|
// Сгруппируем *юниты из UnitsInTemplate* для *этого* potentialUnitId по значению поля
|
||||||
var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
|
var innerGroupedUnitsInTemplate = unitsInTemplateForThisPotentialUnitId
|
||||||
.GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных"))
|
.GroupBy(unitId => unitInTemplateToInnerGroupingValueMap.GetValueOrDefault(unitId, "Нет данных"))
|
||||||
|
.OrderBy(g => g.Key, StringComparer.Ordinal) // <-- Сортировка по имени внутренней группы
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate.", potentialUnitId, innerGroupedUnitsInTemplate.Count);
|
logger.LogDebug("Для UnitId {PotentialUnitId}: сформировано {Count} внутренних групп UnitsInTemplate (отсортировано).", potentialUnitId, innerGroupedUnitsInTemplate.Count);
|
||||||
|
|
||||||
// 8. Цикл по внутренним группам UnitsInTemplate
|
// --- СОБЕРЕМ ВСЕ ИТОГОВЫЕ ПОДГРУППЫ ДЛЯ ЭТОГО potentialUnitId ---
|
||||||
foreach (var innerGroup in innerGroupedUnitsInTemplate)
|
var allFinalSubGroups = new List<(List<Guid> UnitsInTemplateSubGroup, string InnerGroupName, int SubGroupSizeWithinInnerGroup)>(); // (UnitsInTemplate, Имя_внутренней_группы, размер_подгруппы_внутри_внутренней_группы)
|
||||||
|
|
||||||
|
foreach (var innerGroup in innerGroupedUnitsInTemplate) // Теперь проходит в отсортированном порядке по groupingValueName
|
||||||
{
|
{
|
||||||
var groupingValueName = innerGroup.Key;
|
var groupingValueName = innerGroup.Key;
|
||||||
|
|
||||||
var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Список юнитов (UnitId), связанных с potentialUnitId и имеющих одно и то же значение поля
|
var unitsInTemplateInInnerGroup = innerGroup.ToList(); // Список юнитов (UnitId), связанных с potentialUnitId и имеющих одно и то же значение поля
|
||||||
|
|
||||||
logger.LogDebug("Обработка внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId} с {Count} юнитами.", groupingValueName, potentialUnitId, unitsInTemplateInInnerGroup.Count);
|
logger.LogDebug("Обработка внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId} с {Count} юнитами.", groupingValueName, potentialUnitId, unitsInTemplateInInnerGroup.Count);
|
||||||
|
|
||||||
// Разбиваем юниты из *этой* внутренней группы на подгруппы по maxJob.MaxValueRelationships
|
// Разбиваем юниты из *этой* внутренней группы на подгруппы по maxJob.MaxValueRelationships
|
||||||
|
// Это нужно делать для каждой внутренней группы отдельно
|
||||||
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
|
int maxValueForSplitting = maxJob.MaxValueRelationships!.Value;
|
||||||
var unitsInTemplateSubGroups = unitsInTemplateInInnerGroup
|
var unitsInTemplateSubGroups = unitsInTemplateInInnerGroup
|
||||||
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
|
.Select((id, index) => new { id, groupIndex = index / maxValueForSplitting })
|
||||||
@@ -327,149 +305,154 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
|
|
||||||
logger.LogDebug("Внутренняя группа '{GroupingValue}' для UnitId {PotentialUnitId}: разбит на {GroupCount} подгрупп UnitsInTemplate.", groupingValueName, potentialUnitId, unitsInTemplateSubGroups.Count);
|
logger.LogDebug("Внутренняя группа '{GroupingValue}' для UnitId {PotentialUnitId}: разбит на {GroupCount} подгрупп UnitsInTemplate.", groupingValueName, potentialUnitId, unitsInTemplateSubGroups.Count);
|
||||||
|
|
||||||
// 9. Цикл по подгруппам UnitsInTemplate для создания/обновления шаблонов
|
// Добавим каждую *итоговую* подгруппу в общий список
|
||||||
for (int i = 1; i <= unitsInTemplateSubGroups.Count; i++) // Индекс начинается с 1
|
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)
|
||||||
{
|
{
|
||||||
var unitsInTemplateSubGroup = unitsInTemplateSubGroups[i - 1]; // корректируем индекс для доступа к коллекции
|
// === 1. Получаем текущие и новые ID юнитов ===
|
||||||
var subGroupSize = unitsInTemplateSubGroup.Count;
|
var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
|
||||||
logger.LogDebug("Обработка подгруппы {Index} внутренней группы '{GroupingValue}' для UnitId {PotentialUnitId}, размер UnitsInTemplate {Size}.", i, groupingValueName, potentialUnitId, subGroupSize);
|
var proposedUnitIds = unitsInTemplateSubGroup.ToList();
|
||||||
|
|
||||||
Job? targetJob = SelectTargetJob(jobsInGroup, subGroupSize, maxJob);
|
// === 2. Сравниваем детерминированно с сортировкой по имени ===
|
||||||
|
var allUnitIdsForSort = currentUnitIds.Concat(proposedUnitIds).Distinct().ToList();
|
||||||
// --- Добавляем ключ в список ожидаемых прямо здесь ---
|
var unitNamesForSort = await unitService.Get()
|
||||||
expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, i)); // potentialUnitId - это UnitId шаблона
|
|
||||||
|
|
||||||
var existingTemplatesForRelationship = await templateService.Get()
|
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(t => t.Unit)
|
.Where(u => allUnitIdsForSort.Contains(u.Id))
|
||||||
.Include(t => t.Job)
|
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString());
|
||||||
.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 == i && t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var existingTemplateForSubGroup = existingTemplatesForRelationship.FirstOrDefault();
|
var sortedCurrentUnitIds = currentUnitIds
|
||||||
|
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (existingTemplateForSubGroup != null)
|
var sortedProposedUnitIds = proposedUnitIds
|
||||||
|
.OrderBy(id => unitNamesForSort.GetValueOrDefault(id, id.ToString()))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
bool unitsAreEqual = sortedCurrentUnitIds.SequenceEqual(sortedProposedUnitIds);
|
||||||
|
|
||||||
|
if (unitsAreEqual)
|
||||||
{
|
{
|
||||||
// === 1. Получаем текущие и новые ID юнитов ===
|
logger.LogDebug("Шаблон {TemplateId} актуален по юнитам и их порядку (после сортировки).", existingTemplateForSubGroup.Id);
|
||||||
var currentUnitIds = existingTemplateForSubGroup.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
|
existingTemplateForSubGroup.UnitsInTemplate = sortedProposedUnitIds.Select(id => new UnitsInTemplate { UnitId = id }).ToList();
|
||||||
var proposedUnitIds = unitsInTemplateSubGroup.ToList();
|
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplateForSubGroup);
|
||||||
|
|
||||||
// === 2. Сравниваем детерминированно с сортировкой по имени ===
|
if (!string.Equals(existingTemplateForSubGroup.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||||||
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);
|
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 = i,
|
|
||||||
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, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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, i);
|
|
||||||
|
|
||||||
var tempTemplateForName = new Template
|
|
||||||
{
|
|
||||||
Id = reusableTemplate.Id,
|
|
||||||
Name = reusableTemplate.Name,
|
|
||||||
JobId = targetJob.Id,
|
|
||||||
UnitId = potentialUnitId,
|
|
||||||
Index = i,
|
|
||||||
Job = targetJob,
|
|
||||||
Unit = reusableTemplate.Unit,
|
|
||||||
UnitsInTemplate = unitsInTemplateSubGroup.Select(id => new UnitsInTemplate { UnitId = id }).ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
|
||||||
|
|
||||||
var updateRequest = new TemplateUpdaterMq
|
var updateRequest = new TemplateUpdaterMq
|
||||||
{
|
{
|
||||||
TemplateId = reusableTemplate.Id,
|
TemplateId = existingTemplateForSubGroup.Id,
|
||||||
JobId = targetJob.Id,
|
JobId = targetJob.Id,
|
||||||
UnitId = potentialUnitId,
|
UnitId = potentialUnitId,
|
||||||
Name = expectedName,
|
Name = expectedName,
|
||||||
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
|
IsActiveTemplate = existingTemplateForSubGroup.IsActiveTemplate,
|
||||||
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
|
IsActiveSchedule = existingTemplateForSubGroup.IsActiveSchedule,
|
||||||
|
IsNew = false,
|
||||||
|
Index = globalIndexForThisUnitId,
|
||||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||||
Initiator = initiator,
|
Initiator = initiator,
|
||||||
IsNew = true,
|
UnitsInTemplate = sortedProposedUnitIds
|
||||||
Index = i,
|
|
||||||
UnitsInTemplate = unitsInTemplateSubGroup
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Создание нового шаблона для Job {JobId}, связанного юнита {RelationshipId}, Index {Index}, с {Count} юнитами.", targetJob.Id, potentialUnitId, i, unitsInTemplateSubGroup.Count);
|
logger.LogDebug("Шаблон {TemplateId} полностью актуален.", existingTemplateForSubGroup.Id);
|
||||||
await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, i, initiator);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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++; // Увеличиваем индекс для следующей итоговой подгруппы
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Деактивация ===
|
// === Деактивация ===
|
||||||
// Теперь expectedTemplateKeys уже собран
|
|
||||||
|
|
||||||
// Получаем ВСЕ шаблоны для 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()
|
||||||
@@ -560,7 +543,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
};
|
};
|
||||||
|
|
||||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
|
||||||
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
|
||||||
var updateRequest = new TemplateUpdaterMq
|
var updateRequest = new TemplateUpdaterMq
|
||||||
{
|
{
|
||||||
TemplateId = template.Id,
|
TemplateId = template.Id,
|
||||||
@@ -569,8 +552,6 @@ internal class GroupedTemplateSynchronizer : 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 = newIndex,
|
Index = newIndex,
|
||||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||||
@@ -618,6 +599,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#region Вспомогательные методы
|
#region Вспомогательные методы
|
||||||
private async Task<Guid> GetFieldIdByAihitNameAsync(string fieldName)
|
private async Task<Guid> GetFieldIdByAihitNameAsync(string fieldName)
|
||||||
{
|
{
|
||||||
@@ -629,5 +611,6 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
return field.Id;
|
return field.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
|
|||||||
public async Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator)
|
public async Task<bool> DeactivateTemplateAsync(Template template, HistoryInitiator initiator)
|
||||||
{
|
{
|
||||||
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
|
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating)
|
||||||
return true; // уже в обработке
|
return true;
|
||||||
|
|
||||||
if (template.StatusTypeId == TemplateStatusTypeEnum.Unused)
|
if (template.StatusTypeId == TemplateStatusTypeEnum.Unused)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
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;
|
||||||
|
|
||||||
|
internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
|
||||||
|
{
|
||||||
|
private readonly ILogger<UnitInTemplateConflictMapper> logger;
|
||||||
|
private readonly IUnitRepository unitRepository;
|
||||||
|
private readonly IUnitKiiUnitRepository unitKiiUnitRepository;
|
||||||
|
|
||||||
|
public UnitInTemplateConflictMapper(
|
||||||
|
ILogger<UnitInTemplateConflictMapper> logger,
|
||||||
|
IUnitRepository unitRepository,
|
||||||
|
IUnitKiiUnitRepository unitKiiUnitRepository)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.unitRepository = unitRepository;
|
||||||
|
this.unitKiiUnitRepository = unitKiiUnitRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
||||||
|
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
||||||
|
Job maxJob,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Начало разрешения конфликтов и построения маппинга для юнитов в шаблонах.");
|
||||||
|
|
||||||
|
var unitFilterResultsList = unitFilterResults.ToList();
|
||||||
|
if (!unitFilterResultsList.Any())
|
||||||
|
return new Dictionary<Guid, List<Guid>>();
|
||||||
|
|
||||||
|
var potentialAssignments = new Dictionary<Guid, List<Guid>>();
|
||||||
|
var allPotentialRelatedUnitIds = new HashSet<Guid>();
|
||||||
|
var allUnitInTemplateIds = new HashSet<Guid>();
|
||||||
|
|
||||||
|
foreach (var dto in unitFilterResultsList)
|
||||||
|
{
|
||||||
|
var relatedUnitIds = maxJob.IsParentRelationships == true
|
||||||
|
? dto.Children.Select(c => c.UnitId).ToList()
|
||||||
|
: dto.Parents.Select(p => p.UnitId).ToList();
|
||||||
|
|
||||||
|
var unitInTemplateId = dto.Id;
|
||||||
|
|
||||||
|
allUnitInTemplateIds.Add(unitInTemplateId);
|
||||||
|
allPotentialRelatedUnitIds.UnionWith(relatedUnitIds);
|
||||||
|
|
||||||
|
foreach (var relatedId in relatedUnitIds)
|
||||||
|
{
|
||||||
|
if (!potentialAssignments.TryGetValue(relatedId, out var list))
|
||||||
|
{
|
||||||
|
list = new List<Guid>();
|
||||||
|
potentialAssignments[relatedId] = list;
|
||||||
|
}
|
||||||
|
list.Add(unitInTemplateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allPotentialRelatedUnitIds.Any())
|
||||||
|
return new Dictionary<Guid, List<Guid>>();
|
||||||
|
|
||||||
|
var relatedUnitNames = await unitRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(u => allPotentialRelatedUnitIds.Contains(u.Id))
|
||||||
|
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString(), cancellationToken);
|
||||||
|
|
||||||
|
// Загружаем идентификаторы КИИ через ваш реальный репозиторий
|
||||||
|
var kiiUnitIds = new HashSet<Guid>(await unitKiiUnitRepository.Get()
|
||||||
|
.Select(k => k.UnitId)
|
||||||
|
.ToListAsync(cancellationToken));
|
||||||
|
|
||||||
|
logger.LogDebug("Загружено {KiiCount} идентификаторов КИИ юнитов для приоритезации.", kiiUnitIds.Count);
|
||||||
|
|
||||||
|
var unitInTemplateToBestRelatedUnit = new Dictionary<Guid, Guid>();
|
||||||
|
|
||||||
|
foreach (var unitInTemplateId in allUnitInTemplateIds)
|
||||||
|
{
|
||||||
|
var candidates = potentialAssignments
|
||||||
|
.Where(kvp => kvp.Value.Contains(unitInTemplateId))
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (candidates.Count == 0) continue;
|
||||||
|
if (candidates.Count == 1)
|
||||||
|
{
|
||||||
|
unitInTemplateToBestRelatedUnit[unitInTemplateId] = candidates[0];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var kiiCandidates = candidates.Where(id => kiiUnitIds.Contains(id)).ToList();
|
||||||
|
var candidatesToConsider = kiiCandidates.Any() ? kiiCandidates : candidates;
|
||||||
|
|
||||||
|
var relationshipCounts = new Dictionary<Guid, int>();
|
||||||
|
foreach (var candidateId in candidatesToConsider)
|
||||||
|
{
|
||||||
|
int totalRelationships = 0;
|
||||||
|
foreach (var dto in unitFilterResultsList)
|
||||||
|
{
|
||||||
|
if (maxJob.IsParentRelationships == true && dto.Children.Any(c => c.UnitId == candidateId))
|
||||||
|
totalRelationships += dto.Children.Count;
|
||||||
|
else if (maxJob.IsParentRelationships == false && dto.Parents.Any(p => p.UnitId == candidateId))
|
||||||
|
totalRelationships += dto.Parents.Count;
|
||||||
|
}
|
||||||
|
relationshipCounts[candidateId] = totalRelationships;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestCandidate = candidatesToConsider[0];
|
||||||
|
int bestCount = relationshipCounts[bestCandidate];
|
||||||
|
|
||||||
|
foreach (var candidateId in candidatesToConsider.Skip(1))
|
||||||
|
{
|
||||||
|
int currentCount = relationshipCounts[candidateId];
|
||||||
|
if (currentCount > bestCount ||
|
||||||
|
(currentCount == bestCount && string.Compare(
|
||||||
|
relatedUnitNames.GetValueOrDefault(candidateId, candidateId.ToString()),
|
||||||
|
relatedUnitNames.GetValueOrDefault(bestCandidate, bestCandidate.ToString()),
|
||||||
|
StringComparison.OrdinalIgnoreCase) < 0))
|
||||||
|
{
|
||||||
|
bestCandidate = candidateId;
|
||||||
|
bestCount = currentCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unitInTemplateToBestRelatedUnit[unitInTemplateId] = bestCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Разрешение конфликтов завершено. Найдено {Count} однозначных назначений.", unitInTemplateToBestRelatedUnit.Count);
|
||||||
|
|
||||||
|
var reverseMapping = new Dictionary<Guid, List<Guid>>();
|
||||||
|
foreach (var kvp in unitInTemplateToBestRelatedUnit)
|
||||||
|
{
|
||||||
|
if (!reverseMapping.TryGetValue(kvp.Value, out var list))
|
||||||
|
{
|
||||||
|
list = new List<Guid>();
|
||||||
|
reverseMapping[kvp.Value] = list;
|
||||||
|
}
|
||||||
|
list.Add(kvp.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reverseMapping;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using PARR.Core.Services.UnitFilterService.Models;
|
||||||
|
using PARR.Domain.Entities.Job;
|
||||||
|
|
||||||
|
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUnitInTemplateConflictMapper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Строит маппинг: UnitId шаблона -> [Список юнитов в UnitsInTemplate].
|
||||||
|
/// При наличии нескольких кандидатов для одного юнита выбирается лучший по приоритету:
|
||||||
|
/// 1. Наличие в таблице UnitKiiUnit
|
||||||
|
/// 2. Наибольшее количество связей
|
||||||
|
/// 3. Алфавитный порядок имени юнита
|
||||||
|
/// </summary>
|
||||||
|
Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
||||||
|
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
||||||
|
Job maxJob,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ namespace PARR.TemplateMatcher
|
|||||||
services.AddTransient<ITemplateNameNormalizer, TemplateNameNormalizer>();
|
services.AddTransient<ITemplateNameNormalizer, TemplateNameNormalizer>();
|
||||||
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
|
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
|
||||||
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
|
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
|
||||||
|
services.AddTransient<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
|
||||||
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
||||||
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user