| | | 1 | | using Microsoft.Extensions.Options; |
| | | 2 | | using Newtonsoft.Json.Linq; |
| | | 3 | | using Services.ApiServices.ApiSupplierRequestLog; |
| | | 4 | | |
| | | 5 | | namespace Services.ApiServices.FavoritPartsApi; |
| | | 6 | | |
| | | 7 | | /// <summary>Клиент REST v1 FavoritParts (ws/v1/) — справочники, реальная серверная корзина, реальные заказы. |
| | | 8 | | /// Отдельный от проценки хост-путь. Авторизация — заголовки X-Favorit-ClientKey/X-Favorit-DeveloperKey |
| | | 9 | | /// (ключи не попадают в query, поэтому не требуют редактирования в логе запросов). |
| | | 10 | | /// Коды ответа: 200 успех (JSON), 401 ошибка авторизации, 400 ошибка сервиса (текст в теле, Content-Type |
| | | 11 | | /// text/html — не JSON), 500 ошибка сервера, 403/404 на GET order/{id} — чужой/несуществующий заказ.</summary> |
| | | 12 | | public class FavoritPartsOrderHttpClient |
| | | 13 | | { |
| | | 14 | | private const string ProviderKey = "FavoritParts"; |
| | | 15 | | private readonly IHttpClientFactory _factory; |
| | | 16 | | private readonly IOptionsMonitor<FavoritPartsConfig> _config; |
| | | 17 | | private readonly ApiSupplierRequestLogStore _logs; |
| | | 18 | | private readonly string _providerKey; |
| | | 19 | | private readonly string _optionsName; |
| | | 20 | | |
| | 34 | 21 | | public FavoritPartsOrderHttpClient(IHttpClientFactory factory, IOptionsMonitor<FavoritPartsConfig> config, ApiSuppli |
| | 34 | 22 | | string providerKey = "FavoritParts", string optionsName = "") |
| | | 23 | | { |
| | 34 | 24 | | _factory = factory; |
| | 34 | 25 | | _config = config; |
| | 34 | 26 | | _logs = logs; |
| | 34 | 27 | | _providerKey = providerKey; |
| | 34 | 28 | | _optionsName = optionsName; |
| | 34 | 29 | | } |
| | | 30 | | |
| | | 31 | | /// <summary>query поддерживает повторяющиеся ключи (например orderID[]=..&orderID[]=..) — обычный Dictionary дл |
| | | 32 | | public Task<JToken> GetAsync(string path, IEnumerable<KeyValuePair<string, string?>>? query = null, CancellationToke |
| | 31 | 33 | | SendAsync(HttpMethod.Get, path, query, body: null, ct); |
| | | 34 | | |
| | | 35 | | public Task<JToken> PostAsync(string path, string jsonBody, CancellationToken ct = default) => |
| | 0 | 36 | | SendAsync(HttpMethod.Post, path, query: null, body: jsonBody, ct); |
| | | 37 | | |
| | | 38 | | private async Task<JToken> SendAsync(HttpMethod method, string path, IEnumerable<KeyValuePair<string, string?>>? que |
| | | 39 | | { |
| | 31 | 40 | | var config = string.IsNullOrEmpty(_optionsName) ? _config.CurrentValue : _config.Get(_optionsName); |
| | 31 | 41 | | if (string.IsNullOrWhiteSpace(config.ClientKey)) |
| | 0 | 42 | | throw new FavoritPartsApiException("Не настроен FavoritParts:ClientKey."); |
| | 31 | 43 | | if (string.IsNullOrWhiteSpace(config.DeveloperKey)) |
| | 0 | 44 | | throw new FavoritPartsApiException("Не настроен FavoritParts:DeveloperKey. Запрос в боевой API без него не о |
| | | 45 | | |
| | 31 | 46 | | var uri = BuildUri(config, path, query ?? Array.Empty<KeyValuePair<string, string?>>()); |
| | 31 | 47 | | var requestLog = $"{uri.Query}\n" + |
| | 31 | 48 | | $"Client authentication: present (length={config.ClientKey.Length})\n" + |
| | 31 | 49 | | $"Developer authentication: present (length={config.DeveloperKey.Length})"; |
| | | 50 | | |
| | 31 | 51 | | var timer = System.Diagnostics.Stopwatch.StartNew(); |
| | | 52 | | try |
| | | 53 | | { |
| | 31 | 54 | | using var request = new HttpRequestMessage(method, uri); |
| | 31 | 55 | | request.Headers.Add("X-Favorit-ClientKey", config.ClientKey); |
| | 31 | 56 | | request.Headers.Add("X-Favorit-DeveloperKey", config.DeveloperKey); |
| | 31 | 57 | | if (body is not null) |
| | 0 | 58 | | request.Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json"); |
| | | 59 | | |
| | 31 | 60 | | using var response = await _factory.CreateClient("FavoritPartsOrderApi").SendAsync(request, ct); |
| | 29 | 61 | | var text = await response.Content.ReadAsStringAsync(ct); |
| | 29 | 62 | | _logs.Add(_providerKey, method.Method, path, (int)response.StatusCode, timer, requestLog, text); |
| | | 63 | | |
| | 29 | 64 | | if (!response.IsSuccessStatusCode) |
| | | 65 | | { |
| | | 66 | | // Текст ошибки сервиса приходит без какого-либо контекста (вплоть до «Что-то пошло не так»), |
| | | 67 | | // поэтому в сообщение всегда добавляем код и адрес: иначе в UI не отличить сбой поставщика |
| | | 68 | | // от нашей ошибки. |
| | 1 | 69 | | var message = response.StatusCode switch |
| | 1 | 70 | | { |
| | 0 | 71 | | System.Net.HttpStatusCode.Unauthorized => "FavoritParts: ошибка авторизации (неверный/просроченный C |
| | 0 | 72 | | System.Net.HttpStatusCode.Forbidden => "FavoritParts: неверный идентификатор заказа.", |
| | 0 | 73 | | System.Net.HttpStatusCode.NotFound => "FavoritParts: заказ не найден или принадлежит другому покупат |
| | 1 | 74 | | _ => string.IsNullOrWhiteSpace(text) |
| | 1 | 75 | | ? $"FavoritParts вернул HTTP {(int)response.StatusCode} на {path}." |
| | 1 | 76 | | : $"FavoritParts (HTTP {(int)response.StatusCode}, {path}): {Shorten(text.Trim())}" |
| | 1 | 77 | | }; |
| | 1 | 78 | | throw new FavoritPartsApiException(message, (int)response.StatusCode, path); |
| | | 79 | | } |
| | | 80 | | |
| | 28 | 81 | | if (string.IsNullOrWhiteSpace(text)) return JValue.CreateNull(); |
| | | 82 | | |
| | 28 | 83 | | try { return JToken.Parse(text); } |
| | 0 | 84 | | catch (Exception ex) { throw new FavoritPartsApiException("FavoritParts вернул невалидный JSON.", requestPat |
| | | 85 | | } |
| | 2 | 86 | | catch (FavoritPartsApiException) { throw; } |
| | 2 | 87 | | catch (Exception ex) |
| | | 88 | | { |
| | 2 | 89 | | _logs.Add(_providerKey, method.Method, path, null, timer, requestLog, ex.Message); |
| | 2 | 90 | | throw new FavoritPartsApiException("Не удалось выполнить запрос к FavoritParts.", requestPath: path, innerEx |
| | | 91 | | } |
| | 28 | 92 | | } |
| | | 93 | | |
| | | 94 | | /// <summary>Тело ошибки приходит как text/html и может оказаться целой страницей — в сообщение |
| | | 95 | | /// для пользователя тащим только начало.</summary> |
| | 1 | 96 | | private static string Shorten(string text) => text.Length <= 300 ? text : text[..300] + "…"; |
| | | 97 | | |
| | | 98 | | private static Uri BuildUri(FavoritPartsConfig config, string path, IEnumerable<KeyValuePair<string, string?>> query |
| | | 99 | | { |
| | 31 | 100 | | var builder = new UriBuilder(config.BaseUrl.TrimEnd('/') + "/" + path.TrimStart('/')); |
| | 31 | 101 | | builder.Query = string.Join("&", query.Where(x => !string.IsNullOrWhiteSpace(x.Value)) |
| | 31 | 102 | | .Select(x => $"{Uri.EscapeDataString(x.Key)}={Uri.EscapeDataString(x.Value!)}")); |
| | 31 | 103 | | return builder.Uri; |
| | | 104 | | } |
| | | 105 | | } |