feat(aihitSyncer): RemoveFromCacheAsync после обновления атрибутов ЭК

This commit is contained in:
Mikhail Kuznetsov
2026-06-04 14:14:10 +10:00
parent 1f4d338422
commit 0e8dea4c48
4 changed files with 105 additions and 109 deletions

View File

@@ -1,9 +1,9 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using PARR.AIHITMainSyncer.Settings; using PARR.AIHITMainSyncer.Settings;
using PARR.Core.Common.Interfaces; using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitService.Interfaces;
using PARR.Domain.Common.Rabbit.Messages; using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Entities.Unit; using PARR.Domain.Entities.Unit;
@@ -13,26 +13,29 @@ namespace PARR.AIHITMainSyncer.Services
{ {
private readonly ILogger<SyncerService> logger; private readonly ILogger<SyncerService> logger;
private readonly ITransformService transformService; private readonly ITransformService transformService;
private readonly IUnitRepository unitService; private readonly IUnitRepository unitRepository;
private readonly IUnitFieldRepository unitFieldService; private readonly IUnitFieldRepository unitFieldRepository;
private readonly IUnitFieldValueRepository unitFieldValueService; private readonly IUnitFieldValueRepository unitFieldValueRepository;
private readonly FieldValueReplacementsSettings fieldValueReplacementsSettings; private readonly FieldValueReplacementsSettings fieldValueReplacementsSettings;
private readonly IUnitService unitService;
public SyncerService( public SyncerService(
ILogger<SyncerService> logger, ILogger<SyncerService> logger,
ITransformService transformService, ITransformService transformService,
IUnitRepository unitService, IUnitRepository unitRepository,
IUnitFieldRepository unitFieldService, IUnitFieldRepository unitFieldRepository,
IUnitFieldValueRepository unitFieldValueService, IUnitFieldValueRepository unitFieldValueRepository,
FieldValueReplacementsSettings fieldValueReplacementsSettings FieldValueReplacementsSettings fieldValueReplacementsSettings,
IUnitService unitService
) )
{ {
this.logger = logger; this.logger = logger;
this.transformService = transformService; this.transformService = transformService;
this.unitService = unitService; this.unitRepository = unitRepository;
this.unitFieldService = unitFieldService; this.unitFieldRepository = unitFieldRepository;
this.unitFieldValueService = unitFieldValueService; this.unitFieldValueRepository = unitFieldValueRepository;
this.fieldValueReplacementsSettings = fieldValueReplacementsSettings; this.fieldValueReplacementsSettings = fieldValueReplacementsSettings;
this.unitService = unitService;
} }
@@ -45,7 +48,6 @@ namespace PARR.AIHITMainSyncer.Services
logger.LogWarning("Не удалось десериализовать сообщение в объект AihitMainDataMq. Сообщение: {Message}", msg); logger.LogWarning("Не удалось десериализовать сообщение в объект AihitMainDataMq. Сообщение: {Message}", msg);
return; return;
} }
await SyncUnitAsync(objFromQuery); await SyncUnitAsync(objFromQuery);
} }
@@ -55,7 +57,7 @@ namespace PARR.AIHITMainSyncer.Services
logger.LogDebug("Начинаем синхронизацию Unit для объекта с именем: {UnitName}", objFromQuery.Name); logger.LogDebug("Начинаем синхронизацию Unit для объекта с именем: {UnitName}", objFromQuery.Name);
//Получаем сразу словарь атрибутов, чтобы не ходить каждый раз в базу //Получаем сразу словарь атрибутов, чтобы не ходить каждый раз в базу
var fieldsFromDB = await unitFieldService.Get().AsNoTracking().ToListAsync(); var fieldsFromDB = await unitFieldRepository.Get().AsNoTracking().ToListAsync();
if (!objFromQuery.Properties.Any()) if (!objFromQuery.Properties.Any())
{ {
@@ -63,24 +65,24 @@ namespace PARR.AIHITMainSyncer.Services
return; return;
} }
// 1. Создаем копию и нормализуем свойства (удаляем лишние пробелы) // 1. Создаем копию и нормализуем свойства
var normalizedProperties = new Dictionary<string, string?>(); var normalizedProperties = new Dictionary<string, string?>();
foreach (var kvp in objFromQuery.Properties) foreach (var kvp in objFromQuery.Properties)
normalizedProperties[kvp.Key.Trim()] = kvp.Value?.Trim(); normalizedProperties[kvp.Key.Trim()] = kvp.Value?.Trim();
// 2. Применяем замены значений к нормализованным свойствам // 2. Применяем замены значений
ApplyFieldValueReplacements(normalizedProperties); ApplyFieldValueReplacements(normalizedProperties);
var listAihitData = new List<KeyValuePair<string, string?>>(); var listAihitData = new List<KeyValuePair<string, string?>>();
// 3. Обрабатываем простые значения из нормализованных свойств // 3. Обрабатываем простые значения
var propertisWithSimpleValues = normalizedProperties var propertisWithSimpleValues = normalizedProperties
.Where(t => fieldsFromDB.Any(a => IsStringEqual(t.Key.Trim(), a.AihitName) && a.IsMultipleValue != true)) .Where(t => fieldsFromDB.Any(a => IsStringEqual(t.Key.Trim(), a.AihitName) && a.IsMultipleValue != true))
.Select(s => new KeyValuePair<string, string?>(s.Key.Trim(), s.Value?.Trim())).ToList(); .Select(s => new KeyValuePair<string, string?>(s.Key.Trim(), s.Value?.Trim())).ToList();
listAihitData.AddRange(propertisWithSimpleValues); listAihitData.AddRange(propertisWithSimpleValues);
// 4. Обрабатываем множественные значения из нормализованных свойств // 4. Обрабатываем множественные значения
var propertiesWithMultipleValues = objFromQuery.Properties.Where(t => !propertisWithSimpleValues.Any(a => t.Key.Trim() == a.Key)).ToList(); var propertiesWithMultipleValues = objFromQuery.Properties.Where(t => !propertisWithSimpleValues.Any(a => t.Key.Trim() == a.Key)).ToList();
propertiesWithMultipleValues.ForEach(p => listAihitData.AddRange( propertiesWithMultipleValues.ForEach(p => listAihitData.AddRange(
@@ -110,10 +112,13 @@ namespace PARR.AIHITMainSyncer.Services
) )
.ToList(); .ToList();
bool hasChanges = false;
if (unitValuesToRemove.Any()) if (unitValuesToRemove.Any())
{ {
logger.LogDebug("Удаляем {Count} устаревших значений для Unit {UnitName}", unitValuesToRemove.Count, unit.Name); logger.LogDebug("Удаляем {Count} устаревших значений для Unit {UnitName}", unitValuesToRemove.Count, unit.Name);
unitValuesToRemove.ForEach(uv => unit.UnitValues.Remove(uv)); unitValuesToRemove.ForEach(uv => unit.UnitValues.Remove(uv));
hasChanges = true;
} }
var unitValueMissing = listAihitData.Where(t => var unitValueMissing = listAihitData.Where(t =>
@@ -130,7 +135,6 @@ namespace PARR.AIHITMainSyncer.Services
var valuesInChanges = unitValueMissing.Select(s => s.Value).Distinct().ToList(); var valuesInChanges = unitValueMissing.Select(s => s.Value).Distinct().ToList();
await SyncFieldsAsync(fieldsInChanges!, fieldsFromDB); await SyncFieldsAsync(fieldsInChanges!, fieldsFromDB);
await SyncValuesAsync(valuesInChanges); await SyncValuesAsync(valuesInChanges);
var normalizedFieldNames = fieldsInChanges var normalizedFieldNames = fieldsInChanges
@@ -138,7 +142,7 @@ namespace PARR.AIHITMainSyncer.Services
.Where(n => n != null) .Where(n => n != null)
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
var fieldsNameWithId = await unitFieldService.Get().AsNoTracking() var fieldsNameWithId = await unitFieldRepository.Get().AsNoTracking()
.Where(f => normalizedFieldNames.Contains(f.AihitName)) .Where(f => normalizedFieldNames.Contains(f.AihitName))
.ToListAsync(); .ToListAsync();
@@ -147,7 +151,7 @@ namespace PARR.AIHITMainSyncer.Services
.Select(v => v!.Trim()) .Select(v => v!.Trim())
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
var valuesWithId = await unitFieldValueService.Get().AsNoTracking() var valuesWithId = await unitFieldValueRepository.Get().AsNoTracking()
.Where(v => .Where(v =>
(v.Value == null && valuesInChanges.Contains(null)) || (v.Value == null && valuesInChanges.Contains(null)) ||
(v.Value != null && normalizedValues.Contains(v.Value))) (v.Value != null && normalizedValues.Contains(v.Value)))
@@ -156,17 +160,23 @@ namespace PARR.AIHITMainSyncer.Services
foreach (var change in unitValueMissing) foreach (var change in unitValueMissing)
ApplyChange(unit, fieldsNameWithId, valuesWithId, change); ApplyChange(unit, fieldsNameWithId, valuesWithId, change);
if (!await unitService.CommitAsync()) hasChanges = true;
}
else
logger.LogDebug("Нет новых значений для синхронизации для Unit {UnitName}", unit.Name);
// Сохраняем изменения и инвалидируем кэш только если были реальные изменения
if (hasChanges)
{
if (!await unitRepository.CommitAsync())
{ {
logger.LogError("Не удалось применить изменения по актуализиции аттрибуто Unit {UnitName} в базе данных", unit.Name); logger.LogError("Не удалось применить изменения по актуализации атрибутов Unit {UnitName} в базе данных", unit.Name);
return; return;
} }
logger.LogInformation("Успешно обновлены атрибуты для Unit {UnitName}. Добавлено {AddedCount} значений", unit.Name, unitValueMissing.Count); // Инвалидация кэша после успешного сохранения
} await unitService.RemoveFromCacheAsync(unit.Id);
else logger.LogInformation("Успешно обновлены атрибуты для Unit {UnitName}. Кэш инвалидирован.", unit.Name);
{
logger.LogDebug("Нет новых значений для синхронизации для Unit {UnitName}", unit.Name);
} }
logger.LogInformation("Завершена синхронизация Unit {UnitName}", unit.Name); logger.LogInformation("Завершена синхронизация Unit {UnitName}", unit.Name);
@@ -191,7 +201,6 @@ namespace PARR.AIHITMainSyncer.Services
bool isTagProperty = tagUnitField != null && multiValueProperty.Key == tagUnitField.AihitName; bool isTagProperty = tagUnitField != null && multiValueProperty.Key == tagUnitField.AihitName;
string[] splitValues; string[] splitValues;
if (isTagProperty) if (isTagProperty)
{ {
// Для тегов нормализуем разделители до пробела // Для тегов нормализуем разделители до пробела
@@ -249,10 +258,9 @@ namespace PARR.AIHITMainSyncer.Services
} }
private async Task SyncValuesAsync(List<string?> values) private async Task SyncValuesAsync(List<string?> values)
{ {
bool hasNull = values.Any(v => v is null); bool hasNull = values.Any(v => v is null);
// все поля из АИХИТ
var normalizedNoneNullValues = values var normalizedNoneNullValues = values
.Where(v => v != null) .Where(v => v != null)
.Select(v => v!.Trim()) .Select(v => v!.Trim())
@@ -261,8 +269,7 @@ namespace PARR.AIHITMainSyncer.Services
logger.LogDebug("Синхронизируем {Count} значений (включая null: {HasNull})", normalizedNoneNullValues.Count, hasNull); logger.LogDebug("Синхронизируем {Count} значений (включая null: {HasNull})", normalizedNoneNullValues.Count, hasNull);
// Получаем существующие значения из БД var existingValuesInDb = await unitFieldValueRepository.Get()
var existingValuesInDb = await unitFieldValueService.Get()
.Where(v => normalizedNoneNullValues.Contains(v.Value!) || v.Value == null) .Where(v => normalizedNoneNullValues.Contains(v.Value!) || v.Value == null)
.Select(v => v.Value) .Select(v => v.Value)
.ToListAsync(); .ToListAsync();
@@ -287,35 +294,22 @@ namespace PARR.AIHITMainSyncer.Services
return; return;
} }
// Создаём сущности — сохраняем оригинальный регистр из nonNullValues!
// (для null — просто null)
var newFieldValues = newValuesToInsert.Select(v => var newFieldValues = newValuesToInsert.Select(v =>
new UnitFieldValue new UnitFieldValue
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Value = v // ← v — уже trim()-нутый non-null, или null Value = v
}) })
.ToList(); .ToList();
if (!await unitFieldValueService.AddRangeAsync(newFieldValues) || !await unitFieldValueService.CommitAsync()) // Только добавляем в контекст, НЕ коммитим
if (!await unitFieldValueRepository.AddRangeAsync(newFieldValues))
{ {
logger.LogError( logger.LogError("Не удалось добавить {Count} новых значений FieldValues в контекст", newValuesToInsert.Count);
"Не удалось сохранить {Count} новых значений FieldValues",
newValuesToInsert.Count);
} }
else else
{ {
if (newValuesToInsert.Count <= 10) logger.LogDebug("Подготовлено {Count} новых значений FieldValues для сохранения", newValuesToInsert.Count);
{
var preview = string.Join(", ", newValuesToInsert.Select(v => v ?? "<null>"));
logger.LogInformation("Добавлено {Count} значений FieldValues: [{Values}]", newValuesToInsert.Count, preview);
}
else
{
logger.LogInformation("Добавлено {Count} значений FieldValues (первые 5: {Preview})",
newValuesToInsert.Count,
string.Join(", ", newValuesToInsert.Take(5).Select(v => v ?? "<null>")));
}
} }
} }
@@ -341,24 +335,16 @@ namespace PARR.AIHITMainSyncer.Services
EsppName = null, EsppName = null,
}; };
if (!await unitFieldService.CreateAsync(field)) // Только добавляем в контекст, НЕ коммитим
if (!await unitFieldRepository.CreateAsync(field))
{ {
logger.LogError("Не удалось создать запись в таблице Fields: {FieldName}", item); logger.LogError("Не удалось добавить поле в контекст: {FieldName}", item);
} }
else else
{ {
logger.LogInformation("Создана запись в таблице Fields: {FieldName}", item); logger.LogDebug("Подготовлено поле для сохранения: {FieldName}", item);
} }
} }
if (!await unitFieldService.CommitAsync())
{
logger.LogError("Не удалось применить изменения по добавлению новых Fields в базе данных");
}
else
{
logger.LogDebug("Успешно зафиксированы изменения по добавлению новых Fields");
}
} }
@@ -393,7 +379,7 @@ namespace PARR.AIHITMainSyncer.Services
{ {
var unit = new Unit { Name = name.Trim().ToUpperInvariant() }; var unit = new Unit { Name = name.Trim().ToUpperInvariant() };
if (!await unitService.CreateAsync(unit) || !await unitService.CommitAsync()) if (!await unitRepository.CreateAsync(unit) || !await unitRepository.CommitAsync())
{ {
logger.LogError("Не удалось создать Unit {UnitName}", unit.Name); logger.LogError("Не удалось создать Unit {UnitName}", unit.Name);
throw new InvalidOperationException($"Не удалось создать Unit {unit.Name}"); throw new InvalidOperationException($"Не удалось создать Unit {unit.Name}");
@@ -402,7 +388,6 @@ namespace PARR.AIHITMainSyncer.Services
{ {
logger.LogInformation("Создан Unit: {UnitName}", unit.Name); logger.LogInformation("Создан Unit: {UnitName}", unit.Name);
} }
return unit; return unit;
} }
@@ -417,7 +402,7 @@ namespace PARR.AIHITMainSyncer.Services
private IQueryable<Unit> GetUnitWithFieldsAndValues() private IQueryable<Unit> GetUnitWithFieldsAndValues()
{ {
return unitService.Get() return unitRepository.Get()
.Include(t => t.UnitValues) .Include(t => t.UnitValues)
.ThenInclude(uv => uv.Field) .ThenInclude(uv => uv.Field)
.Include(t => t.UnitValues) .Include(t => t.UnitValues)
@@ -442,7 +427,6 @@ namespace PARR.AIHITMainSyncer.Services
replacement.FieldName, currentValue, replacement.NewValue); replacement.FieldName, currentValue, replacement.NewValue);
properties[replacement.FieldName] = replacement.NewValue; properties[replacement.FieldName] = replacement.NewValue;
} }
} }
} }
} }

