- ValidateInput: возвращает пустой список вместо null! при отсутствии данных - SyncAsync: добавлена транзакция для гарантии атомарности зеркалирования - GetOrCreateUnitsAsync: пакетное создание юнитов через CreateRange вместо поштучного цикла - Удалены избыточные комментарии, код самодокументируем через имена методов - Единообразие имён переменных: _unitRepository, unitsByName, parentName/childName - PassSafeguardCheckAsync: выделен в отдельный метод для читаемости SyncAsync - CommitChangesAsync: бросает исключение при ошибке коммита вместо тихого лога
257 lines
9.5 KiB
C#
257 lines
9.5 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using PARR.AIHITRelationshipsSyncer.Models;
|
||
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
|
||
using PARR.Core.Repositories.Interfaces.Unit;
|
||
using PARR.Domain.Entities.Unit;
|
||
using PARR.Domain.Settings;
|
||
|
||
namespace PARR.AIHITRelationshipsSyncer.Services.Implementations;
|
||
|
||
internal class RelationshipsSyncService : IRelationshipsSyncService
|
||
{
|
||
private readonly ILogger<RelationshipsSyncService> _logger;
|
||
private readonly IUnitRepository _unitRepository;
|
||
private readonly SettingsFromDb _settingsFromDb;
|
||
|
||
public RelationshipsSyncService(
|
||
ILogger<RelationshipsSyncService> logger,
|
||
IUnitRepository unitRepository,
|
||
SettingsFromDb settingsFromDb)
|
||
{
|
||
_logger = logger;
|
||
_unitRepository = unitRepository;
|
||
_settingsFromDb = settingsFromDb;
|
||
}
|
||
|
||
public async Task SyncAsync(List<AihitData> aihitData)
|
||
{
|
||
var validPairs = ValidateInput(aihitData);
|
||
|
||
if (validPairs.Count == 0)
|
||
return;
|
||
|
||
if (!await PassSafeguardCheckAsync(validPairs.Count))
|
||
return;
|
||
|
||
var unitNames = ExtractUnitNames(validPairs);
|
||
var unitsByName = await GetOrCreateUnitsAsync(unitNames);
|
||
|
||
var (toRemove, toAdd) = await GetDeltaAsync(validPairs);
|
||
|
||
await using var transaction = await _unitRepository.BeginTransactionAsync();
|
||
try
|
||
{
|
||
await RemoveRelationshipsAsync(toRemove);
|
||
await AddRelationshipsAsync(toAdd, unitsByName);
|
||
await CommitChangesAsync(toRemove.Count, toAdd.Count);
|
||
await transaction.CommitAsync();
|
||
}
|
||
catch
|
||
{
|
||
await transaction.RollbackAsync();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private List<(string ParentName, string ChildName)> ValidateInput(List<AihitData> data)
|
||
{
|
||
if (data == null || data.Count == 0)
|
||
{
|
||
_logger.LogWarning("Получены пустые данные из АИХ ИТ. Синхронизация пропущена");
|
||
return new List<(string, string)>();
|
||
}
|
||
|
||
var validPairs = data
|
||
.Where(item => !string.IsNullOrWhiteSpace(item.ParentName) && !string.IsNullOrWhiteSpace(item.ChildName))
|
||
.Where(item => item.ParentName != item.ChildName)
|
||
.Select(item => (
|
||
ParentName: item.ParentName!.Trim().ToUpperInvariant(),
|
||
ChildName: item.ChildName!.Trim().ToUpperInvariant()
|
||
))
|
||
.Distinct()
|
||
.ToList();
|
||
|
||
if (validPairs.Count == 0)
|
||
{
|
||
_logger.LogWarning("После нормализации не осталось валидных связей. Синхронизация пропущена");
|
||
return new List<(string, string)>();
|
||
}
|
||
|
||
_logger.LogInformation("Получено {Count} валидных связей", validPairs.Count);
|
||
return validPairs;
|
||
}
|
||
|
||
private async Task<bool> PassSafeguardCheckAsync(int incomingCount)
|
||
{
|
||
var previousCount = await _unitRepository.Get()
|
||
.SelectMany(u => u.ChildUnits)
|
||
.CountAsync();
|
||
|
||
if (previousCount == 0)
|
||
return true;
|
||
|
||
var currentPercentage = Math.Round((decimal)incomingCount / previousCount * 100, 1, MidpointRounding.AwayFromZero);
|
||
|
||
if (currentPercentage < _settingsFromDb.MinRelationshipsThresholdPct)
|
||
{
|
||
_logger.LogWarning(
|
||
"Синхронизация отменена: количество полученных связей ниже порогового значения. " +
|
||
"Получено: {CurrentCount} ({CurrentPercentage:F1}%), порог: {Threshold}% от прошлого объема ({PreviousCount})",
|
||
incomingCount, currentPercentage, _settingsFromDb.MinRelationshipsThresholdPct, previousCount);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private static List<string> ExtractUnitNames(List<(string ParentName, string ChildName)> pairs)
|
||
{
|
||
return pairs
|
||
.SelectMany(pair => new[] { pair.ParentName, pair.ChildName })
|
||
.Distinct()
|
||
.ToList();
|
||
}
|
||
|
||
private async Task<Dictionary<string, Unit>> GetOrCreateUnitsAsync(List<string> names)
|
||
{
|
||
var existingUnits = await _unitRepository.Get()
|
||
.AsNoTracking()
|
||
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
||
|
||
var missingNames = names.Except(existingUnits.Keys).ToList();
|
||
|
||
if (missingNames.Count == 0)
|
||
return existingUnits;
|
||
|
||
_logger.LogInformation("Создание отсутствующих юнитов: {Count}", missingNames.Count);
|
||
|
||
var newUnits = missingNames.Select(name => new Unit
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Name = name,
|
||
DateCreated = DateTimeOffset.UtcNow
|
||
}).ToList();
|
||
|
||
_unitRepository.CreateRange(newUnits);
|
||
await _unitRepository.CommitAsync();
|
||
|
||
foreach (var unit in newUnits)
|
||
{
|
||
existingUnits[unit.Name] = unit;
|
||
_logger.LogDebug("Создан юнит: {Name}", unit.Name);
|
||
}
|
||
|
||
return existingUnits;
|
||
}
|
||
|
||
private async Task<(List<ExistingRelationship> ToRemove, List<(string ParentName, string ChildName)> ToAdd)> GetDeltaAsync(
|
||
List<(string ParentName, string ChildName)> sourcePairs)
|
||
{
|
||
var allExisting = await _unitRepository.Get()
|
||
.AsNoTracking()
|
||
.Include(u => u.ChildUnits).ThenInclude(r => r.ChildUnit)
|
||
.Where(u => u.ChildUnits.Any())
|
||
.SelectMany(u => u.ChildUnits, (parent, relationship) => new ExistingRelationship
|
||
{
|
||
ParentName = parent.Name,
|
||
ChildName = relationship.ChildUnit!.Name,
|
||
Relationship = relationship
|
||
})
|
||
.ToListAsync();
|
||
|
||
var sourceSet = new HashSet<(string, string)>(sourcePairs);
|
||
var existingSet = new HashSet<(string, string)>(allExisting.Select(x => (x.ParentName, x.ChildName)));
|
||
|
||
var toRemove = allExisting.Where(r => !sourceSet.Contains((r.ParentName, r.ChildName))).ToList();
|
||
var toAdd = sourcePairs.Where(p => !existingSet.Contains(p)).ToList();
|
||
|
||
_logger.LogInformation("Дельта: удаление {ToRemove}, добавление {ToAdd}", toRemove.Count, toAdd.Count);
|
||
return (toRemove, toAdd);
|
||
}
|
||
|
||
private async Task RemoveRelationshipsAsync(List<ExistingRelationship> toRemove)
|
||
{
|
||
if (toRemove.Count == 0)
|
||
return;
|
||
|
||
var parentNames = toRemove.Select(r => r.ParentName).Distinct().ToList();
|
||
|
||
var parents = await _unitRepository.Get()
|
||
.Include(u => u.ChildUnits)
|
||
.Where(u => parentNames.Contains(u.Name))
|
||
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
||
|
||
foreach (var relationship in toRemove)
|
||
{
|
||
if (!parents.TryGetValue(relationship.ParentName, out var parent))
|
||
{
|
||
_logger.LogWarning("Родитель '{Parent}' не найден при удалении связи с '{Child}'",
|
||
relationship.ParentName, relationship.ChildName);
|
||
continue;
|
||
}
|
||
|
||
var entityToRemove = parent.ChildUnits
|
||
.FirstOrDefault(r => r.ChildUnitId == relationship.Relationship.ChildUnitId);
|
||
|
||
if (entityToRemove != null)
|
||
parent.ChildUnits.Remove(entityToRemove);
|
||
}
|
||
}
|
||
|
||
private async Task AddRelationshipsAsync(
|
||
List<(string ParentName, string ChildName)> toAdd,
|
||
Dictionary<string, Unit> unitsByName)
|
||
{
|
||
if (toAdd.Count == 0)
|
||
return;
|
||
|
||
var parentNames = toAdd.Select(x => x.ParentName).Distinct().ToList();
|
||
|
||
var parents = await _unitRepository.Get()
|
||
.Include(u => u.ChildUnits)
|
||
.Where(u => parentNames.Contains(u.Name))
|
||
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
|
||
|
||
foreach (var (parentName, childName) in toAdd)
|
||
{
|
||
if (!parents.TryGetValue(parentName, out var parent) ||
|
||
!unitsByName.TryGetValue(childName, out var child))
|
||
{
|
||
_logger.LogWarning("Пропущена связь '{Parent}' - '{Child}': юнит не найден", parentName, childName);
|
||
continue;
|
||
}
|
||
|
||
if (parent.ChildUnits.Any(r => r.ChildUnitId == child.Id))
|
||
continue;
|
||
|
||
parent.ChildUnits.Add(new UnitInUnit
|
||
{
|
||
ParentUnitId = parent.Id,
|
||
ChildUnitId = child.Id,
|
||
DateCreated = DateTimeOffset.UtcNow
|
||
});
|
||
}
|
||
}
|
||
|
||
private async Task CommitChangesAsync(int removedCount, int addedCount)
|
||
{
|
||
if (removedCount == 0 && addedCount == 0)
|
||
{
|
||
_logger.LogInformation("Изменений не обнаружено");
|
||
return;
|
||
}
|
||
|
||
if (!await _unitRepository.CommitAsync())
|
||
throw new InvalidOperationException("Не удалось сохранить изменения связей в БД");
|
||
|
||
_logger.LogInformation("Синхронизация завершена. Удалено: {Removed}, добавлено: {Added}", removedCount, addedCount);
|
||
}
|
||
|
||
private sealed class ExistingRelationship
|
||
{
|
||
public string ParentName { get; init; } = null!;
|
||
public string ChildName { get; init; } = null!;
|
||
public UnitInUnit Relationship { get; init; } = null!;
|
||
}
|
||
} |