| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using Services.ApiServices._1CApi.Stock; |
| | | 3 | | using Services.ApiServices.TelegramBotApi; |
| | | 4 | | using Services.SystemMonitoringServices; |
| | | 5 | | |
| | | 6 | | namespace OzonOrdersWeb.WebServices.SystemAlerts; |
| | | 7 | | |
| | | 8 | | /// <summary>Немедленно сообщает о дефиците, который реально блокирует проведение документа 1С.</summary> |
| | 2 | 9 | | public sealed class OneCStockAlertReporter( |
| | 2 | 10 | | IServiceScopeFactory scopeFactory, |
| | 2 | 11 | | IsolatedSystemMonitoringSignalWriter signals, |
| | 2 | 12 | | TelegramBotConfig telegramConfig, |
| | 2 | 13 | | ILogger<OneCStockAlertReporter> logger) : IOneCStockAlertReporter |
| | | 14 | | { |
| | 1 | 15 | | private static readonly ConcurrentDictionary<string, DateTime> Sent = new(StringComparer.Ordinal); |
| | 1 | 16 | | private static readonly TimeSpan IdempotencyTtl = TimeSpan.FromMinutes(30); |
| | | 17 | | |
| | | 18 | | public async Task ReportAsync(StockCheckResult result, string? operation = null, CancellationToken cancellationToken |
| | | 19 | | { |
| | 2 | 20 | | if (!result.HasBlocking) |
| | 1 | 21 | | return; |
| | | 22 | | |
| | 1 | 23 | | var key = "shortage|" + string.Join(";", result.Blocking |
| | 1 | 24 | | .OrderBy(x => x.Article, StringComparer.OrdinalIgnoreCase) |
| | 1 | 25 | | .ThenBy(x => x.WarehouseName, StringComparer.OrdinalIgnoreCase) |
| | 2 | 26 | | .Select(x => $"{x.Article}|{x.WarehouseName}|{string.Join(',', x.OrderIds.Order())}")); |
| | 1 | 27 | | var now = DateTime.UtcNow; |
| | 1 | 28 | | if (Sent.TryGetValue(key, out var until) && until > now) |
| | 0 | 29 | | return; |
| | 1 | 30 | | if (!Sent.TryAdd(key, now + IdempotencyTtl)) |
| | 0 | 31 | | return; |
| | | 32 | | |
| | | 33 | | try |
| | | 34 | | { |
| | 1 | 35 | | await signals.RecordAsync("onec-stock", "failure", "shortage", SignalMessage(result, operation), cancellatio |
| | | 36 | | |
| | 1 | 37 | | using var scope = scopeFactory.CreateScope(); |
| | 1 | 38 | | var telegram = scope.ServiceProvider.GetService<ITelegramNotifier>(); |
| | 1 | 39 | | if (telegram?.IsConfigured == true) |
| | 1 | 40 | | await telegram.SendAsync(new TelegramMessage(BuildMessage(result, operation), Channel: TelegramChannel.O |
| | 1 | 41 | | } |
| | 0 | 42 | | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) |
| | | 43 | | { |
| | 0 | 44 | | throw; |
| | | 45 | | } |
| | 0 | 46 | | catch (Exception ex) |
| | | 47 | | { |
| | 0 | 48 | | logger.LogWarning(ex, "Не удалось отправить уведомление о нехватке остатков 1С"); |
| | 0 | 49 | | } |
| | | 50 | | finally |
| | | 51 | | { |
| | 3 | 52 | | foreach (var item in Sent.Where(x => x.Value <= now).Take(100)) |
| | 0 | 53 | | Sent.TryRemove(item.Key, out _); |
| | | 54 | | } |
| | 2 | 55 | | } |
| | | 56 | | |
| | | 57 | | internal string BuildMessage(StockCheckResult result, string? operation) |
| | | 58 | | { |
| | 1 | 59 | | var lines = result.Blocking; |
| | 2 | 60 | | var orderIds = lines.SelectMany(x => x.OrderIds).Distinct().Order().ToList(); |
| | 1 | 61 | | var builder = new TelegramMessageBuilder() |
| | 1 | 62 | | .Title("Нехватка остатка перед проведением документа 1С", "⚠️") |
| | 1 | 63 | | .Line("#1с_остатки_нехватка") |
| | 1 | 64 | | .Field("Операция", operation) |
| | 1 | 65 | | .Field("Позиции", lines.Count.ToString()); |
| | | 66 | | |
| | 4 | 67 | | foreach (var line in lines.Take(20)) |
| | 1 | 68 | | builder.Line($"{line.Article}: склад {line.WarehouseName}, нужно {line.Required:0.###}, остаток {line.Availa |
| | 1 | 69 | | if (lines.Count > 20) |
| | 0 | 70 | | builder.Line($"…и ещё {lines.Count - 20} поз."); |
| | 1 | 71 | | if (orderIds.Count > 0) |
| | | 72 | | { |
| | 2 | 73 | | builder.Field("Заказы", string.Join(", ", orderIds.Take(30).Select(id => "#" + id))); |
| | 1 | 74 | | builder.Link("Открыть заказы", Absolute(SystemAlertLinks.Orders(orderIds))); |
| | | 75 | | } |
| | 1 | 76 | | builder.Link("Системные алерты", Absolute("/Studio2/JobLogs?tab=alerts")); |
| | 1 | 77 | | return builder.Build(); |
| | | 78 | | } |
| | | 79 | | |
| | 2 | 80 | | private string Absolute(string path) => Uri.TryCreate(telegramConfig.SiteBaseUrl, UriKind.Absolute, out var baseUri) |
| | 2 | 81 | | ? new Uri(baseUri.ToString().TrimEnd('/') + path).ToString() |
| | 2 | 82 | | : path; |
| | | 83 | | |
| | | 84 | | private static string SignalMessage(StockCheckResult result, string? operation) => |
| | 1 | 85 | | $"{operation ?? "Документ 1С"}: " + string.Join("; ", result.Blocking.Take(10).Select(x => |
| | 2 | 86 | | $"{x.Article}, {x.WarehouseName}: нужно {x.Required:0.###}, остаток {x.Available:0.###}")); |
| | | 87 | | } |