- PdfConversionOptions 新增 Width/Height(自定义纸张尺寸)和 ViewportWidth/ViewportHeight(浏览器视口) - PuppeteerPdfService 渲染前设置视口,BuildPdfOptions 支持自定义宽高优先于预设Format - ConversionTask 新增 PdfWidth/PdfHeight 字段 - 全链路透传:API DTO → TaskOrchestrator → TaskProcessor → PuppeteerPdfService - Client SDK PdfOptions 同步更新 - 新增 docs/外部对接文档.md
19 KiB
HTML 转 PDF 服务 — 外部对接文档
版本:2.0.0 | 更新日期:2026-03-16
1. 概述
本服务提供 HTML/URL 转 PDF 和图片的能力,支持同步和异步两种调用模式:
- 同步模式:请求后直接返回文件流,适合简单、低并发场景
- 异步模式(推荐):提交任务后通过轮询或回调获取结果,适合高并发、大批量场景
服务地址:https://pdf-service.example.com(请替换为实际地址)
2. 认证
服务支持 API Key 认证。如果服务端开启了认证,所有业务接口都需要携带 Token。
2.1 获取 Token
POST /api/auth/token
Content-Type: application/json
请求体:
{
"apiKey": "your-api-key",
"userId": "your-user-id" // 可选
}
响应:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"expiresAt": "2026-03-16T11:00:00Z"
}
2.2 请求认证方式
服务支持两种认证方式,任选其一即可:
方式一:JWT Token(通过 API Key 换取)
先调用 /api/auth/token 获取 Token,然后在请求头中携带:
Authorization: Bearer {accessToken}
Token 有过期时间,过期后需要重新获取或刷新。
方式二:直接使用 API Key(无需换 Token)
每次请求直接携带 API Key,支持以下三种传递方式:
X-API-Key: your-api-key
Authorization: ApiKey your-api-key
GET /api/tasks/xxx?api_key=your-api-key
推荐使用 X-API-Key Header,Query String 方式存在 Key 泄露到日志的风险。
以下路径无需认证:
/health、/swagger、/metrics、/api/auth/token
2.3 刷新 Token
使用 JWT Token 方式时,可在 Token 过期前刷新:
POST /api/auth/refresh
Authorization: Bearer {当前Token}
3. 同步转换接口
适合简单场景,请求后直接返回文件二进制流。
3.1 HTML 转 PDF
POST /api/pdf/convert/html
Content-Type: application/json
请求体:
{
"html": "<html><body><h1>Hello World</h1><p>这是测试内容</p></body></html>",
"options": {
"format": "A4",
"landscape": false,
"printBackground": true,
"margin": {
"top": "10mm",
"right": "10mm",
"bottom": "10mm",
"left": "10mm"
}
},
"saveLocal": false
}
响应:application/pdf 文件流
3.2 URL 转 PDF
POST /api/pdf/convert/url
Content-Type: application/json
请求体:
{
"url": "https://example.com",
"waitUntil": "networkidle0",
"timeout": 30,
"options": {
"format": "A4",
"printBackground": true
},
"saveLocal": false
}
3.3 HTML 转图片
POST /api/image/convert/html
Content-Type: application/json
请求体:
{
"html": "<html><body><h1>Hello</h1></body></html>",
"options": {
"format": "png",
"quality": 90,
"width": 1920,
"height": 1080,
"fullPage": true,
"omitBackground": false
},
"saveLocal": false
}
响应:对应格式的图片文件流(image/png、image/jpeg、image/webp)
3.4 URL 转图片
POST /api/image/convert/url
Content-Type: application/json
请求体:
{
"url": "https://example.com",
"waitUntil": "networkidle0",
"timeout": 30,
"options": {
"format": "png",
"width": 1920,
"height": 1080,
"fullPage": true
},
"saveLocal": false
}
4. 异步任务接口(推荐)
异步模式下,提交任务后会立即返回任务 ID,通过轮询状态或配置回调来获取结果。
4.1 提交 PDF 任务
POST /api/tasks/pdf
Content-Type: application/json
Idempotency-Key: {可选,幂等键}
请求体:
{
"source": {
"type": "html",
"content": "<h1>Hello World</h1><p>这是测试内容</p>"
},
"options": {
"format": "A4",
"landscape": false,
"printBackground": true,
"margin": {
"top": "10mm",
"right": "10mm",
"bottom": "10mm",
"left": "10mm"
}
},
"waitUntil": "networkidle0",
"timeout": 30,
"callback": {
"url": "https://your-app.com/webhook/pdf-done",
"headers": {
"X-Custom-Header": "value"
},
"includeFileData": false
},
"saveLocal": true,
"metadata": {
"orderId": "12345",
"source": "billing-system"
}
}
source.type为"html"时,content填 HTML 字符串;为"url"时,content填页面 URL。
响应(202 Accepted):
{
"taskId": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"message": "任务已创建,正在排队处理",
"createdAt": "2026-03-16T10:00:00Z",
"estimatedWaitTime": 3,
"queuePosition": 2,
"links": {
"self": "/api/tasks/550e8400-e29b-41d4-a716-446655440000",
"status": "/api/tasks/550e8400-e29b-41d4-a716-446655440000/status",
"download": "/api/tasks/550e8400-e29b-41d4-a716-446655440000/download",
"cancel": "/api/tasks/550e8400-e29b-41d4-a716-446655440000"
}
}
4.2 提交图片任务
POST /api/tasks/image
Content-Type: application/json
Idempotency-Key: {可选}
请求体:
{
"source": {
"type": "url",
"content": "https://example.com"
},
"options": {
"format": "png",
"quality": 90,
"width": 1920,
"height": 1080,
"fullPage": true,
"omitBackground": false
},
"delayAfterLoad": 1000,
"callback": {
"url": "https://your-app.com/webhook/image-done"
},
"saveLocal": true
}
4.3 批量提交任务
POST /api/tasks/batch
Content-Type: application/json
请求体:
{
"tasks": [
{
"type": "pdf",
"source": { "type": "html", "content": "<h1>文档 1</h1>" },
"pdfOptions": { "format": "A4", "printBackground": true }
},
{
"type": "pdf",
"source": { "type": "url", "content": "https://example.com" }
},
{
"type": "image",
"source": { "type": "html", "content": "<h1>截图</h1>" },
"imageOptions": { "format": "png", "width": 1920 }
}
],
"callback": {
"url": "https://your-app.com/webhook/batch-done"
},
"onEachComplete": false,
"onAllComplete": true
}
响应(202 Accepted):
{
"batchId": "batch-xxxx-xxxx",
"taskIds": ["task-1", "task-2", "task-3"],
"totalTasks": 3,
"successCount": 3,
"failedCount": 0,
"links": {
"status": "/api/tasks/batch/batch-xxxx-xxxx"
}
}
4.4 查询任务详情
GET /api/tasks/{taskId}
响应:
{
"taskId": "550e8400-...",
"type": "pdf",
"source": { "type": "html", "content": "..." },
"status": "completed",
"createdAt": "2026-03-16T10:00:00Z",
"startedAt": "2026-03-16T10:00:01Z",
"completedAt": "2026-03-16T10:00:04Z",
"duration": 3000,
"retryCount": 0,
"result": {
"fileSize": 102400,
"fileType": "pdf",
"downloadUrl": "/api/tasks/550e8400-.../download"
},
"error": null,
"links": {
"self": "/api/tasks/550e8400-...",
"download": "/api/tasks/550e8400-.../download"
}
}
4.5 轻量级状态查询
适合高频轮询场景,返回数据量小:
GET /api/tasks/{taskId}/status
响应:
{
"taskId": "550e8400-...",
"status": "processing",
"createdAt": "2026-03-16T10:00:00Z",
"startedAt": "2026-03-16T10:00:01Z",
"completedAt": null
}
4.6 下载结果文件
GET /api/tasks/{taskId}/download
响应:文件二进制流,Content-Type 根据任务类型自动设置。
响应头包含:
X-Task-Id: 任务 IDX-Expires-At: 文件过期时间(如有)
任务未完成时返回
409 Conflict。
4.7 取消任务
DELETE /api/tasks/{taskId}
响应:
{
"taskId": "550e8400-...",
"status": "cancelled",
"message": "任务已取消"
}
仅
pending状态的任务可取消,其他状态返回409 Conflict。
4.8 重试任务
POST /api/tasks/{taskId}/retry
响应(202 Accepted):
{
"taskId": "550e8400-...",
"status": "pending",
"message": "任务已重新加入队列"
}
仅
failed状态的任务可重试。
4.9 查询批量任务状态
GET /api/tasks/batch/{batchId}
响应:
{
"batchId": "batch-xxxx",
"status": "completed",
"totalTasks": 3,
"completedTasks": 3,
"failedTasks": 0,
"processingTasks": 0,
"pendingTasks": 0,
"createdAt": "2026-03-16T10:00:00Z",
"completedAt": "2026-03-16T10:00:10Z",
"tasks": [
{
"taskId": "task-1",
"status": "completed",
"type": "pdf",
"duration": 3000,
"downloadUrl": "/api/tasks/task-1/download",
"errorMessage": null
}
]
}
4.10 预检(Dry-run)
提交前检查内容是否可渲染、是否存在 SSRF 风险:
POST /api/tasks/validate
Content-Type: application/json
请求体:
{
"source": {
"type": "url",
"content": "https://example.com"
}
}
响应:
{
"ok": true,
"canRender": true,
"suggestedQueue": "normal",
"estimatedRenderTimeMs": 3000,
"issues": [],
"ssrfBlocked": false
}
5. 配额查询
5.1 查看当前配额
GET /api/quota
响应:
{
"userId": "user-123",
"daily": {
"used": 50,
"limit": 1000,
"remaining": 950,
"resetAt": "2026-03-17T00:00:00Z"
},
"monthly": {
"used": 200,
"limit": 10000,
"remaining": 9800,
"resetAt": "2026-04-01T00:00:00Z"
},
"concurrent": {
"used": 2,
"limit": 10,
"remaining": 8
}
}
5.2 检查配额是否足够
提交任务前可先调用此接口:
GET /api/quota/check
响应:
{
"allowed": true,
"denyReason": null,
"dailyRemaining": 950,
"monthlyRemaining": 9800,
"concurrentRemaining": 8
}
6. 健康检查
GET /health
可用于监控服务是否正常运行。
7. 回调机制
异步任务支持配置回调 URL,任务完成后服务会主动 POST 通知你的系统。
7.1 回调请求格式
服务会向你配置的 callback.url 发送 POST 请求:
POST https://your-app.com/webhook/pdf-done
Content-Type: application/json
X-Callback-Signature: {HMAC签名}
回调 Body 包含任务完成信息(taskId、status、result 等)。如果 includeFileData 设为 true,回调中会包含 Base64 编码的文件数据。
7.2 回调重试
回调失败时,服务会自动进行指数退避重试。
8. 参数参考
8.1 PDF 选项(options)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
format |
string | "A4" |
纸张格式:A4, Letter, Legal, A3 等 |
landscape |
bool | false |
是否横向 |
printBackground |
bool | true |
是否打印背景色/图 |
width |
string | — | 自定义页面宽度(如 "1309px", "210mm"),设置后忽略 format |
height |
string | — | 自定义页面高度(如 "926px", "297mm"),设置后忽略 format |
viewportWidth |
int | — | 浏览器视口宽度(像素),控制页面渲染时的窗口宽度 |
viewportHeight |
int | — | 浏览器视口高度(像素),控制页面渲染时的窗口高度 |
margin.top |
string | "10mm" |
上边距 |
margin.right |
string | "10mm" |
右边距 |
margin.bottom |
string | "10mm" |
下边距 |
margin.left |
string | "10mm" |
左边距 |
width/height和format二选一。设置了width+height后format会被忽略。viewportWidth/viewportHeight用于控制浏览器渲染窗口大小,适合需要在固定分辨率下打开才能正常显示的页面。
8.2 图片选项(options)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
format |
string | "png" |
图片格式:png, jpeg, webp |
quality |
int | 90 |
图片质量(1-100),仅 jpeg/webp 有效 |
width |
int | 1920 |
浏览器视口宽度(像素),即页面渲染分辨率的宽 |
height |
int | 1080 |
浏览器视口高度(像素),即页面渲染分辨率的高 |
fullPage |
bool | true |
是否截取整个页面(超出视口的滚动区域也会截取) |
omitBackground |
bool | false |
是否忽略背景色(透明背景,仅 png 有效) |
width/height直接控制浏览器窗口大小。比如传"width": 1309, "height": 926,页面就会在 1309×926 的分辨率下渲染后再截图。
8.3 waitUntil 参数
控制页面加载等待策略,URL 转换时特别有用:
| 值 | 说明 |
|---|---|
load |
等待 load 事件触发 |
domcontentloaded |
等待 DOMContentLoaded 事件 |
networkidle0 |
等待 500ms 内无网络请求(推荐 SPA 页面) |
networkidle2 |
等待 500ms 内不超过 2 个网络请求 |
8.4 任务状态
| 状态 | 说明 |
|---|---|
pending |
排队中 |
processing |
处理中 |
completed |
已完成 |
failed |
失败 |
cancelled |
已取消 |
stalled |
卡死(超时未完成) |
9. 错误处理
所有错误响应遵循 RFC 7807 ProblemDetails 格式:
{
"status": 400,
"title": "请求参数无效",
"detail": "HTML 内容不能为空"
}
常见错误码
| HTTP 状态码 | 含义 | 处理建议 |
|---|---|---|
| 400 | 参数错误 | 检查请求体格式和必填字段 |
| 401 | 认证失败 | 检查 Token 或 API Key |
| 404 | 任务不存在 | 确认 taskId 是否正确 |
| 409 | 状态冲突 | 任务状态不允许当前操作(如取消已完成的任务) |
| 429 | 请求限流 | 降低请求频率,稍后重试 |
| 503 | 服务繁忙 | 队列已满或服务过载,稍后重试 |
10. .NET SDK 接入(推荐)
项目提供了 .NET 客户端 SDK,支持依赖注入,开箱即用。
10.1 安装
dotnet add package HtmlToPdfService.Client
10.2 注册服务
// Program.cs
using HtmlToPdfService.Client.Extensions;
builder.Services.AddHtmlToPdfClient(options =>
{
options.BaseUrl = "https://pdf-service.example.com";
options.ApiKey = "your-api-key";
options.TimeoutSeconds = 120;
options.EnableRetry = true; // 启用指数退避重试
options.RetryCount = 3;
});
或从配置文件读取:
// appsettings.json
{
"HtmlToPdfClient": {
"BaseUrl": "https://pdf-service.example.com",
"ApiKey": "your-api-key",
"TimeoutSeconds": 120,
"EnableRetry": true,
"RetryCount": 3
}
}
builder.Services.AddHtmlToPdfClient(builder.Configuration);
10.3 使用示例
public class ReportService
{
private readonly IHtmlToPdfClient _pdfClient;
public ReportService(IHtmlToPdfClient pdfClient)
{
_pdfClient = pdfClient;
}
// 同步转换 — 直接拿到 PDF 字节数组
public async Task<byte[]> GenerateSimplePdfAsync(string html)
{
return await _pdfClient.ConvertHtmlToPdfAsync(html, new PdfOptions
{
Format = "A4",
PrintBackground = true
});
}
// 异步任务 — 提交后等待完成再下载
public async Task<byte[]> GeneratePdfAsync(string html)
{
var result = await _pdfClient.SubmitPdfTaskAsync(
source: new SourceInfo { Type = "html", Content = html },
options: new PdfOptions { Format = "A4" });
// 等待任务完成并下载(内部自动轮询)
return await _pdfClient.WaitAndDownloadAsync(result.TaskId);
}
// 异步任务 + 回调 — 提交后不等待,由回调通知
public async Task<string> SubmitPdfTaskAsync(string html, string callbackUrl)
{
var result = await _pdfClient.SubmitPdfTaskAsync(
source: new SourceInfo { Type = "html", Content = html },
callback: new CallbackInfo { Url = callbackUrl });
return result.TaskId;
}
}
10.4 异常处理
using HtmlToPdfService.Client.Exceptions;
try
{
var pdf = await _pdfClient.ConvertHtmlToPdfAsync(html);
}
catch (ValidationException ex) // 400 参数错误
{
logger.LogWarning("参数错误: {Message}", ex.Message);
}
catch (AuthenticationException ex) // 401 认证失败
{
logger.LogWarning("认证失败: {Message}", ex.Message);
}
catch (TaskNotFoundException ex) // 404 任务不存在
{
logger.LogWarning("任务不存在: {TaskId}", ex.TaskId);
}
catch (ServiceUnavailableException ex) // 503 服务繁忙
{
logger.LogWarning("服务繁忙,稍后重试: {Message}", ex.Message);
}
catch (HtmlToPdfClientException ex) // 其他错误
{
logger.LogError("转换失败: {Message}, 状态码: {StatusCode}", ex.Message, ex.StatusCode);
}
10.5 SDK 配置项
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
BaseUrl |
string | — | 服务地址(必填) |
ApiKey |
string | null | API Key |
TimeoutSeconds |
int | 120 | HTTP 请求超时 |
EnableRetry |
bool | false | 是否启用自动重试 |
RetryCount |
int | 3 | 重试次数 |
RetryBaseDelayMs |
int | 500 | 重试基础延迟(指数退避) |
CustomHeaders |
Dictionary | null | 自定义请求头 |
11. 其他语言接入
非 .NET 项目直接调用 HTTP API 即可,以下是 curl 示例:
同步 HTML 转 PDF
curl -X POST https://pdf-service.example.com/api/pdf/convert/html \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {token}" \
-d '{"html": "<h1>Hello World</h1>"}' \
-o document.pdf
异步提交任务
curl -X POST https://pdf-service.example.com/api/tasks/pdf \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {token}" \
-d '{
"source": {"type": "html", "content": "<h1>Hello</h1>"},
"options": {"format": "A4"}
}'
查询状态
curl https://pdf-service.example.com/api/tasks/{taskId}/status \
-H "Authorization: Bearer {token}"
下载文件
curl https://pdf-service.example.com/api/tasks/{taskId}/download \
-H "Authorization: Bearer {token}" \
-o result.pdf
12. 典型接入流程
方式一:同步调用(简单直接)
客户端 → POST /api/pdf/convert/html → 等待 → 返回 PDF 文件流
适合:单次转换、低并发、对延迟不敏感。
方式二:异步轮询
客户端 → POST /api/tasks/pdf → 返回 taskId
客户端 → GET /api/tasks/{taskId}/status → 轮询直到 completed
客户端 → GET /api/tasks/{taskId}/download → 下载文件
适合:需要异步处理但不方便接收回调。
方式三:异步回调(推荐)
客户端 → POST /api/tasks/pdf(带 callback.url)→ 返回 taskId
服务端 → 处理完成后 POST 回调到 callback.url
客户端 → 收到回调后 GET /api/tasks/{taskId}/download → 下载文件
适合:高并发、批量处理、生产环境。
13. 注意事项
- 幂等性:异步接口支持
Idempotency-Key请求头,相同 Key 不会重复创建任务 - 文件有效期:生成的文件有保留时间限制(默认 7 天),请及时下载
- SSRF 防护:URL 模式下,服务会阻止访问内网地址(10.x、192.168.x、127.x 等)
- 请求限流:服务有 IP/用户维度的速率限制,超限返回 429
- 超时设置:复杂页面建议适当增大
timeout参数 - SPA 页面:转换 React/Vue/Angular 等 SPA 页面时,建议
waitUntil设为networkidle0