| | | 1 | | using System; |
| | | 2 | | using System.Linq; |
| | | 3 | | using System.Threading; |
| | | 4 | | using System.Threading.Tasks; |
| | | 5 | | using Microsoft.EntityFrameworkCore; |
| | | 6 | | using Microsoft.Extensions.Logging; |
| | | 7 | | using OzonDomains.Models; |
| | | 8 | | using OzonDomains.Models.OzonWebhook; |
| | | 9 | | using OzonRepositories.Context; |
| | | 10 | | using Services.OzonWebhookServices.Models; |
| | | 11 | | using Services.OrderControlServices; |
| | | 12 | | using Services.ParserServices.HelpDictEnum; |
| | | 13 | | |
| | | 14 | | namespace Services.OzonWebhookServices.Handlers |
| | | 15 | | { |
| | | 16 | | /// <summary> |
| | | 17 | | /// Обработчик уведомления <c>TYPE_STATE_CHANGED</c>. |
| | | 18 | | /// |
| | | 19 | | /// При получении уведомления об изменении статуса отправления: |
| | | 20 | | /// 1. Переводит Ozon-статус из нотации <c>new_state</c> ("posting_delivered" и т.д.) |
| | | 21 | | /// в читаемое название через <see cref="OzonStatus.PostingSubStatuses"/> |
| | | 22 | | /// (с fallback на <see cref="OzonStatus.OrderStatuses"/>). |
| | | 23 | | /// 2. Находит или создаёт запись <see cref="ClientStatus"/> с этим названием. |
| | | 24 | | /// 3. Обновляет <see cref="Order.ClientStatusId"/> и <see cref="Order.LastStatusChangeDate"/> |
| | | 25 | | /// у всех заказов с совпадающим <see cref="Order.ShipmentNumber"/>. |
| | | 26 | | /// 4. Возвращает строку с кратким описанием выполненных действий (для <c>ActionDetails</c>). |
| | | 27 | | /// |
| | | 28 | | /// Для добавления обработчика нового типа уведомления создайте аналогичный класс, |
| | | 29 | | /// реализующий <see cref="IWebhookNotificationHandler"/>, и зарегистрируйте его в DI. |
| | | 30 | | /// </summary> |
| | | 31 | | public sealed class StateChangedNotificationHandler : IWebhookNotificationHandler |
| | | 32 | | { |
| | | 33 | | private readonly ApplicationDbContext _context; |
| | | 34 | | private readonly ILogger<StateChangedNotificationHandler> _logger; |
| | | 35 | | private readonly IOrderDeliveryStatusSynchronizer _deliveryStatusSynchronizer; |
| | | 36 | | |
| | 5 | 37 | | public StateChangedNotificationHandler( |
| | 5 | 38 | | ApplicationDbContext context, |
| | 5 | 39 | | ILogger<StateChangedNotificationHandler> logger, |
| | 5 | 40 | | IOrderDeliveryStatusSynchronizer deliveryStatusSynchronizer) |
| | | 41 | | { |
| | 5 | 42 | | _context = context; |
| | 5 | 43 | | _logger = logger; |
| | 5 | 44 | | _deliveryStatusSynchronizer = deliveryStatusSynchronizer; |
| | 5 | 45 | | } |
| | | 46 | | |
| | | 47 | | /// <inheritdoc/> |
| | | 48 | | public bool CanHandle(OzonMessageType messageType) => |
| | 3 | 49 | | messageType == OzonMessageType.TYPE_STATE_CHANGED; |
| | | 50 | | |
| | | 51 | | /// <inheritdoc/> |
| | | 52 | | public async Task<WebhookHandlingResult> HandleAsync(IOzonWebhookMessage notification, CancellationToken ct) |
| | | 53 | | { |
| | 4 | 54 | | if (notification is not OzonStateChangedNotification stateChanged) |
| | 0 | 55 | | throw new ArgumentException( |
| | 0 | 56 | | $"Ожидался тип {nameof(OzonStateChangedNotification)}, получен {notification.GetType().Name}"); |
| | | 57 | | |
| | 4 | 58 | | var postingNumber = stateChanged.PostingNumber; |
| | 4 | 59 | | var newState = stateChanged.NewState; |
| | | 60 | | |
| | | 61 | | // Переводим Ozon-статус в читаемое название. |
| | | 62 | | // new_state приходит в формате "posting_*" → ищем в PostingSubStatuses, |
| | | 63 | | // затем в OrderStatuses (для совместимости), иначе оставляем raw-значение. |
| | 4 | 64 | | var humanStatus = OzonStatus.PostingSubStatuses.TryGetValue(newState, out var sub) |
| | 4 | 65 | | ? sub |
| | 4 | 66 | | : OzonStatus.OrderStatuses.TryGetValue(newState, out var plain) |
| | 4 | 67 | | ? plain |
| | 4 | 68 | | : newState; |
| | | 69 | | |
| | | 70 | | // Находим или создаём запись ClientStatus с данным именем (по аналогии с OrderCaster). |
| | 4 | 71 | | var clientStatus = await _context.ClientStatuses |
| | 4 | 72 | | .FirstOrDefaultAsync(cs => cs.Name == humanStatus, ct); |
| | | 73 | | |
| | 4 | 74 | | if (clientStatus == null) |
| | | 75 | | { |
| | 4 | 76 | | clientStatus = new ClientStatus { Name = humanStatus }; |
| | 4 | 77 | | await _context.ClientStatuses.AddAsync(clientStatus, ct); |
| | 4 | 78 | | await _context.SaveChangesAsync(ct); |
| | | 79 | | |
| | 4 | 80 | | _logger.LogInformation( |
| | 4 | 81 | | "TYPE_STATE_CHANGED: создан новый ClientStatus '{Name}' (из Ozon new_state='{NewState}')", |
| | 4 | 82 | | humanStatus, newState); |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | // Ищем заказы по номеру отправления (с суффиксом /N и без него). |
| | 4 | 86 | | var orders = await _context.Orders |
| | 4 | 87 | | .Where(o => o.ShipmentNumber != null |
| | 4 | 88 | | && (o.ShipmentNumber == postingNumber |
| | 4 | 89 | | || o.ShipmentNumber.StartsWith(postingNumber + "/"))) |
| | 4 | 90 | | .ToListAsync(ct); |
| | | 91 | | |
| | 4 | 92 | | if (!orders.Any()) |
| | | 93 | | { |
| | 1 | 94 | | _logger.LogInformation( |
| | 1 | 95 | | "TYPE_STATE_CHANGED: заказы с номером отправления '{PostingNumber}' не найдены в БД (статус Ozon: '{ |
| | 1 | 96 | | postingNumber, humanStatus); |
| | 1 | 97 | | return $"Отправление {postingNumber} не найдено в Studio2. Статус Ozon: «{humanStatus}»."; |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | // Ozon не гарантирует порядок доставки пушей (и ретраит их): уведомление, событие которого старше |
| | | 101 | | // последней известной смены статуса, не должно откатывать более свежий статус. |
| | 3 | 102 | | var eventTime = ToLocalEventTime(stateChanged.ChangedStateDate); |
| | | 103 | | |
| | 3 | 104 | | var updatedCount = 0; |
| | 3 | 105 | | var staleCount = 0; |
| | 12 | 106 | | foreach (var order in orders) |
| | | 107 | | { |
| | 3 | 108 | | if (order.ClientStatusId == clientStatus.Id) |
| | | 109 | | continue; |
| | | 110 | | |
| | 3 | 111 | | if (eventTime.HasValue && order.LastStatusChangeDate.HasValue && eventTime.Value < order.LastStatusChang |
| | | 112 | | { |
| | 1 | 113 | | staleCount++; |
| | 1 | 114 | | continue; |
| | | 115 | | } |
| | | 116 | | |
| | 2 | 117 | | order.ClientStatusId = clientStatus.Id; |
| | 2 | 118 | | order.LastStatusChangeDate = eventTime ?? DateTime.Now; |
| | 2 | 119 | | updatedCount++; |
| | | 120 | | } |
| | | 121 | | |
| | 3 | 122 | | if (staleCount > 0) |
| | | 123 | | { |
| | 1 | 124 | | _logger.LogInformation( |
| | 1 | 125 | | "TYPE_STATE_CHANGED: {PostingNumber} — пропущено устаревшее уведомление '{HumanStatus}' от {EventTim |
| | 1 | 126 | | postingNumber, humanStatus, eventTime, staleCount); |
| | | 127 | | } |
| | | 128 | | |
| | 3 | 129 | | if (updatedCount > 0) |
| | 2 | 130 | | await _context.SaveChangesAsync(ct); |
| | | 131 | | |
| | 6 | 132 | | var orderIds = orders.Select(x => x.Id).ToArray(); |
| | 3 | 133 | | await _deliveryStatusSynchronizer.SynchronizeCancelledAsync(orderIds, cancellationToken: ct); |
| | 3 | 134 | | await _deliveryStatusSynchronizer.SynchronizeDeliveredAsync(orderIds, cancellationToken: ct); |
| | | 135 | | |
| | 3 | 136 | | var result = updatedCount > 0 |
| | 3 | 137 | | ? $"Отправление {postingNumber}. Статус: «{humanStatus}». Обновлено заказов: {updatedCount}." |
| | 3 | 138 | | : staleCount > 0 |
| | 3 | 139 | | ? $"Отправление {postingNumber}. Уведомление «{humanStatus}» устарело (статус изменён позже) — пропу |
| | 3 | 140 | | : $"Отправление {postingNumber}. Статус уже «{humanStatus}» — изменений нет."; |
| | | 141 | | |
| | 3 | 142 | | _logger.LogInformation("TYPE_STATE_CHANGED: {Result}", result); |
| | 3 | 143 | | return result; |
| | 4 | 144 | | } |
| | | 145 | | |
| | | 146 | | /// <summary>changed_state_date приходит в UTC; LastStatusChangeDate хранится в локальном времени сервера.</summ |
| | | 147 | | private static DateTime? ToLocalEventTime(DateTime changedStateDate) |
| | | 148 | | { |
| | 3 | 149 | | if (changedStateDate == default) |
| | 1 | 150 | | return null; |
| | | 151 | | |
| | 2 | 152 | | var utc = changedStateDate.Kind == DateTimeKind.Local |
| | 2 | 153 | | ? changedStateDate.ToUniversalTime() |
| | 2 | 154 | | : DateTime.SpecifyKind(changedStateDate, DateTimeKind.Utc); |
| | 2 | 155 | | return utc.ToLocalTime(); |
| | | 156 | | } |
| | | 157 | | } |
| | | 158 | | } |