977 lines
34 KiB
C#
977 lines
34 KiB
C#
using System.Net;
|
||
using System.Net.Http.Headers;
|
||
using System.Net.Http.Json;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
using WorkCameraExport.Models;
|
||
using WorkCameraExport.Services.Interfaces;
|
||
|
||
namespace WorkCameraExport.Services
|
||
{
|
||
/// <summary>
|
||
/// API 服务类 - 处理与后端服务器的所有 HTTP 通信
|
||
/// 实现 Token 认证、请求重试、Token 自动刷新
|
||
/// </summary>
|
||
public class ApiService : IApiService
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ILogService? _logService;
|
||
private string _baseUrl = "";
|
||
private string _token = "";
|
||
private DateTime _tokenExpireTime = DateTime.MinValue;
|
||
private string _refreshToken = "";
|
||
private bool _disposed;
|
||
|
||
// 配置常量
|
||
private const int MaxRetryCount = 3;
|
||
private const int RetryDelayMs = 1000;
|
||
private const int TokenRefreshThresholdMinutes = 5;
|
||
|
||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||
{
|
||
PropertyNameCaseInsensitive = true,
|
||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||
Converters = { new FlexibleDateTimeConverter() }
|
||
};
|
||
|
||
public ApiService(ILogService? logService = null)
|
||
{
|
||
_logService = logService;
|
||
_httpClient = new HttpClient
|
||
{
|
||
Timeout = TimeSpan.FromSeconds(30)
|
||
};
|
||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("WorkCameraExport/2.0 (Windows; .NET)");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用于测试的构造函数,允许注入 HttpClient
|
||
/// </summary>
|
||
internal ApiService(HttpClient httpClient, ILogService? logService = null)
|
||
{
|
||
_httpClient = httpClient;
|
||
_logService = logService;
|
||
}
|
||
|
||
#region 配置方法
|
||
|
||
/// <summary>
|
||
/// 设置服务器地址
|
||
/// </summary>
|
||
public void SetBaseUrl(string baseUrl)
|
||
{
|
||
_baseUrl = baseUrl.TrimEnd('/');
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置认证 Token
|
||
/// </summary>
|
||
public void SetToken(string token)
|
||
{
|
||
_token = token;
|
||
if (!string.IsNullOrEmpty(token))
|
||
{
|
||
_httpClient.DefaultRequestHeaders.Authorization =
|
||
new AuthenticationHeaderValue("Bearer", token);
|
||
// 默认设置 Token 有效期为 2 小时
|
||
_tokenExpireTime = DateTime.Now.AddHours(2);
|
||
}
|
||
else
|
||
{
|
||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||
_tokenExpireTime = DateTime.MinValue;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 Token 及其过期时间
|
||
/// </summary>
|
||
public void SetToken(string token, DateTime expireTime)
|
||
{
|
||
_token = token;
|
||
_tokenExpireTime = expireTime;
|
||
if (!string.IsNullOrEmpty(token))
|
||
{
|
||
_httpClient.DefaultRequestHeaders.Authorization =
|
||
new AuthenticationHeaderValue("Bearer", token);
|
||
}
|
||
else
|
||
{
|
||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置刷新 Token
|
||
/// </summary>
|
||
public void SetRefreshToken(string refreshToken)
|
||
{
|
||
_refreshToken = refreshToken;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前 Token
|
||
/// </summary>
|
||
public string GetToken() => _token;
|
||
|
||
/// <summary>
|
||
/// 获取 Token 过期时间
|
||
/// </summary>
|
||
public DateTime GetTokenExpireTime() => _tokenExpireTime;
|
||
|
||
/// <summary>
|
||
/// 检查是否已登录
|
||
/// </summary>
|
||
public bool IsLoggedIn => !string.IsNullOrEmpty(_token);
|
||
|
||
/// <summary>
|
||
/// 检查 Token 是否即将过期
|
||
/// </summary>
|
||
public bool IsTokenExpiringSoon =>
|
||
_tokenExpireTime != DateTime.MinValue &&
|
||
_tokenExpireTime.Subtract(DateTime.Now).TotalMinutes < TokenRefreshThresholdMinutes;
|
||
|
||
#endregion
|
||
|
||
#region 认证相关
|
||
|
||
/// <summary>
|
||
/// 获取验证码
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, CaptchaResponse? Data)> GetCaptchaAsync()
|
||
{
|
||
try
|
||
{
|
||
var response = await ExecuteWithRetryAsync(() => GetAsync<CaptchaResponse>("/captchaImage"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
return (true, "获取成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "获取验证码失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"获取验证码异常: {ex.Message}", ex);
|
||
return (false, $"获取验证码异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用户登录
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, LoginResponse? Data)> LoginAsync(
|
||
string username, string password, string code = "", string uuid = "")
|
||
{
|
||
try
|
||
{
|
||
var request = new LoginRequest
|
||
{
|
||
Username = username,
|
||
Password = password,
|
||
Code = code,
|
||
Uuid = uuid
|
||
};
|
||
|
||
var url = $"{_baseUrl}/login";
|
||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
|
||
var httpResponse = await _httpClient.PostAsync(url, content);
|
||
var responseContent = await httpResponse.Content.ReadAsStringAsync();
|
||
|
||
var response = JsonSerializer.Deserialize<ApiResult<string>>(responseContent, JsonOptions);
|
||
|
||
if (response != null && response.IsSuccess && !string.IsNullOrEmpty(response.Data))
|
||
{
|
||
var token = response.Data;
|
||
SetToken(token);
|
||
_logService?.Info($"用户 {username} 登录成功");
|
||
return (true, "登录成功", new LoginResponse { Token = token });
|
||
}
|
||
|
||
_logService?.Warn($"用户 {username} 登录失败: {response?.Msg}");
|
||
return (false, response?.Msg ?? "登录失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"登录异常: {ex.Message}", ex);
|
||
return (false, $"登录异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新 Token
|
||
/// </summary>
|
||
public async Task<bool> RefreshTokenAsync()
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrEmpty(_refreshToken))
|
||
{
|
||
_logService?.Warn("无刷新 Token,无法刷新");
|
||
return false;
|
||
}
|
||
|
||
var response = await PostAsync<TokenRefreshResponse>("/refreshToken", new { refreshToken = _refreshToken });
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
SetToken(response.Data.Token, response.Data.ExpireTime);
|
||
if (!string.IsNullOrEmpty(response.Data.RefreshToken))
|
||
{
|
||
_refreshToken = response.Data.RefreshToken;
|
||
}
|
||
_logService?.Info("Token 刷新成功");
|
||
return true;
|
||
}
|
||
|
||
_logService?.Warn($"Token 刷新失败: {response.Msg}");
|
||
return false;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"Token 刷新异常: {ex.Message}", ex);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 退出登录
|
||
/// </summary>
|
||
public void Logout()
|
||
{
|
||
_logService?.Info("用户退出登录");
|
||
SetToken("");
|
||
_refreshToken = "";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前用户信息(用于验证 Token 有效性)
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, UserInfo? Data)> GetCurrentUserAsync()
|
||
{
|
||
try
|
||
{
|
||
var response = await GetAsync<UserProfileResponse>("/system/user/profile");
|
||
|
||
if (response.IsSuccess && response.Data?.User != null)
|
||
{
|
||
return (true, "获取成功", new UserInfo
|
||
{
|
||
UserId = response.Data.User.UserId,
|
||
UserName = response.Data.User.UserName ?? "",
|
||
NickName = response.Data.User.NickName ?? ""
|
||
});
|
||
}
|
||
|
||
return (false, response.Msg ?? "获取用户信息失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"获取用户信息异常: {ex.Message}", ex);
|
||
return (false, $"获取用户信息异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 统计接口
|
||
|
||
/// <summary>
|
||
/// 获取统计信息
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, StatisticsDto? Data)> GetStatisticsAsync()
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var response = await ExecuteWithRetryAsync(() => GetAsync<StatisticsDto>("/api/workrecord/statistics"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
return (true, "获取成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "获取统计信息失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"获取统计信息异常: {ex.Message}", ex);
|
||
return (false, $"获取统计信息异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 工作记录 CRUD
|
||
|
||
/// <summary>
|
||
/// 查询工作记录列表
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, PagedData<WorkRecordDto>? Data)> GetWorkRecordsAsync(
|
||
WorkRecordQueryDto query)
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var queryParams = BuildQueryString(query);
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
GetPagedAsync<WorkRecordDto>($"/business/CamWorkrecord/list?{queryParams}"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
// 调试日志:记录反序列化后的 Workers 数据
|
||
foreach (var record in response.Data.Result.Take(3)) // 只记录前3条
|
||
{
|
||
var workersInfo = record.Workers != null
|
||
? $"Workers数量={record.Workers.Count}, 名称=[{string.Join(", ", record.Workers.Select(w => w.WorkerName ?? "null"))}]"
|
||
: "Workers=null";
|
||
_logService?.Info($"[反序列化] 记录ID={record.Id}, {workersInfo}");
|
||
}
|
||
|
||
return (true, "查询成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "查询失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"查询工作记录异常: {ex.Message}", ex);
|
||
return (false, $"查询异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取单条工作记录
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, WorkRecordDto? Data)> GetWorkRecordAsync(int id)
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
GetAsync<WorkRecordDto>($"/business/CamWorkrecord/{id}"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
return (true, "获取成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "获取失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"获取工作记录异常: {ex.Message}", ex);
|
||
return (false, $"获取异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增工作记录
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, int? Id)> AddWorkRecordAsync(WorkRecordSaveDto record)
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
PostAsync<int>("/business/CamWorkrecord", record));
|
||
|
||
if (response.IsSuccess)
|
||
{
|
||
_logService?.Info($"新增工作记录成功,ID: {response.Data}");
|
||
return (true, "新增成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "新增失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"新增工作记录异常: {ex.Message}", ex);
|
||
return (false, $"新增异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新工作记录
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message)> UpdateWorkRecordAsync(WorkRecordSaveDto record)
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
PutAsync<object>("/business/CamWorkrecord", record));
|
||
|
||
if (response.IsSuccess)
|
||
{
|
||
_logService?.Info($"更新工作记录成功,ID: {record.Id}");
|
||
return (true, "更新成功");
|
||
}
|
||
|
||
return (false, response.Msg ?? "更新失败");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"更新工作记录异常: {ex.Message}", ex);
|
||
return (false, $"更新异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除工作记录
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message)> DeleteWorkRecordAsync(int id)
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
DeleteAsync<object>($"/business/CamWorkrecord/{id}"));
|
||
|
||
if (response.IsSuccess)
|
||
{
|
||
_logService?.Info($"删除工作记录成功,ID: {id}");
|
||
return (true, "删除成功");
|
||
}
|
||
|
||
return (false, response.Msg ?? "删除失败");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"删除工作记录异常: {ex.Message}", ex);
|
||
return (false, $"删除异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 月报表接口
|
||
|
||
/// <summary>
|
||
/// 获取月报表数据
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, List<MonthlyReportDto>? Data)> GetMonthlyReportAsync(
|
||
MonthReportQueryDto query)
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var queryParams = BuildQueryString(query);
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
GetAsync<List<MonthlyReportDto>>($"/business/CamWorkers/list?{queryParams}"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
return (true, "查询成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "查询失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"获取月报表异常: {ex.Message}", ex);
|
||
return (false, $"查询异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定月份的所有图片
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, List<MonthImageDto>? Data)> GetMonthImagesAsync(
|
||
string yearMonth)
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
GetAsync<List<MonthImageDto>>($"/api/workrecord/monthImages?yearMonth={Uri.EscapeDataString(yearMonth)}"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
return (true, "获取成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "获取失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"获取月份图片异常: {ex.Message}", ex);
|
||
return (false, $"获取异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 导出查询相关
|
||
|
||
/// <summary>
|
||
/// 查询工作记录(导出用)
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, PagedData<WorkRecordExportDto>? Data)>
|
||
GetExportListAsync(WorkRecordExportQuery query)
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var queryParams = BuildQueryString(query);
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
GetPagedAsync<WorkRecordExportDto>($"/api/workrecord/export/list?{queryParams}"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
return (true, "查询成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "查询失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"查询导出列表异常: {ex.Message}", ex);
|
||
return (false, $"查询异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取导出记录总数
|
||
/// </summary>
|
||
public async Task<(bool Success, int TotalCount, int TotalImages)>
|
||
GetExportCountAsync(WorkRecordExportQuery query)
|
||
{
|
||
var result = await GetExportListAsync(new WorkRecordExportQuery
|
||
{
|
||
PageNum = 1,
|
||
PageSize = 1,
|
||
StartDate = query.StartDate,
|
||
EndDate = query.EndDate,
|
||
DeptName = query.DeptName,
|
||
WorkerName = query.WorkerName,
|
||
Content = query.Content
|
||
});
|
||
|
||
if (result.Success && result.Data != null)
|
||
{
|
||
return (true, result.Data.TotalNum, 0);
|
||
}
|
||
|
||
return (false, 0, 0);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 迁移相关
|
||
|
||
/// <summary>
|
||
/// 查询待迁移记录
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, PagedData<MigrationRecordDto>? Data)>
|
||
GetMigrationListAsync(MigrationQuery query)
|
||
{
|
||
try
|
||
{
|
||
_logService?.Info($"[迁移] 开始查询迁移列表, 页码={query.PageNum}, 状态={query.Status}");
|
||
await EnsureTokenValidAsync();
|
||
var queryParams = BuildQueryString(query);
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
GetPagedAsync<MigrationRecordDto>($"/api/workrecord/migration/list?{queryParams}"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
_logService?.Info($"[迁移] 查询成功, 共 {response.Data.TotalNum} 条记录");
|
||
return (true, "查询成功", response.Data);
|
||
}
|
||
|
||
_logService?.Warn($"[迁移] 查询失败: {response.Msg}, Code={response.Code}");
|
||
return (false, response.Msg ?? "查询失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"[迁移] 查询迁移列表异常: {ex.Message}", ex);
|
||
return (false, $"查询异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新迁移后的 URL
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message)> UpdateMigrationUrlsAsync(
|
||
MigrationUpdateRequest request)
|
||
{
|
||
try
|
||
{
|
||
_logService?.Info($"[迁移] 开始更新URL, 记录ID={request.RecordId}, 图片数={request.ImageUrls?.Count ?? 0}");
|
||
await EnsureTokenValidAsync();
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
PostAsync<object>("/api/workrecord/migration/update", request));
|
||
|
||
if (response.IsSuccess)
|
||
{
|
||
_logService?.Info($"[迁移] 更新URL成功, 记录ID={request.RecordId}");
|
||
return (true, "更新成功");
|
||
}
|
||
|
||
_logService?.Warn($"[迁移] 更新URL失败: {response.Msg}, Code={response.Code}");
|
||
return (false, response.Msg ?? "更新失败");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"[迁移] 更新迁移URL异常: {ex.Message}", ex);
|
||
return (false, $"更新异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取 COS 临时密钥
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, CosTempCredentials? Data)>
|
||
GetTempCredentialsAsync()
|
||
{
|
||
try
|
||
{
|
||
await EnsureTokenValidAsync();
|
||
var response = await ExecuteWithRetryAsync(() =>
|
||
GetAsync<CosTempCredentials>("/api/cos/getTempCredentials"));
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
return (true, "获取成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "获取临时密钥失败", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"获取临时密钥异常: {ex.Message}", ex);
|
||
return (false, $"获取临时密钥异常: {ex.Message}", null);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 图片下载
|
||
|
||
/// <summary>
|
||
/// 下载图片
|
||
/// </summary>
|
||
public async Task<(bool Success, byte[]? Data, string Message)> DownloadImageAsync(
|
||
string imageUrl, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
var fullUrl = imageUrl.StartsWith("http") ? imageUrl : $"{_baseUrl}{imageUrl}";
|
||
|
||
using var response = await _httpClient.GetAsync(fullUrl, cancellationToken);
|
||
|
||
if (response.IsSuccessStatusCode)
|
||
{
|
||
var data = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||
return (true, data, "下载成功");
|
||
}
|
||
|
||
return (false, null, $"下载失败: {response.StatusCode}");
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
return (false, null, "下载已取消");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService?.Error($"下载图片异常: {ex.Message}", ex);
|
||
return (false, null, $"下载异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region HTTP 基础方法(带重试和 Token 刷新)
|
||
|
||
/// <summary>
|
||
/// 确保 Token 有效,如果即将过期则自动刷新
|
||
/// </summary>
|
||
private async Task EnsureTokenValidAsync()
|
||
{
|
||
if (IsTokenExpiringSoon && !string.IsNullOrEmpty(_refreshToken))
|
||
{
|
||
_logService?.Info("Token 即将过期,尝试自动刷新");
|
||
await RefreshTokenAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 带重试机制的请求执行
|
||
/// </summary>
|
||
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> action, int maxRetries = MaxRetryCount)
|
||
where T : class
|
||
{
|
||
Exception? lastException = null;
|
||
|
||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||
{
|
||
try
|
||
{
|
||
return await action();
|
||
}
|
||
catch (HttpRequestException ex) when (IsRetryableException(ex))
|
||
{
|
||
lastException = ex;
|
||
_logService?.Warn($"请求失败,第 {attempt}/{maxRetries} 次重试: {ex.Message}");
|
||
|
||
if (attempt < maxRetries)
|
||
{
|
||
await Task.Delay(RetryDelayMs * attempt);
|
||
}
|
||
}
|
||
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
||
{
|
||
lastException = ex;
|
||
_logService?.Warn($"请求超时,第 {attempt}/{maxRetries} 次重试");
|
||
|
||
if (attempt < maxRetries)
|
||
{
|
||
await Task.Delay(RetryDelayMs * attempt);
|
||
}
|
||
}
|
||
}
|
||
|
||
_logService?.Error($"请求失败,已达最大重试次数: {lastException?.Message}", lastException);
|
||
throw lastException ?? new Exception("请求失败");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断异常是否可重试
|
||
/// </summary>
|
||
private static bool IsRetryableException(HttpRequestException ex)
|
||
{
|
||
// 网络错误、服务器错误(5xx)可重试
|
||
if (ex.StatusCode.HasValue)
|
||
{
|
||
var statusCode = (int)ex.StatusCode.Value;
|
||
return statusCode >= 500 && statusCode < 600;
|
||
}
|
||
// 无状态码的网络错误也可重试
|
||
return true;
|
||
}
|
||
|
||
private async Task<ApiResult<T>> GetAsync<T>(string endpoint)
|
||
{
|
||
var url = $"{_baseUrl}{endpoint}";
|
||
var response = await _httpClient.GetAsync(url);
|
||
|
||
// 处理 401 未授权
|
||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||
{
|
||
return new ApiResult<T> { Code = 401, Msg = "登录已失效,请重新登录" };
|
||
}
|
||
|
||
var content = await response.Content.ReadAsStringAsync();
|
||
|
||
return JsonSerializer.Deserialize<ApiResult<T>>(content, JsonOptions)
|
||
?? new ApiResult<T> { Code = 500, Msg = "解析响应失败" };
|
||
}
|
||
|
||
private async Task<PagedResult<T>> GetPagedAsync<T>(string endpoint)
|
||
{
|
||
var url = $"{_baseUrl}{endpoint}";
|
||
var response = await _httpClient.GetAsync(url);
|
||
|
||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||
{
|
||
return new PagedResult<T> { Code = 401, Msg = "登录已失效,请重新登录" };
|
||
}
|
||
|
||
var content = await response.Content.ReadAsStringAsync();
|
||
|
||
// 调试日志:记录原始 JSON 响应(完整内容)
|
||
_logService?.Info($"[API响应] {endpoint} 原始JSON: {content}");
|
||
|
||
return JsonSerializer.Deserialize<PagedResult<T>>(content, JsonOptions)
|
||
?? new PagedResult<T> { Code = 500, Msg = "解析响应失败" };
|
||
}
|
||
|
||
private async Task<ApiResult<T>> PostAsync<T>(string endpoint, object data)
|
||
{
|
||
var url = $"{_baseUrl}{endpoint}";
|
||
var json = JsonSerializer.Serialize(data, JsonOptions);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
|
||
var response = await _httpClient.PostAsync(url, content);
|
||
|
||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||
{
|
||
return new ApiResult<T> { Code = 401, Msg = "登录已失效,请重新登录" };
|
||
}
|
||
|
||
var responseContent = await response.Content.ReadAsStringAsync();
|
||
|
||
return JsonSerializer.Deserialize<ApiResult<T>>(responseContent, JsonOptions)
|
||
?? new ApiResult<T> { Code = 500, Msg = "解析响应失败" };
|
||
}
|
||
|
||
private async Task<ApiResult<T>> PutAsync<T>(string endpoint, object data)
|
||
{
|
||
var url = $"{_baseUrl}{endpoint}";
|
||
var json = JsonSerializer.Serialize(data, JsonOptions);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
|
||
var response = await _httpClient.PutAsync(url, content);
|
||
|
||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||
{
|
||
return new ApiResult<T> { Code = 401, Msg = "登录已失效,请重新登录" };
|
||
}
|
||
|
||
var responseContent = await response.Content.ReadAsStringAsync();
|
||
|
||
return JsonSerializer.Deserialize<ApiResult<T>>(responseContent, JsonOptions)
|
||
?? new ApiResult<T> { Code = 500, Msg = "解析响应失败" };
|
||
}
|
||
|
||
private async Task<ApiResult<T>> DeleteAsync<T>(string endpoint)
|
||
{
|
||
var url = $"{_baseUrl}{endpoint}";
|
||
var response = await _httpClient.DeleteAsync(url);
|
||
|
||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||
{
|
||
return new ApiResult<T> { Code = 401, Msg = "登录已失效,请重新登录" };
|
||
}
|
||
|
||
var responseContent = await response.Content.ReadAsStringAsync();
|
||
|
||
return JsonSerializer.Deserialize<ApiResult<T>>(responseContent, JsonOptions)
|
||
?? new ApiResult<T> { Code = 500, Msg = "解析响应失败" };
|
||
}
|
||
|
||
private static string BuildQueryString(object obj)
|
||
{
|
||
var properties = obj.GetType().GetProperties();
|
||
var queryParams = new List<string>();
|
||
|
||
foreach (var prop in properties)
|
||
{
|
||
var value = prop.GetValue(obj);
|
||
if (value == null) continue;
|
||
|
||
string stringValue;
|
||
if (value is DateTime dt)
|
||
{
|
||
stringValue = dt.ToString("yyyy-MM-dd");
|
||
}
|
||
else
|
||
{
|
||
stringValue = value.ToString() ?? "";
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(stringValue))
|
||
{
|
||
var name = char.ToLower(prop.Name[0]) + prop.Name[1..];
|
||
queryParams.Add($"{name}={Uri.EscapeDataString(stringValue)}");
|
||
}
|
||
}
|
||
|
||
return string.Join("&", queryParams);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region IDisposable
|
||
|
||
public void Dispose()
|
||
{
|
||
Dispose(true);
|
||
GC.SuppressFinalize(this);
|
||
}
|
||
|
||
protected virtual void Dispose(bool disposing)
|
||
{
|
||
if (!_disposed)
|
||
{
|
||
if (disposing)
|
||
{
|
||
_httpClient.Dispose();
|
||
}
|
||
_disposed = true;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
/// <summary>
|
||
/// Token 刷新响应
|
||
/// </summary>
|
||
public class TokenRefreshResponse
|
||
{
|
||
public string Token { get; set; } = "";
|
||
public string RefreshToken { get; set; } = "";
|
||
public DateTime ExpireTime { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 灵活的日期时间转换器,支持多种日期格式
|
||
/// </summary>
|
||
public class FlexibleDateTimeConverter : JsonConverter<DateTime?>
|
||
{
|
||
private static readonly string[] DateFormats = new[]
|
||
{
|
||
"yyyy-MM-dd HH:mm:ss",
|
||
"yyyy-MM-ddTHH:mm:ss",
|
||
"yyyy-MM-ddTHH:mm:ss.fff",
|
||
"yyyy-MM-ddTHH:mm:ss.fffffffZ",
|
||
"yyyy-MM-ddTHH:mm:ssZ",
|
||
"yyyy-MM-dd",
|
||
"yyyy/MM/dd HH:mm:ss",
|
||
"yyyy/MM/dd"
|
||
};
|
||
|
||
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||
{
|
||
if (reader.TokenType == JsonTokenType.Null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
if (reader.TokenType == JsonTokenType.String)
|
||
{
|
||
var dateString = reader.GetString();
|
||
if (string.IsNullOrWhiteSpace(dateString))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
foreach (var format in DateFormats)
|
||
{
|
||
if (DateTime.TryParseExact(dateString, format,
|
||
System.Globalization.CultureInfo.InvariantCulture,
|
||
System.Globalization.DateTimeStyles.None, out var result))
|
||
{
|
||
return result;
|
||
}
|
||
}
|
||
|
||
if (DateTime.TryParse(dateString, out var defaultResult))
|
||
{
|
||
return defaultResult;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||
{
|
||
if (value.HasValue)
|
||
{
|
||
writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss"));
|
||
}
|
||
else
|
||
{
|
||
writer.WriteNullValue();
|
||
}
|
||
}
|
||
}
|
||
}
|