diff --git a/PARR.API/Controllers/V1/RobotTaskController.cs b/PARR.API/Controllers/V1/RobotTaskController.cs index a601af7d..787a9be3 100644 --- a/PARR.API/Controllers/V1/RobotTaskController.cs +++ b/PARR.API/Controllers/V1/RobotTaskController.cs @@ -12,11 +12,10 @@ using PARR.Common.Domain; using PARR.Constants; using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Shortcodes; -using PARR.DAL.DomainServices.Shortcodes.Models; using PARR.DAL.Models; using PARR.DAL.NextRunServices; using PARR.DAL.Services.Interfaces; -using PARR.DAL.TransformServices; +using PARR.DAL.Services.Interfaces.Schedule; namespace PARR.API.Controllers.V1 { @@ -32,6 +31,7 @@ namespace PARR.API.Controllers.V1 private readonly IRobotHistoryService robotHistoryService; private readonly IShortcodesService shortcodesService; private readonly INextRunService nextRunService; + private readonly IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService; public RobotTaskController( IMapper mapper, @@ -42,7 +42,8 @@ namespace PARR.API.Controllers.V1 IClientService clientService, IRobotHistoryService robotHistoryService, IShortcodesService shortcodesService, - INextRunService nextRunService + INextRunService nextRunService, + IScheduleResponseAreaTimeOffsetService scheduleResponseAreaTimeOffsetService ) { this.mapper = mapper; @@ -54,6 +55,7 @@ namespace PARR.API.Controllers.V1 this.robotHistoryService = robotHistoryService; this.shortcodesService = shortcodesService; this.nextRunService = nextRunService; + this.scheduleResponseAreaTimeOffsetService = scheduleResponseAreaTimeOffsetService; } @@ -198,38 +200,6 @@ namespace PARR.API.Controllers.V1 { //RobotTaskTemplateResponse var robotTaskTemplateResponse = mapper.Map(task); - //TODO Вынести в отдельный метод ShortcodesService - - // Построим TemplateForShortcodes из уже загруженного task.Template - //var templateForShortcodes = new TemplateForShortcodes - //{ - // Id = task.Template!.Id, - // Index = task.Template.Index, - // JobId = task.Template.JobId, - // UnitId = task.Template.UnitId, - // Job = task.Template.Job == null ? null : new JobForShortcodes - // { - // Group = task.Template.Job.Group == null ? null : new JobGroupForShortcodes - // { - // Id = task.Template.Job.Group.Id, - // GroupingUnitFieldId = task.Template.Job.Group.GroupingUnitFieldId, - // GroupType = task.Template.Job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes - // { - // Code = task.Template.Job.Group.GroupType.Code - // }, - // GroupName = task.Template.Job.Group.GroupName - // }, - // Tnk = task.Template.Job.Tnk == null ? null : new TnkForShortcodes - // { - // Name = task.Template.Job.Tnk.Name, - // ShortName = task.Template.Job.Tnk.ShortName ?? "" - // }, - // WorkName = task.Template.Job.WorkName, - // Name = task.Template.Job.Name - // }, - // UnitsInTemplate = task.Template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcodes { UnitId = uit.UnitId }).ToList() ?? new List() - //}; - //if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.FullDescription)) robotTaskTemplateResponse.FullDescription = await shortcodesService.ApplyShortcodesAsync(robotTaskTemplateResponse.FullDescription, task.Template!); //if (shortcodesService.IsAnyShortcodes(robotTaskTemplateResponse.ShortDescription)) @@ -253,39 +223,13 @@ namespace PARR.API.Controllers.V1 //RobotTaskScheduleResponse var robotTaskScheduleResponse = mapper.Map(task); - //// Построим TemplateForShortcodes из уже загруженного task.Template - //var templateForShortcodes = new TemplateForShortcodes - //{ - // Id = task.Template!.Id, - // Index = task.Template.Index, - // JobId = task.Template.JobId, - // UnitId = task.Template.UnitId, - // Job = task.Template.Job == null ? null : new JobForShortcodes - // { - // Group = task.Template.Job.Group == null ? null : new JobGroupForShortcodes - // { - // GroupingUnitFieldId = task.Template.Job.Group.GroupingUnitFieldId, - // GroupType = task.Template.Job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes - // { - // Code = task.Template.Job.Group.GroupType.Code - // }, - // GroupName = task.Template.Job.Group.GroupName - // }, - // Tnk = task.Template.Job.Tnk == null ? null : new TnkForShortcodes - // { - // Name = task.Template.Job.Tnk.Name, - // ShortName = task.Template.Job.Tnk.ShortName - // }, - // WorkName = task.Template.Job.WorkName, - // Name = task.Template.Job.Name - // }, - // UnitsInTemplate = task.Template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcodes { UnitId = uit.UnitId }).ToList() ?? new List() - //}; + robotTaskScheduleResponse.Timezone = await GetTimezoneForTemplateAsync(task.Template!); - //if (shortcodesService.IsAnyShortcodes(robotTaskScheduleResponse.WorkGroup)) robotTaskScheduleResponse.WorkGroup = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.WorkGroup, task.Template!); robotTaskScheduleResponse.ResponseArea = await shortcodesService.ApplyShortcodesAsync(robotTaskScheduleResponse.ResponseArea, task.Template!); + + return Ok(new Response(robotTaskScheduleResponse, true)); } default: @@ -387,5 +331,33 @@ namespace PARR.API.Controllers.V1 return true; } + private async Task GetTimezoneForTemplateAsync(Template template) + { + if (template.Job?.Group?.IsWorkGroupTimezone != true) + return settingsFromDb.ScheduleTimezone; + + var responseArea = template.Unit?.BaseFields?.ResponseArea; + + if (string.IsNullOrEmpty(responseArea)) + { + throw new InvalidOperationException( + $"У шаблона Id={template.Id}, Name='{template.Name}' не задана ResponseArea в Unit.BaseFields, " + + "но включена настройка 'использовать часовой пояс рабочей группы'."); + } + + var offsets = await scheduleResponseAreaTimeOffsetService + .Get() + .ToDictionaryAsync(t => t.ResponseArea, t => t.TimeOffset); + + if (!offsets.TryGetValue(responseArea, out var offset)) + { + throw new InvalidOperationException( + $"Не найдено временное смещение для ResponseArea '{responseArea}' у шаблона Id={template.Id}, Name='{template.Name}'. " + + "Проверьте наличие записи в таблице ScheduleResponseAreaTimeOffset."); + } + + return offset; + } + } } diff --git a/PARR.EsppScheduleSync/ScheduleSyncher.cs b/PARR.EsppScheduleSync/ScheduleSyncher.cs index 9081eba1..8cd0d300 100644 --- a/PARR.EsppScheduleSync/ScheduleSyncher.cs +++ b/PARR.EsppScheduleSync/ScheduleSyncher.cs @@ -6,6 +6,7 @@ using PARR.BLL.Services.Interfaces; using PARR.DAL.Contracts; using PARR.DAL.Models; using PARR.DAL.Services.Interfaces; +using PARR.DAL.Services.Interfaces.Schedule; using PARR.DAL.TransformServices; using PARR.EsppScheduleSync.Domain; using PARR.EsppScheduleSync.Settings; @@ -21,7 +22,9 @@ namespace PARR.EsppScheduleSync private readonly ISyncService syncService; private readonly SettingsFromDb settingsFromDb; private readonly IServiceProvider serviceProvider; - //private readonly INextRunModifierService nextRunModifierService; + private string noneExcludeCalendarEsppValue; + private Dictionary responseAreaTimeOffsetDict; + public ScheduleSyncher( ILogger logger, @@ -30,7 +33,6 @@ namespace PARR.EsppScheduleSync ISyncService syncService, SettingsFromDb settingsFromDb, IServiceProvider serviceProvider - //INextRunModifierService nextRunModifierService ) { this.logger = logger; @@ -39,17 +41,30 @@ namespace PARR.EsppScheduleSync this.syncService = syncService; this.settingsFromDb = settingsFromDb; this.serviceProvider = serviceProvider; - //this.nextRunModifierService = nextRunModifierService; + + this.responseAreaTimeOffsetDict = new Dictionary(); + this.noneExcludeCalendarEsppValue = string.Empty; + if (globalSettings.MqSettings == null) { logger.LogError("Нет секции настроек хранилища. MqSettings, EsppTemplates"); throw new Exception("Нет секции настроек хранилища. MqSettings, EsppTemplates"); } + + if (string.IsNullOrEmpty(globalSettings.ParsingSeparator)) + throw new ArgumentException("ParsingSeparator не задан в GlobalSettings."); } public async Task StartAsync() { + + if (string.IsNullOrEmpty(noneExcludeCalendarEsppValue)) + await GetNoneExcludeCalendarEsppValueAsync(); + + if (responseAreaTimeOffsetDict.Count == 0) + await GetResponseAreaTimeOffsetsAsync(); + var isConnected = await mqService.InitConsumerAsync(globalSettings!.MqSettings!, SyncScheduleAsync); if (!isConnected) @@ -58,6 +73,26 @@ namespace PARR.EsppScheduleSync logger.LogInformation("Запущена проверка очереди {QueueName}.", globalSettings.MqSettings!.QueueName); } + private async Task GetResponseAreaTimeOffsetsAsync() + { + using var scope = serviceProvider.CreateScope(); + var service = scope.ServiceProvider.GetRequiredService(); + + responseAreaTimeOffsetDict = await service.Get().ToDictionaryAsync(t => t.ResponseArea, t => t.TimeOffset); + } + + private async Task GetNoneExcludeCalendarEsppValueAsync() + { + using var scope = serviceProvider.CreateScope(); + var service = scope.ServiceProvider.GetRequiredService(); + + var noneExcludeType = await service.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Code == nameof(ScheduleExcludeTypeEnum.None)); + + if (noneExcludeType == null) + throw new InvalidOperationException($"Не найден тип исключения с кодом '{nameof(ScheduleExcludeTypeEnum.None)}'"); + + noneExcludeCalendarEsppValue = noneExcludeType.EsppValue; + } public async Task StopAsync() { @@ -69,108 +104,116 @@ namespace PARR.EsppScheduleSync private async Task SyncScheduleAsync(string str) { - const int expectedParts = 24; - var separator = globalSettings.ParsingSeparator; - - var parts = str.Split(separator); - if (parts.Length != expectedParts) + try { - logger.LogWarning("Некорректное количество полей в строке расписания: {Actual} (ожидается {Expected})", parts.Length, expectedParts); - await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj); - return; - } + const int expectedParts = 24; + var separator = globalSettings.ParsingSeparator; - var esppScheduleId = parts[0]; // ScheduleEsppId из ЕСПП - var templateNameRaw = parts[6]; - var templateName = templateNameRaw.ToUpper(); - - // Проверка префикса (как в ParseStrToEsppObject) - if (string.IsNullOrEmpty(settingsFromDb.TemplatePrefixWithoutVariable) || - !templateName.Contains(settingsFromDb.TemplatePrefixWithoutVariable)) - { - logger.LogWarning("Имя шаблона '{TemplateName}' не соответствует префиксу '{Prefix}'. Пропущено.", templateName, settingsFromDb.TemplatePrefixWithoutVariable); - await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj); - return; - } - - // === ЕДИНЫЙ ЗАПРОС: ищем по имени ИЛИ по ScheduleEsppId === - using var scope = serviceProvider.CreateScope(); - var templateService = scope.ServiceProvider.GetRequiredService(); - - var candidates = await templateService.Get() - .Where(t => t.Name == templateName || t.ScheduleEsppId == esppScheduleId) - .ToListAsync(); - - var templateByName = candidates.FirstOrDefault(t => t.Name == templateName); - var templateByScheduleId = candidates.FirstOrDefault(t => t.ScheduleEsppId == esppScheduleId); - - if (templateByName == null && templateByScheduleId == null) - { - // Нет ни по имени, ни по ID - logger.LogWarning( - "Расписание из ЕСПП не привязано ни к одному шаблону: TemplateName='{TemplateName}', ScheduleEsppId='{EsppId}'", - templateName, - esppScheduleId - ); - } - else if (templateByName == null && templateByScheduleId != null) - { - // Есть только по ID → имя не совпадает - logger.LogWarning( - "Расхождение привязки: расписание из ЕСПП с ScheduleEsppId='{EsppId}' и TemplateName='{TemplateName}' " + - "соответствует шаблону в БД с именем '{DbTemplateName}', Id='{TemplateId}'.", - esppScheduleId, - templateName, - templateByScheduleId.Name, - templateByScheduleId.Id - ); - } - else if (templateByName != null) - { - var dbScheduleId = templateByName.ScheduleEsppId ?? string.Empty; - - if (string.IsNullOrEmpty(dbScheduleId)) + var parts = str.Split(separator); + if (parts.Length != expectedParts) { - // Утерян ID в БД + logger.LogWarning("Некорректное количество полей в строке расписания: {Actual} (ожидается {Expected})", parts.Length, expectedParts); + await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj); + return; + } + + var esppScheduleId = parts[0]; // ScheduleEsppId из ЕСПП + var templateNameRaw = parts[6]; + var templateName = templateNameRaw.ToUpper(); + + // Проверка префикса (как в ParseStrToEsppObject) + if (string.IsNullOrEmpty(settingsFromDb.TemplatePrefixWithoutVariable) || + !templateName.Contains(settingsFromDb.TemplatePrefixWithoutVariable)) + { + logger.LogWarning("Имя шаблона '{TemplateName}' не соответствует префиксу '{Prefix}'. Пропущено.", templateName, settingsFromDb.TemplatePrefixWithoutVariable); + await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj); + return; + } + + // === ЕДИНЫЙ ЗАПРОС: ищем по имени ИЛИ по ScheduleEsppId === + using var scope = serviceProvider.CreateScope(); + var templateService = scope.ServiceProvider.GetRequiredService(); + + var candidates = await templateService.Get() + .Where(t => t.Name == templateName || t.ScheduleEsppId == esppScheduleId) + .ToListAsync(); + + var templateByName = candidates.FirstOrDefault(t => t.Name == templateName); + var templateByScheduleId = candidates.FirstOrDefault(t => t.ScheduleEsppId == esppScheduleId); + + if (templateByName == null && templateByScheduleId == null) + { + // Нет ни по имени, ни по ID logger.LogWarning( - "У шаблона '{TemplateName}' (Id='{TemplateId}') отсутствует ScheduleEsppId в БД, но в ЕСПП он равен '{EsppId}'", + "Расписание из ЕСПП не привязано ни к одному шаблону: TemplateName='{TemplateName}', ScheduleEsppId='{EsppId}'", templateName, - templateByName.Id, esppScheduleId ); } - else if (dbScheduleId != esppScheduleId) + else if (templateByName == null && templateByScheduleId != null) { - // ID не совпадают — проверяем, не занят ли esppScheduleId другим шаблоном - var conflictingTemplate = candidates.FirstOrDefault(t => - t.Id != templateByName.Id && t.ScheduleEsppId == esppScheduleId); + // Есть только по ID → имя не совпадает + logger.LogWarning( + "Расхождение привязки: расписание из ЕСПП с ScheduleEsppId='{EsppId}' и TemplateName='{TemplateName}' " + + "соответствует шаблону в БД с именем '{DbTemplateName}', Id='{TemplateId}'.", + esppScheduleId, + templateName, + templateByScheduleId.Name, + templateByScheduleId.Id + ); + } + else if (templateByName != null) + { + var dbScheduleId = templateByName.ScheduleEsppId ?? string.Empty; - if (conflictingTemplate != null) + if (string.IsNullOrEmpty(dbScheduleId)) { + // Утерян ID в БД logger.LogWarning( - "Конфликт ScheduleEsppId: расписание '{EsppId}' из ЕСПП с именем '{TemplateName}' " + - "уже привязано к другому шаблону '{OtherTemplateName}' (Id='{OtherTemplateId}') в БД.", - esppScheduleId, - templateName, - conflictingTemplate.Name, - conflictingTemplate.Id - ); - } - else - { - logger.LogWarning( - "Несовпадение ScheduleEsppId для шаблона '{TemplateName}' (Id='{TemplateId}'): в БД='{DbId}', в ЕСПП='{EsppId}'", + "У шаблона '{TemplateName}' (Id='{TemplateId}') отсутствует ScheduleEsppId в БД, но в ЕСПП он равен '{EsppId}'", templateName, templateByName.Id, - dbScheduleId, esppScheduleId ); } + else if (dbScheduleId != esppScheduleId) + { + // ID не совпадают — проверяем, не занят ли esppScheduleId другим шаблоном + var conflictingTemplate = candidates.FirstOrDefault(t => + t.Id != templateByName.Id && t.ScheduleEsppId == esppScheduleId); + + if (conflictingTemplate != null) + { + logger.LogWarning( + "Конфликт ScheduleEsppId: расписание '{EsppId}' из ЕСПП с именем '{TemplateName}' " + + "уже привязано к другому шаблону '{OtherTemplateName}' (Id='{OtherTemplateId}') в БД.", + esppScheduleId, + templateName, + conflictingTemplate.Name, + conflictingTemplate.Id + ); + } + else + { + logger.LogWarning( + "Несовпадение ScheduleEsppId для шаблона '{TemplateName}' (Id='{TemplateId}'): в БД='{DbId}', в ЕСПП='{EsppId}'", + templateName, + templateByName.Id, + dbScheduleId, + esppScheduleId + ); + } + } } + + // Передаём оригинальную строку в стандартный синхронизатор + await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при синхронизации расписания из строки: {InputString}", str); } - // Передаём оригинальную строку в стандартный синхронизатор - await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj); } @@ -194,7 +237,6 @@ namespace PARR.EsppScheduleSync nextRunByAccountRobotTimeZone = nextRunModifierService.GetNextRunByAccountRobotTimeZone(template.NextRun); } - var esppObjectFromDb = new EsppObjectSchedule { TemplateName = template.Name.ToUpper(), @@ -213,7 +255,7 @@ namespace PARR.EsppScheduleSync //BasisTime = EsppScheduleHelpers.GetGenerationTime(nextRunModifierService.GetNextRunByAccountRobotTimeZone(template.NextRun)), Scheduled = EsppScheduleHelpers.GetNextRun(nextRunByAccountRobotTimeZone), BasisTime = EsppScheduleHelpers.GetGenerationTime(nextRunByAccountRobotTimeZone), - Timezone = settingsFromDb.ScheduleTimezone, + Timezone = GetTimezone(template), //Мы решили, что для всех расписаний "Отсутствует дата завершения", если что-то поменяется, тут нужно переделать TerminationType = settingsFromDb.ScheduleRepeatRange == "Отсутствует дата завершения" ? "forever" : "", CompleteAfter = "" @@ -223,6 +265,31 @@ namespace PARR.EsppScheduleSync return ClearOptionalFields(esppObjectFromDb); } + private string GetTimezone(Template template) + { + + if (template.Job?.Group?.IsWorkGroupTimezone != true) + return settingsFromDb.ScheduleTimezone; + + var responseArea = template.Unit?.BaseFields?.ResponseArea; + + if (string.IsNullOrEmpty(responseArea)) + { + throw new InvalidOperationException( + $"У шаблона Id={template.Id}, Name='{template.Name}' не задана ResponseArea в Unit.BaseFields, " + + "но включена настройка 'использовать часовой пояс рабочей группы'."); + } + + if (!responseAreaTimeOffsetDict.TryGetValue(responseArea, out var offset)) + { + throw new InvalidOperationException( + $"Не найдено временное смещение для ResponseArea '{responseArea}' у шаблона Id={template.Id}, Name='{template.Name}'. " + + "Проверьте наличие записи в таблице ScheduleResponseAreaTimeOffset."); + } + + return offset; + } + /// /// Получить исключение - Календарь @@ -476,7 +543,9 @@ namespace PARR.EsppScheduleSync esppObject.ResponseArea = string.Empty; esppObject.WorkGroup = string.Empty; esppObject.CompleteAfter = string.Empty; - //esppObject.V60calendar = string.Empty; + + if (esppObject.TypeV60calendar == noneExcludeCalendarEsppValue) + esppObject.V60calendar = string.Empty; // В ЕСПП, при изменении "Повторять задачу", остаются предыдущие значения, их не нужно синхронизировать (касается только данных полученных из ЕСПП, в БД все ок) // т.е. если стояло Ежедненвно:понедельник, а изменили например на Еженедельно..., то в ежедневно значения останутся, но будут отрабатывать значения из Еженедельно.