# 一次询价与下单

本专题按集成的构建顺序逐个请求地介绍 Uniorder API（`/api/v1/uniorder/...`）：登录、询价、按所选 `rate_id` 下单、打印面单、查询、追踪和取消订单，以及批量处理货件。一次询价即列出账号寄送包裹的所有方式：商户自有配送，以及按请求返回的每项面单承运商服务。使用 `rate_id` 下单即为该服务创建订单：配送订单，或按所询承运商服务购买面单的面单订单。本专题面向通过商户账号发货的网店、订单管理系统和 ERP 的开发人员。

## 1. 可以构建的应用

以下示例均以同一家商户为例：**Fleurs du Plateau**，一家位于 4500 Rue Saint-Denis, Montreal (H2J 2L3) 的花店，在线销售花束。一个典型包裹是一个 1.2 kg、40 × 25 × 25 cm 的盒子，寄给位于 6841 Rue Saint-Denis, Montreal (H2S 2S3) 的 Jane Recipient，网店订单号为 `WEB-10045`。

- **提供全部运输选项的结账页面。** 网店对包裹询价一次，将当日本地配送与账号的每项承运商面单服务并列显示，每项附带价格，然后按客户所选的选项创建订单。
- **自动打印面单。** 订单创建后，网店下载面单 PDF 并发送到包装工位的打印机，无论包裹由商户配送还是由承运商运送。
- **带实时追踪的订单页面。** 客户的订单页面显示货件的状态和事件时间线，花束送达后还显示签收凭证。
- **来自 ERP 的夜间批量处理。** 当天的批发订单在一个最多 500 行的排队任务中完成询价和创建，每条结果按 `reference` 与其订单行对应。

## 2. 本专题的适用范围

本专题是 Uniorder API 的逐步操作指南。Uniorder 提供的功能及其原因见 **Uniorder：所有货件统一一个接口**；本专题给出每个调用的请求、响应和检查项。

对于通过本地配送或承运商面单寄送包裹的新集成，推荐以 Uniorder 作为统一入口：它以一种请求结构取代对本地配送 API 和承运商面单 API 的分别调用。**自有车队取件与配送** 和 **承运商打单** 中介绍的原有接口继续可用且保持不变。Uniorder 不适用于客户账号预订的运输服务，也不适用于仓储与出库订单；这两类请使用 **运输服务** 和 **仓储与出库**。

## 3. 开始之前

- **账号。** 使用具有 API 权限的商户（客户）账号或该商户的员工账号。商户的客户账号也可以调用 Uniorder，且始终以其自身身份询价和计费。创建配送订单需要下单权限。
- **客户。** 商户账号或员工账号可以在询价中通过 `customer_id` 或 `customer_code` 为其某个客户询价和下单；此时 `rate_id` 带有该客户，价格按该客户的价格方案计算。
- **面单服务。** 要获得 `label_service` 报价，账号（或指定的客户）至少需要配置一个面单承运商账号。
- **测试数据。** 对于 `self_delivery` 报价，请使用商户配送区域内的地址，并使用 `WEB-10045` 等之后可取消的测试参考号。
- **令牌。** 从您的服务器请求访问令牌并保存在服务器上。切勿将其发送到浏览器或移动应用。
- **占位符。** 将 `YOUR_HOST` 替换为您所在环境的 API 主机，将 `ACCESS_TOKEN` 替换为第 4 步获得的令牌。

## 4. 登录

每个 Uniorder 调用都以某个账号的身份发起。请从服务器登录一次，保存返回的令牌，并在每个请求中携带。

