Files
ichni_Official/Assets/Scripts/Online/Network/ApiClient.cs
2026-06-18 18:03:47 +08:00

167 lines
5.9 KiB
C#

using System;
using System.IO;
using System.Text;
using System.Threading;
using Best.HTTP;
using Cysharp.Threading.Tasks;
using IchniOnline.Online.Network.Models;
using UnityEngine;
namespace IchniOnline.Online.Network
{
/// <summary>
/// BestHTTP-based API client singleton for IchniOnline backend communication.
/// Pure HTTP layer — no business logic.
/// </summary>
public class IchniOnlineApiClient
{
private static IchniOnlineApiClient _instance;
public static IchniOnlineApiClient Instance => _instance ??= new IchniOnlineApiClient();
public string BaseUrl { get; set; } = "https://ichni.hoshino.fan";
public string JwtToken { get; set; }
private IchniOnlineApiClient() { }
public async UniTask<ApiResult<T>> GetAsync<T>(string endpoint, CancellationToken cancellationToken = default)
{
string url = BuildUrl(endpoint);
var request = new HTTPRequest(new Uri(url), HTTPMethods.Get);
AddAuthHeader(request);
try
{
var resp = await SendAsync(request, cancellationToken);
return ProcessResponse<T>(resp);
}
catch (OperationCanceledException)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Request canceled");
}
catch (Exception ex)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Network error", ex.Message);
}
}
public async UniTask<ApiResult<T>> PostAsync<T>(string endpoint, object body, CancellationToken cancellationToken = default)
{
string url = BuildUrl(endpoint);
var request = new HTTPRequest(new Uri(url), HTTPMethods.Post);
request.SetHeader("Content-Type", "application/json");
AddAuthHeader(request);
if (body != null)
{
string jsonBody = JsonUtility.ToJson(body);
request.UploadSettings.UploadStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonBody));
}
try
{
var resp = await SendAsync(request, cancellationToken);
return ProcessResponse<T>(resp);
}
catch (OperationCanceledException)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Request canceled");
}
catch (Exception ex)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Network error", ex.Message);
}
}
private string BuildUrl(string endpoint)
{
if (string.IsNullOrEmpty(BaseUrl))
throw new InvalidOperationException("BaseUrl is not configured.");
string baseUrl = BaseUrl.TrimEnd('/');
string path = endpoint.StartsWith("/") ? endpoint : $"/{endpoint}";
return baseUrl + path;
}
private void AddAuthHeader(HTTPRequest request)
{
if (!string.IsNullOrEmpty(JwtToken))
{
request.SetHeader("Authorization", $"Bearer {JwtToken}");
}
}
private UniTask<HTTPResponse> SendAsync(HTTPRequest request, CancellationToken cancellationToken = default)
{
var completionSource = new UniTaskCompletionSource<HTTPResponse>();
var cancellationRegistration = default(CancellationTokenRegistration);
if (cancellationToken.CanBeCanceled)
{
cancellationRegistration = cancellationToken.Register(() =>
{
request.Abort();
completionSource.TrySetCanceled();
});
}
request.Callback = (req, resp) =>
{
cancellationRegistration.Dispose();
switch (req.State)
{
case HTTPRequestStates.Finished:
completionSource.TrySetResult(resp);
break;
case HTTPRequestStates.Aborted:
completionSource.TrySetCanceled();
break;
default:
completionSource.TrySetException(req.Exception ?? new Exception($"HTTP request failed: {req.State}"));
break;
}
};
request.Send();
return completionSource.Task;
}
private ApiResult<T> ProcessResponse<T>(HTTPResponse resp)
{
string json = resp.DataAsText;
if (resp.StatusCode >= 200 && resp.StatusCode < 300)
{
if (string.IsNullOrEmpty(json))
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Empty response body");
}
var response = JsonUtility.FromJson(json, typeof(GlobalResponse<T>)) as GlobalResponse<T>;
if (response == null)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Failed to parse response JSON");
}
if (response.code == ResponseCode.Ok)
{
return ApiResult<T>.Ok(response.data);
}
return ApiResult<T>.Fail(response.code, response.message);
}
// Non-2xx: try to parse server error body
if (!string.IsNullOrEmpty(json))
{
var errorResponse = JsonUtility.FromJson(json, typeof(GlobalResponseBase)) as GlobalResponseBase;
if (errorResponse != null)
{
return ApiResult<T>.Fail(errorResponse.code, errorResponse.message);
}
}
return ApiResult<T>.Fail(ResponseCode.InternalServerError, $"HTTP error {resp.StatusCode}");
}
}
}