2026-04-16 14:57:19 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.Networking;
|
|
|
|
|
|
using Debug = UnityEngine.Debug;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加 JSON 解析支持
|
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Kill.Network
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// HTTP请求管理器 - 统一管理所有HTTP请求
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class HttpRequestManager : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
public static HttpRequestManager Instance { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 基础URL
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string BaseUrl { get; set; } = "";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 全局请求头
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Dictionary<string, string> _globalHeaders = new Dictionary<string, string>();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 请求拦截器
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public event Func<NetworkRequestTask, Task> OnRequestIntercept;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 响应拦截器
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public event Func<string, Task<string>> OnResponseIntercept;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 默认请求配置
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private HttpRequestConfig _defaultConfig = new HttpRequestConfig();
|
|
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Instance == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Instance = this;
|
|
|
|
|
|
DontDestroyOnLoad(gameObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Destroy(gameObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#region 全局配置
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 设置基础URL
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void SetBaseUrl(string baseUrl)
|
|
|
|
|
|
{
|
|
|
|
|
|
BaseUrl = baseUrl.TrimEnd('/');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 添加全局请求头
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void AddGlobalHeader(string key, string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
_globalHeaders[key] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 移除全局请求头
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void RemoveGlobalHeader(string key)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_globalHeaders.ContainsKey(key))
|
|
|
|
|
|
{
|
|
|
|
|
|
_globalHeaders.Remove(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 设置默认配置
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void SetDefaultConfig(HttpRequestConfig config)
|
|
|
|
|
|
{
|
|
|
|
|
|
_defaultConfig = config;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 清除所有全局请求头
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void ClearGlobalHeaders()
|
|
|
|
|
|
{
|
|
|
|
|
|
_globalHeaders.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region GET请求
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送GET请求
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<T>> Get<T>(string url, Dictionary<string, string> queryParams = null, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
string fullUrl = BuildUrl(url, queryParams);
|
|
|
|
|
|
return await SendRequest<T>(fullUrl, HttpMethod.GET, null, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送GET请求(返回原始字符串)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<string>> Get(string url, Dictionary<string, string> queryParams = null, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
string fullUrl = BuildUrl(url, queryParams);
|
|
|
|
|
|
return await SendRequest<string>(fullUrl, HttpMethod.GET, null, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region POST请求
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送POST请求(JSON数据)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<T>> Post<T>(string url, object data, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
2026-06-08 08:55:10 +08:00
|
|
|
|
string jsonData = data != null ? JsonConvert.SerializeObject(data) : "";
|
2026-06-10 15:04:14 +08:00
|
|
|
|
Debug.Log("发送数据:" + jsonData);
|
2026-04-16 14:57:19 +08:00
|
|
|
|
return await SendRequest<T>(url, HttpMethod.POST, jsonData, config, "application/json");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送POST请求(表单数据)
|
|
|
|
|
|
/// </summary>
|
2026-06-08 08:55:10 +08:00
|
|
|
|
public async Task<HttpResponse<T>> PostForm<T>(string url, WWWForm formData, HttpRequestConfig config = null)
|
2026-04-16 14:57:19 +08:00
|
|
|
|
{
|
2026-06-08 08:55:10 +08:00
|
|
|
|
return await SendRequestWithForm<T>(url, HttpMethod.POST, formData, config);
|
2026-04-16 14:57:19 +08:00
|
|
|
|
}
|
2026-06-10 15:04:14 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送POST请求(IMultipartFormSection)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<T>> PostFormData<T>(string url, List<IMultipartFormSection> formData, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await SendRequestWithFormData<T>(url, HttpMethod.POST, formData, config);
|
|
|
|
|
|
}
|
2026-04-16 14:57:19 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送POST请求(原始字符串)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<string>> Post(string url, object data, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await Post<string>(url, data, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region PUT请求
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送PUT请求(JSON数据)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<T>> Put<T>(string url, object data, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
string jsonData = data != null ? JsonUtility.ToJson(data) : "";
|
2026-05-11 08:39:33 +08:00
|
|
|
|
Debug.Log(jsonData);
|
2026-04-16 14:57:19 +08:00
|
|
|
|
return await SendRequest<T>(url, HttpMethod.PUT, jsonData, config, "application/json");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送PUT请求(表单数据)
|
|
|
|
|
|
/// </summary>
|
2026-06-08 08:55:10 +08:00
|
|
|
|
public async Task<HttpResponse<T>> PutForm<T>(string url, WWWForm form, HttpRequestConfig config = null)
|
2026-04-16 14:57:19 +08:00
|
|
|
|
{
|
|
|
|
|
|
return await SendRequestWithForm<T>(url, HttpMethod.PUT, form, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送PUT请求(原始字符串)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<string>> Put(string url, object data, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await Put<string>(url, data, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region DELETE请求
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送DELETE请求
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<T>> Delete<T>(string url, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await SendRequest<T>(url, HttpMethod.DELETE, null, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送DELETE请求(带body)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<T>> Delete<T>(string url, object data, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
string jsonData = data != null ? JsonUtility.ToJson(data) : "";
|
|
|
|
|
|
return await SendRequest<T>(url, HttpMethod.DELETE, jsonData, config, "application/json");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 文件下载
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 下载文件
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<HttpResponse<string>> DownloadFile(string url, string savePath, Action<DownloadProgress> onProgress = null, HttpRequestConfig config = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mergedConfig = MergeConfig(config);
|
|
|
|
|
|
var stopwatch = Stopwatch.StartNew();
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
|
|
|
|
|
{
|
|
|
|
|
|
request.timeout = mergedConfig.Timeout;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
// 添加请求头
|
|
|
|
|
|
AddHeadersToRequest(request, mergedConfig.Headers);
|
|
|
|
|
|
|
|
|
|
|
|
// 发送请求
|
|
|
|
|
|
var operation = request.SendWebRequest();
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
// 监控进度
|
|
|
|
|
|
while (!operation.isDone)
|
|
|
|
|
|
{
|
|
|
|
|
|
long totalBytes = 0;
|
|
|
|
|
|
string contentLength = request.GetResponseHeader("Content-Length");
|
|
|
|
|
|
if (!string.IsNullOrEmpty(contentLength) && long.TryParse(contentLength, out long parsedLength))
|
|
|
|
|
|
{
|
|
|
|
|
|
totalBytes = parsedLength;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onProgress?.Invoke(new DownloadProgress
|
|
|
|
|
|
{
|
|
|
|
|
|
DownloadedBytes = (long)(request.downloadedBytes),
|
|
|
|
|
|
TotalBytes = totalBytes
|
|
|
|
|
|
});
|
|
|
|
|
|
await Task.Yield();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopwatch.Stop();
|
|
|
|
|
|
|
|
|
|
|
|
if (request.result == UnityWebRequest.Result.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 确保目录存在
|
|
|
|
|
|
string directory = Path.GetDirectoryName(savePath);
|
|
|
|
|
|
if (!Directory.Exists(directory))
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(directory);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存文件
|
|
|
|
|
|
File.WriteAllBytes(savePath, request.downloadHandler.data);
|
|
|
|
|
|
|
|
|
|
|
|
return new HttpResponse<string>
|
|
|
|
|
|
{
|
|
|
|
|
|
IsSuccess = true,
|
|
|
|
|
|
StatusCode = request.responseCode,
|
|
|
|
|
|
Data = savePath,
|
|
|
|
|
|
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
return HttpResponse<string>.Failure(request.error, request.responseCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
stopwatch.Stop();
|
|
|
|
|
|
return HttpResponse<string>.Failure($"下载异常: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 核心请求方法
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送HTTP请求(核心方法)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<HttpResponse<T>> SendRequest<T>(string url, HttpMethod method, string data, HttpRequestConfig config, string contentType = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mergedConfig = MergeConfig(config);
|
|
|
|
|
|
string fullUrl = GetFullUrl(url);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查网络状态
|
|
|
|
|
|
if (NetworkStateManager.Instance != null && !NetworkStateManager.Instance.IsConnected)
|
|
|
|
|
|
{
|
|
|
|
|
|
return HttpResponse<T>.Failure("网络未连接");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int retryCount = 0;
|
|
|
|
|
|
while (retryCount <= mergedConfig.MaxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
var stopwatch = Stopwatch.StartNew();
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using (UnityWebRequest request = CreateWebRequest(fullUrl, method, data, contentType))
|
|
|
|
|
|
{
|
|
|
|
|
|
request.timeout = mergedConfig.Timeout;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
// 添加请求头
|
|
|
|
|
|
AddHeadersToRequest(request, mergedConfig.Headers);
|
|
|
|
|
|
|
|
|
|
|
|
// 发送请求
|
|
|
|
|
|
var operation = request.SendWebRequest();
|
|
|
|
|
|
while (!operation.isDone)
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.Yield();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopwatch.Stop();
|
2026-06-08 08:55:10 +08:00
|
|
|
|
Debug.Log(request.uri.ToString());
|
2026-04-16 14:57:19 +08:00
|
|
|
|
// 处理响应
|
|
|
|
|
|
if (request.result == UnityWebRequest.Result.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
string responseText = request.downloadHandler.text;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
Debug.Log("response:" + responseText);
|
2026-04-16 14:57:19 +08:00
|
|
|
|
// 响应拦截器
|
|
|
|
|
|
if (OnResponseIntercept != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
responseText = await OnResponseIntercept.Invoke(responseText);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
T resultData = default;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(responseText) && typeof(T) != typeof(string))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 使用 Newtonsoft.Json 解析,支持嵌套对象
|
|
|
|
|
|
resultData = JsonConvert.DeserializeObject<T>(responseText);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"Newtonsoft JSON解析失败: {ex.Message},尝试使用 Unity 的 JsonUtility");
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 降级使用 Unity 的 JsonUtility
|
|
|
|
|
|
resultData = JsonUtility.FromJson<T>(responseText);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex2)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"JsonUtility 解析也失败: {ex2.Message}");
|
|
|
|
|
|
resultData = (T)(object)responseText;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (typeof(T) == typeof(string))
|
|
|
|
|
|
{
|
|
|
|
|
|
resultData = (T)(object)responseText;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new HttpResponse<T>
|
|
|
|
|
|
{
|
|
|
|
|
|
IsSuccess = true,
|
|
|
|
|
|
StatusCode = request.responseCode,
|
|
|
|
|
|
Data = resultData,
|
|
|
|
|
|
RawResponse = responseText,
|
|
|
|
|
|
Headers = GetResponseHeaders(request),
|
|
|
|
|
|
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 请求失败,检查是否需要重试
|
|
|
|
|
|
if (retryCount < mergedConfig.MaxRetries && ShouldRetry(request.responseCode))
|
|
|
|
|
|
{
|
|
|
|
|
|
retryCount++;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
int delay = mergedConfig.UseExponentialBackoff
|
2026-04-16 14:57:19 +08:00
|
|
|
|
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
|
|
|
|
|
|
: mergedConfig.RetryDelayMs;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
Debug.Log($"请求失败,{delay}ms后重试({retryCount}/{mergedConfig.MaxRetries}): {fullUrl}");
|
|
|
|
|
|
await Task.Delay(delay);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure(request.error, request.responseCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
stopwatch.Stop();
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
if (retryCount < mergedConfig.MaxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
retryCount++;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
int delay = mergedConfig.UseExponentialBackoff
|
2026-04-16 14:57:19 +08:00
|
|
|
|
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
|
|
|
|
|
|
: mergedConfig.RetryDelayMs;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
Debug.Log($"请求异常,{delay}ms后重试({retryCount}/{mergedConfig.MaxRetries}): {ex.Message}");
|
|
|
|
|
|
await Task.Delay(delay);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure($"请求异常: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure("请求失败,已重试所有次数");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送带表单的请求
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<HttpResponse<T>> SendRequestWithForm<T>(string url, HttpMethod method, WWWForm form, HttpRequestConfig config)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mergedConfig = MergeConfig(config);
|
|
|
|
|
|
string fullUrl = GetFullUrl(url);
|
|
|
|
|
|
|
|
|
|
|
|
int retryCount = 0;
|
|
|
|
|
|
while (retryCount <= mergedConfig.MaxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
var stopwatch = Stopwatch.StartNew();
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using (UnityWebRequest request = UnityWebRequest.Post(fullUrl, form))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (method == HttpMethod.PUT)
|
|
|
|
|
|
{
|
|
|
|
|
|
request.method = "PUT";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
request.timeout = mergedConfig.Timeout;
|
|
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
// 添加请求头(不要手动设置 Content-Type,WWWForm 会自动设置)
|
|
|
|
|
|
AddHeadersToRequest(request, mergedConfig.Headers);
|
2026-04-16 14:57:19 +08:00
|
|
|
|
var operation = request.SendWebRequest();
|
|
|
|
|
|
while (!operation.isDone)
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.Yield();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopwatch.Stop();
|
|
|
|
|
|
|
|
|
|
|
|
if (request.result == UnityWebRequest.Result.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
string responseText = request.downloadHandler.text;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
T resultData = default;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(responseText) && typeof(T) != typeof(string))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
resultData = JsonUtility.FromJson<T>(responseText);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
resultData = (T)(object)responseText;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (typeof(T) == typeof(string))
|
|
|
|
|
|
{
|
|
|
|
|
|
resultData = (T)(object)responseText;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new HttpResponse<T>
|
|
|
|
|
|
{
|
|
|
|
|
|
IsSuccess = true,
|
|
|
|
|
|
StatusCode = request.responseCode,
|
|
|
|
|
|
Data = resultData,
|
|
|
|
|
|
RawResponse = responseText,
|
|
|
|
|
|
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
if (retryCount < mergedConfig.MaxRetries && ShouldRetry(request.responseCode))
|
|
|
|
|
|
{
|
|
|
|
|
|
retryCount++;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
int delay = mergedConfig.UseExponentialBackoff
|
2026-04-16 14:57:19 +08:00
|
|
|
|
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
|
|
|
|
|
|
: mergedConfig.RetryDelayMs;
|
|
|
|
|
|
await Task.Delay(delay);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure(request.error, request.responseCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
stopwatch.Stop();
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
if (retryCount < mergedConfig.MaxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
retryCount++;
|
2026-06-10 15:04:14 +08:00
|
|
|
|
int delay = mergedConfig.UseExponentialBackoff
|
2026-04-16 14:57:19 +08:00
|
|
|
|
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
|
|
|
|
|
|
: mergedConfig.RetryDelayMs;
|
|
|
|
|
|
await Task.Delay(delay);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure($"请求异常: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure("请求失败,已重试所有次数");
|
|
|
|
|
|
}
|
2026-06-10 15:04:14 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送带表单的请求
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<HttpResponse<T>> SendRequestWithFormData<T>(string url, HttpMethod method, List<IMultipartFormSection> formData, HttpRequestConfig config)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mergedConfig = MergeConfig(config);
|
|
|
|
|
|
string fullUrl = GetFullUrl(url);
|
2026-04-16 14:57:19 +08:00
|
|
|
|
|
2026-06-10 15:04:14 +08:00
|
|
|
|
int retryCount = 0;
|
|
|
|
|
|
while (retryCount <= mergedConfig.MaxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
var stopwatch = Stopwatch.StartNew();
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using (UnityWebRequest request = UnityWebRequest.Post(fullUrl, formData))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (method == HttpMethod.PUT)
|
|
|
|
|
|
{
|
|
|
|
|
|
request.method = "PUT";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
request.timeout = mergedConfig.Timeout;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加请求头(不要手动设置 Content-Type,WWWForm 会自动设置)
|
|
|
|
|
|
AddHeadersToRequest(request, mergedConfig.Headers);
|
|
|
|
|
|
var operation = request.SendWebRequest();
|
|
|
|
|
|
while (!operation.isDone)
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.Yield();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopwatch.Stop();
|
|
|
|
|
|
|
|
|
|
|
|
if (request.result == UnityWebRequest.Result.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
string responseText = request.downloadHandler.text;
|
|
|
|
|
|
|
|
|
|
|
|
T resultData = default;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(responseText) && typeof(T) != typeof(string))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
resultData = JsonUtility.FromJson<T>(responseText);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
resultData = (T)(object)responseText;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (typeof(T) == typeof(string))
|
|
|
|
|
|
{
|
|
|
|
|
|
resultData = (T)(object)responseText;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new HttpResponse<T>
|
|
|
|
|
|
{
|
|
|
|
|
|
IsSuccess = true,
|
|
|
|
|
|
StatusCode = request.responseCode,
|
|
|
|
|
|
Data = resultData,
|
|
|
|
|
|
RawResponse = responseText,
|
|
|
|
|
|
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
if (retryCount < mergedConfig.MaxRetries && ShouldRetry(request.responseCode))
|
|
|
|
|
|
{
|
|
|
|
|
|
retryCount++;
|
|
|
|
|
|
int delay = mergedConfig.UseExponentialBackoff
|
|
|
|
|
|
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
|
|
|
|
|
|
: mergedConfig.RetryDelayMs;
|
|
|
|
|
|
await Task.Delay(delay);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure(request.error, request.responseCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
stopwatch.Stop();
|
|
|
|
|
|
|
|
|
|
|
|
if (retryCount < mergedConfig.MaxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
retryCount++;
|
|
|
|
|
|
int delay = mergedConfig.UseExponentialBackoff
|
|
|
|
|
|
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
|
|
|
|
|
|
: mergedConfig.RetryDelayMs;
|
|
|
|
|
|
await Task.Delay(delay);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure($"请求异常: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse<T>.Failure("请求失败,已重试所有次数");
|
|
|
|
|
|
}
|
2026-04-16 14:57:19 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 辅助方法
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建WebRequest
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private UnityWebRequest CreateWebRequest(string url, HttpMethod method, string data, string contentType)
|
|
|
|
|
|
{
|
|
|
|
|
|
UnityWebRequest request;
|
|
|
|
|
|
|
|
|
|
|
|
switch (method)
|
|
|
|
|
|
{
|
|
|
|
|
|
case HttpMethod.GET:
|
|
|
|
|
|
request = UnityWebRequest.Get(url);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case HttpMethod.POST:
|
|
|
|
|
|
request = UnityWebRequest.Post(url, data, contentType ?? "application/json");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case HttpMethod.PUT:
|
|
|
|
|
|
byte[] bodyRaw = Encoding.UTF8.GetBytes(data ?? "");
|
|
|
|
|
|
request = UnityWebRequest.Put(url, bodyRaw);
|
|
|
|
|
|
request.SetRequestHeader("Content-Type", contentType ?? "application/json");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case HttpMethod.DELETE:
|
|
|
|
|
|
request = UnityWebRequest.Delete(url);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(data))
|
|
|
|
|
|
{
|
|
|
|
|
|
byte[] deleteBody = Encoding.UTF8.GetBytes(data);
|
|
|
|
|
|
request.uploadHandler = new UploadHandlerRaw(deleteBody);
|
|
|
|
|
|
request.SetRequestHeader("Content-Type", contentType ?? "application/json");
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
request = UnityWebRequest.Get(url);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置下载处理器
|
|
|
|
|
|
if (request.downloadHandler == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
request.downloadHandler = new DownloadHandlerBuffer();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return request;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 添加请求头
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void AddHeadersToRequest(UnityWebRequest request, Dictionary<string, string> customHeaders)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 添加全局请求头
|
|
|
|
|
|
foreach (var header in _globalHeaders)
|
|
|
|
|
|
{
|
|
|
|
|
|
request.SetRequestHeader(header.Key, header.Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加自定义请求头
|
|
|
|
|
|
if (customHeaders != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var header in customHeaders)
|
|
|
|
|
|
{
|
|
|
|
|
|
request.SetRequestHeader(header.Key, header.Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取响应头
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Dictionary<string, string> GetResponseHeaders(UnityWebRequest request)
|
|
|
|
|
|
{
|
|
|
|
|
|
var headers = new Dictionary<string, string>();
|
|
|
|
|
|
if (request.GetResponseHeaders() != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var header in request.GetResponseHeaders())
|
|
|
|
|
|
{
|
|
|
|
|
|
headers[header.Key] = header.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return headers;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 合并配置
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private HttpRequestConfig MergeConfig(HttpRequestConfig customConfig)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (customConfig == null) return _defaultConfig;
|
|
|
|
|
|
|
|
|
|
|
|
return new HttpRequestConfig
|
|
|
|
|
|
{
|
|
|
|
|
|
Timeout = customConfig.Timeout > 0 ? customConfig.Timeout : _defaultConfig.Timeout,
|
|
|
|
|
|
MaxRetries = customConfig.MaxRetries >= 0 ? customConfig.MaxRetries : _defaultConfig.MaxRetries,
|
|
|
|
|
|
RetryDelayMs = customConfig.RetryDelayMs > 0 ? customConfig.RetryDelayMs : _defaultConfig.RetryDelayMs,
|
|
|
|
|
|
UseExponentialBackoff = customConfig.UseExponentialBackoff,
|
|
|
|
|
|
Headers = customConfig.Headers ?? _defaultConfig.Headers,
|
|
|
|
|
|
Priority = customConfig.Priority,
|
|
|
|
|
|
EnableCache = customConfig.EnableCache,
|
|
|
|
|
|
CacheDuration = customConfig.CacheDuration > 0 ? customConfig.CacheDuration : _defaultConfig.CacheDuration
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 构建完整URL
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private string GetFullUrl(string url)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(BaseUrl) || url.StartsWith("http://") || url.StartsWith("https://"))
|
|
|
|
|
|
{
|
|
|
|
|
|
return url;
|
|
|
|
|
|
}
|
|
|
|
|
|
return $"{BaseUrl}/{url.TrimStart('/')}";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 构建带查询参数的URL
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private string BuildUrl(string url, Dictionary<string, string> queryParams)
|
|
|
|
|
|
{
|
|
|
|
|
|
string fullUrl = GetFullUrl(url);
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
if (queryParams == null || queryParams.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return fullUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder(fullUrl);
|
|
|
|
|
|
sb.Append(fullUrl.Contains("?") ? "&" : "?");
|
2026-06-10 15:04:14 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
bool first = true;
|
|
|
|
|
|
foreach (var param in queryParams)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!first) sb.Append("&");
|
|
|
|
|
|
sb.Append($"{UnityWebRequest.EscapeURL(param.Key)}={UnityWebRequest.EscapeURL(param.Value)}");
|
|
|
|
|
|
first = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 判断是否应该重试
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private bool ShouldRetry(long statusCode)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 5xx 服务器错误或 0(网络错误) 时重试
|
|
|
|
|
|
return statusCode >= 500 || statusCode == 0 || statusCode == 408;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|