Skip to content

大批量发票交付

发票 ALL 有三种交付方式,在创建授权时通过 invoice_delivery_mode 选定。三种模式交付的都是同一份原始 invoice.v1 数据, 只是取数通道不同。

模式适用通道
LEGACY(默认)小批量/api/channel/data/invoice/all 分页
CURSOR边拉边处理,内存占用可控POST /api/v2/channel/invoices/cursor
PACKAGE大批量离线导入状态 → 清单 → 逐片下载

只影响发票 ALL

CURSOR / PACKAGE 不影响 Worker 原始文件,也不影响发票 STANDARD。 PACKAGE 模式同时可用游标接口;CURSOR 模式不能用分片接口。

V2 接口强制签名

三个 V2 POST 接口必须同时携带 X-Channel-KeyX-Channel-TimestampX-Channel-NonceX-Channel-Signature,见鉴权与签名。 请求体禁止额外字段,且不需要 channelCode

CURSOR · 游标续拉

POST /api/v2/channel/invoices/cursor

json
{
  "channelOrderNo": "BANK-20260812-0001",
  "orderNo": "1e3f2f55-8da4-4ba9-b941-84860eb1d243",
  "limit": 500
}
字段说明
cursor上一批返回的 nextCursor;首次调用省略
limit本批条数,默认 500,服务端上限 2,000;也接受 pageSize

响应:

json
{
  "success": true,
  "data": {
    "schemaVersion": "invoice.v1",
    "datasetId": "8c1a0b2e-8b1e-4a35-9a54-1f1b6b0b8e01",
    "status": "READY",
    "total": 2841,
    "count": 500,
    "invoiceCount": 1204,
    "invoiceCountStatus": "VERIFIED",
    "records": [],
    "nextCursor": "eyJ2IjoxLCJzbmFwc2hvdCI6Ii4uLiJ9.abcdef",
    "hasMore": true,
    "expiresAt": "2026-08-19T02:00:00Z"
  }
}

终止条件:hasMore === falsenextCursor === null。 游标有效期 24 小时,过期返回 INVOICE_CURSOR_EXPIRED

python
cursor = None
while True:
    body = signed_post(
        "/api/v2/channel/invoices/cursor",
        {
            "channelOrderNo": ORDER_ID,
            "orderNo": job_id,
            "cursor": cursor,
            "limit": 500,
        },
    )
    data = body["data"]
    store_batch(data["records"])       # 逐批落库,以 cursor 做幂等
    if not data["hasMore"] or not data["nextCursor"]:
        break
    cursor = data["nextCursor"]

PACKAGE · 分片包

mermaid
sequenceDiagram
    participant S as 客户服务端
    participant A as 平台 API
    participant D as 签名下载地址

    S->>A: POST package/status
    A-->>S: PENDING / BUILDING
    S->>A: POST package/status(重试)
    A-->>S: READY + exportId
    S->>A: POST package/manifest(exportId)
    A-->>S: parts[] + sha256 + downloadUrl
    loop 每个分片
        S->>D: GET downloadUrl
        D-->>S: gzip 分片
        S->>S: 校验 sha256,按 partNo 合并
    end

第 1 步 · 查询状态

POST /api/v2/channel/invoices/package/status

json
{
  "channelOrderNo": "BANK-20260812-0001",
  "orderNo": "1e3f2f55-8da4-4ba9-b941-84860eb1d243"
}

调用本接口会触发分片包的准备流程,可安全重复调用。 status 变为 READY 后才能取清单。

json
{
  "success": true,
  "data": {
    "mode": "PACKAGE",
    "schemaVersion": "invoice.v1",
    "datasetId": "8c1a0b2e-8b1e-4a35-9a54-1f1b6b0b8e01",
    "status": "READY",
    "recordCount": 2841,
    "exportId": "3f4d1e77-2c9a-4b21-8f60-5d7c0a1b2c3d",
    "partCount": 3,
    "totalSize": 5242880,
    "manifestSha256": "…",
    "manifestEndpoint": "/api/v2/channel/invoices/package/manifest",
    "expiresAt": "2026-08-19T02:00:00Z"
  }
}

第 2 步 · 获取清单

POST /api/v2/channel/invoices/package/manifest

json
{
  "channelOrderNo": "BANK-20260812-0001",
  "orderNo": "1e3f2f55-8da4-4ba9-b941-84860eb1d243",
  "exportId": "3f4d1e77-2c9a-4b21-8f60-5d7c0a1b2c3d"
}
json
{
  "success": true,
  "data": {
    "format": "JSONL",
    "compression": "gzip",
    "totalRecords": 2841,
    "chunkSize": 1000,
    "partCount": 3,
    "parts": [
      {
        "partNo": 1,
        "fileName": "invoices-part-1.jsonl.gz",
        "recordCount": 1000,
        "firstSequence": 1,
        "lastSequence": 1000,
        "fileSize": 1747626,
        "sha256": "…",
        "downloadUrl": "https://rpa-api.xiaowu005.xyz/api/v2/channel/invoices/packages/…/parts/1/download?expires=…&signature=…"
      }
    ]
  }
}

第 3 步 · 下载分片

GET /api/v2/channel/invoices/packages/{export_id}/parts/{part_no}/download

下载规则

  • 直接使用响应中的 downloadUrl,不得自行拼接 URL。
  • 不要为下载请求附加渠道签名请求头 —— 链接本身已带 expiressignature
  • 下载后必须用清单中的 sha256 校验,再按 partNo 顺序合并。
  • 支持断点续传(Accept-Ranges: bytes)。
python
import gzip
import hashlib
import json
import requests

for part in manifest["parts"]:
    raw = requests.get(part["downloadUrl"], timeout=300).content
    if hashlib.sha256(raw).hexdigest() != part["sha256"]:
        raise RuntimeError(f"分片 {part['partNo']} 校验失败")
    for line in gzip.decompress(raw).decode("utf-8").splitlines():
        store_record(json.loads(line))

错误处理

HTTPcode处理
400INVOICE_CURSOR_INVALID游标不属于本数据集,从头重拉
400INVOICE_EXPORT_NOT_FOUNDexportId 不属于本订单,重新查状态
404INVOICE_ORDER_NOT_FOUND核对 channelOrderNoorderNo
409INVOICE_DELIVERY_MODE_MISMATCH本单未申请该交付模式
409INVOICE_DATASET_NOT_READY快照未就绪,等产品成功后重试
409INVOICE_EXPORT_NOT_READY继续查询 package/status
409INVOICE_DATASET_EXPIRED / INVOICE_CURSOR_EXPIRED / INVOICE_EXPORT_EXPIRED停止续拉,联系平台
409CHANNEL_REQUEST_REPLAYEDNonce 重复,换新 Nonce 重发

已签发的链接继续有效

已签发的 CURSOR、快照、PACKAGE、分片哈希及下载地址在字段兼容升级后继续有效; 新任务和明确重建的 PACKAGE 才会包含新的兼容键。

适配平台 1.5.3 · 客户交付 R4.4;本文档仅供已签约渠道客户使用。