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 { /// /// API 服务类 - 处理与后端服务器的所有 HTTP 通信 /// 实现 Token 认证、请求重试、Token 自动刷新 /// 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)"); } /// /// 用于测试的构造函数,允许注入 HttpClient /// internal ApiService(HttpClient httpClient, ILogService? logService = null) { _httpClient = httpClient; _logService = logService; } #region 配置方法 /// /// 设置服务器地址 /// public void SetBaseUrl(string baseUrl) { _baseUrl = baseUrl.TrimEnd('/'); } /// /// 设置认证 Token /// 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; } } /// /// 设置 Token 及其过期时间 /// 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; } } /// /// 设置刷新 Token /// public void SetRefreshToken(string refreshToken) { _refreshToken = refreshToken; } /// /// 获取当前 Token /// public string GetToken() => _token; /// /// 获取 Token 过期时间 /// public DateTime GetTokenExpireTime() => _tokenExpireTime; /// /// 检查是否已登录 /// public bool IsLoggedIn => !string.IsNullOrEmpty(_token); /// /// 检查 Token 是否即将过期 /// public bool IsTokenExpiringSoon => _tokenExpireTime != DateTime.MinValue && _tokenExpireTime.Subtract(DateTime.Now).TotalMinutes < TokenRefreshThresholdMinutes; #endregion #region 认证相关 /// /// 获取验证码 /// public async Task<(bool Success, string Message, CaptchaResponse? Data)> GetCaptchaAsync() { try { var response = await ExecuteWithRetryAsync(() => GetAsync("/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); } } /// /// 用户登录 /// 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>(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); } } /// /// 刷新 Token /// public async Task RefreshTokenAsync() { try { if (string.IsNullOrEmpty(_refreshToken)) { _logService?.Warn("无刷新 Token,无法刷新"); return false; } var response = await PostAsync("/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; } } /// /// 退出登录 /// public void Logout() { _logService?.Info("用户退出登录"); SetToken(""); _refreshToken = ""; } /// /// 获取当前用户信息(用于验证 Token 有效性) /// public async Task<(bool Success, string Message, UserInfo? Data)> GetCurrentUserAsync() { try { var response = await GetAsync("/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 统计接口 /// /// 获取统计信息 /// public async Task<(bool Success, string Message, StatisticsDto? Data)> GetStatisticsAsync() { try { await EnsureTokenValidAsync(); var response = await ExecuteWithRetryAsync(() => GetAsync("/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 /// /// 查询工作记录列表 /// public async Task<(bool Success, string Message, PagedData? Data)> GetWorkRecordsAsync( WorkRecordQueryDto query) { try { await EnsureTokenValidAsync(); var queryParams = BuildQueryString(query); var response = await ExecuteWithRetryAsync(() => GetPagedAsync($"/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); } } /// /// 获取单条工作记录 /// public async Task<(bool Success, string Message, WorkRecordDto? Data)> GetWorkRecordAsync(int id) { try { await EnsureTokenValidAsync(); var response = await ExecuteWithRetryAsync(() => GetAsync($"/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); } } /// /// 新增工作记录 /// public async Task<(bool Success, string Message, int? Id)> AddWorkRecordAsync(WorkRecordSaveDto record) { try { await EnsureTokenValidAsync(); var response = await ExecuteWithRetryAsync(() => PostAsync("/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); } } /// /// 更新工作记录 /// public async Task<(bool Success, string Message)> UpdateWorkRecordAsync(WorkRecordSaveDto record) { try { await EnsureTokenValidAsync(); var response = await ExecuteWithRetryAsync(() => PutAsync("/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}"); } } /// /// 删除工作记录 /// public async Task<(bool Success, string Message)> DeleteWorkRecordAsync(int id) { try { await EnsureTokenValidAsync(); var response = await ExecuteWithRetryAsync(() => DeleteAsync($"/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 月报表接口 /// /// 获取月报表数据 /// public async Task<(bool Success, string Message, List? Data)> GetMonthlyReportAsync( MonthReportQueryDto query) { try { await EnsureTokenValidAsync(); var queryParams = BuildQueryString(query); var response = await ExecuteWithRetryAsync(() => GetAsync>($"/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); } } /// /// 获取指定月份的所有图片 /// public async Task<(bool Success, string Message, List? Data)> GetMonthImagesAsync( string yearMonth) { try { await EnsureTokenValidAsync(); var response = await ExecuteWithRetryAsync(() => GetAsync>($"/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 导出查询相关 /// /// 查询工作记录(导出用) /// public async Task<(bool Success, string Message, PagedData? Data)> GetExportListAsync(WorkRecordExportQuery query) { try { await EnsureTokenValidAsync(); var queryParams = BuildQueryString(query); var response = await ExecuteWithRetryAsync(() => GetPagedAsync($"/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); } } /// /// 获取导出记录总数 /// 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 迁移相关 /// /// 查询待迁移记录 /// public async Task<(bool Success, string Message, PagedData? Data)> GetMigrationListAsync(MigrationQuery query) { try { _logService?.Info($"[迁移] 开始查询迁移列表, 页码={query.PageNum}, 状态={query.Status}"); await EnsureTokenValidAsync(); var queryParams = BuildQueryString(query); var response = await ExecuteWithRetryAsync(() => GetPagedAsync($"/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); } } /// /// 更新迁移后的 URL /// 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("/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}"); } } /// /// 获取 COS 临时密钥 /// public async Task<(bool Success, string Message, CosTempCredentials? Data)> GetTempCredentialsAsync() { try { await EnsureTokenValidAsync(); var response = await ExecuteWithRetryAsync(() => GetAsync("/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 图片下载 /// /// 下载图片 /// 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 刷新) /// /// 确保 Token 有效,如果即将过期则自动刷新 /// private async Task EnsureTokenValidAsync() { if (IsTokenExpiringSoon && !string.IsNullOrEmpty(_refreshToken)) { _logService?.Info("Token 即将过期,尝试自动刷新"); await RefreshTokenAsync(); } } /// /// 带重试机制的请求执行 /// private async Task ExecuteWithRetryAsync(Func> 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("请求失败"); } /// /// 判断异常是否可重试 /// 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> GetAsync(string endpoint) { var url = $"{_baseUrl}{endpoint}"; var response = await _httpClient.GetAsync(url); // 处理 401 未授权 if (response.StatusCode == HttpStatusCode.Unauthorized) { return new ApiResult { Code = 401, Msg = "登录已失效,请重新登录" }; } var content = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize>(content, JsonOptions) ?? new ApiResult { Code = 500, Msg = "解析响应失败" }; } private async Task> GetPagedAsync(string endpoint) { var url = $"{_baseUrl}{endpoint}"; var response = await _httpClient.GetAsync(url); if (response.StatusCode == HttpStatusCode.Unauthorized) { return new PagedResult { Code = 401, Msg = "登录已失效,请重新登录" }; } var content = await response.Content.ReadAsStringAsync(); // 调试日志:记录原始 JSON 响应(完整内容) _logService?.Info($"[API响应] {endpoint} 原始JSON: {content}"); return JsonSerializer.Deserialize>(content, JsonOptions) ?? new PagedResult { Code = 500, Msg = "解析响应失败" }; } private async Task> PostAsync(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 { Code = 401, Msg = "登录已失效,请重新登录" }; } var responseContent = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize>(responseContent, JsonOptions) ?? new ApiResult { Code = 500, Msg = "解析响应失败" }; } private async Task> PutAsync(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 { Code = 401, Msg = "登录已失效,请重新登录" }; } var responseContent = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize>(responseContent, JsonOptions) ?? new ApiResult { Code = 500, Msg = "解析响应失败" }; } private async Task> DeleteAsync(string endpoint) { var url = $"{_baseUrl}{endpoint}"; var response = await _httpClient.DeleteAsync(url); if (response.StatusCode == HttpStatusCode.Unauthorized) { return new ApiResult { Code = 401, Msg = "登录已失效,请重新登录" }; } var responseContent = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize>(responseContent, JsonOptions) ?? new ApiResult { Code = 500, Msg = "解析响应失败" }; } private static string BuildQueryString(object obj) { var properties = obj.GetType().GetProperties(); var queryParams = new List(); 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 } /// /// Token 刷新响应 /// public class TokenRefreshResponse { public string Token { get; set; } = ""; public string RefreshToken { get; set; } = ""; public DateTime ExpireTime { get; set; } } /// /// 灵活的日期时间转换器,支持多种日期格式 /// public class FlexibleDateTimeConverter : JsonConverter { 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(); } } } }