# Uniorder：所有货件统一一个接口

Uniorder 是一组统一的接口，集成方通过它对账号的每一票货件进行询价、下单、打印、追踪和取消，无论该货件以何种方式履约。一次询价返回商户自有配送，并可按请求返回账号的每一项承运商面单服务，每项均带有 `rate_id`。下单时回传所选的 `rate_id` 即可；请求中的其他任何内容都不用于选择服务。

## 1. 可以构建的应用

- **一次列出全部运输选项的结账页面。** 客户填写地址，结账页面调用一个接口，页面即可将本地配送与 UPS、Canada Post 及账号使用的其他承运商并列展示，每项附带价格。
- **只需一套代码路径的订单管理或 ERP 连接器。** 来自各渠道的订单均通过相同的下单、查询、面单、追踪和取消调用处理。连接器无需为本地配送和承运商面单分别编写逻辑。
- **夜间批量处理。** 一个排队任务最多可询价或创建 500 票货件，并按任务 id 读取结果。
- **客服界面。** 客服人员查询订单、重新打印面单、查看追踪时间线并取消订单，所有订单均使用相同的四个调用。

## 2. Uniorder 带来的变化

| 不使用 Uniorder | 使用 Uniorder |
|---|---|
| 本地配送订单使用一套 API，承运商面单使用另一套 API，各自有不同的字段和响应 | 所有服务使用同一种请求结构（`from_*`、`to_*`、`packages`）和同一种响应结构 |
| 由集成方决定调用哪个承运商的 API | 询价列出所有服务；由所选报价的 `rate_id` 决定 |
| 每种服务各有单独的面单、追踪和取消接口 | `GET /label`、`GET /tracking` 和 `POST /cancel` 适用于所有订单 |
| 批量下单仅支持本地配送 | 所有服务均支持批量询价和批量下单，可同步或排队执行 |

Uniorder 不取代现有接口；现有接口继续可用且保持不变。新集成建议以 Uniorder 为入口。

## 3. 接口一览（按集成调用顺序）

| 步骤 | 用途 | REST | GraphQL |
|---|---|---|---|
| 1 | 获取访问令牌 | `POST /api/v1/user/login` | `userLogin` |
| 2 | 对所有服务询价 | `POST /api/v1/uniorder/rate` | `uniorderRate` |
| 3 | 按所选报价创建订单 | `POST /api/v1/uniorder` | `uniorderCreate` |
| 4 | 打印面单 | `GET /api/v1/uniorder/{orderId}/label` | `uniorderLabel` |
| 5 | 查询订单 | `GET /api/v1/uniorder/{orderId}` | `uniorder` |
| 6 | 追踪订单 | `GET /api/v1/uniorder/{orderId}/tracking` | `uniorderTracking` |
| 7 | 取消订单 | `POST /api/v1/uniorder/{orderId}/cancel` | `uniorderCancel` |
| — | 购买下单时未能购买的面单 | `POST /api/v1/uniorder/{orderId}/label` | `uniorderPurchaseLabel` |
| — | 一次询价或创建最多 20 行 | `POST /api/v1/uniorder/rate/batch`, `POST /api/v1/uniorder/batch` | `uniorderRateBatch`, `uniorderCreateBatch` |
| — | 排队处理最多 500 行 | `POST /api/v1/uniorder/rate/batch-async`, `POST /api/v1/uniorder/batch-async`, `GET /api/v1/uniorder/jobs/{jobId}` | `uniorderRateBatchAsync`, `uniorderCreateBatchAsync`, `uniorderJob` |

[REST 手册](/api/documentation#/paths/v1-uniorder-rate/post) · [GraphQL 手册](/api/graphql/documentation#/orders/uniorderRate)

逐步的请求、响应和检查项见专题 **一次询价与下单**。

## 4. 示例：提供全部选项的结账页面

蒙特利尔的一家花店在线销售。结账时，它对包裹询价一次，并包含面单承运商：

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -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": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
  }'
```

响应列出一条 `self_delivery` 报价，以及每项承运商服务各一条 `label_service` 报价。结账页面将其作为选项展示；客户选择当日本地配送。使用该报价的 `rate_id` 以及相同的地址和包裹创建订单：

```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",
    "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 }]
  }'
```

响应返回订单 `id` 及其运单号。店铺通过 `GET /api/v1/uniorder/{orderId}/label` 打印面单，并在客户的订单页面上展示 `GET /api/v1/uniorder/{orderId}/tracking` 返回的时间线。

## 5. 示例：每晚发货的 ERP

ERP 在 22:00 导出当天的订单。它将地址发送至 `POST /api/v1/uniorder/rate/batch-async`，轮询该任务直到 `status` 为 `done`，按自身规则为每一行选择报价，再将所选的行发送至 `POST /api/v1/uniorder/batch-async`。每条结果都带有该行的 `reference`，ERP 据此将每条结果与自己的订单行对应。`rate_id` 的有效期为 30 分钟，因此下单任务应在询价任务完成后尽快提交。

## 6. 示例：客服界面

处理客户来电时，客服界面调用 `GET /api/v1/uniorder/{orderId}` 获取状态和地址，调用 `GET /api/v1/uniorder/{orderId}/tracking` 获取时间线和签收凭证，客户取消时调用 `POST /api/v1/uniorder/{orderId}/cancel`。配送订单和面单订单使用相同的调用；通过 `type` 字段区分两者。

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

通过 GraphQL 查询同一订单（[GraphQL 手册](/api/graphql/documentation#/orders/uniorder)）：

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

## 7. 设计时须遵循的规则

- **由 `rate_id` 决定服务。** 其有效期为 30 分钟，且仅对发起询价的账号有效。
- **订单在创建时定价。** `shipping_price` 为实际收取的价格；`quoted_price` 为询价时的价格。两者可能不同。
- **面单订单不会丢失。** 下单时若无法购买面单，订单仍会保留，响应为 `LABEL_PURCHASE_FAILED` 并附带订单 `id`；之后通过 `POST /api/v1/uniorder/{orderId}/label` 购买面单。
- **重试是安全的。** 每次下单调用都应发送 `Idempotency-Key` 请求头；对已取消的订单再次取消时返回 `already_cancelled` `true`。
- **状态统一。** 配送订单的状态为 `pending`、`in_transit`、`out_for_delivery`、`delivered`、`exception` 或 `cancelled`；面单订单的状态为 `label_pending`、`label_purchased` 或 `cancelled`，包裹所在位置由其承运商的追踪信息反映。

## 8. 上线检查清单

- [ ] 账号已开通 API 权限，令牌保存在服务器端，而非浏览器中。
- [ ] 询价时提供完整的发件人和收件人地址。
- [ ] 在询价后 30 分钟内创建订单，并携带 `Idempotency-Key`。
- [ ] 遇到 `LABEL_PURCHASE_FAILED` 时，稍后购买面单，而不是重新创建订单。
- [ ] 通过 `GET /api/v1/uniorder/{orderId}/tracking` 读取追踪信息，或通过 Webhook 接收。
- [ ] 取消遇到 `ORDER_STATUS_NOT_CANCELLABLE` 和 `ORDER_CANCEL_REFUSED` 时，保持订单现状不变。
