using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Common.Interfaces.RabbitServices; using PARR.Core.Repositories.Interfaces; using PARR.Domain.Common.Rabbit.Messages; using PARR.Domain.Entities; using PARR.Domain.Enums; using PARR.Master.Settings; using System.Text.Encodings.Web; using System.Text.Json; namespace PARR.Master.Services { internal class OrderMasterService : IOrderMasterService { private readonly ILogger logger; private readonly IOrderRepository orderService; private readonly IAgentHistoryRepository agentHistoryService; private readonly OrderManageSettings orderManageSettings; private readonly MqSettings mqSettings; private readonly IRabbitService mqService; private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; public OrderMasterService( ILogger logger, IOrderRepository orderService, IAgentHistoryRepository agentHistoryService, OrderManageSettings orderManageSettings, MqSettings mqSettings, IRabbitService mqService ) { this.logger = logger; this.orderService = orderService; this.agentHistoryService = agentHistoryService; this.orderManageSettings = orderManageSettings; this.mqSettings = mqSettings; this.mqService = mqService; } public async Task ManageOrdersAsync() { // логика работы описана в документации проекта logger.LogInformation($"--- --- Начало поиска нарядов --- ---"); //ищем наряды в статусе 1 и 2 var ordersToManage = await GetOrdersNewOrWorkStatusesAsync(); logger.LogInformation($"Ищу наряды со статусом 1 и 2. Найдено нарядов: {ordersToManage.Count()} шт."); foreach (var order in ordersToManage) { //присваиваем им историю работы агента, меняем стаус наряда logger.LogInformation($"--- Найден наряд id: {order.Id}, {order.Number} ---"); await SetHistoryAndUpdateOrderStatus(order); } //находим наряды где статусы не равны, добавляем их в Rabbit var ordersWithStatusNotEqual = await GetOrdresWithStatusCodeNotEqualAsync(); var objs = ordersWithStatusNotEqual.Select(t => JsonSerializer.Serialize(new OrderManageMq { OrderId = t.Id }, jsonOptions)); logger.LogInformation($"Ищу наряды где статусы не равны. Найдено нарядов: {ordersWithStatusNotEqual.Count()}. Добавляю их в RabbitMQ."); if (objs.Any()) { var sendResult = await mqService.SendAsync(mqSettings, objs.ToArray()); if (sendResult.IsSuccess) { logger.LogInformation($"Наряды переданы в RabbitMQ: {objs.Count()}"); logger.LogDebug($"Список нардов переданных в RabbitMQ: {string.Join(", ", objs)}"); } else logger.LogError($"Ошибка при передаче нарядов в Rabbit, не переданные сообщения: {string.Join(", ", sendResult!.NotSendMessages)}"); } logger.LogInformation($"=== Конец работы с нарядами=== "); } /// /// Находит наряды в статусе 1 и 2, присваивает им историю работы агента, меняет стаус наряда /// /// /// private async Task SetHistoryAndUpdateOrderStatus(Order order) { // получаем историю агента, которая не присовена этому наряду var newAgentHistory = await GetNewAgentHistoryByOrderAsync(order); logger.LogInformation($"Для наряда {order.Id}, {order.Number} найдено новых историй агентов: {newAgentHistory.Count()} шт."); // присваиваем наряду SetOrderToAgentHistory(ref newAgentHistory, ref order); // совмещенный список истории агента по этому наряду var allHistory = newAgentHistory; order.AgentHistories.ToList().ForEach(item => allHistory.Add(item)); allHistory = allHistory.OrderBy(t => t.DateCreated).ToList(); // смотрим историю агента, и тут же меняем статус наряда // Ищем статус "Агент начал выполнять задание" var isAgentStarted = allHistory.Any(t => t.HistoryLevelId == (int)AgentHistoryLevelEnum.Start); // Ищем статус "Агент завершил выполнение задания" var isAgentEnded = allHistory.Any(t => t.HistoryLevelId == (int)AgentHistoryLevelEnum.End); // Закрыть наряд if (isAgentEnded) ChangeNextStatus(ref order, OrderStatusEnum.Complete, ""); // Наряд в работу if (isAgentStarted && !isAgentEnded && order.StatusCode == (int)OrderStatusEnum.New) ChangeNextStatus(ref order, OrderStatusEnum.InWork, ""); // Закрываем по таймауту. Если есть начало, и нет завершения по АгентТаймаут, то закрываем наряд. if (isAgentStarted == true && isAgentEnded == false) { var lastStrartDate = allHistory.Last(t => t.HistoryLevelId == (int)AgentHistoryLevelEnum.Start).DateCreated; var timeout = (double)order.Template!.Job!.Group!.AgentTimeOutSec!; if (lastStrartDate.AddSeconds(timeout) <= DateTimeOffset.UtcNow) { var logMsg = $"Закрываю наряд по таймауту ожидания агента. Время старта агента: {lastStrartDate}, допустимый timeOut: {timeout} сек."; ChangeNextStatus(ref order, OrderStatusEnum.Complete, logMsg); } else logger.LogDebug($"Для наряда {order.Id}, {order.Number} оставляю статус прежним, ожидаю завершение работы агента. Время старта агента: {lastStrartDate}, допустимый timeOut: {timeout} сек."); } //TODO: тут можно подумать, и закрывать наряды, у которых нет истории агента. сейчас мы такие наряды пропускаем, их будет закрывать руками админ. if (!allHistory.Any()) { logger.LogInformation($"Для наряда {order.Id}, {order.Number} нет истории агента. Ничего не делаем."); } if (!await orderService.CommitAsync()) { logger.LogError($"Ошибка при сохранении наряда, id: {order.Id}"); } logger.LogInformation($"Закончил работу с нарядом {order.Id}, {order.Number}"); } /// /// Присваивает истории агента идентивикатор наряда /// private void SetOrderToAgentHistory(ref List newAgentHistory, ref Order order) { foreach (var historyItem in newAgentHistory) { historyItem.OrderId = order.Id; logger.LogDebug($"Для наряда {order.Id}, {order.Number} добавил истрию агента {historyItem.Id}, {historyItem.HistoryLevelId}"); // если статус агента = завершил работу. то историю после завешения, не записываем, это возможно относится к другому наряду, или нам уже пофиг на неё if (historyItem.HistoryLevelId == (int)AgentHistoryLevelEnum.End) { logger.LogDebug($"Для наряда {order.Id}, {order.Number} прекращаю добавлять историю агента, так как добавлена запись со статусом Стоп"); continue; } } //отбрасываем все записи которые не добавлены этому наряду newAgentHistory = newAgentHistory.Where(t => t.OrderId.HasValue).ToList(); } /// /// Получить историю агента не привязанную к наряду, попадающую в дельту существования наряда /// /// /// private async Task> GetNewAgentHistoryByOrderAsync(Order order) { var agentHistory = await agentHistoryService.Get() .Where(t => t.TemplateId == order.TemplateId && t.DateCreated > order.GenerateDate!.Value.Add(-orderManageSettings.FindAgentHistoryStartOffset) //&& t.DateCreated <= order.GenerateDate!.Value.AddSeconds(order.Template!.ApplicationsInWork!.AgentTimeOutSec!.Value) && t.DateCreated <= order.GenerateDate!.Value.Add(order.Template!.Job!.Group!.TemplateDurationTimeSpan!.Value) && t.OrderId == null ) .OrderBy(t => t.DateCreated) .ToListAsync(); return agentHistory; } /// /// Изменить статус наряда /// /// /// private void ChangeNextStatus(ref Order order, OrderStatusEnum nextStatus, string logCompleteMsg) { if (order.NextStatusCode != (int)nextStatus) { order.NextStatusCode = (int)nextStatus; logger.LogInformation($"Для наряда {order.Id}, {order.Number} меняю статус на {nextStatus}. {logCompleteMsg}"); } else { logger.LogDebug($"Для наряда {order.Id}, {order.Number} оставляю статус {nextStatus}, он соответствует дейстивительности."); } } /// /// Получить список нарядов, со статусами 1 или 2 и у которых nextStatus<3 /// private async Task> GetOrdersNewOrWorkStatusesAsync() { return await GetOrdres() .Where(t => (t.StatusCode == (int)OrderStatusEnum.New || t.StatusCode == (int)OrderStatusEnum.InWork) && (!t.NextStatusCode.HasValue || t.NextStatusCode < (int)OrderStatusEnum.Stop) ) .OrderBy(t => t.DateCreated) .ToListAsync(); } /// /// Получить список нарядов, у которых StatusCode!=NextStatusCode /// private async Task> GetOrdresWithStatusCodeNotEqualAsync() { return await GetOrdres() .Where(t => t.NextStatusCode.HasValue && t.NextStatusCode.Value != t.StatusCode) .ToListAsync(); } /// /// Основной запрос на получения списка нарядов со связями, по базовому условию /// /// private IQueryable GetOrdres() { return orderService.Get() .Include(t => t.Template).ThenInclude(t => t!.Job) .Include(t => t.AgentHistories) .Where(t => t.TemplateId.HasValue && t.Template!.Job!.Group!.IsAgent == true && t.GenerateDate.HasValue && t.Template!.Job!.Group!.AgentTimeOutSec.HasValue && string.IsNullOrEmpty(t.Template!.Job!.Group!.TemplateDuration) == false && t.ExpirationDate > DateTimeOffset.UtcNow ).OrderBy(t => t.DateCreated); } } }