Files

367 lines
19 KiB
Markdown
Raw Permalink 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 内部调用契约
本文是其他 Java 业务模块及代码生成 AI 接入 Print 的实现约束,不是终端用户操作手册。Print 是无数据库的内部渲染服务:调用方提交完整打印数据,Print 同步返回一次性 PDF,不保存任务、文件或业务数据。
机器可读 HTTP 契约以 [`src/main/resources/openapi/print-internal-v1.yaml`](src/main/resources/openapi/print-internal-v1.yaml) 为准;本文件补充调用方必须实现、但 OpenAPI 无法完整表达的权限、字段映射、响应分支和日志约束。调用方必须自行声明 DTO,不得依赖 Print 模块的 Java 实现类。
## 1. 调用边界
- 服务发现名:`print-service`。
- 内部地址:`POST /internal/print/v1/pdfs/inbound-marks`。
- FBA 亚马逊托盘标:`POST /internal/print/v1/pdfs/fba-pallet-labels`;国内散货打印与海外 FBA 单托补打统一使用 `FBA_PALLET_A4_LANDSCAPE_V1`。每托一页 297×210 mm 横向 A4,仅展示 FBA 仓码、完整托盘号的 Code 128 条码和完整托盘号文字。
- 一件代发 A4 托盘信息单:`POST /internal/print/v1/pdfs/dropship-pallet-a4-documents`;模板固定为 `DROPSHIP_PALLET_A4_V2`,采用黑白打印版,每托一页 A4,包含托盘 Code 128 一维码、目的仓、最多 10 行入仓码/客户昵称汇总和最多 34 行 SKU 汇总,不展示托盘序号、作业状态、重量或体积。
- A4 海运单据内部地址:`POST /internal/print/v1/pdfs/freight-a4-documents`。
- 海外仓排车 BOL 内部地址:`POST /internal/print/v1/pdfs/dispatch-bol-documents`;模板固定为 `DISPATCH_BILL_OF_LADING_A4_V2`,每个 document 对应一张 A4、包含 1-26 行明细。调用方首张按 18 行、续页按 26 行稳定分页;同页连续相同柜号和托盘号由 Print 纵向合并显示。
- 本地直连:`http://127.0.0.1:9207`。
- 请求必须携带 `X-Internal-Service-Token`,值使用各服务已有的 `baiyue.security.internal-service-token` 配置。
- 不把该接口加入 Gateway,也不允许 Admin 浏览器直接携带内部令牌访问 Print。
- Admin 请求业务服务;业务服务完成 JWT、客户归属、权限和数据范围校验,组装完整 `labels[]` 或 `documents[]` 后调用 Print,再把 PDF 响应转发给浏览器。JWT 中的 `tenant_id` 当前是客户隔离兼容字段,Print 不解析或引入真实租户层。
调用方必须从本模块拥有的数据自动组装打印字段,不得把 Print 请求 DTO 暴露成要求用户逐项填写的表单。调用方不得要求 Print 查询订单、补箱号、排序、推导或修正数据。数组顺序就是 PDF 页序,每个数组元素生成一页。
## 2. 请求契约
### FBA 亚马逊托盘标
```json
{
"templateCode": "FBA_PALLET_A4_LANDSCAPE_V1",
"pageSpec": { "widthMm": 297, "heightMm": 210 },
"downloadFileName": "FBA托盘标.pdf",
"labels": [{ "fbaWarehouseCode": "SCK8", "palletCode": "MSCU6639870-141-001" }]
}
```
两个业务字段均必填:`fbaWarehouseCode` 为正式 FBA 仓码(1–32 位大写字母、数字或连字符,首位不得为连字符);`palletCode` 为已保存的完整托盘业务编码(1–80 位可打印 ASCII,无首尾空格),不另传条码内容以免文字与条码不一致。`141` 等地址内部编号保留在托盘号中,不取代顶部的 `SCK8`。调用方不重新编号,Print 不裁切、不补号、不反查仓库;缺失或不合法数据返回 `A0422`。每个请求支持 1–2000 张,沿用全局请求体、输出大小及并发上限。文件名必填,最多 120 字符,以 `.pdf` 结尾且无路径、保留字符或控制字符。
Firstleg 从冻结标签的 `destinationWarehouseCode/palletCode` 映射;海外资源服务从当前托盘的 `fbaWarehouseCode/palletCode` 映射。两个入口共用本接口、模板和校验;非 FBA 托盘与头程箱唛使用各自模板,不由 FBA 模板推断类型。
### 2.1 入仓标签
所有头程箱唛(包括一件代发订单的箱唛)均不打印 SKU,标签请求不包含 `skuCode`。箱内 SKU 快照由业务服务保留用于清点、组托;一件代发 A4 托盘信息单继续展示 SKU 明细。二维码只使用稳定箱码。
```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。
### 2.2 海运 A4 单据
```json
{
"templateCode": "SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1",
"pageSpec": {
"widthMm": 210,
"heightMm": 297
},
"downloadFileName": "BYSE2607044-托运书.pdf",
"documents": [
{
"fields": {
"toCompany": "宁波胜泽远航国际物流有限公司",
"shipper": "ZHEJIANG BAIYUE TRADE CO.,LIMITED\nROOM 8613...",
"consignee": "SANATRK FOR IMPORT AND TRADE OF AUTO SPARE PARTS",
"jobNumber": "BYSE2607044"
},
"cargoRows": [
{
"marksAndNos": "N/M",
"quantity": "700",
"packaging": "PACKAGES",
"descriptionOfGoods": "CAR ACCESSORIES\nACID: 4120661061019510011",
"grossWeight": "28000.00 KGS",
"measurement": "28.000 CBM"
}
]
}
]
}
```
| 字段 | 必填 | 规则 |
|---|---|---|
| `templateCode` | 是 | `SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1` 或 `BILL_OF_LADING_CONFIRMATION_A4_V1` |
| `pageSpec.widthMm` | 是 | 固定数值 `210` |
| `pageSpec.heightMm` | 是 | 固定数值 `297` |
| `downloadFileName` | 否 | 最多 120 字符,必须以 `.pdf` 结尾,不得含路径或文件系统保留字符 |
| `documents` | 是 | 1-2000 项;一个元素对应一页,按数组顺序生成 |
| `documents[].fields` | 是 | 只能使用所选模板登记的键;未知键按 `A0422` 拒绝,字段值允许换行,单值最多 4000 字符 |
| `documents[].cargoRows` | 是 | 恰好 1 项;六个货物字段均必填,空值或第二行按 `A0422` 拒绝 |
托运书模板支持的完整 `fields` 键:`toCompany`、`issuerNameCn`、`issuerNameEn`、`issuerAddress`、`issuerPostalCode`、`issuerTel`、`issuerFax`、`shipper`、`consignee`、`notifyParty`、`picName`、`picTel`、`picFax`、`documentName`、`documentTel`、`documentFax`、`jobNumber`、`etdDate`、`serviceTerm`、`freightTerm`、`apCode`、`apName`、`cargoQuantity`、`transshipment`、`partialShipment`、`oceanVesselVoyage`、`portOfLoading`、`portOfDischarge`、`placeOfDelivery`、`totalPackagesInWords`、`remark`、`carrier`、`bookingAgent`、`printDate`。
托运书必填键固定为:`toCompany`、`shipper`、`consignee`、`notifyParty`、`jobNumber`、`cargoQuantity`、`portOfLoading`、`portOfDischarge`、`placeOfDelivery`、`totalPackagesInWords`;其余登记键可选。
提单确认件模板支持的完整 `fields` 键:`shipper`、`consignee`、`notifyParty`、`bookingNumber`、`ensAmsCutoffDate`、`customsCutoffDate`、`expectedDepartureDate`、`oceanVesselVoyage`、`portOfLoading`、`portOfDischarge`、`placeOfDelivery`、`finalDestination`、`serviceTerm`、`freightTerm`、`shipperLoadCountSeal`、`containerSpec`、`shippedOnBoard`、`draftText`、`totalPackagesInWords`、`containerSummary`、`notes`、`issuePlace`、`issueDate`。
提单确认件必填键固定为:`shipper`、`consignee`、`notifyParty`、`bookingNumber`、`portOfLoading`、`portOfDischarge`、`placeOfDelivery`、`serviceTerm`、`freightTerm`、`totalPackagesInWords`;其余登记键可选。
调用方 MUST 只发送所选模板支持的键;Print 会拒绝未知键、缺少必填键、空白必填值及不完整货物行,避免拼写错误生成缺字段 PDF。可选空值可以省略;不得为了填满模板要求用户重复录入订单、客户、航线或货物中已经存在的数据。
未传 `downloadFileName` 时,托运书使用 `{jobNumber}-出口货物托运单.pdf`,提单确认件使用 `{bookingNumber}-提单确认件.pdf`。请求体最大 2 MiB,PDF 最大 32 MiB,成功 `Content-Type` 固定为 `application/pdf`。
## 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);
@PostMapping(value = "/freight-a4-documents", consumes = "application/json")
ResponseEntity<byte[]> generateFreightA4Documents(
@RequestHeader("X-Internal-Service-Token") String internalToken,
@RequestHeader(value = "X-Request-Id", required = false) String requestId,
@RequestBody FreightA4PdfRequest 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) {}
record FreightA4PdfRequest(
String templateCode,
PageSpec pageSpec,
String downloadFileName,
List<FreightDocument> documents) {}
record FreightDocument(Map<String, String> fields, List<CargoRow> cargoRows) {}
record CargoRow(
String marksAndNos,
String quantity,
String packaging,
String descriptionOfGoods,
String grossWeight,
String measurement) {}
}
```
调用封装必须区分 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[]`、完整 `documents[]`、二维码内容或 PDF 二进制。
## 6. 联调检查清单
1. `GET /actuator/health` 返回 `UP`。
2. 使用正确内部令牌提交 1 页,响应为 `application/pdf`。
3. 检查入仓标签页面为严格 `100mm x 100mm`,一件代发托盘信息单及其他 A4 单据为严格 `210mm x 297mm`,`X-Print-Page-Count` 与数组长度一致。
4. 提交多页并检查页序与数组顺序一致。
5. 分别验证错误令牌、非法尺寸、空数组、超过 2000 页和超过 2 MiB 请求体。
6. Admin 下载链路不得在浏览器请求或日志中出现 `X-Internal-Service-Token`。
## 7. AI 实施与验证清单
修改其他业务模块接入 Print 时,按以下顺序实施,不得从 Admin 浏览器直连 Print:
1. 从 OpenAPI 和本文件在调用模块内声明请求 DTO 与 Feign Client;不得新增对 `print-service` Java 包的编译依赖。
2. 在调用模块的 Service 中完成权限、客户归属和数据范围校验,再从已有订单、客户、航线及货物数据自动映射打印字段。
3. `labels[]`、`documents[]` 和 `cargoRows` 顺序必须由调用模块明确决定;A4 V1 的 `cargoRows` 只能包含 0 或 1 项。
4. 同时处理 `application/pdf`、HTTP 200 的 `application/json` 业务错误及 HTTP 413;不得把任意 HTTP 200 当作成功 PDF。
5. 只向浏览器转发安全响应头,不得转发或记录 `X-Internal-Service-Token`,不得记录完整打印请求或 PDF 二进制。
6. 为字段映射、PDF/JSON 响应分支、`A0403`、`A0422`、`A0429`、HTTP 413 和超时增加调用模块测试。
只验证 Print 本身时,从 `baiyue-firstleg/` 构建并启动单个服务;不需要启动业务服务、Nacos 或数据库:
```powershell
mvn -pl print -am package -DskipTests
$env:PRINT_PORT="9207"
$env:SPRING_PROFILES_ACTIVE="dev"
$env:INTERNAL_SERVICE_TOKEN="print-local-contract-test-token-20260806"
$env:SPRING_CLOUD_NACOS_CONFIG_ENABLED="false"
$env:NACOS_DISCOVERY_ENABLED="false"
$env:NACOS_REGISTER_ENABLED="false"
java -jar print\target\print-service-1.0.0-SNAPSHOT.jar
```
启动后向 `POST http://127.0.0.1:9207/internal/print/v1/pdfs/freight-a4-documents` 发送 `Content-Type: application/json`、`Accept: application/pdf, application/json`、上述内部令牌及任意 `X-Request-Id`。托运书验证请求体:
```json
{
"templateCode": "SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1",
"pageSpec": {
"widthMm": 210,
"heightMm": 297
},
"downloadFileName": "BYSE2607044-托运书.pdf",
"documents": [
{
"fields": {
"toCompany": "宁波胜泽远航国际物流有限公司",
"issuerNameCn": "浙江柏悦贸易有限公司(香港)",
"issuerNameEn": "ZHEJIANG BAIYUE TRADE CO.,LIMITED",
"issuerAddress": "地址:浙江省义乌市稠州北路 1399 号新光国际 8613 室",
"issuerPostalCode": "322000",
"issuerTel": "0579-85875557",
"issuerFax": "0579-85601277",
"shipper": "ZHEJIANG BAIYUE TRADE CO.,LIMITED\nROOM 8613,NO.1399,CHOUZHOU NORTH ROAD,FUTIAN STREET,YIWU CITY,ZHEJIANG PROVINCE\nTEL:0579-85695557",
"consignee": "SANATRK FOR IMPORT AND TRADE OF AUTO SPARE PARTS\n3 ATIA ELZEKRY ST- OMAR EBN ELKHTAB CITY SHOBRA\nELKHIMA QALYUBIA SECOND-OWNER/ MENA MATTA GABALLA\nMATTA\nTEL:01225672354",
"notifyParty": "SAME AS CONSIGNEE",
"picName": "苏天翔",
"jobNumber": "BYSE2607044",
"etdDate": "2026/07/24",
"serviceTerm": "CY-CY",
"freightTerm": "FREIGHT PREPAID",
"cargoQuantity": "1*20GP",
"portOfLoading": "NINGBO",
"portOfDischarge": "EL DEKHEILA",
"placeOfDelivery": "EL DEKHEILA",
"totalPackagesInWords": "SAY SIX HUNDRED AND FIFTY NINE PACKAGES ONLY.",
"carrier": "万海航运(香港)股份有限公司",
"bookingAgent": "宁波胜泽远航国际物流有限公司",
"printDate": "2026 年 07 月 16 日"
},
"cargoRows": [
{
"marksAndNos": "N/M",
"quantity": "700",
"packaging": "PACKAGES",
"descriptionOfGoods": "CAR ACCESSORIES\nACID: 4120661061019510011\n申请目的港 21 天免用箱",
"grossWeight": "28000.00 KGS",
"measurement": "28.000 CBM"
}
]
}
]
}
```
提单确认件必须把 `templateCode` 改为 `BILL_OF_LADING_CONFIRMATION_A4_V1`,并仅传上一节列出的提单确认件字段。成功响应必须是 `Content-Type: application/pdf`;`application/json` 必须按响应体 `code/message` 作为失败处理。