467 lines
15 KiB
C#
467 lines
15 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// API 服务类 - 处理与后端服务器的所有 HTTP 通信
|
||
/// </summary>
|
||
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)");
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
else
|
||
{
|
||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前 Token
|
||
/// </summary>
|
||
public string GetToken() => _token;
|
||
|
||
/// <summary>
|
||
/// 检查是否已登录
|
||
/// </summary>
|
||
public bool IsLoggedIn => !string.IsNullOrEmpty(_token);
|
||
|
||
#region 登录相关
|
||
|
||
/// <summary>
|
||
/// 获取验证码
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, CaptchaResponse? Data)> GetCaptchaAsync()
|
||
{
|
||
try
|
||
{
|
||
var response = await GetAsync<CaptchaResponse>("/captchaImage");
|
||
|
||
if (response.IsSuccess && response.Data != null)
|
||
{
|
||
return (true, "获取成功", response.Data);
|
||
}
|
||
|
||
return (false, response.Msg ?? "获取验证码失败", null);
|
||
}
|
||
catch (Exception 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
|
||
};
|
||
|
||
// 登录接口直接返回 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<ApiResult<string>>(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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 退出登录
|
||
/// </summary>
|
||
public void Logout()
|
||
{
|
||
SetToken("");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 导出查询相关
|
||
|
||
/// <summary>
|
||
/// 查询工作记录(导出用)
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, PagedData<WorkRecordExportDto>? Data)>
|
||
GetExportListAsync(WorkRecordExportQuery query)
|
||
{
|
||
try
|
||
{
|
||
var queryParams = BuildQueryString(query);
|
||
var response = await 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)
|
||
{
|
||
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
|
||
{
|
||
var queryParams = BuildQueryString(query);
|
||
var response = await GetPagedAsync<MigrationRecordDto>(
|
||
$"/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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新迁移后的 URL
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message)> UpdateMigrationUrlsAsync(
|
||
MigrationUpdateRequest request)
|
||
{
|
||
try
|
||
{
|
||
var response = await PostAsync<object>("/api/workrecord/migration/update", request);
|
||
|
||
if (response.IsSuccess)
|
||
{
|
||
return (true, "更新成功");
|
||
}
|
||
|
||
return (false, response.Msg ?? "更新失败");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return (false, $"更新异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取 COS 临时密钥
|
||
/// </summary>
|
||
public async Task<(bool Success, string Message, CosTempCredentials? Data)>
|
||
GetTempCredentialsAsync()
|
||
{
|
||
try
|
||
{
|
||
var response = await GetAsync<CosTempCredentials>("/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 图片下载
|
||
|
||
/// <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)
|
||
{
|
||
return (false, null, $"下载异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region HTTP 基础方法
|
||
|
||
private async Task<ApiResult<T>> GetAsync<T>(string endpoint)
|
||
{
|
||
var url = $"{_baseUrl}{endpoint}";
|
||
var response = await _httpClient.GetAsync(url);
|
||
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);
|
||
var content = await response.Content.ReadAsStringAsync();
|
||
|
||
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);
|
||
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>
|
||
/// 灵活的日期时间转换器,支持多种日期格式
|
||
/// </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();
|
||
}
|
||
}
|
||
}
|
||
}
|