337 lines
14 KiB
C#
337 lines
14 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Net.Http;
|
||
using System.Net.Http.Headers;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using System.Text.Json;
|
||
|
||
namespace Электронная_Фармация.Classes
|
||
{
|
||
public class ApiClient
|
||
{
|
||
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||
{
|
||
PropertyNamingPolicy = null
|
||
};
|
||
|
||
private readonly HttpClient _httpClient;
|
||
private readonly string _baseUrl;
|
||
private string _token;
|
||
|
||
public ApiClient(string baseUrl = null)
|
||
{
|
||
_baseUrl = AppConfig.NormalizeApiBaseUrl(baseUrl ?? AppConfig.ApiBaseUrl);
|
||
_httpClient = new HttpClient
|
||
{
|
||
// Крупный прайс тянем одним запросом — даём серверу время посчитать и отдать.
|
||
Timeout = TimeSpan.FromSeconds(120)
|
||
};
|
||
|
||
AppDebugLog.Info("ApiClient", $"Создан клиент. BaseUrl={_baseUrl}, Timeout={_httpClient.Timeout.TotalSeconds}s");
|
||
}
|
||
|
||
public string BaseUrl => _baseUrl;
|
||
|
||
public async Task<string> LoginAsync(string username, string password)
|
||
{
|
||
var request = new LoginRequest
|
||
{
|
||
Username = username,
|
||
Password = password
|
||
};
|
||
|
||
var path = "/auth/login";
|
||
var fullUrl = BuildFullUrl(path);
|
||
var requestJson = JsonSerializer.Serialize(request, JsonOptions);
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
AppDebugLog.ApiRequest("POST", fullUrl, requestJson, $"username={username}");
|
||
|
||
try
|
||
{
|
||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fullUrl))
|
||
{
|
||
httpRequest.Content = new StringContent(requestJson, Encoding.UTF8, "application/json");
|
||
var response = await _httpClient.SendAsync(httpRequest);
|
||
var responseBody = await response.Content.ReadAsStringAsync();
|
||
stopwatch.Stop();
|
||
|
||
AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody);
|
||
return ParseLoginResponse(response, responseBody);
|
||
}
|
||
}
|
||
catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP"))
|
||
{
|
||
stopwatch.Stop();
|
||
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||
throw WrapNetworkError(ex, "авторизацию");
|
||
}
|
||
catch (TaskCanceledException ex)
|
||
{
|
||
stopwatch.Stop();
|
||
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||
throw WrapNetworkError(ex, "авторизацию");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
stopwatch.Stop();
|
||
if (ex is UnauthorizedAccessException || ex is HttpRequestException)
|
||
{
|
||
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||
throw;
|
||
}
|
||
|
||
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||
throw new HttpRequestException($"Ошибка при авторизации ({_baseUrl}): {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
public void SetToken(string token)
|
||
{
|
||
_token = string.IsNullOrWhiteSpace(token) ? null : token;
|
||
AppDebugLog.Info("ApiClient", $"Токен установлен: {AppDebugLog.MaskToken(_token)}");
|
||
}
|
||
|
||
public string Token => _token;
|
||
|
||
public bool IsAuthenticated => !string.IsNullOrEmpty(_token);
|
||
|
||
public async Task<PriceSummaryResponse> GetPriceSummaryAsync(string supplierId = null, string regionId = null, string q = null, int? limit = null, int? offset = null)
|
||
{
|
||
EnsureAuthenticated();
|
||
|
||
var queryParams = new List<string>();
|
||
if (!string.IsNullOrEmpty(supplierId))
|
||
{
|
||
queryParams.Add($"supplier_id={Uri.EscapeDataString(supplierId)}");
|
||
}
|
||
if (!string.IsNullOrEmpty(regionId))
|
||
{
|
||
queryParams.Add($"region_id={Uri.EscapeDataString(regionId)}");
|
||
}
|
||
if (!string.IsNullOrWhiteSpace(q))
|
||
{
|
||
queryParams.Add($"q={Uri.EscapeDataString(q)}");
|
||
}
|
||
if (limit.HasValue)
|
||
{
|
||
queryParams.Add($"limit={limit.Value}");
|
||
}
|
||
if (offset.HasValue)
|
||
{
|
||
queryParams.Add($"offset={offset.Value}");
|
||
}
|
||
|
||
var path = "/api/supplier-prices/summary";
|
||
if (queryParams.Count > 0)
|
||
{
|
||
path += "?" + string.Join("&", queryParams);
|
||
}
|
||
|
||
var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса");
|
||
try
|
||
{
|
||
var summary = JsonSerializer.Deserialize<PriceSummaryResponse>(responseBody, JsonOptions);
|
||
var count = summary?.Summary?.Count ?? 0;
|
||
AppDebugLog.Info("ApiClient", $"Прайс получен: {count} позиций");
|
||
return summary;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ прайса", ex);
|
||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайса: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
public async Task<List<InvoiceApiItem>> GetInvoicesAsync()
|
||
{
|
||
EnsureAuthenticated();
|
||
|
||
var responseBody = await SendAuthorizedGetAsync("/api/buyer/invoices", "загрузку накладных");
|
||
try
|
||
{
|
||
var items = JsonSerializer.Deserialize<List<InvoiceApiItem>>(responseBody, JsonOptions) ?? new List<InvoiceApiItem>();
|
||
AppDebugLog.Info("ApiClient", $"Накладные получены: {items.Count} шт.");
|
||
return items;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ накладных", ex);
|
||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке накладных: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
private void EnsureAuthenticated()
|
||
{
|
||
if (!IsAuthenticated)
|
||
{
|
||
var ex = new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных");
|
||
AppDebugLog.Error("ApiClient", "Запрос без токена авторизации", ex);
|
||
throw ex;
|
||
}
|
||
}
|
||
|
||
private async Task<string> SendAuthorizedGetAsync(string path, string operationName)
|
||
{
|
||
var fullUrl = BuildFullUrl(path);
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
AppDebugLog.ApiRequest("GET", fullUrl, extra: $"token={AppDebugLog.MaskToken(_token)}");
|
||
|
||
try
|
||
{
|
||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl))
|
||
{
|
||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||
var response = await _httpClient.SendAsync(httpRequest);
|
||
var responseBody = await response.Content.ReadAsStringAsync();
|
||
stopwatch.Stop();
|
||
|
||
AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody);
|
||
|
||
if (response.IsSuccessStatusCode)
|
||
{
|
||
return responseBody;
|
||
}
|
||
|
||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||
{
|
||
_token = null;
|
||
var error = TryDeserializeError(responseBody);
|
||
var message = $"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}";
|
||
AppDebugLog.ApiHttpError("GET", fullUrl, (int)response.StatusCode, responseBody, operationName);
|
||
var unauthorized = new UnauthorizedAccessException(message);
|
||
AppDebugLog.Error("ApiClient", message, unauthorized);
|
||
throw unauthorized;
|
||
}
|
||
|
||
var httpError = TryDeserializeError(responseBody);
|
||
var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}";
|
||
AppDebugLog.ApiHttpError("GET", fullUrl, (int)response.StatusCode, responseBody, operationName);
|
||
var httpEx = new HttpRequestException(httpMessage);
|
||
AppDebugLog.Error("ApiClient", httpMessage, httpEx);
|
||
throw httpEx;
|
||
}
|
||
}
|
||
catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP"))
|
||
{
|
||
stopwatch.Stop();
|
||
AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||
throw WrapNetworkError(ex, operationName);
|
||
}
|
||
catch (TaskCanceledException ex)
|
||
{
|
||
stopwatch.Stop();
|
||
AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||
throw WrapNetworkError(ex, operationName);
|
||
}
|
||
catch (UnauthorizedAccessException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (HttpRequestException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
stopwatch.Stop();
|
||
AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private string ParseLoginResponse(HttpResponseMessage response, string responseBody)
|
||
{
|
||
if (response.IsSuccessStatusCode)
|
||
{
|
||
try
|
||
{
|
||
var loginResponse = JsonSerializer.Deserialize<LoginResponse>(responseBody, JsonOptions);
|
||
if (loginResponse == null || string.IsNullOrWhiteSpace(loginResponse.Token))
|
||
{
|
||
var emptyTokenEx = new HttpRequestException($"Сервер {_baseUrl} вернул пустой токен авторизации");
|
||
AppDebugLog.Error("ApiClient", "Пустой токен в ответе логина", emptyTokenEx);
|
||
throw emptyTokenEx;
|
||
}
|
||
|
||
_token = loginResponse.Token;
|
||
AppDebugLog.Info("ApiClient", $"Авторизация успешна. token={AppDebugLog.MaskToken(_token)}");
|
||
return _token;
|
||
}
|
||
catch (HttpRequestException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ логина", ex);
|
||
throw new HttpRequestException($"Некорректный ответ сервера при авторизации: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||
{
|
||
var error = TryDeserializeError(responseBody);
|
||
var message = $"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}";
|
||
AppDebugLog.ApiHttpError("POST", BuildFullUrl("/auth/login"), (int)response.StatusCode, responseBody, "login");
|
||
var unauthorized = new UnauthorizedAccessException(message);
|
||
AppDebugLog.Error("ApiClient", message, unauthorized);
|
||
throw unauthorized;
|
||
}
|
||
|
||
var httpError = TryDeserializeError(responseBody);
|
||
var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}";
|
||
AppDebugLog.ApiHttpError("POST", BuildFullUrl("/auth/login"), (int)response.StatusCode, responseBody, "login");
|
||
var httpEx = new HttpRequestException(httpMessage);
|
||
AppDebugLog.Error("ApiClient", httpMessage, httpEx);
|
||
throw httpEx;
|
||
}
|
||
|
||
private string BuildFullUrl(string path)
|
||
{
|
||
return new Uri(new Uri(_baseUrl + "/"), path.TrimStart('/')).ToString();
|
||
}
|
||
|
||
private static ErrorResponse TryDeserializeError(string body)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(body))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
return JsonSerializer.Deserialize<ErrorResponse>(body, JsonOptions);
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private HttpRequestException WrapNetworkError(Exception ex, string operation)
|
||
{
|
||
var details = ex.InnerException != null && ex.InnerException.Message != ex.Message
|
||
? $" ({ex.InnerException.Message})"
|
||
: string.Empty;
|
||
|
||
return new HttpRequestException(
|
||
$"Не удалось выполнить {operation}. Сервер: {_baseUrl}{details}. {ex.Message}",
|
||
ex);
|
||
}
|
||
}
|
||
|
||
public sealed class InvoiceApiItem
|
||
{
|
||
public string InvoiceNumber { get; set; }
|
||
public string InvoiceDate { get; set; }
|
||
public string SupplierName { get; set; }
|
||
public string ConsigneeName { get; set; }
|
||
public string InvoiceSum { get; set; }
|
||
public string RefuseSum { get; set; }
|
||
}
|
||
}
|