View File

@@ -58,7 +58,7 @@ namespace PARR.Core.Services.MatchingStatusService
case SyncTaskEntityTypeEnum.Template: case SyncTaskEntityTypeEnum.Template:
throw new Exception("Нет реализации получения кэша статуса matching для template"); throw new Exception("Нет реализации получения кэша статуса matching для template");
//todo: У нас вообще нет matching для Template, позже надо удалить это вообще из Enum SyncTaskEntityTypeEnum //todo: У нас вообще нет matching для Template, позже надо удалить это вообще из Enum SyncTaskEntityTypeEnum
break; //break;
} }
// Если списки пустые, мало ли, говорим что нет matching`a // Если списки пустые, мало ли, говорим что нет matching`a

View File

@@ -1,29 +1,12 @@
using Elastic.CommonSchema.Serilog; using Elastic.CommonSchema.Serilog;
using PARR.Core;
using PARR.DAL; using PARR.DAL;
using PARR.EsppApi; using PARR.EsppApi;
using PARR.Infrastructure; using PARR.TemplateMatcher;
using PARR.Test; using PARR.Test;
using PARR.Test.NextRun; using PARR.Test.NextRun;
using Serilog; using Serilog;
/*IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
//services.InstallEsppApiServices(hostContext.Configuration);
//services.InstallBllServices(hostContext.Configuration);
services.InstallDalServices(hostContext.Configuration);
//services.InstallTemplateDistributorSerivces(hostContext.Configuration);
services.AddHostedService<Worker>();
})
.UseSerilog((hostContext, services, config) =>
{
config
.WriteTo.Console()
.ReadFrom.Configuration(hostContext.Configuration);
})
.Build();*/
var builder = Host.CreateApplicationBuilder(); var builder = Host.CreateApplicationBuilder();
builder.Services.AddLogging(config => builder.Services.AddLogging(config =>
@@ -42,12 +25,17 @@ builder.Services.AddLogging(config =>
config.AddSerilog(logger.CreateLogger()); config.AddSerilog(logger.CreateLogger());
}); });
builder.Services.AddDalServices(builder.Configuration);
builder.Services.AddCoreServices(builder.Configuration); builder.Services.InstallTemplateMatcherSerivces(builder.Configuration);
builder.Services.AddInfrastructureServices(builder.Configuration); // builder.Services.AddDalServices(builder.Configuration);
// builder.Services.AddCoreServices(builder.Configuration);
// builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.InstallEsppApiServices(builder.Configuration); builder.Services.InstallEsppApiServices(builder.Configuration);
builder.Configuration.AddTemplateMatcherConfigurations(builder.Services);
builder.Configuration.AddDalConfigurations(builder.Services); builder.Configuration.AddDalConfigurations(builder.Services);
builder.Services.AddTemplateMatcherSettings(builder.Configuration);
builder.Services.AddDallSettings(builder.Configuration); builder.Services.AddDallSettings(builder.Configuration);
builder.Services.AddTransient<NextRunTest>(); builder.Services.AddTransient<NextRunTest>();

