具备界面基础功能
This commit is contained in:
299
SHH.CameraDashboard/Services/WebApis/WebApiService.cs
Normal file
299
SHH.CameraDashboard/Services/WebApis/WebApiService.cs
Normal file
@@ -0,0 +1,299 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局 Web API 服务
|
||||
/// 职责:
|
||||
/// 1. 统一管理 HttpClient 实例,避免资源耗尽。
|
||||
/// 2. 封装标准的 CRUD (GET, POST, PUT, DELETE, PATCH) 操作。
|
||||
/// 3. 提供详细的请求日志(耗时、状态码、请求/响应内容)。
|
||||
/// 4. 处理超时和异常,提供一致的错误处理机制。
|
||||
/// </summary>
|
||||
public class WebApiService
|
||||
{
|
||||
#region --- 静态成员 ---
|
||||
|
||||
/// <summary>
|
||||
/// WebAPI 设为单例
|
||||
/// </summary>
|
||||
public static WebApiService Instance { get; } = new WebApiService();
|
||||
|
||||
/// <summary>
|
||||
/// 静态 HttpClient 实例。
|
||||
/// 最佳实践:应用程序应共享单个 HttpClient 实例以避免套接字耗尽。
|
||||
/// </summary>
|
||||
private static readonly HttpClient _client;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 实例成员 ---
|
||||
|
||||
/// <summary>
|
||||
/// 当一个请求完成(无论成功或失败)时发生。
|
||||
/// 可用于记录详细的 API 调用日志。
|
||||
/// </summary>
|
||||
public event Action<LogWebApiModel>? OnRequestCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 日志内容的最大长度(5KB),防止过大的响应体撑爆内存。
|
||||
/// </summary>
|
||||
private const int MAX_LOG_LENGTH = 5120;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
private WebApiService()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数,用于初始化 HttpClient。
|
||||
/// </summary>
|
||||
static WebApiService()
|
||||
{
|
||||
// 配置 HttpClient 处理器
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
// 设置连接池寿命为 15 分钟。
|
||||
// 这有助于解决 DNS 变更后,HttpClient 仍使用旧 IP 的问题。
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
|
||||
};
|
||||
|
||||
_client = new HttpClient(handler)
|
||||
{
|
||||
// 设置默认请求超时时间为 15 秒。
|
||||
Timeout = TimeSpan.FromSeconds(15)
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 核心 CRUD 方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 GET 请求。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> GetAsync(string url, string moduleName = "System", CancellationToken token = default, bool isAutoPost = false)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "GET", null, moduleName, token, async () =>
|
||||
{
|
||||
return await _client.GetAsync(url, token);
|
||||
}, isAutoPost);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 POST 请求。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="jsonBody">请求体的 JSON 字符串。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> PostAsync(string url, string jsonBody, string moduleName = "System", CancellationToken token = default)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "POST", jsonBody, moduleName, token, async () =>
|
||||
{
|
||||
using var content = CreateJsonContent(jsonBody);
|
||||
return await _client.PostAsync(url, content, token);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 PUT 请求(通常用于替换整个资源)。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="jsonBody">请求体的 JSON 字符串。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> PutAsync(string url, string jsonBody, string moduleName = "System", CancellationToken token = default)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "PUT", jsonBody, moduleName, token, async () =>
|
||||
{
|
||||
using var content = CreateJsonContent(jsonBody);
|
||||
return await _client.PutAsync(url, content, token);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 PATCH 请求(通常用于更新资源的部分属性)。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="jsonBody">请求体的 JSON 字符串。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> PatchAsync(string url, string jsonBody, string moduleName = "System", CancellationToken token = default)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "PATCH", jsonBody, moduleName, token, async () =>
|
||||
{
|
||||
using var content = CreateJsonContent(jsonBody);
|
||||
return await _client.PatchAsync(url, content, token);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一个 DELETE 请求。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的完整 URL。</param>
|
||||
/// <param name="moduleName">发起请求的模块名称,用于日志分类。</param>
|
||||
/// <param name="token">用于取消操作的令牌。</param>
|
||||
/// <returns>成功时返回响应内容的字符串表示。</returns>
|
||||
public async Task<string> DeleteAsync(string url, string moduleName = "System", CancellationToken token = default)
|
||||
{
|
||||
return await ExecuteRequestAsync(url, "DELETE", null, moduleName, token, async () =>
|
||||
{
|
||||
return await _client.DeleteAsync(url, token);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 私有辅助方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个 UTF-8 编码的 JSON 内容对象。
|
||||
/// </summary>
|
||||
private StringContent CreateJsonContent(string json)
|
||||
{
|
||||
return new StringContent(json ?? "", Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心执行引擎,封装了所有 HTTP 请求的通用逻辑:计时、日志、异常处理。
|
||||
/// </summary>
|
||||
private async Task<string> ExecuteRequestAsync(
|
||||
string url,
|
||||
string method,
|
||||
string? requestData,
|
||||
string moduleName,
|
||||
CancellationToken token,
|
||||
Func<Task<HttpResponseMessage>> action,
|
||||
bool isAutoPost = false)
|
||||
{
|
||||
if (requestData == null)
|
||||
requestData = "(请求数据为空)";
|
||||
|
||||
var log = new LogWebApiModel
|
||||
{
|
||||
Time = DateTime.Now,
|
||||
Method = method,
|
||||
Url = url,
|
||||
RequestData = TruncateLog(requestData),
|
||||
AppModule = moduleName,
|
||||
IsAutoPost = isAutoPost,
|
||||
};
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// 执行具体的 HTTP 请求
|
||||
using var response = await action();
|
||||
|
||||
// 读取响应内容
|
||||
var responseString = await response.Content.ReadAsStringAsync(token);
|
||||
|
||||
// 更新日志
|
||||
log.StatusCode = ((int)response.StatusCode).ToString();
|
||||
log.ResponseData = TruncateLog(responseString);
|
||||
|
||||
// 如果 HTTP 状态码不是 2xx,则抛出异常
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return responseString;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 捕获所有异常,更新日志状态为 "Error"
|
||||
log.StatusCode = "Error";
|
||||
log.ResponseData = $"Client Exception: {ex.Message}";
|
||||
|
||||
// 重新抛出异常,让调用者知道请求失败
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 无论成功或失败,都记录耗时并触发日志事件
|
||||
sw.Stop();
|
||||
log.ElapsedMilliseconds = sw.ElapsedMilliseconds;
|
||||
|
||||
// 安全地触发事件,防止订阅者抛出异常影响主线程
|
||||
try
|
||||
{
|
||||
OnRequestCompleted?.Invoke(log);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
// 记录日志系统自身的错误,但不中断程序
|
||||
Debug.WriteLine($"Log System Error: {logEx.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 截断过长的日志内容,并尝试格式化 JSON 以便阅读。
|
||||
/// </summary>
|
||||
private string TruncateLog(string? content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content)) return string.Empty;
|
||||
|
||||
// 1. 【新增步骤】尝试将内容格式化为漂亮的 JSON
|
||||
// 注意:格式化会增加空格和换行,导致长度变长,这是符合预期的
|
||||
string finalContent = TryFormatJson(content);
|
||||
|
||||
// 2. 截断逻辑 (基于格式化后的长度)
|
||||
if (finalContent.Length > MAX_LOG_LENGTH)
|
||||
{
|
||||
// 保留前 N 个字符,并在末尾添加提示
|
||||
return finalContent.Substring(0, MAX_LOG_LENGTH) +
|
||||
$"\r\n\r\n... [日志过长已截断,显示前 {MAX_LOG_LENGTH} 字符]";
|
||||
}
|
||||
|
||||
return finalContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将字符串解析并格式化为缩进的 JSON。
|
||||
/// 如果解析失败(说明不是 JSON),则原样返回。
|
||||
/// </summary>
|
||||
private string TryFormatJson(string content)
|
||||
{
|
||||
// 简单的性能优化:如果不像 JSON(不以 { 或 [ 开头),直接跳过
|
||||
var trimmed = content.Trim();
|
||||
if ((trimmed.StartsWith("{") && trimmed.EndsWith("}")) ||
|
||||
(trimmed.StartsWith("[") && trimmed.EndsWith("]")))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用 JToken 解析,它可以处理 Object {} 和 Array []
|
||||
var token = JToken.Parse(content);
|
||||
|
||||
// 格式化输出 (Indented = 缩进模式)
|
||||
return token.ToString(Formatting.Indented);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 解析失败(可能只是普通的文本消息,或者 HTML),忽略异常,返回原样
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user