HtmlToPdf/docs/外部对接文档.md
zpc f7495e5fd4 feat: 支持PDF自定义页面尺寸和浏览器视口设置,新增外部对接文档
- PdfConversionOptions 新增 Width/Height(自定义纸张尺寸)和 ViewportWidth/ViewportHeight(浏览器视口)
- PuppeteerPdfService 渲染前设置视口,BuildPdfOptions 支持自定义宽高优先于预设Format
- ConversionTask 新增 PdfWidth/PdfHeight 字段
- 全链路透传:API DTO → TaskOrchestrator → TaskProcessor → PuppeteerPdfService
- Client SDK PdfOptions 同步更新
- 新增 docs/外部对接文档.md
2026-03-16 18:33:06 +08:00

19 KiB
Raw Blame History

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 HeaderQuery 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/pngimage/jpegimage/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: 任务 ID
  • X-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/heightformat 二选一。设置了 width+heightformat 会被忽略。 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. 注意事项

  1. 幂等性:异步接口支持 Idempotency-Key 请求头,相同 Key 不会重复创建任务
  2. 文件有效期:生成的文件有保留时间限制(默认 7 天),请及时下载
  3. SSRF 防护URL 模式下服务会阻止访问内网地址10.x、192.168.x、127.x 等)
  4. 请求限流:服务有 IP/用户维度的速率限制,超限返回 429
  5. 超时设置:复杂页面建议适当增大 timeout 参数
  6. SPA 页面:转换 React/Vue/Angular 等 SPA 页面时,建议 waitUntil 设为 networkidle0