feat(aihitRelationshipsSyncer): полностью переписан класс+добавлены сообщения дл\ отладки

This commit is contained in:
Mikhail Kuznetsov
2025-10-31 17:35:54 +10:00
parent a3e12816c8
commit e716d9649d
11 changed files with 354 additions and 236 deletions

1
.gitignore vendored
View File

@@ -364,3 +364,4 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/PARR.AIHITRelationshipsSyncerWorker/MockData.json

View File

@@ -29,7 +29,7 @@ namespace PARR.AIHITRelationshipsSyncer
services.AddTransient<IRelationshipsSyncer, RelationshipsSyncer>();
services.AddTransient<IAihitService, AihitService>();
services.AddTransient<IRelationshipsSyncService, RelationshipsSyncSrevice>();
services.AddTransient<IRelationshipsSyncService, RelationshipsSyncService>();
}

View File

@@ -9,5 +9,17 @@ namespace PARR.AIHITRelationshipsSyncer.Context
public DbSet<AihitData> AihitDatas { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//Делаем это чтобы EF не пытался трекать объкты полученые из хранимой процедуры
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<AihitData>(entity =>
{
entity.HasNoKey();
entity.ToView(null);
});
}
}
}

View File

@@ -3,11 +3,11 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.AIHITRelationshipsSyncer.Models
{
[PrimaryKey(nameof(EKFindCode), nameof(ChildEk))]
public class AihitData
[PrimaryKey(nameof(ParentName), nameof(ChildName))]
public record AihitData
{
[Column("КОД_ПОИСКАК")]
public string? EKFindCode { get; set; }
public string? ParentName { get; set; }
//[Column("СТАТУС")]
//public string? Status { get; set; }
@@ -21,6 +21,6 @@ namespace PARR.AIHITRelationshipsSyncer.Models
//[Column("ТИП_ЭК")]
//public string? EKType { get; set; }
[Column("Дочерний ЭК")]
public string? ChildEk { get; set; }
public string? ChildName { get; set; }
}
}

View File

