using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PARR.BLL.Helpers; using PARR.Constants; using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.Models; using PARR.DAL.NextRunServices; using PARR.DAL.Services.Interfaces; using PARR.EsppSync.Domain; 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 ) { this.logger = logger; this.serviceProvider = serviceProvider; } public async Task SyncEsppObjectAsync( string str, ParserHandlerDelegate parser, ConvertDbObjToComparisonObjHandlerDelegate converterToEsppObject ) { logger.LogDebug("Получил строку. Начинаю работать. Строка: {String}", str); if (string.IsNullOrEmpty(str)) { logger.LogWarning("Получил пустую строку, ничего не делаю."); return; } var esppObject = parser.Invoke(str); if (esppObject == null) { logger.LogWarning("После парсинга строки, esppObject = null. Дальше ничего не буду делать."); return; } using (var scope = serviceProvider.CreateScope()) { var templateService = GetServiceInScope(scope); var robotConfigurationService = GetServiceInScope(scope); var shortcodesService = GetServiceInScope(scope); var nextRunService = GetServiceInScope(scope); try { // Загружаем Template и TemplateForShortcodes в одном запросе var query = templateService.Get() //.AsNoTracking() .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 = converterToEsppObject.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); // если это сравнение расписаний, рассчитать nextRun, и сравнить все три nextRun, БД - ЕСПП - Расчитанное // будем считать, только если НЕТ изменений в остальных полях, если изменения есть, зачем лишний раз напрягать, потом в контроллере все пересчитается if (!isChanged && dbObjectInEsppObject is EsppObjectSchedule dbValues && esppObject is EsppObjectSchedule esppValues) { var isEqualNextRun = await IsEqualNextRunAsync(nextRunService, dbValues, esppValues, template.Id); if (isEqualNextRun == false) isChanged = true; } } 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); if (!await templateService.CommitAsync()) 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 { // Если пред статус был 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()) 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.TemplateName); } } } /// /// Применяем шорткоды /// /// /// /// /// 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 void SetUpdateStatus(ref Template template, IRobotConfigurationService robotConfigurationService, RobotsEnum robot) { var robotConfig = robotConfigurationService.GetFromTemplateByRobotCode(robot, template); robotConfigurationService.ChangeTaskStatus(TaskStatusEnum.Updating, robotConfig); } /// /// Получить сервис из scope /// /// /// /// /// private Service GetServiceInScope(IServiceScope scope) { var service = scope.ServiceProvider.GetService(); if (service == null) throw new Exception($"Не найден сервис: {nameof(Service)}"); return service; } /// /// Сравнение объектов /// /// /// /// /// private bool IsChanged(object esppObj, object dbObj, string templateName) { foreach (var prop in dbObj.GetType().GetProperties()) { if (prop == null) continue; var dbValue = dbObj.GetType().GetProperty(prop.Name)?.GetValue(dbObj, null); var esppValue = esppObj.GetType().GetProperty(prop.Name)?.GetValue(esppObj, null); if (dbValue == null || esppValue == null) continue; //Replace("\r","").Replace("\n","") - в подробном описании могут быть переносы строк, в Rabbit прилетает без переносов. Убираем переносы для стравнения var dbValueStr = Normalize(dbValue!.ToString()!); var esppValueStr = Normalize(esppValue!.ToString()!); if (dbValueStr != esppValueStr) { logger.LogInformation("Не совпадают поля ({PropertyName}). dbValueStr: {DbValueStr}, esppValueStr: {EsppValueStr}. Имя шаблона: {TemplateName}", prop.Name, dbValueStr, esppValueStr, templateName); return true; } } return false; } /// /// Удаляет ненужные символы из строки /// /// /// private string Normalize(string str) { str = str.Replace("\r", string.Empty); str = str.Replace("\n", string.Empty); str = str.Replace(" ", string.Empty); return str.ToLower(); } /// /// Рассчитать nextRun в часовом поясе УЗ робота /// /// /// /// private async Task CalcNextRunAsync(INextRunServiceV2 nextRunService, Guid templateId) { var nextRun = await nextRunService.GetNextRunForTemplateAsync(templateId, isNew: false); if (nextRun.HasValue) { var nextRunWithEsppAccountTz = nextRun.Value.Add(nextRunService.GetEsppAccountOffset()); logger.LogDebug("Расчитанный nextRun для шаблона {templateId}, UTC: {nextRun}, EsppAccountTz: {nextRunWithEsppAccountTz}", templateId, nextRun, nextRunWithEsppAccountTz); return nextRunWithEsppAccountTz; } else { logger.LogError("При расчете nextRun для templateId: {templateId} верунлся null", templateId); return null; } } /// /// Сравнить NextRun из БД, ЕСПП, рассчитанный /// /// /// /// /// private async Task IsEqualNextRunAsync(INextRunServiceV2 nextRunService, EsppObjectSchedule dbValues, EsppObjectSchedule esppValues, Guid templateId) { // в часовой зоне робота var calculatedNextRun = await CalcNextRunAsync(nextRunService, templateId); if (calculatedNextRun == null) { return false; } var scheduledCalculated = EsppScheduleHelpers.GetNextRun(calculatedNextRun.Value); var basisTimeCalculated = EsppScheduleHelpers.GetGenerationTime(calculatedNextRun.Value); logger.LogDebug("Рассчитанные значения для шаблона '{templateName}', {templateId}, следующее срабатывание {scheduledCalculated}, время создания наряда: {basisTimeCalculated}", dbValues.TemplateName, templateId, scheduledCalculated, basisTimeCalculated); if (Normalize(dbValues.Scheduled) != Normalize(esppValues.Scheduled) || Normalize(dbValues.Scheduled) != scheduledCalculated || Normalize(esppValues.Scheduled) != scheduledCalculated) { logger.LogInformation("Не совпадают поля ({propName}), dbValueStr: {dbValueStr}, esppValueStr: {esppValueStr}, scheduledCalculated: {scheduledCalculated}. Имя шаблона: {templateName}", nameof(dbValues.Scheduled), dbValues.Scheduled, esppValues.Scheduled, scheduledCalculated, dbValues.TemplateName); return false; } if (Normalize(dbValues.BasisTime) != Normalize(esppValues.BasisTime) || Normalize(dbValues.BasisTime) != basisTimeCalculated || Normalize(esppValues.BasisTime) != basisTimeCalculated) { logger.LogInformation("Не совпадают поля ({propName}), dbValueStr: {dbValueStr}, esppValueStr: {esppValueStr}, basisTimeCalculated: {basisTimeCalculated}. Имя шаблона: {templateName}", nameof(dbValues.BasisTime), dbValues.BasisTime, esppValues.BasisTime, basisTimeCalculated, dbValues.TemplateName); return false; } logger.LogDebug("Значения nextRun в БД, ЕСПП, расчитанное, все совпадают. Scheduled: {scheduled}, basisTime: {basisTime}", scheduledCalculated, basisTimeCalculated); return true; } } }