Files
parr_api/PARR.AIHITRelationshipsSyncer/Services/Implementations/RelationshipsSyncService.cs

261 lines
9.7 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.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);
// Удаление и добавление накапливаются в одном ChangeTracker.
// CommitAsync сохраняет всё атомарно через SaveChangesAsync.
await RemoveRelationshipsAsync(toRemove);
await AddRelationshipsAsync(toAdd, unitsByName);
await CommitChangesAsync(toRemove.Count, toAdd.Count);
}
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);
foreach (var name in missingNames)
{
var unit = new Unit
{
Id = Guid.NewGuid(),
Name = name,
DateCreated = DateTimeOffset.UtcNow
};
if (!await _unitRepository.CreateAsync(unit))
{
_logger.LogError("Не удалось создать юнит: {Name}", name);
continue;
}
existingUnits[name] = unit;
_logger.LogDebug("Создан юнит: {Name}", name);
}
await _unitRepository.CommitAsync();
return existingUnits;
}
private async Task<(List<ExistingRelationship> ToRemove, List<(string ParentName, string ChildName)> ToAdd)> GetDeltaAsync(
List<(string ParentName, string ChildName)> sourcePairs)
{
var relevantParentNames = sourcePairs.Select(p => p.ParentName).Distinct().ToList();
var existingForParents = await _unitRepository.Get()
.AsNoTracking()
.Include(u => u.ChildUnits).ThenInclude(r => r.ChildUnit)
.Where(u => relevantParentNames.Contains(u.Name))
.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)>(
existingForParents.Select(x => (x.ParentName, x.ChildName)));
var toRemove = existingForParents
.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!;
}
}