@@ -1,7 +1,9 @@
using Microsoft.Extensions.Logging;
using PARR.AIHITRelationshipsSyncer.Models;
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
using PARR.AIHITRelationshipsSyncer.Settings;
using PARR.BLL.Services.Interfaces;
using System.Text.Json;
namespace PARR.AIHITRelationshipsSyncer
{
@@ -39,21 +41,62 @@ namespace PARR.AIHITRelationshipsSyncer
{
logger.LogInformation($"Начата загрузка иерархических связей между ЭК из АИХ ИТ");
var aihitdata = aihitService.GetData();
List<AihitData>? aihitDataList = null;
var aihitDataCount = aihitdata?.Count();
#if DEBUG
const string mockFilePath = "MockData.json";
if (aihitdata == null || aihitDataCount == 0)
//Пытаемся загрузить mock-данные
if (File.Exists(mockFilePath) && new FileInfo(mockFilePath).Length > 0)
{
logger.LogInformation("Загрузка данных из mock-файла: {MockFile}", mockFilePath);
try
{
var json = await File.ReadAllTextAsync(mockFilePath);
aihitDataList = JsonSerializer.Deserialize<List<AihitData>>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
logger.LogInformation("Mock-данные успешно загружены");
}
catch (Exception ex)
{
logger.LogWarning(ex, "Не уадлось десериализовать {MockFile}. Будет выполнен запрос в базу АИХ ИТ", mockFilePath);
aihitDataList = null;
}
}
if (aihitDataList == null)
{
logger.LogInformation("Выполнение запроса в АИХ ИТ в режиме отладки");
var rawData = aihitService.GetData();
aihitDataList = rawData?.ToList();
if (aihitDataList != null && aihitDataList.Count > 0)
{
try
{
var json = JsonSerializer.Serialize(aihitDataList, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(mockFilePath, json);
logger.LogInformation("Данные сохранены в {MockFile} для будущей отладки", mockFilePath);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Не удалось сохранить данные в {MockFile}", mockFilePath);
}
}
}
#else
//Release
var rawData = aihitService.GetData();
aihitDataList = rawData?.ToList();
#endif
if (aihitDataList == null || aihitDataList.Count == 0)
{
logger.LogInformation("АИХ ИТ вернул пустые данные");
return;
}
//File.WriteAllText("MockData.json", aihitdata.ToJson());
//string text = File.OpenText("MockData.json").ReadToEnd();
//var aihitdata = JsonSerializer.Deserialize<List<AihitData>>(text);
await relationshipsSyncService.SyncAsync(aihitdata.ToList());
logger.LogInformation("Получено {EkCount} записей для синхронизации", aihitDataList.Count);
await relationshipsSyncService.SyncAsync(aihitDataList);
}
catch (Exception ex)

View File

@@ -0,0 +1,236 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.AIHITRelationshipsSyncer.Models;
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
using PARR.AIHITRelationshipsSyncer.Settings;
using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Interfaces.Unit;
namespace PARR.AIHITRelationshipsSyncer.Services.Implementations
{
internal class RelationshipsSyncService : IRelationshipsSyncService
{
private readonly ILogger<RelationshipsSyncService> logger;
private readonly IUnitService unitService;
private readonly WorkerSettings workerSettings;
public RelationshipsSyncService(
ILogger<RelationshipsSyncService> logger,
IUnitService unitService,
WorkerSettings workerSettings
)
{
this.logger = logger;
this.unitService = unitService;
this.workerSettings = workerSettings;
}
public async Task SyncAsync(List<AihitData> aihitdata)
{
var validPairs = ValidateInput(aihitdata);
if (!validPairs.Any()) return;
var sourceUnitNames = ExtractUnitNames(validPairs);
var existingUnits = await GetOrCreateUnitsAsync(sourceUnitNames);
var (toRemove, toAdd) = await GetDeltaAsync(validPairs);
await RemoveRelationshipsAsync(toRemove);
await AddRelationshipsAsync(toAdd, existingUnits);
await FinalCommitAsync(toRemove.Count, toAdd.Count);
}
private List<(string Parent, string Child)> ValidateInput(List<AihitData> data)
{
if (data == null || data.Count == 0)
{
logger.LogWarning("Получены пустые или null данные из АИХ ИТ. Пропускаю синхронизацию");
return null!;
}
var validPairs = data
.Where(t => !string.IsNullOrWhiteSpace(t.ParentName) && !string.IsNullOrWhiteSpace(t.ChildName))
.Where(t => t.ParentName != t.ChildName)
.Select(t => (
Parent: t.ParentName!.Trim().ToUpperInvariant(),
Child: t.ChildName!.Trim().ToUpperInvariant()
))
.Distinct()
.ToList();
if (validPairs.Count == 0)
{
logger.LogWarning("После нормализации не осталось валидных связей. Пропускаю синхронизацию");
return new();
}
logger.LogInformation("Получено {Count} валидных связей", validPairs.Count);
return validPairs;
}
private static List<string> ExtractUnitNames(List<(string Parent, string Child)> pairs)
{
return pairs
.SelectMany(x => new[] { x.Parent, x.Child })
.Distinct()
.ToList();
}
private async Task<Dictionary<string, Unit>> GetOrCreateUnitsAsync(List<string> names)
{
var existing = await unitService.Get()
.AsNoTracking()
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
var missing = names.Except(existing.Keys).ToList();
if (!missing.Any()) return existing;
logger.LogInformation("Создание отсутствующих Unit:{Count}", missing.Count);
foreach (var name in missing)
{
var unit = new Unit
{
Id = Guid.NewGuid(),
Name = name,
DateCreated = DateTimeOffset.UtcNow
};
if (await unitService.CreateAsync(unit))
{
logger.LogInformation("Создан Unit: {Name}", name);
existing[name] = unit;
}
else
logger.LogError("Не удалось создать Unit: {Name}", name);
}
await unitService.CommitAsync();
return existing;
}
private async Task<(List<ExistingRelationship> ToRemove, List<(string, string)> ToAdd)> GetDeltaAsync(List<(string Parent, string Child)> sourcePairs)
{
var allExistings = await unitService.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)>(allExistings.Select(x => (x.ParentName, x.ChildName)));
var toRemove = allExistings.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.Any()) return;
var parentNames = toRemove.Select(r => r.ParentName).Distinct().ToList();
var parents = await unitService.Get()
.Include(u => u.ChildUnits)
.Where(u => parentNames.Contains(u.Name))
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
foreach (var rel in toRemove)
{
if (!parents.TryGetValue(rel.ParentName, out var parent))
{
logger.LogWarning("Родитель '{Parent}' не найден при попытке удаления связи - '{Child}'", rel.ParentName, rel.ChildName);
continue;
}
var relationshipToRemove = parent.ChildUnits
.FirstOrDefault(r => r.ChildUnitId == rel.Relationship.ChildUnitId);
if (relationshipToRemove != null)
{
logger.LogInformation("Удаление связи: '{Parent}' - '{Child}'", rel.ParentName, rel.ChildName);
parent.ChildUnits.Remove(relationshipToRemove);
}
else
{
logger.LogWarning("Связь '{Parent}'-'{Child}' не найдена в коллекции ChildUnits для удаления", rel.ParentName, rel.ChildName);
}
}
}
private async Task AddRelationshipsAsync(List<(string Parent, string Child)> toAdd, Dictionary<string, Unit> allUnits)
{
if (!toAdd.Any()) return;
var parentNames = toAdd.Select(x => x.Parent).Distinct().ToList();
var parents = await unitService.Get()
.Include(u => u.ChildUnits)
.Where(u => parentNames.Contains(u.Name))
.ToDictionaryAsync(u => u.Name, StringComparer.Ordinal);
foreach (var (Parent, Child) in toAdd)
{
if (!parents.TryGetValue(Parent, out var parent) ||
!allUnits.TryGetValue(Child, out var child))
{
logger.LogWarning("Пропущена связь '{Parent}' - '{Child}': Unit не найден", Parent, Child);
continue;
}
if (parent!.ChildUnits.Any(r => r.ChildUnitId == child.Id))
{
logger.LogWarning("Связь уже существует: '{Parent}' - '{Child}'", Parent, Child);
continue;
}
parent.ChildUnits.Add(new UnitInUnit
{
ParentUnitId = parent.Id,
ChildUnitId = child.Id,
DateCreated = DateTimeOffset.UtcNow
});
logger.LogInformation("Добавлена связь: '{Parent}' - '{Child}'", Parent, Child);
}
}
private async Task FinalCommitAsync(int removedCount, int addedCount)
{
if (removedCount > 0 || addedCount > 0)
{
if (await unitService.CommitAsync())
{
logger.LogInformation("Синхронизация завершена. Удалено: {Removed}, добавлено: {Added}", removedCount, addedCount);
}
else
logger.LogError("Не удалось сохранить изменения в БД");
}
else
logger.LogInformation("Изменений не обнаружено");
}
private sealed class ExistingRelationship
{
public string ParentName { get; init; }
public string ChildName { get; init; }
public UnitInUnit Relationship { get; init; }
}
}
}

