# 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`。 - A4 海运单据内部地址:`POST /internal/print/v1/pdfs/freight-a4-documents`。 - 海外仓排车 BOL 内部地址:`POST /internal/print/v1/pdfs/dispatch-bol-documents`;模板固定为 `DISPATCH_BILL_OF_LADING_A4_V1`,每个 document 对应一张 A4、包含 1-12 行明细。 - 本地直连:`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. 请求契约 ### 2.1 入仓标签 ```http POST /internal/print/v1/pdfs/inbound-marks HTTP/1.1 Content-Type: application/json Accept: application/pdf, application/json X-Internal-Service-Token: X-Request-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 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 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