using CsharpHttpHelper; using AllBase.Model.Result; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace Work.Store.Api; /// 管理 API 的统一 HTTP 出口,负责 JSON、Bearer 认证和错误处理。 public sealed class ApiTransport { private readonly string _baseUrl; private string _token; private LoginRequest _credentials; internal SemaphoreSlim SessionGate { get; } = new SemaphoreSlim(1, 1); // 调用方持有 SessionGate。 internal void SetSession(string token, LoginRequest credentials) { _token = token; _credentials = credentials; } public ApiTransport(string baseUrl) { if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) || (uri.Scheme != "http" && uri.Scheme != "https") || uri.UserInfo.Length != 0 || uri.Query.Length != 0 || uri.Fragment.Length != 0) throw new ArgumentException("必须提供无凭据、查询参数和片段的 HTTP(S) 根地址。", nameof(baseUrl)); _baseUrl = uri.AbsoluteUri.TrimEnd('/') + "/"; } /// 仅存于内存;手动赋值会清除自动登录凭据,设为 null 退出本地会话。 public string Token { get { SessionGate.Wait(); try { return _token; } finally { SessionGate.Release(); } } set { if (value != null && (string.IsNullOrWhiteSpace(value) || value.IndexOfAny(new[] { '\r', '\n' }) >= 0)) throw new ArgumentException("Token 不能为空白或包含换行。", nameof(value)); SessionGate.Wait(); try { SetSession(value, null); } finally { SessionGate.Release(); } } } public T Send(string route, MethodType method, object body = null, IDictionary query = null, bool authenticated = true, int timeoutMilliseconds = 30000) { var item = CreateRequest(route, method, body, query, timeoutMilliseconds); string token = null; if (authenticated) { //SessionGate.Wait(); //try { token = _token ?? throw new HttpException("请先登录。") { IsOnline = false }; } //finally { SessionGate.Release(); } } for (int attempt = 0; ; attempt++) { if (token != null) item.Header["Authorization"] = "Bearer " + token; var response = HttpHelper.Request(item); try { return ReadResult(response); } catch (HttpException ex) when (authenticated && !ex.IsOnline) { SessionGate.Wait(); try { if (_token == token) { _token = null; if (attempt == 0 && _credentials != null) _token = AuthApi.LoginCore(this, _credentials).Token; } if (attempt != 0 || _token == null) throw; token = _token; } finally { SessionGate.Release(); } } } } public async Task SendAsync(string route, MethodType method, object body = null, IDictionary query = null, bool authenticated = true, int timeoutMilliseconds = 30000) { var item = CreateRequest(route, method, body, query, timeoutMilliseconds); string token = null; if (authenticated) { //await SessionGate.WaitAsync().ConfigureAwait(false); //try { token = _token ?? throw new HttpException("请先登录。") { IsOnline = false }; } //finally { SessionGate.Release(); } } for (int attempt = 0; ; attempt++) { if (token != null) item.Header["Authorization"] = "Bearer " + token; var response = await HttpHelper.RequestAsync(item).ConfigureAwait(false); try { return ReadResult(response); } catch (HttpException ex) when (authenticated && !ex.IsOnline) { await SessionGate.WaitAsync().ConfigureAwait(false); try { if (_token == token) { _token = null; if (attempt == 0 && _credentials != null) _token = (await AuthApi.LoginCoreAsync(this, _credentials).ConfigureAwait(false)).Token; } if (attempt != 0 || _token == null) throw; token = _token; } finally { SessionGate.Release(); } } } } private HttpItem CreateRequest(string route, MethodType method, object body, IDictionary query, int timeoutMilliseconds) { string pathOnly = route?.Split('?', '#')[0]; if (string.IsNullOrWhiteSpace(pathOnly) || !Regex.IsMatch(pathOnly, @"\A[A-Za-z0-9_-]+(/[A-Za-z0-9_-]+)*\z")) throw new ArgumentException("接口必须使用相对路由,例如 Auth/Login。", nameof(route)); if (body != null) Validator.ValidateObject(body, new ValidationContext(body), true); if (timeoutMilliseconds <= 0) throw new ArgumentOutOfRangeException(nameof(timeoutMilliseconds)); var url = new StringBuilder(_baseUrl).Append(route); if (query != null) { bool first = true; foreach (var pair in query) { if (pair.Value == null) continue; url.Append(first ? '?' : '&').Append(Uri.EscapeDataString(pair.Key)) .Append('=').Append(Uri.EscapeDataString(pair.Value)); first = false; } } return new HttpItem { URL = url.ToString(), Method = method, ContentType = "application/json; charset=utf-8", Accept = "application/json", PostDataType = PostDataType.Byte, PostdataByte = body == null ? null : Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(body)), Encoding = Encoding.UTF8, Timeout = timeoutMilliseconds, ReadWriteTimeout = timeoutMilliseconds, Allowautoredirect = false }; } private static T ReadResult(HttpResult response) { int status = (int)response.StatusCode; if (status == 0) throw new HttpException("无法连接服务器或请求超时。"); Result result = null; JsonException parseError = null; try { result = JsonConvert.DeserializeObject>(response.Html ?? ""); } catch (JsonException ex) { parseError = ex; } if (status == 401 || (result?.Success == false && result.Code == 401)) throw new HttpException(result?.Msg ?? "登录已失效,请重新登录。") { IsOnline = false }; // 统一错误响应即使使用 HTTP 4xx/5xx,也保留业务码、数据和追踪 ID。 if (result?.Success == false) throw new ApiException("请求失败:" + (result.Msg ?? $"业务码 {result.Code}。"), result.Code, result.Data, result.TraceId); if (status < 200 || status >= 300) throw new HttpException($"请求失败(HTTP {status}):{result?.Msg}"); if (result?.Success == null) throw new HttpException("服务器响应不是有效的 Result 格式。", parseError); if (result.Data == null || result.Data.Type == JTokenType.Null) return default; try { return result.Data.ToObject(); } catch (JsonException ex) { throw new HttpException("服务器响应 data 格式无效。", ex); } } public T GetRequired(string route, MethodType method, object body = null, IDictionary query = null, bool authenticated = true, int timeoutMilliseconds = 30000) where T : class => Send(route, method, body, query, authenticated, timeoutMilliseconds) ?? throw new HttpException("服务器响应缺少 data。"); public async Task GetRequiredAsync(string route, MethodType method, object body = null, IDictionary query = null, bool authenticated = true, int timeoutMilliseconds = 30000) where T : class => await SendAsync(route, method, body, query, authenticated, timeoutMilliseconds).ConfigureAwait(false) ?? throw new HttpException("服务器响应缺少 data。"); }