Files
parr_api/PARR.AIHITRelationshipsSyncer/Services/Implementations/RelationshipsSyncService.cs
Mikhail Kuznetsov 4ee2880a8f fix(aihitRelationshipsSyncer): критические исправления RelationshipsSyncService
- ValidateInput: возврат пустого списка вместо null! для предотвращения NRE
- SyncAsync: транзакция для атомарности зеркалирования снимка состояния
- GetOrCreateUnitsAsync: пакетное создание юнитов через CreateRange
- CommitChangesAsync: исключение при ошибке коммита вместо тихого лога
- PassSafeguardCheckAsync: выделен в отдельный метод
- Удалены избыточные комментарии, унифицированы имена переменных
2026-07-15 14:04:35 +10:00

259 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);
// Транзакция гарантирует атомарность зеркалирования.
// Без неё частичный сбой оставляет БД в состоянии, не совпадающем со снимком.
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!;
}
}