using System.ComponentModel.DataAnnotations; using System.Net; using System.Text.Json; using RestSharp; using Store.Management.Api.Authentication; using Store.Management.Api.Models; namespace Store.Management.Api.Transport; /// 唯一 HTTP 出口;模块仅指定相对路由、请求数据与响应类型。 public sealed class ApiTransport : IDisposable { private readonly RestClient _client; private readonly TokenSession _session; private readonly string _version; private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); internal ApiTransport(ApiClientOptions options, TokenSession session, HttpClient? httpClient) { options.Validate(); _session = session; _version = options.Version; RestClientOptions settings = new RestClientOptions(new Uri(options.BaseUrl.AbsoluteUri.TrimEnd('/') + "/")) { Timeout = options.Timeout, FollowRedirects = false }; _client = httpClient is null ? new RestClient(settings) : new RestClient(httpClient, settings, disposeHttpClient: false); } public async Task SendAsync(string route, Method method, object? body = null, IReadOnlyDictionary? query = null, bool authenticated = true, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrWhiteSpace(route) || route.StartsWith('/') || route.Contains("..") || route.Contains(':') || route.Contains('?') || route.Contains('#') || route.Contains('\\')) { throw new ArgumentException("接口必须使用相对路由,例如 user/GetAllUser。", nameof(route)); } if (body is not null) { Validator.ValidateObject(body, new ValidationContext(body), true); } string? token = authenticated ? _session.Token : null; if (authenticated && token is null) { throw new ApiException(ApiErrorKind.Unauthorized, "请先登录。", code: 401); } RestRequest request = new RestRequest($"{_version}/{route}", method); if (token is not null) { request.AddHeader("Authorization", $"Bearer {token}"); } if (body is not null) { request.AddJsonBody(body); } if (query is not null) { foreach (KeyValuePair pair in query) { if (pair.Value is not null) { request.AddQueryParameter(pair.Key, pair.Value); } } } RestResponse response = await _client.ExecuteAsync(request, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (response.ResponseStatus == ResponseStatus.TimedOut) { throw new ApiException(ApiErrorKind.Timeout, "请求超时,请稍后重试。", innerException: response.ErrorException); } if ((int)response.StatusCode == 0) { throw new ApiException(ApiErrorKind.Network, "无法连接服务器,请检查网络与服务地址。", innerException: response.ErrorException); } // 先判断状态,再解析 data;错误响应的数据结构可能与成功响应不同。 ApiResult? result = null; JsonException? parseError = null; try { result = JsonSerializer.Deserialize>(response.Content ?? "", Json); } catch (JsonException ex) { parseError = ex; } if (response.StatusCode == HttpStatusCode.Unauthorized || result?.Code == 401) { _session.ClearIfCurrent(token); throw new ApiException(ApiErrorKind.Unauthorized, result?.Msg ?? "登录已失效,请重新登录。", response.StatusCode, 401, result?.TraceId); } if (!response.IsSuccessful) { throw new ApiException(ApiErrorKind.Http, result?.Msg ?? $"服务器请求失败({(int)response.StatusCode})。", response.StatusCode, result?.Code, result?.TraceId); } if (result is null) { throw new ApiException(ApiErrorKind.InvalidResponse, "服务器响应格式无效。", response.StatusCode, innerException: parseError); } if (result.Code != 200) { throw new ApiException(ApiErrorKind.Business, result.Msg ?? "操作失败。", response.StatusCode, result.Code, result.TraceId); } if (result.Data.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) { return default; } try { return result.Data.Deserialize(Json); } catch (JsonException ex) { throw new ApiException(ApiErrorKind.InvalidResponse, "服务器响应 data 格式无效。", response.StatusCode, result.Code, result.TraceId, ex); } } public async Task GetRequiredAsync(string route, Method method, object? body = null, IReadOnlyDictionary? query = null, bool authenticated = true, CancellationToken cancellationToken = default) where T : class => await SendAsync(route, method, body, query, authenticated, cancellationToken).ConfigureAwait(false) ?? throw new ApiException(ApiErrorKind.InvalidResponse, "服务器响应缺少 data。"); public void Dispose() => _client.Dispose(); }