View File

@@ -1,219 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.AIHITRelationshipsSyncer.Models;
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
using PARR.AIHITRelationshipsSyncer.Settings;
using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Interfaces.Unit;
using System.Reactive;
using Unit = PARR.DAL.Models.Unit.Unit;
namespace PARR.AIHITRelationshipsSyncer.Services.Implementations
{
internal class RelationshipsSyncSrevice : IRelationshipsSyncService
{
private readonly ILogger<RelationshipsSyncSrevice> logger;
private readonly IUnitService unitService;
private readonly WorkerSettings workerSettings;
public RelationshipsSyncSrevice(ILogger<RelationshipsSyncSrevice> logger,
IUnitService unitService,
WorkerSettings workerSettings
)
{
this.logger = logger;
this.unitService = unitService;
this.workerSettings = workerSettings;
}
public async Task SyncAsync(List<AihitData> aihitdata)
{
var groupedData = aihitdata.Where(t => t.EKFindCode != null && t.ChildEk != null).GroupBy(ad => ad.EKFindCode);
//var i = 0;
//var c = groupedData.Count();
var onlyUpdateTransactionCount = 0;
//Перебираем сгруппированные по родительскому ЭК данные
foreach (var parrent in groupedData)
{
//i = i+1;
//Получаем имя родителя
var parentName = parrent.First().EKFindCode;
//По имени получаем экземпляр Unit со всеми дочерними связями
var unit = await GetUnitByNameAsync(parentName!);
if (unit == null)
{
logger.LogError($"Не удалось получиться родительский ЭК по имени{parentName}");
return;
}
//Из данных АИХ ИТ получаем имена всех дочерних ЭК
var childsName = parrent.Where(t => t.ChildEk != parentName && t.ChildEk != null).Select(t => t.ChildEk);
childsName = childsName.Select(t => t?.Trim()).Distinct();
//Проходим циклом по полученному списку для актуализации связей в полученном нами экземпляре Unit
var isChanged = await SyncChildSUnitAsync(unit, childsName);
if (!isChanged) onlyUpdateTransactionCount = ++onlyUpdateTransactionCount;
//logger.LogInformation($"Обработка № {i.ToString()} из {c}. Выполнено только обновление поля DateSynced - {onlyUpdateTransactionCount}");
if (onlyUpdateTransactionCount == workerSettings.CommitAfter)
{
//применяем изменения в базе данных
if (!await unitService.CommitAsync())
logger.LogError($"Не удалось изменить связи Unit после {workerSettings.CommitAfter} обновлений поля DateSynced");
else
logger.LogDebug($"----- Изменены связи Unit после {workerSettings.CommitAfter} обновлений поля DateSynced -----");
onlyUpdateTransactionCount = 0;
}
//применяем изменения в базе данных
if (isChanged && !await unitService.CommitAsync())
logger.LogError($"Не удалось изменить связи Unit {unit.Name}");
else
logger.LogDebug($"----- Изменены связи Unit: {unit.Name} -----");
}
//Удаляем детей у тех кого детей в выборке АИХ ИТ не оказалось
await RemoveWasteRelationships(groupedData);
//применяем изменения в базе данных
if (!await unitService.CommitAsync())
logger.LogError($"Не удалось удалить лишние связи");
else
logger.LogInformation($"----- Иереархические связи актуализированы -----");
}
private async Task RemoveWasteRelationships(IEnumerable<IGrouping<string?, AihitData>> groupedData)
{
var parentNames = groupedData.Select(t => t.Key?.ToLower().Trim());
var units = await unitService.Get()
.Include(t => t.ChildUnits).ToListAsync();
var parentsToDeleteChild = units.Where(t => parentNames.All(a => t.Name.ToLower() != a) && t.ChildUnits.Any());
if (parentsToDeleteChild.Any())
{
foreach (var unit in parentsToDeleteChild)
{
DeleteChilds(unit, unit.ChildUnits);
}
}
}
private async Task<bool> SyncChildSUnitAsync(Unit unit, IEnumerable<string?> childsName)
{
var isChanged = false;
//удаляем лишние дочерние связи существующие только в БД
var childsToDelete = unit.ChildUnits.Where(t => !childsName.Any(a => t.ChildUnit?.Name.ToLower() == a!.ToLower()));
if (childsToDelete.Any())
{
DeleteChilds(unit, childsToDelete);
if (!isChanged)
isChanged = true;
}
//проверяем отсутсвующие связи
var absentChildrens = childsName.Where(c => !unit.ChildUnits.Any(x => x.ChildUnit?.Name.ToLower() == c?.ToLower()));
foreach (var absentChildren in absentChildrens)
{
var childUnit = await GetUnitByNameAsync(absentChildren!);
if (childUnit == null)
{
logger.LogError($"Ошибка получения дочернего ЭК. Из базы пришел NULL для ЭК{absentChildren}");
return false;
}
if (unit.ChildUnits.Any(x => x.ChildUnitId == childUnit.Id))
logger.LogWarning($"Попытка создать существующую связь для {unit.Name} - {childUnit.Name}");
else
{
unit.ChildUnits.Add(
new UnitInUnit
{
ParentUnitId = unit.Id,
ChildUnitId = childUnit.Id,
DateCreated = DateTimeOffset.UtcNow,
}
);
logger.LogInformation($"----- Создана связь Unit {unit.Name} - {childUnit.Name}-----");
}
if (!isChanged)
isChanged = true;
}
foreach (var child in unit.ChildUnits)
{
child.DateSynced = DateTimeOffset.UtcNow;
}
return isChanged;
}
/// <summary>
/// Удаление дочерних связей у ЭК
/// </summary>
/// <param name="unit"></param>
/// <param name="unitToDeleteChilds"></param>
private void DeleteChilds(Unit unit, IEnumerable<UnitInUnit> unitToDeleteChilds)
{
foreach (var childUnit in unitToDeleteChilds)
{
logger.LogInformation($"----- Удаление связи Unit: {unit.Name} -----");
unit.ChildUnits.Remove(childUnit);
}
}
/// <summary>
/// Создать новый ЭК по имени
/// </summary>
/// <param name="ekName"></param>
/// <returns></returns>
private async Task<Unit?> CreateUnit(string ekName)
{
var unit = new Unit
{
Name = ekName.Trim()
};
if (!await unitService.CreateAsync(unit))
{
logger.LogError($"Не удалось создать Unit {unit.Name}");
return null;
}
else
{
logger.LogInformation($"----- Создан Unit: {unit.Name} -----");
return unit;
}
}
/// <summary>
/// Получить ЭК с дочерними связями по имени, если такого ещё нет создать
/// </summary>
/// <param name="ekName"></param>
/// <returns></returns>
private async Task<Unit?> GetUnitByNameAsync(string ekName)
{
if (!unitService.Get().AsNoTracking().Any(u => u.Name.ToLower() == ekName.ToLower().Trim()))
{
return await CreateUnit(ekName);
}
return await unitService.Get()
.Include(u => u.ChildUnits)
.ThenInclude(t => t.ChildUnit)
.FirstAsync(u => u.Name.ToLower() == ekName.ToLower().Trim());
}
}
}

