| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using Services.ApiServices._1CApi.Nomenclature; |
| | | 3 | | using Services.ApiServices._1CApi.OData; |
| | | 4 | | using Services.ApiServices.TelegramBotApi; |
| | | 5 | | |
| | | 6 | | namespace OzonOrdersWeb.WebServices.SystemAlerts; |
| | | 7 | | |
| | | 8 | | /// <summary>Немедленные, идемпотентные уведомления о решениях резолвера номенклатуры.</summary> |
| | 11 | 9 | | public sealed class NomenclatureTelegramNotifier( |
| | 11 | 10 | | IServiceScopeFactory scopeFactory, |
| | 11 | 11 | | TelegramBotConfig telegramConfig, |
| | 11 | 12 | | OneCODataConfig oneCConfig, |
| | 11 | 13 | | ILogger<NomenclatureTelegramNotifier> logger) : INomenclatureNotificationSink |
| | | 14 | | { |
| | 1 | 15 | | private static readonly ConcurrentDictionary<string, DateTime> Sent = new(StringComparer.Ordinal); |
| | 1 | 16 | | private static readonly TimeSpan IdempotencyTtl = TimeSpan.FromDays(1); |
| | | 17 | | |
| | | 18 | | public async Task NotifyAsync(NomenclatureNotification notification, CancellationToken cancellationToken = default) |
| | | 19 | | { |
| | 3 | 20 | | var key = $"{notification.Kind}|{notification.OrderId}|{notification.RefKey?.ToString() ?? notification.Normaliz |
| | 3 | 21 | | var now = DateTime.UtcNow; |
| | 3 | 22 | | if (Sent.TryGetValue(key, out var until) && until > now) |
| | 1 | 23 | | return; |
| | 2 | 24 | | if (!Sent.TryAdd(key, now + IdempotencyTtl)) |
| | 0 | 25 | | return; |
| | | 26 | | |
| | | 27 | | try |
| | | 28 | | { |
| | 2 | 29 | | using var scope = scopeFactory.CreateScope(); |
| | 2 | 30 | | var telegram = scope.ServiceProvider.GetService<ITelegramNotifier>(); |
| | 2 | 31 | | if (telegram?.IsConfigured != true) |
| | 0 | 32 | | return; |
| | | 33 | | |
| | 2 | 34 | | await telegram.SendAsync(new TelegramMessage(BuildMessage(notification), Channel: TelegramChannel.OneC), can |
| | 1 | 35 | | } |
| | 0 | 36 | | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) |
| | | 37 | | { |
| | 0 | 38 | | throw; |
| | | 39 | | } |
| | 1 | 40 | | catch (Exception ex) |
| | | 41 | | { |
| | 1 | 42 | | logger.LogWarning(ex, "Не удалось отправить Telegram-уведомление о номенклатуре для заказа {OrderId}", notif |
| | 1 | 43 | | } |
| | | 44 | | finally |
| | | 45 | | { |
| | 7 | 46 | | foreach (var item in Sent.Where(x => x.Value <= now).Take(100)) |
| | 0 | 47 | | Sent.TryRemove(item.Key, out _); |
| | | 48 | | } |
| | 3 | 49 | | } |
| | | 50 | | |
| | | 51 | | public string BuildMessage(NomenclatureNotification notification) |
| | | 52 | | { |
| | 11 | 53 | | var (title, icon) = notification.Kind switch |
| | 11 | 54 | | { |
| | 2 | 55 | | NomenclatureNotificationKind.Created => ("Создана карточка номенклатуры 1С", "✅"), |
| | 1 | 56 | | NomenclatureNotificationKind.BrandMismatchCreated => ("Создана карточка другого бренда", "⚠️"), |
| | 1 | 57 | | NomenclatureNotificationKind.NotFound => ("Номенклатура 1С не найдена", "❌"), |
| | 1 | 58 | | NomenclatureNotificationKind.Duplicate => ("Найдены дубли карточек номенклатуры", "⚠️"), |
| | 1 | 59 | | NomenclatureNotificationKind.InputConflict => ("Конфликт артикула заказа и ProductKey", "❌"), |
| | 1 | 60 | | NomenclatureNotificationKind.ProducerUnknown => ("Бренд товара не распознан", "❌"), |
| | 1 | 61 | | NomenclatureNotificationKind.Ambiguous => ("Неоднозначный поиск номенклатуры", "❌"), |
| | 1 | 62 | | NomenclatureNotificationKind.BrandMismatch => ("В 1С карточка только другого бренда", "❌"), |
| | 2 | 63 | | NomenclatureNotificationKind.CreateFailed => ("Не удалось создать карточку номенклатуры", "❌"), |
| | 0 | 64 | | _ => ("Номенклатура 1С", "❌") |
| | 11 | 65 | | }; |
| | 11 | 66 | | var builder = new TelegramMessageBuilder().Title(title, icon) |
| | 11 | 67 | | .Line(Hashtag(notification.Kind)) |
| | 11 | 68 | | .Field("Заказ", OrderLabel(notification)) |
| | 11 | 69 | | .Field("Название", notification.ProductName) |
| | 11 | 70 | | .Field("Артикул", notification.Article, code: true) |
| | 11 | 71 | | .Field("ProductKey", notification.ProductKey, code: true) |
| | 11 | 72 | | .Field("Бренд", notification.ProducerName) |
| | 11 | 73 | | .Field("Статус", notification.Status.ToString()) |
| | 11 | 74 | | .Field("Причина", notification.Reason) |
| | 11 | 75 | | .Field("GUID карточки", notification.RefKey?.ToString(), code: true); |
| | 11 | 76 | | if (notification.ExistingBrands.Count > 0) |
| | 11 | 77 | | builder.Field("Бренды существующих карточек", string.Join(" / ", notification.ExistingBrands)); |
| | | 78 | | |
| | 11 | 79 | | if (notification.OrderId > 0) |
| | | 80 | | { |
| | 11 | 81 | | builder.Link("Открыть заказ", Absolute(SystemAlertLinks.Orders([notification.OrderId]))); |
| | 11 | 82 | | builder.Link("Почему так нашлось", Absolute($"/Studio2/OneCNomenclature/Explain?orderId={notification.OrderI |
| | | 83 | | } |
| | 11 | 84 | | if (notification.RefKey.HasValue && Uri.TryCreate(oneCConfig.BaseUrl, UriKind.Absolute, out var oneCBase)) |
| | 11 | 85 | | builder.Link("Карточка 1С", new Uri(oneCBase.ToString().TrimEnd('/') + "/Catalog_%D0%9D%D0%BE%D0%BC%D0%B5%D0 |
| | 11 | 86 | | builder.Link("Системные алерты", Absolute("/Studio2/JobLogs?tab=alerts")); |
| | 11 | 87 | | return builder.Build(); |
| | | 88 | | } |
| | | 89 | | |
| | 33 | 90 | | private string Absolute(string path) => Uri.TryCreate(telegramConfig.SiteBaseUrl, UriKind.Absolute, out var baseUri) |
| | 33 | 91 | | ? new Uri(baseUri.ToString().TrimEnd('/') + path).ToString() |
| | 33 | 92 | | : path; |
| | | 93 | | |
| | 11 | 94 | | private static string OrderLabel(NomenclatureNotification notification) => notification.OrderId <= 0 |
| | 11 | 95 | | ? notification.ShipmentNumber ?? string.Empty |
| | 11 | 96 | | : string.IsNullOrWhiteSpace(notification.ShipmentNumber) |
| | 11 | 97 | | ? "#" + notification.OrderId |
| | 11 | 98 | | : $"#{notification.OrderId}, отправление {notification.ShipmentNumber}"; |
| | | 99 | | |
| | 11 | 100 | | private static string Hashtag(NomenclatureNotificationKind kind) => kind switch |
| | 11 | 101 | | { |
| | 2 | 102 | | NomenclatureNotificationKind.Created => "#1с_номенклатура_создана", |
| | 1 | 103 | | NomenclatureNotificationKind.BrandMismatchCreated => "#1с_номенклатура_другой_бренд", |
| | 1 | 104 | | NomenclatureNotificationKind.NotFound => "#1с_номенклатура_не_найдена", |
| | 1 | 105 | | NomenclatureNotificationKind.Duplicate => "#1с_номенклатура_дубли", |
| | 1 | 106 | | NomenclatureNotificationKind.InputConflict => "#1с_номенклатура_конфликт_артикула", |
| | 1 | 107 | | NomenclatureNotificationKind.ProducerUnknown => "#1с_номенклатура_бренд_не_распознан", |
| | 1 | 108 | | NomenclatureNotificationKind.Ambiguous => "#1с_номенклатура_неоднозначно", |
| | 1 | 109 | | NomenclatureNotificationKind.BrandMismatch => "#1с_номенклатура_несовпадение_бренда", |
| | 2 | 110 | | NomenclatureNotificationKind.CreateFailed => "#1с_номенклатура_ошибка_создания", |
| | 0 | 111 | | _ => "#1с_номенклатура" |
| | 11 | 112 | | }; |
| | | 113 | | } |