View File

@@ -1,9 +1,13 @@
using Microsoft.EntityFrameworkCore;
using PARR.Core.Common.Interfaces; using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Cache; using PARR.Domain.Cache;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums; using PARR.Domain.Enums;
using PARR.EsppApi; using PARR.EsppApi;
using PARR.EsppApi.Constants; using PARR.EsppApi.Constants;
using PARR.EsppApi.Models.Query; using PARR.EsppApi.Models.Query;
using PARR.TemplateMatcher;
using PARR.Test.NextRun; using PARR.Test.NextRun;
namespace PARR.Test namespace PARR.Test
@@ -37,16 +41,11 @@ namespace PARR.Test
//var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0)); //var bbb = aaa.ToOffset(new TimeSpan(3, 0, 0));
#region NextRunTest await TemplateMatcherTest();
using (var scope = serviceProvider.CreateScope())
{
var service = scope.ServiceProvider.GetService<NextRunTest>();
await service!.Test();
}
#endregion //await NextRunTest();
#region test #region test
@@ -161,21 +160,46 @@ namespace PARR.Test
#endregion #endregion
////test interval
//await intervalService.IntervalInitAsync(async () =>
//{
// _logger.LogInformation("---Выполняю метод---");
// await Task.Delay(29*1000);
// _logger.LogInformation("===Выполнил метод===");
//}, new TimeSpan(0, 0, 30));
//await TestEsppApi();
//await StopDiscontinuedOrdersInEspp();
} }
#region TemplateMatcher
private async Task TemplateMatcherTest()
{
await using var scope = serviceProvider.CreateAsyncScope();
var templateMatcher = scope.ServiceProvider.GetRequiredService<ITemplateMatcher>();
var groupRepository = scope.ServiceProvider.GetRequiredService<IJobGroupRepository>();
var groups = await groupRepository.Get().AsNoTracking()
.Where(t => t.GroupType != null && t.GroupType.Code == JobGroupTypesEnum.Group)
.Select(t => t.Id).ToHashSetAsync();
foreach (var groupId in groups)
{
await templateMatcher.SyncTemplatesForJobGroup(groupId,
new HistoryInitiator
{
InitiatorIp = "10.99.246.156",
InitiatorParrComponentId = ParrComponentsEnum.Master,
InitiatorComment = "Тестовый проект. Синхронизация Группированых работ"
}
);
}
return;
}
#endregion
private async Task NextRunTest()
{
using (var scope = serviceProvider.CreateScope())
{
var service = scope.ServiceProvider.GetService<NextRunTest>();
await service!.Test();
}
}
private async Task TestEsppApi() private async Task TestEsppApi()
{ {