using Atomx.Common.Models; using Atomx.Utils.Json; using Microsoft.AspNetCore.Http; using System.Net; using System.Net.Http.Json; using System.Security.Claims; using System.Text; using System.Text.Json; namespace Atomx.Admin.Client.Services { public class HttpService { private readonly HttpClient _httpClient; private readonly IHttpContextAccessor? _httpContextAccessor; private readonly ILogger _logger; public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null, ILogger? logger = null) { _httpClient = httpClient; _httpContextAccessor = httpContextAccessor; _logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; } public async Task Get(string url) { using var request = new HttpRequestMessage(HttpMethod.Get, url); AttachCookieIfServer(request); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); return content.FromJson(); } else { await LogNonSuccessAsync(url, response); ThrowForStatus(response.StatusCode, url); throw new Exception($"Error: {response.StatusCode}"); } } public async Task Post(string url, object data) { var json = data.ToJson(); using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; AttachCookieIfServer(request); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); return responseContent.FromJson(); } else { await LogNonSuccessAsync(url, response); ThrowForStatus(response.StatusCode, url); throw new Exception($"Error: {response.StatusCode}"); } } public async Task>> GetPagingList(string url, object data, int page, int size = 20) { try { if (page < 1) { page = 1; } url = $"{url}?page={page}&size={size}"; var json = data.ToJson(); using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = new StringContent(json, Encoding.UTF8, "application/json") //Content = JsonContent.Create(data) }; AttachCookieIfServer(request); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); return content.FromJson>>(); } else { await LogNonSuccessAsync(url, response); // 明确在 401/403 场景抛出授权异常以便上层 UI/组件做特殊处理 ThrowForStatus(response.StatusCode, url); throw new Exception($"Error: {response.StatusCode}"); } } catch (HttpRequestException ex) { _logger.LogError(ex, "HttpRequestException while calling {Url}", url); Console.Error.WriteLine($"[{DateTime.UtcNow:o}] HttpRequestException Url:{url} Error:{ex.Message}"); throw new Exception($"api {url} service failure"); } } /// /// 如果在 Server 环境并且 IHttpContextAccessor 可用,则把浏览器请求的 Cookie 转发到后端请求中 /// private void AttachCookieIfServer(HttpRequestMessage request) { try { if (!OperatingSystem.IsBrowser()) { var ctx = _httpContextAccessor?.HttpContext; if (ctx != null && ctx.Request.Headers.TryGetValue("Cookie", out var cookie) && !string.IsNullOrEmpty(cookie)) { // 覆盖或添加 Cookie header if (request.Headers.Contains("Cookie")) { request.Headers.Remove("Cookie"); } request.Headers.Add("Cookie", (string)cookie); } } } catch { // 忽略任何转发异常,保持健壮性 } } private async Task LogNonSuccessAsync(string url, HttpResponseMessage response) { try { var status = response.StatusCode; var reason = response.ReasonPhrase; string userId = "unknown"; string ip = "unknown"; var ctx = _httpContextAccessor?.HttpContext; if (ctx != null) { userId = ctx.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? ctx.User?.FindFirst("sub")?.Value ?? "unknown"; if (ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff)) { ip = xff.ToString().Split(',')[0].Trim(); } else { ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown"; } } // 结构化日志记录 _logger.LogWarning("UserId:{UserId} Url:{Url} Ip:{Ip} HttpStatus:{StatusCode} Reason:{ReasonPhrase}", userId, url, ip, (int)status, reason); // 控制台输出一份,便于本地/容器查看 var consoleMsg = new { Timestamp = DateTime.UtcNow.ToString("o"), Level = "Warning", UserId = userId, Url = url, Ip = ip, Status = (int)status, Reason = reason }; Console.WriteLine(JsonSerializer.Serialize(consoleMsg)); } catch (Exception ex) { // 日志失败不能影响主流程 _logger.LogError(ex, "Failed to log non-success response for {Url}", url); } } private void ThrowForStatus(HttpStatusCode statusCode, string url) { if (statusCode == HttpStatusCode.Unauthorized || statusCode == HttpStatusCode.Forbidden) { // 抛出明确的授权异常,便于上层按需处理(例如提示登陆、重定向或显示权限不足) throw new UnauthorizedAccessException($"Error: {statusCode} when calling {url}"); } } } }