using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.TemplateRepositories; using PARR.Core.Services.Shortcodes; using PARR.Domain.Entities; using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; using PARR.EsppSync.Attributes; using PARR.EsppSync.Helpers; using System.Reflection; namespace PARR.EsppSync { internal class SyncService : ISyncService where EsppObject : class, IEsppObject { private readonly ILogger> _logger; private readonly IServiceProvider _serviceProvider; public SyncService( ILogger> logger, IServiceProvider serviceProvider ) { _logger = logger; _serviceProvider = serviceProvider; } public async Task SyncEsppObjectAsync( string str, ParseStringToEsppObject parseStringToEsppObject, ConvertDbToEsppObject converterDbToEsppObject, CustomComparisionCheckAsync? customComparisionAsync = null, AfterParseStringToEsppObjectAsync? afterParseStringToEsppObjectAsync = null ) { _logger.LogDebug("Получил строку. Начинаю работать. Строка: {String}", str); if (string.IsNullOrEmpty(str)) { _logger.LogWarning("Получил пустую строку, ничего не делаю."); return; } var esppObject = parseStringToEsppObject.Invoke(str); if (esppObject == null) { _logger.LogWarning("После парсинга строки, esppObject = null. Дальше ничего не буду делать."); return; } // Вызовем кастомный метод, если он есть if (afterParseStringToEsppObjectAsync != null) await afterParseStringToEsppObjectAsync.Invoke(esppObject); using (var scope = _serviceProvider.CreateScope()) { var templateService = scope.ServiceProvider.GetRequiredService(); var robotConfigurationService = scope.ServiceProvider.GetRequiredService(); var shortcodesService = scope.ServiceProvider.GetRequiredService(); var templateRenamePendingRepository = scope.ServiceProvider.GetRequiredService(); try { #region Проверка, идет ли переименование // Если есть у этого шаблона запись в TemplateRenamePending, значит сейчас идет переименование, пропускаем синхронизацию этого шаблона // Если имя этого шаблона находится в таблице TemplateRenamePending, значит идет переименование связанного шаблона, пропускаем синхронизацию этого шаблона var isPartOfRename = await templateRenamePendingRepository.Get() .AnyAsync(t => t.Template.Name == esppObject.TemplateName || t.OldName == esppObject.TemplateName); if (isPartOfRename) { _logger.LogInformation("Пропускаем синхронизацию '{TemplateName}', так как шаблон ({EsppObject}) участвует в процессе переименования.", esppObject.TemplateName, nameof(EsppObject)); // Выходим сразу return; } #endregion // Загружаем Template и TemplateForShortcodes в одном запросе var query = templateService.Get() .Include(h => h.Unit) .ThenInclude(t => t!.UnitValues) .ThenInclude(t => t.Value) .Include(h => h.Unit) .ThenInclude(t => t!.UnitValues) .ThenInclude(t => t.Field) .Include(t => t.RobotConfigurations) .Include(t => t.Job) .ThenInclude(j => j.Group) .ThenInclude(g => g.GroupType) .Include(t => t.Job) .ThenInclude(t => t.Group) .ThenInclude(t => t.ScheduleExcludeType) .Include(t => t.Job) .ThenInclude(t => t.Group) .ThenInclude(t => t.ScheduleExcludeTypeCalendar) .Include(t => t.Job) .ThenInclude(j => j.Tnk) .ThenInclude(s => s!.Subprocess) .ThenInclude(p => p!.Process) .Include(t => t.UnitsInTemplate); var template = await query.FirstOrDefaultAsync(t => t.Name == esppObject.TemplateName); if (template == null) { _logger.LogWarning("Найден объект в ЕСПП с именем шаблона '{TemplateName}' незарегистрированный в ПАРР. Строка: {String}", esppObject.TemplateName, str); return; } var dbObjectInEsppObject = converterDbToEsppObject.Invoke(template); await ApplyShortcodesAsync(dbObjectInEsppObject, template, shortcodesService); bool isChanged = false; // если в БД isActive == false, то синхронизировать только по полям из IEsppObject if (dbObjectInEsppObject.IsActive == false) { _logger.LogDebug("Объект деактивирован в ПАРР. Сравниваем только обязательные поля. {TemplateName}", esppObject.TemplateName); var lightDbObj = new EsppLightObject(dbObjectInEsppObject); var lightEsppObject = new EsppLightObject(esppObject); isChanged = IsChanged(lightEsppObject, lightDbObj, esppObject.TemplateName); } else { _logger.LogDebug("Объект активирован в ПАРР. Сравниваем все поля. {TemplateName}", esppObject.TemplateName); isChanged = IsChanged(esppObject, dbObjectInEsppObject, esppObject.TemplateName); // выполняем кастомную дополнительную проверку (только если isChanged==false, чтоб лишний раз не гонять) if (!isChanged) { if (customComparisionAsync != null) { // добавлена дополнительная проверка var isCustomComparision = await customComparisionAsync.Invoke(esppObject, dbObjectInEsppObject, template.Id); if (isCustomComparision) { _logger.LogDebug("Дополнительная проверка прошла."); } else { // если дополнительная проверка не прошла, то говорим что есть изменения isChanged = true; _logger.LogDebug("Дополнительная проверка не прошла, ставим статус isChanged: {isChanged}", isChanged); } } else { _logger.LogDebug("Дополнительная проверка отсутствует"); } } } if (isChanged) { _logger.LogDebug("Есть изменения, требуется обновление. {TemplateName}", esppObject.TemplateName); var config = robotConfigurationService.GetFromTemplateByRobotCode(esppObject.Robot, template); // Если предыдущий статус был Create или Ок, то ставим ему Update // пусть даже Create завершился с ошибками, но раз он уже есть в ЕСПП, то изменим на Update, сбросим все счетчики и пусть попробует обновить и исправить все if (config.TaskStatusCode != (int)TaskStatusEnum.Updating) { //SetUpdateStatus(ref template, robotConfigurationService, esppObject.Robot); var isChangedStatus = robotConfigurationService.SetUpdateTaskStatusIfAllow(config); if (isChangedStatus) { if (!await templateService.CommitAsync(GetInitiator())) _logger.LogError("Не удалось изменить запись Template {TemplateName}, Robot: {Robot}", template.Name, esppObject.Robot); else _logger.LogInformation("Установлен принудительный статус {TaskStatus}, Template {templateName}, Robot: {Robot}", TaskStatusEnum.Updating, template.Name, esppObject.Robot); } else { _logger.LogInformation("Нельзя установить статус Updating для шаблона {templateName}, так как текущий статус это запрещает.", template.Name); } } else { // Если пред статус был Update, то ничего не делаем, так его и оставляем, не сбрасывам кол-во попыток и ошибок _logger.LogInformation("Есть изменения в Template {TemplateName}, но предыдущий статус TaskStatusCode: {TaskStatusCode}. Не меняем статус, будем разбираться вручную.", template.Name, (TaskStatusEnum)config.TaskStatusCode); } }//надо ли проверять если не изменился, но был статус Updating не понятно. Доверяем роботу пока, что после окончания работ он точно сообщит else { _logger.LogDebug("Нет изменений, обновление не требуется. {TemplateName}", esppObject.TemplateName); //если все поля совпали //проверяем, какой был статус предыдущий статус в БД, если он был не Ок, то ставим ему ОК var robotConfig = robotConfigurationService.GetFromTemplateByRobotCode(esppObject.Robot, template); if (robotConfig.TaskStatusCode != (int)TaskStatusEnum.Ok) { robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Ok, robotConfig); if (!await templateService.CommitAsync(GetInitiator())) _logger.LogError("Не удалось изменить запись Template {TemplateName}, Robot: {Robot}", template.Name, esppObject.Robot); else _logger.LogInformation("Установлен принудительный статус {TaskStatus}, Template {TemplateName}, Robot: {Robot}", TaskStatusEnum.Ok, template.Name, esppObject.Robot); } } } catch (Exception ex) { _logger.LogError(ex, "Ошибка синхронизации объекта АСУ ЕСПП {TemplateName}, {EsppObject}", esppObject.TemplateName, nameof(EsppObject)); } } } /// /// Получить инициатора изменений для сохранения в БД /// /// private IHistoryInitiator GetInitiator() { return new HistoryInitiator { InitiatorComment = "Обновление статуса синхронизации, SyncService.", InitiatorParrComponentId = typeof(EsppObject).Name.Contains("Template") ? ParrComponentsEnum.EsppTemplateSync : ParrComponentsEnum.EsppScheduleSync }; } /// /// Применяем шорткоды /// /// /// /// /// private async Task ApplyShortcodesAsync(EsppObject dbObjectInEsppObject, Template template, IShortcodesService shortcodesService) { // Проверяем наличие Shortcode в полях объекта из БД var properties = dbObjectInEsppObject.GetType().GetProperties(); foreach (PropertyInfo property in properties) { if (property.PropertyType == typeof(string)) { var value = property.GetValue(dbObjectInEsppObject)?.ToString(); if (!string.IsNullOrEmpty(value)) { // Передаём исходный template — он уже загружен с Include var processedValue = await shortcodesService.ApplyShortcodesAsync(value, template); property.SetValue(dbObjectInEsppObject, processedValue); } } } } /// /// Сравнение объектов /// /// /// /// /// private bool IsChanged(object esppObj, object dbObj, string templateName) { var dbType = dbObj.GetType(); var esppType = esppObj.GetType(); foreach (var dbProp in dbType.GetProperties()) { // Пропускаем свойства, помеченные атрибутом SkipComparison if (Attribute.IsDefined(dbProp, typeof(SkipComparisonAttribute))) { _logger.LogDebug("Пропущено сравнение поля {PropertyName} (помечено [SkipComparison]). Шаблон: {TemplateName}", dbProp.Name, templateName); continue; } // Значение из объекта БД //var dbValue = dbObj.GetType().GetProperty(prop.Name)?.GetValue(dbObj, null); var dbValue = dbProp.GetValue(dbObj); // Ищем аналогичное свойство в объекте из ЕСПП по имени //var esppValue = esppObj.GetType().GetProperty(dbProp.Name)?.GetValue(esppObj, null); var esppProp = esppType.GetProperty(dbProp.Name); var esppValue = esppProp?.GetValue(esppObj); // Replace("\r","").Replace("\n","") - в подробном описании могут быть переносы строк, в Rabbit прилетает без переносов. Убираем переносы для стравнения // Если значение null, хелпер Normalize вернет string.Empty, что предотвратит NRE и ложные срабатывания. var dbValueStr = EsppSyncHelpers.Normalize(dbValue?.ToString()); var esppValueStr = EsppSyncHelpers.Normalize(esppValue?.ToString()); if (dbValueStr != esppValueStr) { _logger.LogInformation("Не совпадают поля ({PropertyName}). dbValueStr: {DbValueStr}, esppValueStr: {EsppValueStr}. Имя шаблона: {TemplateName}", dbProp.Name, dbValueStr, esppValueStr, templateName); return true; } } return false; } } }