Files
baiyue-firstleg/print
2026-09-29 18:47:37 +08:00
..
2026-09-29 18:47:37 +08:00
2026-09-29 18:47:37 +08:00
2026-08-04 18:54:22 +08:00

Print PDF 内部调用契约

本文是其他 Java 业务模块及代码生成 AI 接入 Print 的实现约束,不是终端用户操作手册。Print 是无数据库的内部渲染服务:调用方提交完整打印数据,Print 同步返回一次性 PDF,不保存任务、文件或业务数据。

机器可读 HTTP 契约以 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 亚马逊托盘标

{
  "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 明细。二维码只使用稳定箱码。

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>
{
  "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 单据

{
  "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。

@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:

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。

调用方建议为该客户端单独配置超时:

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 或数据库:

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。托运书验证请求体:

{
  "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 作为失败处理。