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; namespace WorkCameraExport.Services { /// /// API 服务类 - 处理与后端服务器的所有 HTTP 通信 /// public class ApiService : IDisposable { private readonly HttpClient _httpClient; private string _baseUrl = ""; private string _token = ""; private bool _disposed; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new FlexibleDateTimeConverter() } }; public ApiService() { _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; // 设置 User-Agent,便于服务器识别客户端 _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("WorkCameraExport/1.0 (Windows; .NET)"); } /// /// 设置服务器地址 /// 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); } else { _httpClient.DefaultRequestHeaders.Authorization = null; } } /// /// 获取当前 Token /// public string GetToken() => _token; /// /// 检查是否已登录 /// public bool IsLoggedIn => !string.IsNullOrEmpty(_token); #region 登录相关 /// /// 获取验证码 /// public async Task<(bool Success, string Message, CaptchaResponse? Data)> GetCaptchaAsync() { try { var response = await GetAsync("/captchaImage"); if (response.IsSuccess && response.Data != null) { return (true, "获取成功", response.Data); } return (false, response.Msg ?? "获取验证码失败", null); } catch (Exception 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 }; // 登录接口直接返回 Token 字符串,不是标准的 ApiResult 格式 var url = $"{_baseUrl}/login"; var json = System.Text.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 = System.Text.Json.JsonSerializer.Deserialize>(responseContent, JsonOptions); if (response != null && response.IsSuccess && !string.IsNullOrEmpty(response.Data)) { var token = response.Data; SetToken(token); return (true, "登录成功", new LoginResponse { Token = token }); } return (false, response?.Msg ?? "登录失败", null); } catch (Exception ex) { return (false, $"登录异常: {ex.Message}", null); } } /// /// 退出登录 /// public void Logout() { SetToken(""); } #endregion #region 导出查询相关 /// /// 查询工作记录(导出用) /// public async Task<(bool Success, string Message, PagedData? Data)> GetExportListAsync(WorkRecordExportQuery query) { try { var queryParams = BuildQueryString(query); var response = await GetPagedAsync( $"/api/workrecord/export/list?{queryParams}"); if (response.IsSuccess && response.Data != null) { return (true, "查询成功", response.Data); } return (false, response.Msg ?? "查询失败", null); } catch (Exception 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 { var queryParams = BuildQueryString(query); var response = await GetPagedAsync( $"/api/workrecord/migration/list?{queryParams}"); if (response.IsSuccess && response.Data != null) { return (true, "查询成功", response.Data); } return (false, response.Msg ?? "查询失败", null); } catch (Exception ex) { return (false, $"查询异常: {ex.Message}", null); } } /// /// 更新迁移后的 URL /// public async Task<(bool Success, string Message)> UpdateMigrationUrlsAsync( MigrationUpdateRequest request) { try { var response = await PostAsync("/api/workrecord/migration/update", request); if (response.IsSuccess) { return (true, "更新成功"); } return (false, response.Msg ?? "更新失败"); } catch (Exception ex) { return (false, $"更新异常: {ex.Message}"); } } /// /// 获取 COS 临时密钥 /// public async Task<(bool Success, string Message, CosTempCredentials? Data)> GetTempCredentialsAsync() { try { var response = await GetAsync("/api/cos/getTempCredentials"); if (response.IsSuccess && response.Data != null) { return (true, "获取成功", response.Data); } return (false, response.Msg ?? "获取临时密钥失败", null); } catch (Exception 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) { return (false, null, $"下载异常: {ex.Message}"); } } #endregion #region HTTP 基础方法 private async Task> GetAsync(string endpoint) { var url = $"{_baseUrl}{endpoint}"; var response = await _httpClient.GetAsync(url); 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); var content = await response.Content.ReadAsStringAsync(); 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); 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 } /// /// 灵活的日期时间转换器,支持多种日期格式 /// 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(); } } } }