**REST：** `POST /api/v1/user/login` — [REST 手册](/api/documentation#/paths/v1-user-login/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"email":"orders@fleursduplateau.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```

- `access_token`：将其置于后续每个请求的请求头中：

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL 请求提交至 `POST /api/graphql`，并使用相同的请求头。

**GraphQL：** `userLogin`（[GraphQL 手册](/api/graphql/documentation#/user/userLogin)）

**验证：** 登录成功后返回 `access_token`。后续请求若未携带该令牌，将返回 `401`。

## 5. 对所有服务询价

询价列出包裹的所有寄送方式，每项附带价格和 `rate_id`。结账页面将这些报价作为选项显示；不会创建或预订任何内容。

**REST：** `POST /api/v1/uniorder/rate` — [REST 手册](/api/documentation#/paths/v1-uniorder-rate/post)

寄件人和收件人均须为完整地址；只有 `from_address_2` 和 `to_address_2` 为可选。每个包裹都需要 `weight`、`length`、`width` 和 `height`。将 `quote_labels` 设为 `true` 可加入面单承运商服务；此时两端的姓名和电话均为必填。商户账号或员工账号可以通过 `customer_id` 或 `customer_code` 为其某个客户询价。当价格取决于送货时间窗时，会考虑时间窗（`time_window_start`、`time_window_end`，格式为 `YYYY-MM-DD HH:MM:SS`）。

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "quote_labels": true,
    "packages": [{
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

`weight_unit`：`1` 克，`2` 千克，`3` 盎司，`4` 磅。`dimension_unit`：`1` 毫米，`2` 厘米，`3` 米，`4` 英寸。

```json
{
  "result": true,
  "rates": [
    {
      "rate_id": "eyJpdiI6Ik1rT2Z...",
      "type": "self_delivery",
      "channel_id": null,
      "shipping_price": "14.60",
      "currency": "CAD",
      "price_details": { "shipping_fee": 12.92, "sub_total": "12.92" },
      "warning": null
    },
    {
      "rate_id": "eyJpdiI6IlpxR0...",
      "type": "label_service",
      "shipping_price": "18.40",
      "currency": "CAD",
      "shipping_method_id": 72,
      "shipping_method_name": "UPS",
      "carrier_name": "ups",
      "service_code": "ups_standard",
      "service_name": "UPS STANDARD",
      "transit_days": 3
    }
  ],
  "errors": []
}
```

- `type` `self_delivery`：由商户配送。每次询价最多一条。
- `type` `label_service`：每个面单账号的每项服务各一条。请向客户显示 `service_name`、`shipping_price` 和 `transit_days`。
- `errors` 列出无法询价的项目及其 `type`。配送区域外的地址会产生类型为 `self_delivery`、代码为 `OUT_OF_DELIVERY_AREA` 的错误；此时仅显示面单服务。
- `rate_id` 的有效期为 30 分钟，且仅对发起询价的账号有效。请将其与结账会话一起保存。
- 至少找到一条报价时，`result` 为 `true`。

**GraphQL：** `uniorderRate`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderRate)）。响应为 JSON 标量，因此该操作没有选择集。

```graphql
mutation QuoteBouquet($packages: [Json]!) {
  uniorderRate(
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    quote_labels: true
    packages: $packages
  )
}
```

变量：

```json
{ "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**验证：** 对区域内地址，`rates` 包含一条 `self_delivery` 报价；使用 `quote_labels` 时，每项承运商服务各有一条 `label_service` 报价。不会创建任何内容。

## 6. 按所选报价创建订单

客户付款后，网店使用所选选项的 `rate_id` 和相同的货件信息创建订单。由 `rate_id` 决定服务；请求中的其他任何内容都不用于选择服务。

**REST：** `POST /api/v1/uniorder` — [REST 手册](/api/documentation#/paths/v1-uniorder/post)

每次创建时都应发送 `Idempotency-Key` 请求头，每张订单使用唯一值。使用相同的键和相同的请求体重试时，返回第一次的响应并带有 `replayed` `true`，不会创建第二张订单。

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_email": "jane@example.com",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "delivery_instruction": "Ring the bell at the side door.",
    "packages": [{
      "ref": "WEB-10045-1",
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

`self_delivery` 报价创建配送订单。对于 `type` `D`，收件人为停靠点；将 `need_pick_up` 设为 `1` 可安排在寄件人处取件。对于 `type` `P`，寄件人为停靠点。

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800001"],
  "shipping_price": "14.60",
  "quoted_price": "14.60",
  "currency": "CAD"
}
```

`label_service` 报价创建面单订单，并按所询承运商服务购买面单。`type` 必须为 `D`，且 `from_name`、`from_telephone`、`to_name` 和 `to_telephone` 为必填。若客户选择的是 UPS STANDARD，响应将为：

```json
{
  "result": true,
  "type": "label_service",
  "id": 123457,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800002"],
  "shipping_price": "18.40",
  "quoted_price": "18.40",
  "currency": "CAD",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456784",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `id`：请与网店订单一起保存；之后的每个调用都使用它。
- `tracking_numbers`：货件自身的运单号，每个包裹一个。
- `shipping_price`：实际收取的价格。订单在创建时定价；`quoted_price` 为询价时的价格。两者可能不同。
- `label.main_tracking_number` 和 `label.shipping_label`（仅面单订单）：承运商运单号和 base64 编码的面单 PDF。
- `result` `false` 且代码为 `LABEL_PURCHASE_FAILED`（仅面单订单）：订单已存在，但没有面单。请保留 `id` 并继续执行第 11 步。

**GraphQL：** `uniorderCreate`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderCreate)）

```graphql
mutation CreateBouquetOrder($packages: [Json]!) {
  uniorderCreate(
    rate_id: "eyJpdiI6Ik1rT2Z..."
    ref: "WEB-10045"
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    packages: $packages
  )
}
```

变量：

```json
{ "packages": [{ "ref": "WEB-10045-1", "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**验证：** `result` 为 `true` 且 `id` 已设置。已过期或属于其他账号的 `rate_id` 返回 `400` 及代码 `RATE_ID_INVALID`，且不会创建任何内容。

## 7. 打印面单

订单一经创建，包装工位即打印面单。同一调用对配送订单返回商户自有面单，对面单订单返回已购买的承运商面单。

**REST：** `GET /api/v1/uniorder/{orderId}/label` — [REST 手册](/api/documentation#/paths/v1-uniorder-orderId--label/get)

```bash
curl "https://YOUR_HOST/api/v1/uniorder/123456/label?hide_sender_address=0" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "format": "pdf",
  "tracking_numbers": ["SR26092800001"],
  "pdf_data": "JVBERi0xLjQK..."
}
```

- `pdf_data`：base64 编码的面单 PDF。解码后将文件发送到打印机。
- `hide_sender_address`、`hide_receiver_address`（`1` 表示隐藏）：适用于配送订单的商户自有面单。
- `label_status`（面单订单）：返回文件时为 `ready`。承运商尚未生成文件时，响应为 `200`，带有 `result` `false` 和 `label_status` `pending`；请稍后再次请求面单。
- 本调用不会购买面单：尚未购买的面单返回 `409` 及代码 `LABEL_PURCHASE_FAILED`。请通过第 11 步购买。

**GraphQL：** `uniorderLabel`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderLabel)）

```graphql
query {
  uniorderLabel(order_id: 123456, hide_sender_address: 0)
}
```

**验证：** `result` 为 `true`，解码后的 `pdf_data` 可作为 PDF 打开，并显示该订单的运单号。

## 8. 查询订单

网店查询订单，以便在订单页面或客服界面上显示其状态、地址和包裹。

**REST：** `GET /api/v1/uniorder/{orderId}` — [REST 手册](/api/documentation#/paths/v1-uniorder-orderId/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "order_type": "D",
  "status": "pending",
  "created_at": "2026-10-02 09:14:05",
  "time_window_start": "2026-10-02 13:00:00",
  "time_window_end": "2026-10-02 17:00:00",
  "from": { "name": "Fleurs du Plateau", "address": "4500 Rue Saint-Denis", "city": "Montreal", "postcode": "H2J2L3" },
  "to": { "name": "Jane Recipient", "address": "6841 Rue Saint-Denis", "address_2": "Apt 2", "city": "Montreal", "postcode": "H2S2S3" },
  "packages": [
    { "id": 998877, "ref": "WEB-10045-1", "tracking_number": "SR26092800001", "weight": 1.2 }
  ],
  "shipping_price": "14.60",
  "currency": "CAD"
}
```

- `type`：`self_delivery` 或 `label_service`；其他字段对两者采用相同的结构。
- `status`：配送订单为 `pending`、`in_transit`、`out_for_pickup`、`out_for_delivery`、`ready_for_self_pickup`、`delivered`、`exception` 或 `cancelled`，面单订单为 `label_pending`、`label_purchased` 或 `cancelled`。
- `label`（仅面单订单）：承运商、服务、`carrier_tracking_numbers` 和 `label_status`（`not_purchased`、`pending`、`ready` 或 `failed`）。

**GraphQL：** `uniorder`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorder)）

```graphql
query {
  uniorder(order_id: 123456)
}
```

**验证：** 订单返回其 `status` 和 `packages`，且 `ref` 与网店订单一致。

## 9. 追踪订单

订单页面显示货件的时间线。可在客户打开页面时读取，或通过 Webhook 保持最新。

**REST：** `GET /api/v1/uniorder/{orderId}/tracking` — [REST 手册](/api/documentation#/paths/v1-uniorder-orderId--tracking/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "status": "delivered",
  "tracking_numbers": ["SR26092800001"],
  "events": [
    { "code": "delivered", "description": "Delivered", "location": "Montreal", "time": "2026-10-02 15:42:10", "time_zone": "America/Toronto", "source": "shipper" }
  ],
  "proofs": [
    { "type": "photo", "url": "https://YOUR_HOST/storage/pod/123456.jpg", "uploaded_at": "2026-10-02 15:42:08" }
  ]
}
```

- `events`：时间线，最新的在前，每条带有 `code`、`description`、`location` 和时间。
- `proofs`：签收凭证文件。`status` 为 `delivered` 后显示。
- `carrier`（仅面单订单）：承运商名称、运单号和追踪链接（`tracking_url`）。

**GraphQL：** `uniorderTracking`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderTracking)）

```graphql
query {
  uniorderTracking(order_id: 123456)
}
```

**验证：** 追踪调用返回 `result` `true`、订单 `status` 及其 `events`。

## 10. 取消订单

客户取消网店订单时，网店使用同一调用取消配送订单或面单订单的货件。面单会先在其承运商处作废。

**REST：** `POST /api/v1/uniorder/{orderId}/cancel` — [REST 手册](/api/documentation#/paths/v1-uniorder-orderId--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123456/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-10045" \
  -d '{}'
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "already_cancelled": false,
  "message": "The order has been cancelled."
}
```

- `already_cancelled`：订单在本次调用之前已被取消时为 `true`。按成功处理。
- 订单未被取消时，响应为 `409`，订单保持不变：`ORDER_STATUS_NOT_CANCELLABLE`（为时已晚，无法取消）、`ORDER_CANCEL_REFUSED`（当前无法取消）或 `LABEL_CANCEL_FAILED`（承运商未作废面单）。请保持网店订单为未结状态，并人工处理该货件。

**GraphQL：** `uniorderCancel`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderCancel)）

```graphql
mutation {
  uniorderCancel(order_id: 123456)
}
```

**验证：** `result` 为 `true`。再次取消同一订单返回 `already_cancelled` `true`。

## 11. 稍后购买面单（仅在 LABEL_PURCHASE_FAILED 之后）

本步骤仅适用于创建时响应为 `LABEL_PURCHASE_FAILED` 的面单订单。该响应为 `200`，带有 `result` `false`、代码 `LABEL_PURCHASE_FAILED` 和订单 `id`：订单已保留，但没有面单。请勿再次提交订单；请为该订单购买面单。

**REST：** `POST /api/v1/uniorder/{orderId}/label` — [REST 手册](/api/documentation#/paths/v1-uniorder-orderId--label/post)

未能购买面单的创建请求返回的响应：

```json
{
  "result": false,
  "code": "LABEL_PURCHASE_FAILED",
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "tracking_numbers": ["SR26092800003"],
  "quoted_price": "18.40",
  "message": "The quoted service is not offered for this shipment."
}
```

为订单 `123458` 购买面单：

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123458/label \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: label-10046" \
  -d '{}'
```

面单按创建订单时所选的服务购买。如需按同一账号的其他服务购买，请在请求体中发送第 5 步的新 `label_service` `rate_id`（`{"rate_id": "eyJpdiI6IlpxR0..."}`）。已购买的面单会直接返回，不会再次购买。

```json
{
  "result": true,
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "shipping_price": "18.40",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456791",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `label.shipping_label`：base64 编码的面单 PDF；按第 7 步打印。
- 再次返回 `result` `false` 及 `LABEL_PURCHASE_FAILED`：承运商仍然拒绝。请稍后重试，或使用新的 `rate_id` 按其他服务购买。

**GraphQL：** `uniorderPurchaseLabel`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderPurchaseLabel)）

```graphql
mutation {
  uniorderPurchaseLabel(order_id: 123458)
}
```

**验证：** `result` 为 `true` 且 `label.shipping_label` 包含 PDF，或在承运商生成文件期间 `label.label_status` 为 `pending`。

## 12. 批量处理

批量调用可在一次调用中对多个货件询价或创建，例如 ERP 的批发订单。每一行都按单个调用处理，并返回单个调用会返回的内容；某一行失败不会影响其他行。

**REST：** `POST /api/v1/uniorder/rate/batch` — [REST 手册](/api/documentation#/paths/v1-uniorder-rate-batch/post) · `POST /api/v1/uniorder/batch` — [REST 手册](/api/documentation#/paths/v1-uniorder-batch/post)

每次调用最多 20 行，在同一响应中返回：批量询价使用 `shipments`，批量创建使用 `orders`。每一行的字段与单个调用相同，另可附加一个可选的 `reference`，随其结果一并返回。

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/batch \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-2026-10-01" \
  -d '{
    "orders": [
      {
        "reference": "ERP-7781",
        "rate_id": "eyJpdiI6Ik1rT2Z...",
        "ref": "ERP-7781",
        "from_name": "Fleurs du Plateau",
        "from_telephone": "5145550100",
        "from_address": "4500 Rue Saint-Denis",
        "from_city": "Montreal",
        "from_province": "QC",
        "from_country": "CA",
        "from_postcode": "H2J2L3",
        "to_name": "Jane Recipient",
        "to_telephone": "5145550199",
        "to_address": "6841 Rue Saint-Denis",
        "to_city": "Montreal",
        "to_province": "QC",
        "to_country": "CA",
        "to_postcode": "H2S2S3",
        "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
{
  "result": true,
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `results`：每行一条，带有该行的 `index`、其 `reference`，以及单个调用会返回的 `status` 和 `body`。按 `reference` 将每条结果与其订单行对应。

**REST：** `POST /api/v1/uniorder/rate/batch-async` — [REST 手册](/api/documentation#/paths/v1-uniorder-rate-batch-async/post) · `POST /api/v1/uniorder/batch-async` — [REST 手册](/api/documentation#/paths/v1-uniorder-batch-async/post) · `GET /api/v1/uniorder/jobs/{jobId}` — [REST 手册](/api/documentation#/paths/v1-uniorder-jobs-jobId/get)

最多 500 行，作为一个任务排队处理。调用返回 `job_id`；轮询该任务直到 `status` 为 `done`，然后读取 `results`。在第一个任务仍在排队时再次发送相同的批量请求，将返回第一个任务并带有 `duplicate` `true`。

```bash
curl https://YOUR_HOST/api/v1/uniorder/jobs/8813 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "job_id": 8813,
  "kind": "create",
  "status": "done",
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `status`：`queued`、`done`，或在任务无法处理时为 `failed` 并附带 `message`。
- 任务只执行一次，不会重试。在某行执行前过期的 `rate_id` 会使该行返回 `RATE_ID_INVALID`；请在询价任务完成后尽快发送创建任务。

**GraphQL：** `uniorderRateBatch`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderRateBatch)） · `uniorderCreateBatch`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderCreateBatch)） · `uniorderRateBatchAsync`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderRateBatchAsync)） · `uniorderCreateBatchAsync`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderCreateBatchAsync)） · `uniorderJob`（[GraphQL 手册](/api/graphql/documentation#/orders/uniorderJob)）

```graphql
query {
  uniorderJob(job_id: 8813)
}
```

**验证：** 批量请求每行返回一条结果；异步任务最终达到 `status` `done`。

## 13. 错误处理

| 情形 | HTTP 状态 | 代码 | 集成方的处理 |
|---|---|---|---|
| 必填字段缺失或格式错误 | 400 | `VALIDATION_FAILED` | 更正 `message` 中指出的字段后重新发送请求。 |
| 收件人位于配送区域外（询价） | 200 | `errors` 中的 `OUT_OF_DELIVERY_AREA` | 仅提供 `label_service` 报价。 |
| `rate_id` 已过期、格式错误或属于其他账号 | 400 | `RATE_ID_INVALID` | 重新询价，并使用新的 `rate_id` 创建。未创建任何内容。 |
| 面单订单已创建，但面单未购买 | 200（`result` `false`） | `LABEL_PURCHASE_FAILED` | 保留 `id`；通过 `POST /api/v1/uniorder/{orderId}/label` 购买面单。切勿重新创建订单。 |
| 在面单购买之前请求面单 | 409 | `LABEL_PURCHASE_FAILED` | 通过 `POST /api/v1/uniorder/{orderId}/label` 购买面单。 |
| 订单进度已无法取消 | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | 保持订单现状；另行处理退货。 |
| 订单当前无法取消 | 409 | `ORDER_CANCEL_REFUSED` | 保持订单现状；稍后重试或联系商户。 |
| 承运商未作废面单 | 409 | `LABEL_CANCEL_FAILED` | 订单未更改；稍后重试取消。 |
| 订单或任务不存在，或属于其他账号 | 404 | `ORDER_NOT_FOUND` | 检查与网店订单一起保存的 `id`。 |
| `Idempotency-Key` 被用于不同的请求体 | 409 | `IDEMPOTENCY_CONFLICT` | 不同的请求使用新的键。 |
| 令牌缺失或已过期，或账号无下单权限 | 401 | — | 重新登录；检查账号权限。 |

## 验收清单

请使用 `WEB-10045` 等测试 `ref`：

- [ ] 对区域内地址，询价返回一条 `self_delivery` 报价。
- [ ] 使用 `quote_labels` 时，询价返回 `label_service` 报价，每条带有 `rate_id`。
- [ ] 使用 `self_delivery` 的 `rate_id` 下单，返回 `id` 和 `tracking_numbers`。
- [ ] 使用 `label_service` 的 `rate_id` 下单，返回所询服务的面单。
- [ ] 相同的 `Idempotency-Key` 不会创建第二张订单。
- [ ] 超过 30 分钟的 `rate_id` 返回 `RATE_ID_INVALID`。
- [ ] 每张订单的面单解码后均为可打印的 PDF。
- [ ] 可使用创建时返回的 `id` 查询订单、其面单和追踪信息。
- [ ] 取消测试订单返回 `result: true`；再次取消返回 `already_cancelled: true`。
- [ ] 出现 `LABEL_PURCHASE_FAILED` 后，`POST /api/v1/uniorder/{orderId}/label` 为同一订单购买面单。
- [ ] 两行的批量请求返回两条带有各自 `reference` 的结果。
