21
This commit is contained in:
parent
d88f33e0a5
commit
cf67f4a541
|
|
@ -0,0 +1,149 @@
|
|||
using System.Net;
|
||||
using HtmlToPdfService.Client.Models.Responses;
|
||||
|
||||
namespace HtmlToPdfService.Client.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// HTML 转 PDF 客户端异常
|
||||
/// </summary>
|
||||
public class HtmlToPdfClientException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP 状态码
|
||||
/// </summary>
|
||||
public HttpStatusCode? StatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误详情(如果服务端返回 ProblemDetails)
|
||||
/// </summary>
|
||||
public ProblemDetails? ProblemDetails { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建异常实例
|
||||
/// </summary>
|
||||
public HtmlToPdfClientException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带内部异常的实例
|
||||
/// </summary>
|
||||
public HtmlToPdfClientException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带 HTTP 状态码的实例
|
||||
/// </summary>
|
||||
public HtmlToPdfClientException(string message, HttpStatusCode statusCode)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带 ProblemDetails 的实例
|
||||
/// </summary>
|
||||
public HtmlToPdfClientException(string message, HttpStatusCode statusCode, ProblemDetails? problemDetails)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
ProblemDetails = problemDetails;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带 ProblemDetails 和内部异常的实例
|
||||
/// </summary>
|
||||
public HtmlToPdfClientException(string message, HttpStatusCode statusCode, ProblemDetails? problemDetails, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
ProblemDetails = problemDetails;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务未找到异常
|
||||
/// </summary>
|
||||
public class TaskNotFoundException : HtmlToPdfClientException
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID
|
||||
/// </summary>
|
||||
public string TaskId { get; }
|
||||
|
||||
public TaskNotFoundException(string taskId)
|
||||
: base($"任务不存在: {taskId}", HttpStatusCode.NotFound)
|
||||
{
|
||||
TaskId = taskId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务冲突异常(如任务状态不允许操作)
|
||||
/// </summary>
|
||||
public class TaskConflictException : HtmlToPdfClientException
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID
|
||||
/// </summary>
|
||||
public string TaskId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前任务状态
|
||||
/// </summary>
|
||||
public string? CurrentStatus { get; }
|
||||
|
||||
public TaskConflictException(string taskId, string message, string? currentStatus = null)
|
||||
: base(message, HttpStatusCode.Conflict)
|
||||
{
|
||||
TaskId = taskId;
|
||||
CurrentStatus = currentStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务不可用异常
|
||||
/// </summary>
|
||||
public class ServiceUnavailableException : HtmlToPdfClientException
|
||||
{
|
||||
public ServiceUnavailableException(string message)
|
||||
: base(message, HttpStatusCode.ServiceUnavailable)
|
||||
{
|
||||
}
|
||||
|
||||
public ServiceUnavailableException(string message, ProblemDetails? problemDetails)
|
||||
: base(message, HttpStatusCode.ServiceUnavailable, problemDetails)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 认证失败异常
|
||||
/// </summary>
|
||||
public class AuthenticationException : HtmlToPdfClientException
|
||||
{
|
||||
public AuthenticationException(string message = "认证失败,请检查 API Key")
|
||||
: base(message, HttpStatusCode.Unauthorized)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 参数验证异常
|
||||
/// </summary>
|
||||
public class ValidationException : HtmlToPdfClientException
|
||||
{
|
||||
public ValidationException(string message)
|
||||
: base(message, HttpStatusCode.BadRequest)
|
||||
{
|
||||
}
|
||||
|
||||
public ValidationException(string message, ProblemDetails? problemDetails)
|
||||
: base(message, HttpStatusCode.BadRequest, problemDetails)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
|
||||
namespace HtmlToPdfService.Client.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// IServiceCollection 扩展方法
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 HTML 转 PDF 客户端服务
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configure">配置委托</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddHtmlToPdfClient(
|
||||
this IServiceCollection services,
|
||||
Action<HtmlToPdfClientOptions> configure)
|
||||
{
|
||||
services.Configure(configure);
|
||||
|
||||
// 获取配置以决定是否启用重试
|
||||
var options = new HtmlToPdfClientOptions();
|
||||
configure(options);
|
||||
|
||||
var httpClientBuilder = services.AddHttpClient<IHtmlToPdfClient, HtmlToPdfClient>();
|
||||
|
||||
if (options.EnableRetry)
|
||||
{
|
||||
httpClientBuilder.AddPolicyHandler((sp, _) =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<HtmlToPdfClientOptions>>().Value;
|
||||
return GetRetryPolicy(opts);
|
||||
});
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 HTML 转 PDF 客户端服务(从配置文件读取)
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置对象</param>
|
||||
/// <param name="sectionName">配置节名称,默认 "HtmlToPdfClient"</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddHtmlToPdfClient(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = HtmlToPdfClientOptions.SectionName)
|
||||
{
|
||||
var section = configuration.GetSection(sectionName);
|
||||
services.Configure<HtmlToPdfClientOptions>(section);
|
||||
|
||||
var options = section.Get<HtmlToPdfClientOptions>() ?? new HtmlToPdfClientOptions();
|
||||
|
||||
var httpClientBuilder = services.AddHttpClient<IHtmlToPdfClient, HtmlToPdfClient>();
|
||||
|
||||
if (options.EnableRetry)
|
||||
{
|
||||
httpClientBuilder.AddPolicyHandler((sp, _) =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<HtmlToPdfClientOptions>>().Value;
|
||||
return GetRetryPolicy(opts);
|
||||
});
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加命名的 HTML 转 PDF 客户端服务(支持多个不同配置的实例)
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="name">客户端名称</param>
|
||||
/// <param name="configure">配置委托</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddHtmlToPdfClient(
|
||||
this IServiceCollection services,
|
||||
string name,
|
||||
Action<HtmlToPdfClientOptions> configure)
|
||||
{
|
||||
// 为命名客户端注册选项
|
||||
services.Configure(name, configure);
|
||||
|
||||
var options = new HtmlToPdfClientOptions();
|
||||
configure(options);
|
||||
|
||||
var httpClientBuilder = services.AddHttpClient(name, (sp, client) =>
|
||||
{
|
||||
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<HtmlToPdfClientOptions>>();
|
||||
var opts = optionsMonitor.Get(name);
|
||||
ConfigureHttpClient(client, opts);
|
||||
});
|
||||
|
||||
if (options.EnableRetry)
|
||||
{
|
||||
httpClientBuilder.AddPolicyHandler((sp, _) =>
|
||||
{
|
||||
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<HtmlToPdfClientOptions>>();
|
||||
var opts = optionsMonitor.Get(name);
|
||||
return GetRetryPolicy(opts);
|
||||
});
|
||||
}
|
||||
|
||||
// 注册工厂
|
||||
services.AddSingleton<IHtmlToPdfClientFactory, HtmlToPdfClientFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void ConfigureHttpClient(HttpClient client, HtmlToPdfClientOptions options)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(options.BaseUrl))
|
||||
{
|
||||
client.BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/");
|
||||
}
|
||||
|
||||
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
|
||||
|
||||
if (!string.IsNullOrEmpty(options.ApiKey))
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("X-Api-Key", options.ApiKey);
|
||||
}
|
||||
|
||||
if (options.CustomHeaders != null)
|
||||
{
|
||||
foreach (var header in options.CustomHeaders)
|
||||
{
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(HtmlToPdfClientOptions options)
|
||||
{
|
||||
return HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
|
||||
.WaitAndRetryAsync(
|
||||
options.RetryCount,
|
||||
retryAttempt => TimeSpan.FromMilliseconds(
|
||||
options.RetryBaseDelayMs * Math.Pow(2, retryAttempt - 1)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 命名客户端工厂接口
|
||||
/// </summary>
|
||||
public interface IHtmlToPdfClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建指定名称的客户端实例
|
||||
/// </summary>
|
||||
/// <param name="name">客户端名称</param>
|
||||
/// <returns>客户端实例</returns>
|
||||
IHtmlToPdfClient CreateClient(string name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 命名客户端工厂实现
|
||||
/// </summary>
|
||||
internal class HtmlToPdfClientFactory : IHtmlToPdfClientFactory
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IOptionsMonitor<HtmlToPdfClientOptions> _optionsMonitor;
|
||||
|
||||
public HtmlToPdfClientFactory(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptionsMonitor<HtmlToPdfClientOptions> optionsMonitor)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_optionsMonitor = optionsMonitor;
|
||||
}
|
||||
|
||||
public IHtmlToPdfClient CreateClient(string name)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient(name);
|
||||
var options = _optionsMonitor.Get(name);
|
||||
return new HtmlToPdfClient(httpClient, Options.Create(options));
|
||||
}
|
||||
}
|
||||
|
||||
559
src/HtmlToPdfService.Client/HtmlToPdfClient.cs
Normal file
559
src/HtmlToPdfService.Client/HtmlToPdfClient.cs
Normal file
|
|
@ -0,0 +1,559 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using HtmlToPdfService.Client.Exceptions;
|
||||
using HtmlToPdfService.Client.Models;
|
||||
using HtmlToPdfService.Client.Models.Requests;
|
||||
using HtmlToPdfService.Client.Models.Responses;
|
||||
|
||||
namespace HtmlToPdfService.Client;
|
||||
|
||||
/// <summary>
|
||||
/// HTML 转 PDF/图片服务客户端实现
|
||||
/// </summary>
|
||||
public class HtmlToPdfClient : IHtmlToPdfClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly HtmlToPdfClientOptions _options;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
/// <summary>
|
||||
/// 创建客户端实例
|
||||
/// </summary>
|
||||
/// <param name="httpClient">HttpClient 实例</param>
|
||||
/// <param name="options">配置选项</param>
|
||||
public HtmlToPdfClient(HttpClient httpClient, IOptions<HtmlToPdfClientOptions> options)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
// 配置 HttpClient
|
||||
if (!string.IsNullOrEmpty(_options.BaseUrl))
|
||||
{
|
||||
_httpClient.BaseAddress = new Uri(_options.BaseUrl.TrimEnd('/') + "/");
|
||||
}
|
||||
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);
|
||||
|
||||
// 添加认证头
|
||||
if (!string.IsNullOrEmpty(_options.ApiKey))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Api-Key", _options.ApiKey);
|
||||
}
|
||||
|
||||
// 添加自定义头
|
||||
if (_options.CustomHeaders != null)
|
||||
{
|
||||
foreach (var header in _options.CustomHeaders)
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region 同步 PDF 转换
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> ConvertHtmlToPdfAsync(
|
||||
string html,
|
||||
PdfOptions? options = null,
|
||||
bool saveLocal = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
throw new ArgumentException("HTML 内容不能为空", nameof(html));
|
||||
|
||||
var request = new ConvertHtmlToPdfRequest
|
||||
{
|
||||
Html = html,
|
||||
Options = options,
|
||||
SaveLocal = saveLocal ? true : null
|
||||
};
|
||||
|
||||
return await PostForBytesAsync("api/pdf/convert/html", request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> ConvertUrlToPdfAsync(
|
||||
string url,
|
||||
PdfOptions? options = null,
|
||||
string? waitUntil = null,
|
||||
int? timeout = null,
|
||||
bool saveLocal = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
throw new ArgumentException("URL 不能为空", nameof(url));
|
||||
|
||||
var request = new ConvertUrlToPdfRequest
|
||||
{
|
||||
Url = url,
|
||||
Options = options,
|
||||
WaitUntil = waitUntil,
|
||||
Timeout = timeout,
|
||||
SaveLocal = saveLocal ? true : null
|
||||
};
|
||||
|
||||
return await PostForBytesAsync("api/pdf/convert/url", request, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 同步图片转换
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> ConvertHtmlToImageAsync(
|
||||
string html,
|
||||
ImageOptions? options = null,
|
||||
bool saveLocal = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
throw new ArgumentException("HTML 内容不能为空", nameof(html));
|
||||
|
||||
var request = new ConvertHtmlToImageRequest
|
||||
{
|
||||
Html = html,
|
||||
Options = options,
|
||||
SaveLocal = saveLocal ? true : null
|
||||
};
|
||||
|
||||
return await PostForBytesAsync("api/image/convert/html", request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> ConvertUrlToImageAsync(
|
||||
string url,
|
||||
ImageOptions? options = null,
|
||||
string? waitUntil = null,
|
||||
int? timeout = null,
|
||||
bool saveLocal = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
throw new ArgumentException("URL 不能为空", nameof(url));
|
||||
|
||||
var request = new ConvertUrlToImageRequest
|
||||
{
|
||||
Url = url,
|
||||
Options = options,
|
||||
WaitUntil = waitUntil,
|
||||
Timeout = timeout,
|
||||
SaveLocal = saveLocal ? true : null
|
||||
};
|
||||
|
||||
return await PostForBytesAsync("api/image/convert/url", request, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 异步任务 - PDF
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TaskSubmitResult> SubmitPdfTaskAsync(
|
||||
SourceInfo source,
|
||||
PdfOptions? options = null,
|
||||
string? waitUntil = null,
|
||||
int? timeout = null,
|
||||
CallbackInfo? callback = null,
|
||||
bool saveLocal = true,
|
||||
Dictionary<string, string>? metadata = null,
|
||||
string? idempotencyKey = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new PdfTaskRequest
|
||||
{
|
||||
Source = source,
|
||||
Options = options,
|
||||
WaitUntil = waitUntil,
|
||||
Timeout = timeout,
|
||||
Callback = callback,
|
||||
SaveLocal = saveLocal,
|
||||
Metadata = metadata
|
||||
};
|
||||
|
||||
Dictionary<string, string>? headers = null;
|
||||
if (!string.IsNullOrEmpty(idempotencyKey))
|
||||
{
|
||||
headers = new Dictionary<string, string> { ["Idempotency-Key"] = idempotencyKey };
|
||||
}
|
||||
|
||||
return await PostAsync<TaskSubmitResult>("api/tasks/pdf", request, headers, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 异步任务 - 图片
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TaskSubmitResult> SubmitImageTaskAsync(
|
||||
SourceInfo source,
|
||||
ImageOptions? options = null,
|
||||
string? waitUntil = null,
|
||||
int? timeout = null,
|
||||
int? delayAfterLoad = null,
|
||||
CallbackInfo? callback = null,
|
||||
bool saveLocal = true,
|
||||
Dictionary<string, string>? metadata = null,
|
||||
string? idempotencyKey = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new ImageTaskRequest
|
||||
{
|
||||
Source = source,
|
||||
Options = options,
|
||||
WaitUntil = waitUntil,
|
||||
Timeout = timeout,
|
||||
DelayAfterLoad = delayAfterLoad,
|
||||
Callback = callback,
|
||||
SaveLocal = saveLocal,
|
||||
Metadata = metadata
|
||||
};
|
||||
|
||||
Dictionary<string, string>? headers = null;
|
||||
if (!string.IsNullOrEmpty(idempotencyKey))
|
||||
{
|
||||
headers = new Dictionary<string, string> { ["Idempotency-Key"] = idempotencyKey };
|
||||
}
|
||||
|
||||
return await PostAsync<TaskSubmitResult>("api/tasks/image", request, headers, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 批量任务
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BatchSubmitResult> SubmitBatchAsync(
|
||||
IEnumerable<BatchTaskInput> tasks,
|
||||
CallbackInfo? callback = null,
|
||||
bool onEachComplete = false,
|
||||
bool onAllComplete = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new BatchTaskRequest
|
||||
{
|
||||
Tasks = tasks.Select(t => new BatchTaskItem
|
||||
{
|
||||
Type = t.Type,
|
||||
Source = t.Source,
|
||||
PdfOptions = t.PdfOptions,
|
||||
ImageOptions = t.ImageOptions,
|
||||
WaitUntil = t.WaitUntil,
|
||||
Timeout = t.Timeout,
|
||||
SaveLocal = t.SaveLocal,
|
||||
Metadata = t.Metadata
|
||||
}).ToList(),
|
||||
Callback = callback,
|
||||
OnEachComplete = onEachComplete,
|
||||
OnAllComplete = onAllComplete
|
||||
};
|
||||
|
||||
return await PostAsync<BatchSubmitResult>("api/tasks/batch", request, null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BatchStatusResult?> GetBatchStatusAsync(
|
||||
string batchId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(batchId))
|
||||
throw new ArgumentException("批量任务 ID 不能为空", nameof(batchId));
|
||||
|
||||
return await GetAsync<BatchStatusResult>($"api/tasks/batch/{batchId}", cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 任务管理
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TaskDetail?> GetTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId))
|
||||
throw new ArgumentException("任务 ID 不能为空", nameof(taskId));
|
||||
|
||||
return await GetAsync<TaskDetail>($"api/tasks/{taskId}", cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TaskStatusResult?> GetTaskStatusAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId))
|
||||
throw new ArgumentException("任务 ID 不能为空", nameof(taskId));
|
||||
|
||||
return await GetAsync<TaskStatusResult>($"api/tasks/{taskId}/status", cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TaskListResult> QueryTasksAsync(
|
||||
string? status = null,
|
||||
string? type = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var queryParams = new List<string>
|
||||
{
|
||||
$"page={page}",
|
||||
$"pageSize={pageSize}"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
queryParams.Add($"status={Uri.EscapeDataString(status)}");
|
||||
if (!string.IsNullOrEmpty(type))
|
||||
queryParams.Add($"type={Uri.EscapeDataString(type)}");
|
||||
if (startDate.HasValue)
|
||||
queryParams.Add($"startDate={startDate.Value:O}");
|
||||
if (endDate.HasValue)
|
||||
queryParams.Add($"endDate={endDate.Value:O}");
|
||||
|
||||
var url = $"api/tasks?{string.Join("&", queryParams)}";
|
||||
|
||||
return await GetAsync<TaskListResult>(url, cancellationToken) ?? new TaskListResult();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> DownloadTaskResultAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId))
|
||||
throw new ArgumentException("任务 ID 不能为空", nameof(taskId));
|
||||
|
||||
var response = await _httpClient.GetAsync($"api/tasks/{taskId}/download", cancellationToken);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new TaskNotFoundException(taskId);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
var problem = await TryReadProblemDetailsAsync(response);
|
||||
throw new TaskConflictException(taskId, problem?.Detail ?? "任务尚未完成");
|
||||
}
|
||||
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
return await response.Content.ReadAsByteArrayAsync(
|
||||
#if NET8_0_OR_GREATER
|
||||
cancellationToken
|
||||
#endif
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> CancelTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId))
|
||||
throw new ArgumentException("任务 ID 不能为空", nameof(taskId));
|
||||
|
||||
var response = await _httpClient.DeleteAsync($"api/tasks/{taskId}", cancellationToken);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new TaskNotFoundException(taskId);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
return false;
|
||||
|
||||
await EnsureSuccessAsync(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> RetryTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId))
|
||||
throw new ArgumentException("任务 ID 不能为空", nameof(taskId));
|
||||
|
||||
var response = await _httpClient.PostAsync(
|
||||
$"api/tasks/{taskId}/retry",
|
||||
new StringContent("{}", Encoding.UTF8, "application/json"),
|
||||
cancellationToken);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new TaskNotFoundException(taskId);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
return false;
|
||||
|
||||
await EnsureSuccessAsync(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 等待任务完成
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TaskDetail> WaitForTaskAsync(
|
||||
string taskId,
|
||||
int pollingInterval = 1000,
|
||||
int maxWaitTime = 300000,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId))
|
||||
throw new ArgumentException("任务 ID 不能为空", nameof(taskId));
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
var timeout = TimeSpan.FromMilliseconds(maxWaitTime);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var task = await GetTaskAsync(taskId, cancellationToken);
|
||||
|
||||
if (task == null)
|
||||
throw new TaskNotFoundException(taskId);
|
||||
|
||||
if (task.Status == "completed" || task.Status == "failed" || task.Status == "cancelled")
|
||||
return task;
|
||||
|
||||
if (DateTime.UtcNow - startTime > timeout)
|
||||
throw new TimeoutException($"等待任务 {taskId} 超时");
|
||||
|
||||
await Task.Delay(pollingInterval, cancellationToken);
|
||||
}
|
||||
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> WaitAndDownloadAsync(
|
||||
string taskId,
|
||||
int pollingInterval = 1000,
|
||||
int maxWaitTime = 300000,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var task = await WaitForTaskAsync(taskId, pollingInterval, maxWaitTime, cancellationToken);
|
||||
|
||||
if (task.Status == "failed")
|
||||
throw new HtmlToPdfClientException($"任务失败: {task.Error?.Message ?? "未知错误"}");
|
||||
|
||||
if (task.Status == "cancelled")
|
||||
throw new HtmlToPdfClientException("任务已取消");
|
||||
|
||||
return await DownloadTaskResultAsync(taskId, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有方法
|
||||
|
||||
private async Task<T> PostAsync<T>(
|
||||
string url,
|
||||
object request,
|
||||
Dictionary<string, string>? headers,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(request, _jsonOptions);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
{
|
||||
requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(requestMessage, cancellationToken);
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken);
|
||||
return result ?? throw new HtmlToPdfClientException("服务端返回空响应");
|
||||
}
|
||||
|
||||
private async Task<byte[]> PostForBytesAsync(
|
||||
string url,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(request, _jsonOptions);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync(url, content, cancellationToken);
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
return await response.Content.ReadAsByteArrayAsync(
|
||||
#if NET8_0_OR_GREATER
|
||||
cancellationToken
|
||||
#endif
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<T?> GetAsync<T>(string url, CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task EnsureSuccessAsync(HttpResponseMessage response)
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
|
||||
var problemDetails = await TryReadProblemDetailsAsync(response);
|
||||
var message = problemDetails?.Detail ?? problemDetails?.Title ?? response.ReasonPhrase ?? "请求失败";
|
||||
|
||||
throw response.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => new AuthenticationException(message),
|
||||
HttpStatusCode.BadRequest => new ValidationException(message, problemDetails),
|
||||
HttpStatusCode.ServiceUnavailable => new ServiceUnavailableException(message, problemDetails),
|
||||
_ => new HtmlToPdfClientException(message, response.StatusCode, problemDetails)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ProblemDetails?> TryReadProblemDetailsAsync(HttpResponseMessage response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync(
|
||||
#if NET8_0_OR_GREATER
|
||||
default
|
||||
#endif
|
||||
);
|
||||
|
||||
if (string.IsNullOrEmpty(content))
|
||||
return null;
|
||||
|
||||
return JsonSerializer.Deserialize<ProblemDetails>(content, _jsonOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
50
src/HtmlToPdfService.Client/HtmlToPdfClientOptions.cs
Normal file
50
src/HtmlToPdfService.Client/HtmlToPdfClientOptions.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
namespace HtmlToPdfService.Client;
|
||||
|
||||
/// <summary>
|
||||
/// HTML 转 PDF 客户端配置选项
|
||||
/// </summary>
|
||||
public class HtmlToPdfClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置节名称
|
||||
/// </summary>
|
||||
public const string SectionName = "HtmlToPdfClient";
|
||||
|
||||
/// <summary>
|
||||
/// 服务端基础 URL(必填)
|
||||
/// 例如: https://pdf-service.example.com
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// API Key(可选,用于认证)
|
||||
/// </summary>
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求超时时间(秒),默认 120 秒
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 120;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用重试机制,默认 false
|
||||
/// </summary>
|
||||
public bool EnableRetry { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 重试次数,默认 3 次
|
||||
/// </summary>
|
||||
public int RetryCount { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 重试基础延迟(毫秒),默认 500ms
|
||||
/// 使用指数退避策略
|
||||
/// </summary>
|
||||
public int RetryBaseDelayMs { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义请求头
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? CustomHeaders { get; set; }
|
||||
}
|
||||
|
||||
37
src/HtmlToPdfService.Client/HtmlToPdfService.Client.csproj
Normal file
37
src/HtmlToPdfService.Client/HtmlToPdfService.Client.csproj
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
|
||||
<!-- NuGet 包信息 -->
|
||||
<PackageId>HtmlToPdfService.Client</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Your Name</Authors>
|
||||
<Description>HTML 转 PDF/图片服务的 .NET 客户端库,支持依赖注入</Description>
|
||||
<PackageTags>html;pdf;image;screenshot;puppeteer;client;sdk</PackageTags>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- .NET Standard 2.0 需要额外引用 -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
324
src/HtmlToPdfService.Client/IHtmlToPdfClient.cs
Normal file
324
src/HtmlToPdfService.Client/IHtmlToPdfClient.cs
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
using HtmlToPdfService.Client.Models;
|
||||
using HtmlToPdfService.Client.Models.Requests;
|
||||
using HtmlToPdfService.Client.Models.Responses;
|
||||
|
||||
namespace HtmlToPdfService.Client;
|
||||
|
||||
/// <summary>
|
||||
/// HTML 转 PDF/图片服务客户端接口
|
||||
/// </summary>
|
||||
public interface IHtmlToPdfClient
|
||||
{
|
||||
#region 同步 PDF 转换
|
||||
|
||||
/// <summary>
|
||||
/// 将 HTML 内容转换为 PDF(同步)
|
||||
/// </summary>
|
||||
/// <param name="html">HTML 内容</param>
|
||||
/// <param name="options">PDF 选项</param>
|
||||
/// <param name="saveLocal">是否在服务端保存副本</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>PDF 文件字节数组</returns>
|
||||
Task<byte[]> ConvertHtmlToPdfAsync(
|
||||
string html,
|
||||
PdfOptions? options = null,
|
||||
bool saveLocal = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 将 URL 页面转换为 PDF(同步)
|
||||
/// </summary>
|
||||
/// <param name="url">页面 URL</param>
|
||||
/// <param name="options">PDF 选项</param>
|
||||
/// <param name="waitUntil">等待条件:load, domcontentloaded, networkidle0, networkidle2</param>
|
||||
/// <param name="timeout">超时时间(秒)</param>
|
||||
/// <param name="saveLocal">是否在服务端保存副本</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>PDF 文件字节数组</returns>
|
||||
Task<byte[]> ConvertUrlToPdfAsync(
|
||||
string url,
|
||||
PdfOptions? options = null,
|
||||
string? waitUntil = null,
|
||||
int? timeout = null,
|
||||
bool saveLocal = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 同步图片转换
|
||||
|
||||
/// <summary>
|
||||
/// 将 HTML 内容转换为图片(同步)
|
||||
/// </summary>
|
||||
/// <param name="html">HTML 内容</param>
|
||||
/// <param name="options">图片选项</param>
|
||||
/// <param name="saveLocal">是否在服务端保存副本</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>图片文件字节数组</returns>
|
||||
Task<byte[]> ConvertHtmlToImageAsync(
|
||||
string html,
|
||||
ImageOptions? options = null,
|
||||
bool saveLocal = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 将 URL 页面转换为图片(同步)
|
||||
/// </summary>
|
||||
/// <param name="url">页面 URL</param>
|
||||
/// <param name="options">图片选项</param>
|
||||
/// <param name="waitUntil">等待条件</param>
|
||||
/// <param name="timeout">超时时间(秒)</param>
|
||||
/// <param name="saveLocal">是否在服务端保存副本</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>图片文件字节数组</returns>
|
||||
Task<byte[]> ConvertUrlToImageAsync(
|
||||
string url,
|
||||
ImageOptions? options = null,
|
||||
string? waitUntil = null,
|
||||
int? timeout = null,
|
||||
bool saveLocal = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 异步任务 - PDF
|
||||
|
||||
/// <summary>
|
||||
/// 提交 PDF 转换任务(异步)
|
||||
/// </summary>
|
||||
/// <param name="source">源信息(HTML 或 URL)</param>
|
||||
/// <param name="options">PDF 选项</param>
|
||||
/// <param name="waitUntil">等待条件(URL 模式时有效)</param>
|
||||
/// <param name="timeout">超时时间(秒)</param>
|
||||
/// <param name="callback">回调配置</param>
|
||||
/// <param name="saveLocal">是否保存到服务端</param>
|
||||
/// <param name="metadata">自定义元数据</param>
|
||||
/// <param name="idempotencyKey">幂等键</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>任务提交结果</returns>
|
||||
Task<TaskSubmitResult> SubmitPdfTaskAsync(
|
||||
SourceInfo source,
|
||||
PdfOptions? options = null,
|
||||
string? waitUntil = null,
|
||||
int? timeout = null,
|
||||
CallbackInfo? callback = null,
|
||||
bool saveLocal = true,
|
||||
Dictionary<string, string>? metadata = null,
|
||||
string? idempotencyKey = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 异步任务 - 图片
|
||||
|
||||
/// <summary>
|
||||
/// 提交图片转换任务(异步)
|
||||
/// </summary>
|
||||
/// <param name="source">源信息(HTML 或 URL)</param>
|
||||
/// <param name="options">图片选项</param>
|
||||
/// <param name="waitUntil">等待条件(URL 模式时有效)</param>
|
||||
/// <param name="timeout">超时时间(秒)</param>
|
||||
/// <param name="delayAfterLoad">加载后延迟(毫秒)</param>
|
||||
/// <param name="callback">回调配置</param>
|
||||
/// <param name="saveLocal">是否保存到服务端</param>
|
||||
/// <param name="metadata">自定义元数据</param>
|
||||
/// <param name="idempotencyKey">幂等键</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>任务提交结果</returns>
|
||||
Task<TaskSubmitResult> SubmitImageTaskAsync(
|
||||
SourceInfo source,
|
||||
ImageOptions? options = null,
|
||||
string? waitUntil = null,
|
||||
int? timeout = null,
|
||||
int? delayAfterLoad = null,
|
||||
CallbackInfo? callback = null,
|
||||
bool saveLocal = true,
|
||||
Dictionary<string, string>? metadata = null,
|
||||
string? idempotencyKey = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 批量任务
|
||||
|
||||
/// <summary>
|
||||
/// 提交批量转换任务
|
||||
/// </summary>
|
||||
/// <param name="tasks">任务列表</param>
|
||||
/// <param name="callback">回调配置</param>
|
||||
/// <param name="onEachComplete">每个任务完成时回调</param>
|
||||
/// <param name="onAllComplete">全部完成时回调</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>批量任务提交结果</returns>
|
||||
Task<BatchSubmitResult> SubmitBatchAsync(
|
||||
IEnumerable<BatchTaskInput> tasks,
|
||||
CallbackInfo? callback = null,
|
||||
bool onEachComplete = false,
|
||||
bool onAllComplete = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取批量任务状态
|
||||
/// </summary>
|
||||
/// <param name="batchId">批量任务 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>批量任务状态</returns>
|
||||
Task<BatchStatusResult?> GetBatchStatusAsync(
|
||||
string batchId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 任务管理
|
||||
|
||||
/// <summary>
|
||||
/// 获取任务详情
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>任务详情,不存在返回 null</returns>
|
||||
Task<TaskDetail?> GetTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取任务状态(轻量级)
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>任务状态</returns>
|
||||
Task<TaskStatusResult?> GetTaskStatusAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询任务列表
|
||||
/// </summary>
|
||||
/// <param name="status">状态过滤</param>
|
||||
/// <param name="type">类型过滤:pdf 或 image</param>
|
||||
/// <param name="startDate">开始日期</param>
|
||||
/// <param name="endDate">结束日期</param>
|
||||
/// <param name="page">页码</param>
|
||||
/// <param name="pageSize">每页数量</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>任务列表</returns>
|
||||
Task<TaskListResult> QueryTasksAsync(
|
||||
string? status = null,
|
||||
string? type = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 下载任务结果文件
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>文件字节数组</returns>
|
||||
Task<byte[]> DownloadTaskResultAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 取消任务
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>是否成功取消</returns>
|
||||
Task<bool> CancelTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 重试失败的任务
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>是否成功重新提交</returns>
|
||||
Task<bool> RetryTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 等待任务完成
|
||||
|
||||
/// <summary>
|
||||
/// 等待任务完成并返回结果
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务 ID</param>
|
||||
/// <param name="pollingInterval">轮询间隔(毫秒),默认 1000</param>
|
||||
/// <param name="maxWaitTime">最大等待时间(毫秒),默认 300000(5分钟)</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>任务详情</returns>
|
||||
Task<TaskDetail> WaitForTaskAsync(
|
||||
string taskId,
|
||||
int pollingInterval = 1000,
|
||||
int maxWaitTime = 300000,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 等待任务完成并下载结果
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务 ID</param>
|
||||
/// <param name="pollingInterval">轮询间隔(毫秒)</param>
|
||||
/// <param name="maxWaitTime">最大等待时间(毫秒)</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>文件字节数组</returns>
|
||||
Task<byte[]> WaitAndDownloadAsync(
|
||||
string taskId,
|
||||
int pollingInterval = 1000,
|
||||
int maxWaitTime = 300000,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量任务输入项
|
||||
/// </summary>
|
||||
public class BatchTaskInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务类型:pdf 或 image
|
||||
/// </summary>
|
||||
public string Type { get; set; } = "pdf";
|
||||
|
||||
/// <summary>
|
||||
/// 源信息
|
||||
/// </summary>
|
||||
public SourceInfo Source { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// PDF 选项(Type 为 pdf 时有效)
|
||||
/// </summary>
|
||||
public PdfOptions? PdfOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图片选项(Type 为 image 时有效)
|
||||
/// </summary>
|
||||
public ImageOptions? ImageOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 等待条件
|
||||
/// </summary>
|
||||
public string? WaitUntil { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时时间(秒)
|
||||
/// </summary>
|
||||
public int? Timeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否保存到服务端
|
||||
/// </summary>
|
||||
public bool SaveLocal { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义元数据
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? Metadata { get; set; }
|
||||
}
|
||||
|
||||
46
src/HtmlToPdfService.Client/Models/ImageOptions.cs
Normal file
46
src/HtmlToPdfService.Client/Models/ImageOptions.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HtmlToPdfService.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 图片转换选项
|
||||
/// </summary>
|
||||
public class ImageOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 图片格式:png, jpeg, webp
|
||||
/// </summary>
|
||||
[JsonPropertyName("format")]
|
||||
public string? Format { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图片质量(1-100),仅对 jpeg/webp 有效
|
||||
/// </summary>
|
||||
[JsonPropertyName("quality")]
|
||||
public int? Quality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 视口宽度(像素)
|
||||
/// </summary>
|
||||
[JsonPropertyName("width")]
|
||||
public int? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 视口高度(像素)
|
||||
/// </summary>
|
||||
[JsonPropertyName("height")]
|
||||
public int? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否截取整个页面
|
||||
/// </summary>
|
||||
[JsonPropertyName("fullPage")]
|
||||
public bool? FullPage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否忽略背景色
|
||||
/// </summary>
|
||||
[JsonPropertyName("omitBackground")]
|
||||
public bool? OmitBackground { get; set; }
|
||||
}
|
||||
|
||||
64
src/HtmlToPdfService.Client/Models/PdfOptions.cs
Normal file
64
src/HtmlToPdfService.Client/Models/PdfOptions.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HtmlToPdfService.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// PDF 转换选项
|
||||
/// </summary>
|
||||
public class PdfOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 页面格式,如 A4, Letter, Legal 等
|
||||
/// </summary>
|
||||
[JsonPropertyName("format")]
|
||||
public string? Format { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否横向
|
||||
/// </summary>
|
||||
[JsonPropertyName("landscape")]
|
||||
public bool? Landscape { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否打印背景
|
||||
/// </summary>
|
||||
[JsonPropertyName("printBackground")]
|
||||
public bool? PrintBackground { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页边距设置
|
||||
/// </summary>
|
||||
[JsonPropertyName("margin")]
|
||||
public MarginOptions? Margin { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 页边距选项
|
||||
/// </summary>
|
||||
public class MarginOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 上边距(如 "10mm")
|
||||
/// </summary>
|
||||
[JsonPropertyName("top")]
|
||||
public string? Top { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 右边距
|
||||
/// </summary>
|
||||
[JsonPropertyName("right")]
|
||||
public string? Right { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下边距
|
||||
/// </summary>
|
||||
[JsonPropertyName("bottom")]
|
||||
public string? Bottom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 左边距
|
||||
/// </summary>
|
||||
[JsonPropertyName("left")]
|
||||
public string? Left { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HtmlToPdfService.Client.Models.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// HTML 转 PDF 请求
|
||||
/// </summary>
|
||||
internal class ConvertHtmlToPdfRequest
|
||||
{
|
||||
[JsonPropertyName("html")]
|
||||
public string Html { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public PdfOptions? Options { get; set; }
|
||||
|
||||
[JsonPropertyName("saveLocal")]
|
||||
public bool? SaveLocal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTML 转图片请求
|
||||
/// </summary>
|
||||
internal class ConvertHtmlToImageRequest
|
||||
{
|
||||
[JsonPropertyName("html")]
|
||||
public string Html { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public ImageOptions? Options { get; set; }
|
||||
|
||||
[JsonPropertyName("saveLocal")]
|
||||
public bool? SaveLocal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// URL 转 PDF 请求
|
||||
/// </summary>
|
||||
internal class ConvertUrlToPdfRequest
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("waitUntil")]
|
||||
public string? WaitUntil { get; set; }
|
||||
|
||||
[JsonPropertyName("timeout")]
|
||||
public int? Timeout { get; set; }
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public PdfOptions? Options { get; set; }
|
||||
|
||||
[JsonPropertyName("saveLocal")]
|
||||
public bool? SaveLocal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// URL 转图片请求
|
||||
/// </summary>
|
||||
internal class ConvertUrlToImageRequest
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("waitUntil")]
|
||||
public string? WaitUntil { get; set; }
|
||||
|
||||
[JsonPropertyName("timeout")]
|
||||
public int? Timeout { get; set; }
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public ImageOptions? Options { get; set; }
|
||||
|
||||
[JsonPropertyName("saveLocal")]
|
||||
public bool? SaveLocal { get; set; }
|
||||
}
|
||||
|
||||
151
src/HtmlToPdfService.Client/Models/Requests/TaskRequests.cs
Normal file
151
src/HtmlToPdfService.Client/Models/Requests/TaskRequests.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HtmlToPdfService.Client.Models.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// 异步 PDF 任务请求
|
||||
/// </summary>
|
||||
internal class PdfTaskRequest
|
||||
{
|
||||
[JsonPropertyName("source")]
|
||||
public SourceInfo Source { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public PdfOptions? Options { get; set; }
|
||||
|
||||
[JsonPropertyName("waitUntil")]
|
||||
public string? WaitUntil { get; set; }
|
||||
|
||||
[JsonPropertyName("timeout")]
|
||||
public int? Timeout { get; set; }
|
||||
|
||||
[JsonPropertyName("callback")]
|
||||
public CallbackInfo? Callback { get; set; }
|
||||
|
||||
[JsonPropertyName("saveLocal")]
|
||||
public bool? SaveLocal { get; set; }
|
||||
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, string>? Metadata { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步图片任务请求
|
||||
/// </summary>
|
||||
internal class ImageTaskRequest
|
||||
{
|
||||
[JsonPropertyName("source")]
|
||||
public SourceInfo Source { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public ImageOptions? Options { get; set; }
|
||||
|
||||
[JsonPropertyName("waitUntil")]
|
||||
public string? WaitUntil { get; set; }
|
||||
|
||||
[JsonPropertyName("timeout")]
|
||||
public int? Timeout { get; set; }
|
||||
|
||||
[JsonPropertyName("delayAfterLoad")]
|
||||
public int? DelayAfterLoad { get; set; }
|
||||
|
||||
[JsonPropertyName("callback")]
|
||||
public CallbackInfo? Callback { get; set; }
|
||||
|
||||
[JsonPropertyName("saveLocal")]
|
||||
public bool? SaveLocal { get; set; }
|
||||
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, string>? Metadata { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量任务请求
|
||||
/// </summary>
|
||||
internal class BatchTaskRequest
|
||||
{
|
||||
[JsonPropertyName("tasks")]
|
||||
public List<BatchTaskItem> Tasks { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("callback")]
|
||||
public CallbackInfo? Callback { get; set; }
|
||||
|
||||
[JsonPropertyName("onEachComplete")]
|
||||
public bool OnEachComplete { get; set; }
|
||||
|
||||
[JsonPropertyName("onAllComplete")]
|
||||
public bool OnAllComplete { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量任务项
|
||||
/// </summary>
|
||||
internal class BatchTaskItem
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "pdf";
|
||||
|
||||
[JsonPropertyName("source")]
|
||||
public SourceInfo Source { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("pdfOptions")]
|
||||
public PdfOptions? PdfOptions { get; set; }
|
||||
|
||||
[JsonPropertyName("imageOptions")]
|
||||
public ImageOptions? ImageOptions { get; set; }
|
||||
|
||||
[JsonPropertyName("waitUntil")]
|
||||
public string? WaitUntil { get; set; }
|
||||
|
||||
[JsonPropertyName("timeout")]
|
||||
public int? Timeout { get; set; }
|
||||
|
||||
[JsonPropertyName("saveLocal")]
|
||||
public bool? SaveLocal { get; set; }
|
||||
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, string>? Metadata { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 源信息
|
||||
/// </summary>
|
||||
public class SourceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 源类型:html 或 url
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "html";
|
||||
|
||||
/// <summary>
|
||||
/// 源内容:HTML 字符串或 URL
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回调配置
|
||||
/// </summary>
|
||||
public class CallbackInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 回调 URL
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义回调头
|
||||
/// </summary>
|
||||
[JsonPropertyName("headers")]
|
||||
public Dictionary<string, string>? Headers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否在回调中包含文件数据
|
||||
/// </summary>
|
||||
[JsonPropertyName("includeFileData")]
|
||||
public bool IncludeFileData { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HtmlToPdfService.Client.Models.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// API 错误响应(ProblemDetails 格式)
|
||||
/// </summary>
|
||||
public class ProblemDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP 状态码
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误标题
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误详情
|
||||
/// </summary>
|
||||
[JsonPropertyName("detail")]
|
||||
public string? Detail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误类型 URI
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实例路径
|
||||
/// </summary>
|
||||
[JsonPropertyName("instance")]
|
||||
public string? Instance { get; set; }
|
||||
}
|
||||
|
||||
484
src/HtmlToPdfService.Client/Models/Responses/TaskResponses.cs
Normal file
484
src/HtmlToPdfService.Client/Models/Responses/TaskResponses.cs
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HtmlToPdfService.Client.Models.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// 任务提交响应
|
||||
/// </summary>
|
||||
public class TaskSubmitResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID
|
||||
/// </summary>
|
||||
[JsonPropertyName("taskId")]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 任务状态
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = "pending";
|
||||
|
||||
/// <summary>
|
||||
/// 消息
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 预计等待时间(秒)
|
||||
/// </summary>
|
||||
[JsonPropertyName("estimatedWaitTime")]
|
||||
public int EstimatedWaitTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 队列位置
|
||||
/// </summary>
|
||||
[JsonPropertyName("queuePosition")]
|
||||
public int QueuePosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 相关链接
|
||||
/// </summary>
|
||||
[JsonPropertyName("links")]
|
||||
public TaskLinks? Links { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量任务提交响应
|
||||
/// </summary>
|
||||
public class BatchSubmitResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 批量任务 ID
|
||||
/// </summary>
|
||||
[JsonPropertyName("batchId")]
|
||||
public string BatchId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 所有任务 ID
|
||||
/// </summary>
|
||||
[JsonPropertyName("taskIds")]
|
||||
public List<string> TaskIds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 总任务数
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalTasks")]
|
||||
public int TotalTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成功数
|
||||
/// </summary>
|
||||
[JsonPropertyName("successCount")]
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败数
|
||||
/// </summary>
|
||||
[JsonPropertyName("failedCount")]
|
||||
public int FailedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 相关链接
|
||||
/// </summary>
|
||||
[JsonPropertyName("links")]
|
||||
public BatchLinks? Links { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务详情
|
||||
/// </summary>
|
||||
public class TaskDetail
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID
|
||||
/// </summary>
|
||||
[JsonPropertyName("taskId")]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 任务类型:pdf 或 image
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 源信息
|
||||
/// </summary>
|
||||
[JsonPropertyName("source")]
|
||||
public TaskSourceInfo? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务状态
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("startedAt")]
|
||||
public DateTime? StartedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 完成时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("completedAt")]
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 耗时(毫秒)
|
||||
/// </summary>
|
||||
[JsonPropertyName("duration")]
|
||||
public long Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 重试次数
|
||||
/// </summary>
|
||||
[JsonPropertyName("retryCount")]
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务结果
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public TaskResultInfo? Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public TaskErrorInfo? Error { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 相关链接
|
||||
/// </summary>
|
||||
[JsonPropertyName("links")]
|
||||
public TaskLinks? Links { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务源信息
|
||||
/// </summary>
|
||||
public class TaskSourceInfo
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string? Content { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务结果信息
|
||||
/// </summary>
|
||||
public class TaskResultInfo
|
||||
{
|
||||
[JsonPropertyName("fileSize")]
|
||||
public long? FileSize { get; set; }
|
||||
|
||||
[JsonPropertyName("fileType")]
|
||||
public string? FileType { get; set; }
|
||||
|
||||
[JsonPropertyName("downloadUrl")]
|
||||
public string? DownloadUrl { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务错误信息
|
||||
/// </summary>
|
||||
public class TaskErrorInfo
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public string? Code { get; set; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务状态响应(轻量级)
|
||||
/// </summary>
|
||||
public class TaskStatusResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID
|
||||
/// </summary>
|
||||
[JsonPropertyName("taskId")]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 任务状态
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("startedAt")]
|
||||
public DateTime? StartedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 完成时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("completedAt")]
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量任务状态
|
||||
/// </summary>
|
||||
public class BatchStatusResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 批量任务 ID
|
||||
/// </summary>
|
||||
[JsonPropertyName("batchId")]
|
||||
public string BatchId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = "pending";
|
||||
|
||||
/// <summary>
|
||||
/// 总任务数
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalTasks")]
|
||||
public int TotalTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已完成数
|
||||
/// </summary>
|
||||
[JsonPropertyName("completedTasks")]
|
||||
public int CompletedTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败数
|
||||
/// </summary>
|
||||
[JsonPropertyName("failedTasks")]
|
||||
public int FailedTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理中数
|
||||
/// </summary>
|
||||
[JsonPropertyName("processingTasks")]
|
||||
public int ProcessingTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 等待中数
|
||||
/// </summary>
|
||||
[JsonPropertyName("pendingTasks")]
|
||||
public int PendingTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 完成时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("completedAt")]
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务列表
|
||||
/// </summary>
|
||||
[JsonPropertyName("tasks")]
|
||||
public List<BatchTaskStatus> Tasks { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 相关链接
|
||||
/// </summary>
|
||||
[JsonPropertyName("links")]
|
||||
public BatchLinks? Links { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量任务中的单个任务状态
|
||||
/// </summary>
|
||||
public class BatchTaskStatus
|
||||
{
|
||||
[JsonPropertyName("taskId")]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = "pending";
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "pdf";
|
||||
|
||||
[JsonPropertyName("duration")]
|
||||
public long Duration { get; set; }
|
||||
|
||||
[JsonPropertyName("downloadUrl")]
|
||||
public string? DownloadUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务列表响应
|
||||
/// </summary>
|
||||
public class TaskListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 总数
|
||||
/// </summary>
|
||||
[JsonPropertyName("total")]
|
||||
public int Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前页
|
||||
/// </summary>
|
||||
[JsonPropertyName("page")]
|
||||
public int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页数量
|
||||
/// </summary>
|
||||
[JsonPropertyName("pageSize")]
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务列表
|
||||
/// </summary>
|
||||
[JsonPropertyName("items")]
|
||||
public List<TaskSummary> Items { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 分页链接
|
||||
/// </summary>
|
||||
[JsonPropertyName("links")]
|
||||
public PaginationLinks? Links { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务摘要
|
||||
/// </summary>
|
||||
public class TaskSummary
|
||||
{
|
||||
[JsonPropertyName("taskId")]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("duration")]
|
||||
public long Duration { get; set; }
|
||||
|
||||
[JsonPropertyName("fileSize")]
|
||||
public long? FileSize { get; set; }
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public TaskLinks? Links { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务相关链接
|
||||
/// </summary>
|
||||
public class TaskLinks
|
||||
{
|
||||
[JsonPropertyName("self")]
|
||||
public string? Self { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("download")]
|
||||
public string? Download { get; set; }
|
||||
|
||||
[JsonPropertyName("cancel")]
|
||||
public string? Cancel { get; set; }
|
||||
|
||||
[JsonPropertyName("retry")]
|
||||
public string? Retry { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量任务链接
|
||||
/// </summary>
|
||||
public class BatchLinks
|
||||
{
|
||||
[JsonPropertyName("status")]
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页链接
|
||||
/// </summary>
|
||||
public class PaginationLinks
|
||||
{
|
||||
[JsonPropertyName("first")]
|
||||
public string? First { get; set; }
|
||||
|
||||
[JsonPropertyName("prev")]
|
||||
public string? Prev { get; set; }
|
||||
|
||||
[JsonPropertyName("next")]
|
||||
public string? Next { get; set; }
|
||||
|
||||
[JsonPropertyName("last")]
|
||||
public string? Last { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务取消响应
|
||||
/// </summary>
|
||||
public class TaskCancelResult
|
||||
{
|
||||
[JsonPropertyName("taskId")]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = "cancelled";
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务重试响应
|
||||
/// </summary>
|
||||
public class TaskRetryResult
|
||||
{
|
||||
[JsonPropertyName("taskId")]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = "pending";
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
298
src/HtmlToPdfService.Client/README.md
Normal file
298
src/HtmlToPdfService.Client/README.md
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
# HtmlToPdfService.Client
|
||||
|
||||
HTML 转 PDF/图片服务的 .NET 客户端库,支持依赖注入。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
dotnet add package HtmlToPdfService.Client
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:依赖注入(推荐)
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
using HtmlToPdfService.Client.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// 添加客户端服务
|
||||
builder.Services.AddHtmlToPdfClient(options =>
|
||||
{
|
||||
options.BaseUrl = "https://pdf-service.example.com";
|
||||
options.ApiKey = "your-api-key"; // 可选
|
||||
options.EnableRetry = true; // 可选,启用重试
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// 使用服务
|
||||
public class MyService
|
||||
{
|
||||
private readonly IHtmlToPdfClient _pdfClient;
|
||||
|
||||
public MyService(IHtmlToPdfClient pdfClient)
|
||||
{
|
||||
_pdfClient = pdfClient;
|
||||
}
|
||||
|
||||
public async Task<byte[]> GeneratePdfAsync()
|
||||
{
|
||||
return await _pdfClient.ConvertHtmlToPdfAsync("<h1>Hello World</h1>");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方式二:从配置文件读取
|
||||
|
||||
```json
|
||||
// appsettings.json
|
||||
{
|
||||
"HtmlToPdfClient": {
|
||||
"BaseUrl": "https://pdf-service.example.com",
|
||||
"ApiKey": "your-api-key",
|
||||
"TimeoutSeconds": 120,
|
||||
"EnableRetry": true,
|
||||
"RetryCount": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Services.AddHtmlToPdfClient(builder.Configuration);
|
||||
```
|
||||
|
||||
### 方式三:多服务器配置(命名客户端)
|
||||
|
||||
```csharp
|
||||
// 注册多个命名客户端
|
||||
builder.Services.AddHtmlToPdfClient("production", options =>
|
||||
{
|
||||
options.BaseUrl = "https://pdf-prod.example.com";
|
||||
options.ApiKey = "prod-api-key";
|
||||
});
|
||||
|
||||
builder.Services.AddHtmlToPdfClient("staging", options =>
|
||||
{
|
||||
options.BaseUrl = "https://pdf-staging.example.com";
|
||||
options.ApiKey = "staging-api-key";
|
||||
});
|
||||
```
|
||||
|
||||
```csharp
|
||||
// 使用工厂创建指定客户端
|
||||
public class MyService
|
||||
{
|
||||
private readonly IHtmlToPdfClientFactory _factory;
|
||||
|
||||
public MyService(IHtmlToPdfClientFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
public async Task<byte[]> GeneratePdfAsync(bool useProduction)
|
||||
{
|
||||
var clientName = useProduction ? "production" : "staging";
|
||||
var client = _factory.CreateClient(clientName);
|
||||
return await client.ConvertHtmlToPdfAsync("<h1>Hello</h1>");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方式四:直接实例化
|
||||
|
||||
```csharp
|
||||
using HtmlToPdfService.Client;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
var options = new HtmlToPdfClientOptions
|
||||
{
|
||||
BaseUrl = "https://pdf-service.example.com",
|
||||
ApiKey = "your-api-key"
|
||||
};
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
var client = new HtmlToPdfClient(httpClient, Options.Create(options));
|
||||
|
||||
var pdf = await client.ConvertHtmlToPdfAsync("<h1>Hello</h1>");
|
||||
```
|
||||
|
||||
## 功能示例
|
||||
|
||||
### 同步 PDF 转换
|
||||
|
||||
```csharp
|
||||
// HTML 转 PDF
|
||||
var pdf = await client.ConvertHtmlToPdfAsync(
|
||||
html: "<html><body><h1>Hello</h1></body></html>",
|
||||
options: new PdfOptions
|
||||
{
|
||||
Format = "A4",
|
||||
Landscape = false,
|
||||
PrintBackground = true,
|
||||
Margin = new MarginOptions
|
||||
{
|
||||
Top = "10mm",
|
||||
Right = "10mm",
|
||||
Bottom = "10mm",
|
||||
Left = "10mm"
|
||||
}
|
||||
});
|
||||
|
||||
// URL 转 PDF
|
||||
var pdf = await client.ConvertUrlToPdfAsync(
|
||||
url: "https://example.com",
|
||||
waitUntil: "networkidle0",
|
||||
timeout: 30);
|
||||
```
|
||||
|
||||
### 同步图片转换
|
||||
|
||||
```csharp
|
||||
// HTML 转图片
|
||||
var image = await client.ConvertHtmlToImageAsync(
|
||||
html: "<html><body><h1>Hello</h1></body></html>",
|
||||
options: new ImageOptions
|
||||
{
|
||||
Format = "png",
|
||||
Width = 1920,
|
||||
Height = 1080,
|
||||
FullPage = true
|
||||
});
|
||||
|
||||
// URL 转图片
|
||||
var image = await client.ConvertUrlToImageAsync(
|
||||
url: "https://example.com",
|
||||
options: new ImageOptions { Format = "jpeg", Quality = 90 });
|
||||
```
|
||||
|
||||
### 异步任务
|
||||
|
||||
```csharp
|
||||
// 提交 PDF 任务
|
||||
var result = await client.SubmitPdfTaskAsync(
|
||||
source: new SourceInfo { Type = "html", Content = "<h1>Hello</h1>" },
|
||||
options: new PdfOptions { Format = "A4" },
|
||||
callback: new CallbackInfo { Url = "https://my-app.com/callback" });
|
||||
|
||||
Console.WriteLine($"任务 ID: {result.TaskId}");
|
||||
Console.WriteLine($"队列位置: {result.QueuePosition}");
|
||||
|
||||
// 查询任务状态
|
||||
var status = await client.GetTaskStatusAsync(result.TaskId);
|
||||
Console.WriteLine($"状态: {status.Status}");
|
||||
|
||||
// 等待任务完成并下载
|
||||
var pdf = await client.WaitAndDownloadAsync(result.TaskId);
|
||||
```
|
||||
|
||||
### 批量任务
|
||||
|
||||
```csharp
|
||||
var tasks = new List<BatchTaskInput>
|
||||
{
|
||||
new BatchTaskInput
|
||||
{
|
||||
Type = "pdf",
|
||||
Source = new SourceInfo { Type = "html", Content = "<h1>Doc 1</h1>" }
|
||||
},
|
||||
new BatchTaskInput
|
||||
{
|
||||
Type = "image",
|
||||
Source = new SourceInfo { Type = "url", Content = "https://example.com" },
|
||||
ImageOptions = new ImageOptions { Format = "png" }
|
||||
}
|
||||
};
|
||||
|
||||
var batch = await client.SubmitBatchAsync(tasks);
|
||||
Console.WriteLine($"批量任务 ID: {batch.BatchId}");
|
||||
|
||||
// 查询批量任务状态
|
||||
var batchStatus = await client.GetBatchStatusAsync(batch.BatchId);
|
||||
Console.WriteLine($"已完成: {batchStatus.CompletedTasks}/{batchStatus.TotalTasks}");
|
||||
```
|
||||
|
||||
### 任务管理
|
||||
|
||||
```csharp
|
||||
// 查询任务列表
|
||||
var list = await client.QueryTasksAsync(
|
||||
status: "completed",
|
||||
type: "pdf",
|
||||
page: 1,
|
||||
pageSize: 20);
|
||||
|
||||
// 取消任务
|
||||
await client.CancelTaskAsync(taskId);
|
||||
|
||||
// 重试失败的任务
|
||||
await client.RetryTaskAsync(taskId);
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
| 选项 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `BaseUrl` | string | - | 服务端地址(必填) |
|
||||
| `ApiKey` | string | null | API Key(可选) |
|
||||
| `TimeoutSeconds` | int | 120 | 请求超时时间 |
|
||||
| `EnableRetry` | bool | false | 是否启用重试 |
|
||||
| `RetryCount` | int | 3 | 重试次数 |
|
||||
| `RetryBaseDelayMs` | int | 500 | 重试基础延迟(指数退避) |
|
||||
| `CustomHeaders` | Dictionary | null | 自定义请求头 |
|
||||
|
||||
## 异常处理
|
||||
|
||||
```csharp
|
||||
using HtmlToPdfService.Client.Exceptions;
|
||||
|
||||
try
|
||||
{
|
||||
var pdf = await client.ConvertHtmlToPdfAsync("<h1>Hello</h1>");
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
{
|
||||
// 参数验证失败 (400)
|
||||
Console.WriteLine($"参数错误: {ex.Message}");
|
||||
}
|
||||
catch (AuthenticationException ex)
|
||||
{
|
||||
// 认证失败 (401)
|
||||
Console.WriteLine($"认证失败: {ex.Message}");
|
||||
}
|
||||
catch (TaskNotFoundException ex)
|
||||
{
|
||||
// 任务不存在 (404)
|
||||
Console.WriteLine($"任务不存在: {ex.TaskId}");
|
||||
}
|
||||
catch (TaskConflictException ex)
|
||||
{
|
||||
// 任务状态冲突 (409)
|
||||
Console.WriteLine($"任务状态: {ex.CurrentStatus}");
|
||||
}
|
||||
catch (ServiceUnavailableException ex)
|
||||
{
|
||||
// 服务不可用 (503)
|
||||
Console.WriteLine($"服务繁忙: {ex.Message}");
|
||||
}
|
||||
catch (HtmlToPdfClientException ex)
|
||||
{
|
||||
// 其他错误
|
||||
Console.WriteLine($"错误: {ex.Message}, 状态码: {ex.StatusCode}");
|
||||
}
|
||||
```
|
||||
|
||||
## 支持的框架
|
||||
|
||||
- .NET 8.0
|
||||
- .NET Standard 2.0(兼容 .NET Framework 4.6.1+、.NET Core 2.0+ 等)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
|
@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlToPdfService.Infrastruc
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlToPdfService.Tests", "HtmlToPdfService.Tests\HtmlToPdfService.Tests.csproj", "{E5F6A7B8-C9D0-1234-EF01-345678901234}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlToPdfService.Client", "HtmlToPdfService.Client\HtmlToPdfService.Client.csproj", "{F6A7B8C9-D0E1-2345-F012-456789012345}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -39,6 +41,10 @@ Global
|
|||
{E5F6A7B8-C9D0-1234-EF01-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5F6A7B8-C9D0-1234-EF01-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E5F6A7B8-C9D0-1234-EF01-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F6A7B8C9-D0E1-2345-F012-456789012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F6A7B8C9-D0E1-2345-F012-456789012345}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F6A7B8C9-D0E1-2345-F012-456789012345}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F6A7B8C9-D0E1-2345-F012-456789012345}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user