Files
baiyue-firstleg/print/README.md
T
2026-08-04 18:54:22 +08:00

189 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Print PDF 服务接入指南
本文面向需要生成入仓标签 PDF 的业务服务。Print 是无数据库的内部渲染服务:调用方提交完整标签数据,Print 同步返回一次性 PDF,不保存任务、文件或业务数据。
完整机器可读契约见 [`src/main/resources/openapi/print-internal-v1.yaml`](src/main/resources/openapi/print-internal-v1.yaml),版式与技术决策见 [`../docs/2026-08-04-无状态PDF打印服务设计.md`](../docs/2026-08-04-无状态PDF打印服务设计.md)。
## 1. 调用边界
- 服务发现名:`print-service`。
- 内部地址:`POST /internal/print/v1/pdfs/inbound-marks`。
- 本地直连:`http://127.0.0.1:9207`。
- 请求必须携带 `X-Internal-Service-Token`,值使用各服务已有的 `baiyue.security.internal-service-token` 配置。
- 不把该接口加入 Gateway,也不允许 Admin 浏览器直接携带内部令牌访问 Print。
- Admin 请求业务服务;业务服务完成 JWT、租户、权限和数据范围校验,组装完整 `labels[]` 后调用 Print,再把 PDF 响应转发给浏览器。
调用方不得要求 Print 查询订单、补箱号、排序或修正数据。数组顺序就是 PDF 页序,每个数组元素生成一页。
## 2. 请求契约
```http
POST /internal/print/v1/pdfs/inbound-marks HTTP/1.1
Content-Type: application/json
Accept: application/pdf, application/json
X-Internal-Service-Token: <shared-token>
X-Request-Id: <optional-correlation-id>
```
```json
{
"templateCode": "INBOUND_MARK_V1",
"pageSpec": {
"widthMm": 100,
"heightMm": 100
},
"downloadFileName": "PCHK2608040174-19件-MCC1.pdf",
"labels": [
{
"topCode": "PCHK2608040174",
"warehouseText": "MCC1",
"cartonText": "CTNS:19-1",
"routeText": "洛杉矶普船海卡",
"qrContent": "PCHK2608040174#19-1"
},
{
"topCode": "PCHK2608040174",
"warehouseText": "MCC1",
"cartonText": "CTNS:19-2",
"routeText": "洛杉矶普船海卡",
"qrContent": "PCHK2608040174#19-2"
}
]
}
```
| 字段 | 必填 | 规则 |
|---|---|---|
| `templateCode` | 是 | 固定 `INBOUND_MARK_V1` |
| `pageSpec.widthMm` | 是 | 固定数值 `100` |
| `pageSpec.heightMm` | 是 | 固定数值 `100` |
| `downloadFileName` | 否 | 最多 120 字符,必须以 `.pdf` 结尾,不得含路径或文件系统保留字符 |
| `labels` | 是 | 1-2000 项;一个元素对应一页,按数组顺序生成 |
| `labels[].topCode` | 是 | 非空、单行、无控制字符 |
| `labels[].warehouseText` | 是 | 非空、单行、无控制字符 |
| `labels[].cartonText` | 是 | 最终展示文案;Print 不解析箱号 |
| `labels[].routeText` | 是 | 最终展示文案;非空、单行、无控制字符 |
| `labels[].qrContent` | 是 | 1-34 个可打印 ASCII 字符 |
普通展示字段首版不限制业务字符数,也不会自动换行、缩小或截断。调用方负责保证文案适合固定版式。
未传 `downloadFileName` 时,服务使用 `inbound-marks-{requestId}.pdf`。请求体最大 2 MiB,PDF 最大 32 MiB。
## 3. 响应处理
调用方必须同时判断 HTTP 状态和 `Content-Type`,不能只判断 HTTP 200:
| 场景 | HTTP | Content-Type | 处理方式 |
|---|---:|---|---|
| 生成成功 | 200 | `application/pdf` | 读取二进制并转发或下载 |
| 参数、鉴权、并发或生成失败 | 200 | `application/json` | 按 `ApiResponse.code` 处理 |
| 请求体超过 2 MiB | 413 | `application/json` | 缩小请求,不要重试相同内容 |
成功响应包含:
- `Content-Disposition`:下载文件名;
- `Content-Length`:PDF 字节数;
- `X-Print-Page-Count`:实际页数;
- `X-Request-Id`:请求链路标识;
- `Cache-Control: no-store`、`Pragma: no-cache`。
常见错误码:
| code | 含义 | 调用方建议 |
|---|---|---|
| `A0400` | JSON 无法解析或请求体过大 | 修正请求;HTTP 413 时缩小请求体 |
| `A0403` | 内部令牌缺失或错误 | 检查共享令牌配置,不向浏览器暴露令牌 |
| `A0422` | 字段、模板、尺寸、页数或字符规则不合法 | 修正业务服务组装的数据 |
| `A0429` | PDF 生成并发槽位已满 | 短暂退避后有限重试,避免立即并发重放 |
| `S0500` | PDF 生成失败或超过输出上限 | 记录 `requestId/traceId` 并告警,不返回半个 PDF |
## 4. OpenFeign 接入示例
调用方自行维护请求 DTO,不依赖 Print 模块的 Java 实现类。跨服务事实来源是 OpenAPI。
```java
@FeignClient(
name = "print-service",
contextId = "printPdfClient",
url = "${baiyue.clients.print.url:}",
path = "/internal/print/v1/pdfs")
public interface PrintPdfClient {
@PostMapping(value = "/inbound-marks", consumes = "application/json")
ResponseEntity<byte[]> generate(
@RequestHeader("X-Internal-Service-Token") String internalToken,
@RequestHeader(value = "X-Request-Id", required = false) String requestId,
@RequestBody InboundMarkPdfRequest request);
record InboundMarkPdfRequest(
String templateCode,
PageSpec pageSpec,
String downloadFileName,
List<Label> labels) {}
record PageSpec(BigDecimal widthMm, BigDecimal heightMm) {}
record Label(
String topCode,
String warehouseText,
String cartonText,
String routeText,
String qrContent) {}
}
```
调用封装必须区分 PDF 与 JSON:
```java
ResponseEntity<byte[]> response = printPdfClient.generate(internalToken, requestId, request);
MediaType contentType = response.getHeaders().getContentType();
byte[] body = response.getBody() == null ? new byte[0] : response.getBody();
if (contentType != null && MediaType.APPLICATION_PDF.isCompatibleWith(contentType)) {
return ResponseEntity.ok()
.headers(copySafeDownloadHeaders(response.getHeaders()))
.contentType(MediaType.APPLICATION_PDF)
.body(body);
}
ApiResponse<?> error = objectMapper.readValue(body, ApiResponse.class);
throw mapPrintError(error, requestId);
```
`copySafeDownloadHeaders` 只转发 `Content-Disposition`、`Content-Length`、`Cache-Control`、`Pragma`、`X-Print-Page-Count` 和链路标识,不转发内部令牌。由于业务错误也使用 HTTP 200,Feign fallback 不能替代 `Content-Type + ApiResponse.code` 判断。HTTP 413 会进入 Feign 的非 2xx 异常流程,调用方应在 `FeignException` 中保留状态码、JSON body、`requestId` 和 `traceId`。
调用方建议为该客户端单独配置超时:
```yaml
baiyue:
clients:
print:
url: ${PRINT_SERVICE_URL:}
spring:
cloud:
openfeign:
client:
config:
printPdfClient:
connect-timeout: 3000
read-timeout: 30000
```
`url` 为空时通过 Nacos 的 `print-service` 发现;本地单独联调可设置 `PRINT_SERVICE_URL=http://127.0.0.1:9207`。
## 5. 重试、并发与日志
- Print 默认最多同时生成 4 份 PDF,等待槽位 1 秒后返回 `A0429`。
- 不建议对该 POST 做无限或无退避自动重试;超时或 `A0429` 最多有限重试,并加入退避和抖动。
- 单次最多 2000 页。大批量请求应设置不低于 30 秒的读取超时,并避免调用方自身重复复制 PDF 字节。
- 浏览器下载场景由业务服务原样转发 PDF,不要把 PDF Base64 塞入 JSON,也不要为了“一次性下载”写入数据库。
- 日志记录模板、页数、耗时、PDF 字节数、`requestId`、`traceId` 和错误码;不要记录内部令牌、完整 `labels[]`、二维码内容或 PDF 二进制。
## 6. 联调检查清单
1. `GET /actuator/health` 返回 `UP`。
2. 使用正确内部令牌提交 1 页,响应为 `application/pdf`。
3. 检查页面为严格 `100mm x 100mm`,`X-Print-Page-Count` 与 `labels.length` 一致。
4. 提交多页并检查页序与数组顺序一致。
5. 分别验证错误令牌、非法尺寸、空数组、超过 2000 页和超过 2 MiB 请求体。
6. Admin 下载链路不得在浏览器请求或日志中出现 `X-Internal-Service-Token`。