feat(aihitRelationshipsSyncer): Реализована логика синхронизации связей между Unit+CI/CD

This commit is contained in:
Mikhail Kuznetsov
2025-05-27 11:32:05 +10:00
parent cc52818d29
commit 078c8b540f
33 changed files with 4389 additions and 6 deletions

View File

@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using PARR.AIHITRelationshipsSyncer.Context;
using PARR.AIHITRelationshipsSyncer.Services.Implementations;
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
using PARR.AIHITRelationshipsSyncer.Settings;
using PARR.BLL;
using PARR.DAL;
namespace PARR.AIHITRelationshipsSyncer
{
public static class AihitRelationshpsSyncerInstaller
{
public static void InstallAihitRelationshpsSyncerServices(this IServiceCollection services, IConfiguration configuration)
{
services.InstallBllServices(configuration);
services.InstallDalServices(configuration);
var settings = new WorkerSettings();
configuration.GetSection(nameof(WorkerSettings)).Bind(settings);
services.AddSingleton(settings);
services.AddDbContext<AIHITContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("AihitConnection"),
sqlServerOptions => sqlServerOptions.CommandTimeout(1800)
));
services.AddTransient<IRelationshipsSyncer, RelationshipsSyncer>();
services.AddTransient<IAihitService, AihitService>();
services.AddTransient<IRelationshipsSyncService, RelationshipsSyncSrevice>();
}
public static IConfigurationBuilder AddAihitRelationshpsSyncerConfigurations(this IConfigurationBuilder builder, IServiceCollection services)
{
builder.AddDalConfigurations(services);
return builder;
}
public static void AddAihitRelationshpsSyncerSettings(this IServiceCollection services, IConfiguration configuration)
{
services.AddDallSettings(configuration);
}
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using PARR.AIHITRelationshipsSyncer.Models;
namespace PARR.AIHITRelationshipsSyncer.Context
{
internal class AIHITContext : DbContext
{
public AIHITContext(DbContextOptions<AIHITContext> options) : base(options) { }
public DbSet<AihitData> AihitDatas { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace PARR.AIHITRelationshipsSyncer
{
public interface IRelationshipsSyncer
{
Task StartAsync();
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.AIHITRelationshipsSyncer.Models
{
[PrimaryKey(nameof(EKFindCode), nameof(ChildEk))]
public class AihitData
{
[Column("КОД_ПОИСКАК")]
public string? EKFindCode { get; set; }
//[Column("СТАТУС")]
//public string? Status { get; set; }
//[Column("ЗОНА_ОТВЕТСТВЕННОСТИ")]
//public string? ResponseArea { get; set; }
//[Column("ПОДКАТЕГОРИЯ_ЭК")]
//public string? EKSubCategory { get; set; }
//[Column("ТИП_ЭК")]
//public string? EKType { get; set; }
[Column("Дочерний ЭК")]
public string? ChildEk { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PARR.BLL\PARR.BLL.csproj" />
<ProjectReference Include="..\PARR.DAL\PARR.DAL.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,71 @@
using Microsoft.Extensions.Logging;
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
using PARR.AIHITRelationshipsSyncer.Settings;
using PARR.BLL.Services.Interfaces;
namespace PARR.AIHITRelationshipsSyncer
{
internal class RelationshipsSyncer : IRelationshipsSyncer
{
private readonly ILogger<RelationshipsSyncer> logger;
private readonly IIntervalService intervalService;
private readonly IAihitService aihitService;
private readonly IRelationshipsSyncService relationshipsSyncService;
private readonly WorkerSettings workerSettings;
public RelationshipsSyncer(
ILogger<RelationshipsSyncer> logger,
IIntervalService intervalService,
IAihitService aihitService,
IRelationshipsSyncService relationshipsSyncService,
WorkerSettings workerSettings
)
{
this.logger = logger;
this.intervalService = intervalService;
this.aihitService = aihitService;
this.relationshipsSyncService = relationshipsSyncService;
this.workerSettings = workerSettings;
}
public async Task StartAsync()
{
logger.LogInformation("Запуск сервиса синхронизации связей иерархии ЭК из АИХ ИТ в ПАРР");
await intervalService.IntervalInitAsync(async () =>
{
try
{
logger.LogInformation($"Начата загрузка иерархических связей между ЭК из АИХ ИТ");
var aihitdata = aihitService.GetData();
var aihitDataCount = aihitdata?.Count();
if (aihitdata == null || aihitDataCount == 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());
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка синхронизации иерархических данных Unit из АИХ ИТ в ПАРР");
}
finally
{
logger.LogInformation($"Завершена загрузка иерархических связей между ЭК из АИХ ИТ");
}
}, workerSettings.RepeatEvery);
}
}
}

View File

@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.AIHITRelationshipsSyncer.Context;
using PARR.AIHITRelationshipsSyncer.Models;
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
namespace PARR.AIHITRelationshipsSyncer.Services.Implementations
{
internal class AihitService : IAihitService
{
private readonly ILogger<AihitService> logger;
private readonly AIHITContext context;
public AihitService(
ILogger<AihitService> logger,
AIHITContext context
)
{
this.logger = logger;
this.context = context;
}
/// <summary>
/// Получение данных из АИХ ИТ вызовом хранимой процедуры в базе данных
/// </summary>
/// <returns></returns>
public IEnumerable<AihitData>? GetData()
{
try
{
var result = context.AihitDatas.FromSqlRaw($"EXEC mao2.dbo.sp_IPP_PARR_PTK_get_Relationships").ToList();
if (result == null || !result.Any())
{
logger.LogWarning($"Процедура sp_IPP_PARR_PTK_get_Relationships вернула пустой список ЭК");
}
return result;
}
catch (Exception ex)
{
logger.LogError(ex, $"Ошибка при выполнении ХП sp_IPP_PARR_PTK_get_Relationships");
}
return null;
}
}
}

View File

@@ -0,0 +1,162 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.AIHITRelationshipsSyncer.Models;
using PARR.AIHITRelationshipsSyncer.Services.Interfaces;
using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Interfaces.Unit;
namespace PARR.AIHITRelationshipsSyncer.Services.Implementations
{
internal class RelationshipsSyncSrevice : IRelationshipsSyncService
{
private readonly ILogger<RelationshipsSyncSrevice> logger;
private readonly IUnitService unitService;
public RelationshipsSyncSrevice(ILogger<RelationshipsSyncSrevice> logger,
IUnitService unitService
)
{
this.logger = logger;
this.unitService = unitService;
}
public async Task SyncAsync(List<AihitData> aihitdata)
{
var groupedData = aihitdata.GroupBy(ad => ad.EKFindCode);
var units = await GetUnitsWithChildrens();
//Перебираем сгруппированные по родительскому ЭК данные
foreach (var item in groupedData)
{
//Получаем имя родителя
var parentName = item.Select(t => t.EKFindCode).First();
if (parentName == null)
continue;
//if (parentName.ToLower() != "ас-рп-рцку-гор")
// continue;
//По имени получаем экземпляр Unit со всеми дочерними связями
var unit = await GetUnitByNameAsync(parentName);
//Из данных АИХ ИТ получаем имена всех дочерних ЭК
var childsName = item.Select(t => t.ChildEk).ToList();
if (childsName == null || childsName.Count == 0)
continue;
//Проходим циклом по полученному списку для актуализации связей в полученном нами экземпляре Unit
foreach (var childName in childsName)
{
if (childName == null)
continue;
//if (childName.ToLower() != "рпа-робин-рцку-спиуи-прочее-26-гор")
// continue;
//Ищем детей по имени
var child = unit.ChildUnits.FirstOrDefault(t => t.ChildUnit?.Name.ToLower() == childName.ToLower().Trim());
//не нашли создаём и сразу пишем в базу, чтобы если попадуться дубликаты в списке они были созданы
if (child == null)
{
var childUnit = await GetUnitByNameAsync(childName!);
unit.ChildUnits.Add(
new UnitInUnit
{
ParentUnitId = unit.Id,
ChildUnitId = childUnit.Id,
DateCreated = DateTimeOffset.UtcNow,
DateSynced = DateTimeOffset.UtcNow
}
);
if (!await unitService.CommitAsync())
logger.LogError($"Не удалось создать связь Unit {unit.Name} с {childUnit.Name}");
else
logger.LogDebug($"----- Изменене связи Unit: {unit.Name} -----");
}
//Для всех обновляем дату синхронизации
else
child.DateSynced = DateTimeOffset.UtcNow;
}
//После проверки пришедших к нам из АИХ ИТ дочерних связей удаляем лишние/уже не существующие в базе
var childsToDelete = unit.ChildUnits.Where(t => childsName.All(a => t.ChildUnit?.Name != a)).ToList();
if (childsToDelete != null && childsToDelete.Count() > 0)
DeleteChilds(unit, childsToDelete);
//применяем изменения в базе данных
if (!await unitService.CommitAsync())
logger.LogError($"Не удалось изменить связи Unit {unit.Name}");
else
logger.LogDebug($"----- Изменены связи Unit: {unit.Name} -----");
}
}
/// <summary>
/// Удаление дочерних связей у ЭК
/// </summary>
/// <param name="unit"></param>
/// <param name="unitToDeleteChilds"></param>
private void DeleteChilds(Unit unit, List<UnitInUnit> unitToDeleteChilds)
{
foreach (var childUnit in unitToDeleteChilds)
{
logger.LogInformation($"----- Удаление связи Unit: {childUnit.ParentUnit!.Name} - {childUnit.ChildUnit!.Name} -----");
unit.ChildUnits.Remove(childUnit);
}
}
/// <summary>
/// Получение списка ЭК с дочерними связями
/// </summary>
/// <returns></returns>
private async Task<List<Unit>> GetUnitsWithChildrens()
{
return await unitService.Get()
.Include(t => t.ChildUnits).ToListAsync();
}
/// <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) || !await unitService.CommitAsync())
logger.LogError($"Не удалось создать Unit {unit.Name}");
else
logger.LogDebug($"----- Создан 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)
.FirstAsync(u => u.Name.ToLower() == ekName.ToLower().Trim());
}
}
}

View File

@@ -0,0 +1,9 @@
using PARR.AIHITRelationshipsSyncer.Models;
namespace PARR.AIHITRelationshipsSyncer.Services.Interfaces
{
public interface IAihitService
{
IEnumerable<AihitData>? GetData();
}
}

View File

@@ -0,0 +1,9 @@
using PARR.AIHITRelationshipsSyncer.Models;
namespace PARR.AIHITRelationshipsSyncer.Services.Interfaces
{
public interface IRelationshipsSyncService
{
Task SyncAsync(List<AihitData> aihitdata);
}
}

View File

@@ -0,0 +1,7 @@
namespace PARR.AIHITRelationshipsSyncer.Settings
{
internal class WorkerSettings
{
public TimeSpan RepeatEvery { get; set; }
}
}