< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.OrdersClientStatusSyncResult
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/OrdersClientStatusSyncService.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 275
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Message()100%11100%
get_Error()100%11100%
get_FailedOrderIds()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/OrdersClientStatusSyncService.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Text;
 3using Microsoft.EntityFrameworkCore;
 4using OzonDomains;
 5using OzonDomains.Models;
 6using OzonRepositories.Context;
 7using Services.ApiServices.OzonApi;
 8using Services.DataServices;
 9using Services.LabelServices;
 10using Services.OrderControlServices;
 11using Services.ParserServices.HelpDictEnum;
 12using Services.OzonWebhookServices;
 13
 14namespace OzonOrdersWeb.WebServices.Orders;
 15
 16public sealed class OrdersClientStatusSyncResult
 17{
 1218    public string? Message { get; init; }
 19
 620    public string? Error { get; init; }
 21
 22    /// <summary>
 23    /// Заказы, для которых запрос статуса к Ozon не удался в этом проходе (таймаут, ошибка API и т.п.).
 24    /// Их локальный ClientStatus остался прежним — вызывающий код (SLA-очередь) не должен принимать
 25    /// по ним решения в этом цикле, как будто статус свежий.
 26    /// </summary>
 627    public IReadOnlySet<int> FailedOrderIds { get; init; } = new HashSet<int>();
 28}
 29
 30public interface IOrdersClientStatusSyncService
 31{
 32    Task<OrdersClientStatusSyncResult> SyncOrderStatusesAsync(
 33        IReadOnlyList<int> orderIds,
 34        CancellationToken cancellationToken = default);
 35}
 36
 37/// <summary>
 38/// Синхронизация ClientStatus заказов с Ozon API (логика бывшего ProcessOrdersStatusUpdate).
 39/// </summary>
 40public sealed class OrdersClientStatusSyncService : IOrdersClientStatusSyncService
 41{
 42    private readonly ApplicationDbContext _context;
 43    private readonly OrdersDataServices _orderServices;
 44    private readonly OzonClientServices _ozonClientServices;
 45    private readonly OzonJsonDataBuilder _jsonDataBuilder;
 46    private readonly ClientStatusDataServices _clientStatusServices;
 47    private readonly IOrderDeliveryStatusSynchronizer _deliveryStatusSynchronizer;
 48    private readonly IOzonPostingCancellationService _postingCancellationService;
 49
 50    public OrdersClientStatusSyncService(
 51        ApplicationDbContext context,
 52        OrdersDataServices orderServices,
 53        OzonClientServices ozonClientServices,
 54        OzonJsonDataBuilder jsonDataBuilder,
 55        ClientStatusDataServices clientStatusServices,
 56        IOrderDeliveryStatusSynchronizer deliveryStatusSynchronizer,
 57        IOzonPostingCancellationService postingCancellationService)
 58    {
 59        _context = context;
 60        _orderServices = orderServices;
 61        _ozonClientServices = ozonClientServices;
 62        _jsonDataBuilder = jsonDataBuilder;
 63        _clientStatusServices = clientStatusServices;
 64        _deliveryStatusSynchronizer = deliveryStatusSynchronizer;
 65        _postingCancellationService = postingCancellationService;
 66    }
 67
 68    public async Task<OrdersClientStatusSyncResult> SyncOrderStatusesAsync(
 69        IReadOnlyList<int> orderIds,
 70        CancellationToken cancellationToken = default)
 71    {
 72        if (orderIds.Count == 0)
 73        {
 74            return new OrdersClientStatusSyncResult { Error = "Нет доступных заказов для обновления" };
 75        }
 76
 77        var idArray = orderIds.Distinct().ToArray();
 78        var totalStopwatch = Stopwatch.StartNew();
 79        var dbOrdersStopwatch = Stopwatch.StartNew();
 80
 81        try
 82        {
 83            var ordersLight = await _context.Orders
 84                .AsNoTracking()
 85                .Where(o => idArray.Contains(o.Id))
 86                .Select(o => new
 87                {
 88                    o.Id,
 89                    o.ShipmentNumber,
 90                    o.OzonClientId,
 91                    o.ClientStatusId,
 92                    ClientStatusName = o.ClientStatus != null ? o.ClientStatus.Name : null
 93                })
 94                .ToListAsync(cancellationToken);
 95
 96            dbOrdersStopwatch.Stop();
 97            var dbOrdersTime = dbOrdersStopwatch.Elapsed;
 98
 99            if (ordersLight.Count == 0)
 100            {
 101                return new OrdersClientStatusSyncResult { Error = "Нет доступных заказов для обновления" };
 102            }
 103
 104            // Статус на момент чтения — условие для UPDATE: если за время опроса пришёл свежий пуш, не перезаписываем.
 105            var expectedStatusIds = ordersLight.ToDictionary(o => o.Id, o => o.ClientStatusId);
 106
 107            var ordersToUpdate = ordersLight
 108                .Select(o => new Order
 109                {
 110                    Id = o.Id,
 111                    ShipmentNumber = o.ShipmentNumber,
 112                    OzonClientId = o.OzonClientId,
 113                    ClientStatus = string.IsNullOrEmpty(o.ClientStatusName)
 114                        ? null
 115                        : new ClientStatus { Name = o.ClientStatusName }
 116                })
 117                .ToList();
 118
 119            int updatingCount = 0;
 120            var updateInfoBuilder = new StringBuilder("");
 121            var updatedOrderIds = new List<int>();
 122            var failedOrderIds = new List<int>();
 123
 124            var clientsLoadStopwatch = Stopwatch.StartNew();
 125            TimeSpan ozonApiTime = TimeSpan.Zero;
 126            TimeSpan clientStatusDbTime = TimeSpan.Zero;
 127
 128            var distinctClientIds = ordersToUpdate
 129                .Where(o => o.OzonClientId.HasValue)
 130                .Select(o => o.OzonClientId!.Value)
 131                .Distinct()
 132                .ToList();
 133
 134            var allClients = await _ozonClientServices.GetOzonClients();
 135            var clientsDict = allClients
 136                .Where(c => distinctClientIds.Contains(c.Id))
 137                .ToDictionary(c => c.Id, c => c);
 138
 139            clientsLoadStopwatch.Stop();
 140            var clientsLoadTime = clientsLoadStopwatch.Elapsed;
 141
 142            foreach (var order in ordersToUpdate)
 143            {
 144                if (order.OzonClientId.HasValue &&
 145                    clientsDict.TryGetValue(order.OzonClientId.Value, out var client))
 146                {
 147                    order.OzonClient = client;
 148                }
 149            }
 150
 151            // Группируем по клиенту Ozon и внутри группы запрашиваем статусы параллельно (батчем) —
 152            // раньше каждый заказ ждал свой HTTP-запрос последовательно, что на 50-100 заказов давало
 153            // столько же round-trip'ов подряд внутри одного прохода SLA-джобы. SetClient меняет креды
 154            // на общем _jsonDataBuilder, поэтому между клиентами группы идут строго последовательно,
 155            // а параллелится только пачка shipment-номеров ОДНОГО клиента (безопасно, креды не меняются).
 156            var ozonApiSw = Stopwatch.StartNew();
 157            var clientGroups = ordersToUpdate
 158                .Where(o => o.OzonClient is { ClientType: ClientType.OZON } &&
 159                            !string.IsNullOrEmpty(o.OzonClient.DecryptClientId) &&
 160                            !string.IsNullOrEmpty(o.OzonClient.DecryptApiKey) &&
 161                            !string.IsNullOrEmpty(o.ShipmentNumber))
 162                .GroupBy(o => o.OzonClient!.Id);
 163
 164            foreach (var group in clientGroups)
 165            {
 166                var client = group.First().OzonClient!;
 167                _jsonDataBuilder.SetClient(client.DecryptClientId!, client.DecryptApiKey!);
 168
 169                var shipmentNumbers = group
 170                    .Select(o => o.ShipmentNumber!)
 171                    .Where(s => !string.IsNullOrWhiteSpace(s))
 172                    .Distinct()
 173                    .ToArray();
 174                var statusResults = await _jsonDataBuilder.GetProductStatusBatch(shipmentNumbers);
 175
 176                foreach (var order in group)
 177                {
 178                    // Заказ не считается актуализированным, если Ozon не ответил — иначе SLA-очередь
 179                    // принимала бы решение по устаревшему локальному статусу, думая, что он свежий.
 180                    var apiPosting = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 181                    var hasStatus = statusResults.TryGetValue(order.ShipmentNumber!, out var statusResult) ||
 182                                   (!string.IsNullOrEmpty(apiPosting) && statusResults.TryGetValue(apiPosting, out statu
 183
 184                    if (!hasStatus || !statusResult.Success)
 185                    {
 186                        failedOrderIds.Add(order.Id);
 187                        updateInfoBuilder.AppendLine($"• <b>{order.ShipmentNumber}</b> ошибка запроса статуса.");
 188                        continue;
 189                    }
 190
 191                    var newStatus = statusResult.Status;
 192                    string newStatusName;
 193                    if (string.IsNullOrWhiteSpace(newStatus) ||
 194                        !OzonStatus.OrderStatuses.TryGetValue(newStatus, out var mappedStatusName))
 195                    {
 196                        newStatusName = string.IsNullOrWhiteSpace(newStatus)
 197                            ? order.ClientStatus?.Name ?? string.Empty
 198                            : newStatus;
 199                    }
 200                    else
 201                    {
 202                        newStatusName = mappedStatusName;
 203                    }
 204
 205                    updateInfoBuilder.AppendLine(
 206                        $"• <b>{order.ShipmentNumber}</b> {order.ClientStatus?.Name ?? ""} → {newStatusName}");
 207
 208                    if (!string.IsNullOrEmpty(newStatusName) &&
 209                        (order.ClientStatus?.Name == null ||
 210                         !order.ClientStatus.Name.Equals(newStatusName, StringComparison.OrdinalIgnoreCase)))
 211                    {
 212                        var statusSw = Stopwatch.StartNew();
 213                        order.ClientStatus = await _clientStatusServices.GetClientStatusByNameAsync(newStatusName);
 214
 215                        if (order.ClientStatus == null)
 216                        {
 217                            order.ClientStatus = new ClientStatus { Name = newStatusName };
 218                            await _clientStatusServices.AddClientStatus(order.ClientStatus);
 219                        }
 220
 221                        statusSw.Stop();
 222                        clientStatusDbTime += statusSw.Elapsed;
 223                        updatingCount++;
 224                        updatedOrderIds.Add(order.Id);
 225                    }
 226                }
 227            }
 228            ozonApiSw.Stop();
 229            ozonApiTime += ozonApiSw.Elapsed;
 230
 231            var saveOrdersSw = Stopwatch.StartNew();
 232            var idsToUpdate = updatedOrderIds.Distinct().ToList();
 233            var savedCount = await _orderServices.SaveClientStatusUpdatesAsync(idsToUpdate, ordersToUpdate, expectedStat
 234            updatingCount = savedCount;
 235            await _postingCancellationService.LinkOrdersAsync(ordersToUpdate, cancellationToken);
 236            await _deliveryStatusSynchronizer.SynchronizeCancelledAsync(idArray, cancellationToken: cancellationToken);
 237            await _deliveryStatusSynchronizer.SynchronizeDeliveredAsync(idArray, cancellationToken: cancellationToken);
 238            saveOrdersSw.Stop();
 239            var saveOrdersTime = saveOrdersSw.Elapsed;
 240
 241            totalStopwatch.Stop();
 242            var totalTime = totalStopwatch.Elapsed;
 243
 244            var timingBuilder = new StringBuilder();
 245            timingBuilder.AppendLine("<br/><b>Время выполнения (сек):</b>");
 246            timingBuilder.AppendLine($"<br/>- Загрузка заказов из БД: {dbOrdersTime.TotalSeconds:F2}");
 247            timingBuilder.AppendLine($"<br/>- Загрузка клиентов Ozon: {clientsLoadTime.TotalSeconds:F2}");
 248            timingBuilder.AppendLine($"<br/>- Запросы в Ozon API: {ozonApiTime.TotalSeconds:F2}");
 249            timingBuilder.AppendLine($"<br/>- Работа с БД статусов клиентов: {clientStatusDbTime.TotalSeconds:F2}");
 250            timingBuilder.AppendLine($"<br/>- Сохранение заказов: {saveOrdersTime.TotalSeconds:F2}");
 251            timingBuilder.AppendLine($"<br/>- Итого: {totalTime.TotalSeconds:F2}");
 252
 253            var failedIdsSet = (IReadOnlySet<int>)failedOrderIds.Distinct().ToHashSet();
 254
 255            if (updatingCount > 0)
 256            {
 257                var messageBuilder = new StringBuilder();
 258                messageBuilder.AppendLine($"Обновлено <b>{updatingCount}</b> статусов.<br/>{updateInfoBuilder}");
 259                messageBuilder.AppendLine(timingBuilder.ToString());
 260                return new OrdersClientStatusSyncResult { Message = messageBuilder.ToString(), FailedOrderIds = failedId
 261            }
 262
 263            var noChangesMessageBuilder = new StringBuilder();
 264            noChangesMessageBuilder.AppendLine("Нет изменений в статусах заказов");
 265            noChangesMessageBuilder.AppendLine(timingBuilder.ToString());
 266            return new OrdersClientStatusSyncResult { Message = noChangesMessageBuilder.ToString(), FailedOrderIds = fai
 267        }
 268        catch (Exception ex)
 269        {
 270            totalStopwatch.Stop();
 271            var error = $"Ошибка при обновлении статусов заказов: {ex.Message}";
 272            return new OrdersClientStatusSyncResult { Message = error, Error = error, FailedOrderIds = idArray.ToHashSet
 273        }
 274    }
 275}