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
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlToPdfService.Tests", "HtmlToPdfService.Tests\HtmlToPdfService.Tests.csproj", "{E5F6A7B8-C9D0-1234-EF01-345678901234}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlToPdfService.Tests", "HtmlToPdfService.Tests\HtmlToPdfService.Tests.csproj", "{E5F6A7B8-C9D0-1234-EF01-345678901234}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlToPdfService.Client", "HtmlToPdfService.Client\HtmlToPdfService.Client.csproj", "{F6A7B8C9-D0E1-2345-F012-456789012345}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{E5F6A7B8-C9D0-1234-EF01-345678901234}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user