View File

@@ -1,8 +1,11 @@
{
"Logging": {
"LogLevel": {
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
"Override": {
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
}

View File

@@ -108,6 +108,17 @@ namespace PARR.DAL.Context
var dateCreated = new DateTimeOffset(2023, 05, 01, 0, 0, 0, new TimeSpan(0));
#region Unit
//Добавляем ограничение по имени ЭК. нельзя писать прописными!!!
modelBuilder.Entity<Unit>(f =>
{
f.ToTable(t => t.HasCheckConstraint(
"CK_Units_Name_Upper",
"\"Name\" = UPPER(\"Name\") AND \"Name\" IS NOT NULL AND LENGTH(\"Name\")>0"
));
});
#endregion
#region ApplicationType
modelBuilder.Entity<ApplicationType>(f =>
{

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PARR.DAL.Migrations
{
/// <inheritdoc />
public partial class AddUnitNameUppercaseCheck : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddCheckConstraint(
name: "CK_Units_Name_Upper",
schema: "unit",
table: "Units",
sql: "\"Name\" = UPPER(\"Name\") AND \"Name\" IS NOT NULL AND LENGTH(\"Name\")>0");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropCheckConstraint(
name: "CK_Units_Name_Upper",
schema: "unit",
table: "Units");
}
}
}

View File

@@ -2768,6 +2768,8 @@ namespace PARR.DAL.Migrations
b.ToTable("Units", "unit", t =>
{
t.HasComment("Таблица с ЭК");
t.HasCheckConstraint("CK_Units_Name_Upper", "\"Name\" = UPPER(\"Name\") AND \"Name\" IS NOT NULL AND LENGTH(\"Name\")>0");
});
});