# 卡派查价 — 第三方系统对接指南

本文档面向**宿主业务系统**（如美美与共）开发方，说明如何通过 HTTP API 接入实时卡派（LTL）询价。

> 接口速查见 [`API.md`](./API.md)。示例 JSON 见 [`host-candidates-req.example.json`](./host-candidates-req.example.json)。

---

## 1. 对接前准备

| 项 | 说明 |
|----|------|
| **Base URL** | 由我方提供，例如 `https://if.dev.51track.vip` |
| **推荐方式** | **方式 A**：两步宿主接口，免 Token，服务端同步返回报价 |
| **备用方式** | **方式 B**：Token + 三步接口 + 宿主自行轮询（后期启用鉴权时使用） |
| **协议** | HTTPS + JSON，UTF-8 |

**服务器开关（我方运维配置）：**

```env
HOST_PUBLIC_API_ENABLED=true
HOST_PUBLIC_DEFAULT_CUSTOMER_ID=CUST_001
```

---

## 2. 方式 A：两步接口（推荐，美美与共当前使用）

### 2.1 流程概览

```mermaid
sequenceDiagram
    participant 用户
    participant 宿主前端 as 宿主前端
    participant 查价 as 查价 API

    用户->>宿主前端: 填写地址 + 货物，点「获取报价」
    宿主前端->>查价: ① POST /api/host/quote/candidates
    查价-->>宿主前端: 候选列表 + host_session_id
    宿主前端->>用户: 展示候选，选择提货/派送地址
    用户->>宿主前端: 确认候选
    宿主前端->>查价: ② POST /api/host/quote/submit
    Note over 查价: 内部提交询价 + 轮询，10–420 秒
    查价-->>宿主前端: status=done + quotes
    宿主前端->>用户: 展示 final_total 等
```

| 调用次序 | 接口 | 宿主做什么 | 查价做什么 |
|----------|------|------------|------------|
| 第 1 次 | `POST /api/host/quote/candidates` | 传地址 + 货物 | 返回联想候选 + `host_session_id` |
| 第 2 次 | `POST /api/host/quote/submit` | 传会话 ID + 用户选的候选 | **内部轮询**，直接返回最终报价 |

**宿主无需：** Token、GET 轮询、`quote_id` 管理。

**宿主需要：** 地址候选选择 UI、报价结果 UI、第 2 步 Loading（约 10–30 秒，最长约 7 分钟）。

### 2.2 鉴权

方式 A **不需要**任何 Authorization 头，只需：

```http
Content-Type: application/json
```

### 2.3 第一步：联想地址 + 暂存货物

```http
POST /api/host/quote/candidates
Content-Type: application/json
```

**请求体：**

```json
{
  "pickup_address": {
    "street": "1234 Warehouse Blvd",
    "city": "Los Angeles",
    "state": "CA",
    "zip": "90001"
  },
  "delivery_address": {
    "street": "5678 Distribution Dr",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201"
  },
  "cargo": {
    "weight": { "value": 500, "unit": "kg" },
    "dimensions": { "length": 120, "width": 100, "height": 150, "unit": "cm" },
    "pallet_count": 2,
    "cargo_type": "general_freight"
  }
}
```

| 字段 | 必填 | 说明 |
|------|------|------|
| `pickup_address` / `delivery_address` | 是 | 草稿地址 |
| `*.street` / `*.city` / `*.state` | 是 | 美国地址；`state` 为二字码 |
| `*.zip` | 否 | 美国邮编，可省略或空字符串 |
| `cargo` | 是 | 货物信息（第一步传入后由服务端暂存，第二步无需重复传） |

**成功响应示例：**

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "host_session_id": "eb75548a-2c0b-4282-becd-ca3a0f5a3c15",
    "pickup_candidates": [
      {
        "option_id": "Eisx...",
        "display_label": "1234 Warehouse Street, Los Angeles, CA, USA",
        "formatted_address": "1234 Warehouse Street, Los Angeles, CA, USA",
        "street": "1234 Warehouse Street",
        "city": "Los Angeles",
        "state": "CA",
        "zip": "90001",
        "selectable": true
      }
    ],
    "delivery_candidates": [
      {
        "option_id": "EihE...",
        "display_label": "Distribution Dr, Wilmer, Dallas, TX, USA",
        "formatted_address": "Distribution Dr, Wilmer, Dallas, TX, USA",
        "street": "5678 Distribution Dr, Wilmer",
        "city": "Dallas",
        "state": "TX",
        "zip": "75201",
        "selectable": true
      }
    ],
    "requires_selection": true
  }
}
```

**前端处理：**

- 用 `display_label` 展示给用户。
- 仅允许选择 `selectable: true` 的项；不可选项展示 `unavailable_reason`。
- 保存 `host_session_id`（30 分钟有效）和用户选中的**完整候选对象**。

### 2.4 第二步：确认候选并取报价（同步）

```http
POST /api/host/quote/submit
Content-Type: application/json
```

**请求体：**

```json
{
  "host_session_id": "eb75548a-2c0b-4282-becd-ca3a0f5a3c15",
  "pickup_candidate": {
    "option_id": "Eisx...",
    "display_label": "1234 Warehouse Street, Los Angeles, CA, USA",
    "formatted_address": "1234 Warehouse Street, Los Angeles, CA, USA",
    "street": "1234 Warehouse Street",
    "city": "Los Angeles",
    "state": "CA",
    "zip": "90001",
    "selectable": true
  },
  "delivery_candidate": {
    "option_id": "EihE...",
    "display_label": "Distribution Dr, Wilmer, Dallas, TX, USA",
    "formatted_address": "Distribution Dr, Wilmer, Dallas, TX, USA",
    "street": "5678 Distribution Dr, Wilmer",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "selectable": true
  }
}
```

| 字段 | 必填 | 说明 |
|------|------|------|
| `host_session_id` | 是 | 第一步返回的 UUID |
| `pickup_candidate` | 是 | 第一步 `pickup_candidates` 中用户选中项（完整对象） |
| `delivery_candidate` | 是 | 第一步 `delivery_candidates` 中用户选中项（完整对象） |

**成功响应（节选）：**

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "quote_id": "QTE_20260630_0003",
    "status": "done",
    "currency": "USD",
    "valid_until": "2026-06-30T10:30:00.000Z",
    "quotes": [
      {
        "service_level": "standard",
        "rate_option": "lowest",
        "carrier": "Frontline Freight",
        "transit_days": "Est. 5 business days",
        "final_total": 337.06
      }
    ]
  }
}
```

**展示给用户：** 使用 `quotes[].final_total`（USD）、`carrier`、`transit_days`。

**失败：** HTTP 200 时 `data.status` 可能为 `failed`，读 `error_message`；HTTP 504 表示服务端轮询超时。

### 2.5 美美与共前端职责

| 模块 | 说明 |
|------|------|
| 表单 | 收集地址（仓库代码需先转为 street/city/state/zip）和货物 |
| 地址候选 UI | 第 1 步后展示 `pickup_candidates` / `delivery_candidates` 供用户选择 |
| 报价结果 UI | 第 2 步后展示 `quotes` 列表 |
| Loading | 第 2 步显示「正在查询报价，约需 10–30 秒」，禁止重复提交 |
| 不需要 | Token、轮询、`quote_id` 管理 |

### 2.6 美美与共调用示例（JavaScript）

```javascript
// 第 1 步：联想
async function fetchCandidates(form) {
  const res = await fetch("https://if.dev.51track.vip/api/host/quote/candidates", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      pickup_address: form.pickup,
      delivery_address: form.delivery,
      cargo: form.cargo,
    }),
  });
  const json = await res.json();
  if (json.code !== 0) throw new Error(json.message);
  return json.data; // { host_session_id, pickup_candidates, delivery_candidates }
}

// 第 2 步：确认并取报价（一次请求，无需轮询）
async function submitQuote(hostSessionId, pickupCandidate, deliveryCandidate) {
  const res = await fetch("https://if.dev.51track.vip/api/host/quote/submit", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      host_session_id: hostSessionId,
      pickup_candidate: pickupCandidate,
      delivery_candidate: deliveryCandidate,
    }),
  });
  const json = await res.json();
  if (json.code !== 0) throw new Error(json.message);
  if (json.data.status === "failed") throw new Error(json.data.error_message ?? "询价失败");
  return json.data.quotes;
}
```

---

## 3. 统一响应格式

```json
{ "code": 0, "message": "ok", "data": { } }
```

| 字段 | 说明 |
|------|------|
| `code` | `0` 成功；非 0 为错误码 |
| `message` | 人类可读说明 |
| `data` | 业务数据 |

---

## 4. 货物类型与限制

**`cargo_type` 枚举：**

`general_freight` · `machinery` · `furniture` · `electronics` · `building_materials` · `auto_parts` · `food_nonperishable` · `apparel` · `other`

| 项 | 限制 |
|----|------|
| 托盘数 | 1 – 25 |
| 单托重量 | 0.01 – 9,999 lb |
| 单托尺寸（换算英寸后） | 长 ≤ 999，宽 ≤ 99，高 ≤ 99 |

---

## 5. 自测方法

### 5.1 第一步：联想（PowerShell）

> **注意：** Windows PowerShell 下 `curl.exe -d '{...}'` 容易因引号转义导致 JSON 无效，返回 `请求体格式无效`。**请用 JSON 文件或 Invoke-RestMethod。**

**推荐：JSON 文件**

```powershell
curl.exe -s -X POST "https://if.dev.51track.vip/api/host/quote/candidates" `
  -H "Content-Type: application/json" `
  --data-binary "@deploy/host-candidates-req.example.json"
```

**推荐：Invoke-RestMethod**

```powershell
$body = Get-Content "deploy/host-candidates-req.example.json" -Raw
Invoke-RestMethod -Uri "https://if.dev.51track.vip/api/host/quote/candidates" `
  -Method POST -ContentType "application/json" -Body $body
```

成功标志：`code: 0`，且 `data.host_session_id` 存在。

### 5.2 第二步：取报价

从第一步响应中各取一条 `selectable: true` 的候选，构造 body 调用：

```powershell
# 将 host_session_id、pickup_candidate、delivery_candidate 填入 body 后：
Invoke-RestMethod -Uri "https://if.dev.51track.vip/api/host/quote/submit" `
  -Method POST -ContentType "application/json" -Body $submitJson
```

成功标志：`data.status === "done"`，`data.quotes` 含 `final_total`。

### 5.3 Linux / macOS

```bash
curl -s -X POST "https://if.dev.51track.vip/api/host/quote/candidates" \
  -H "Content-Type: application/json" \
  --data-binary "@deploy/host-candidates-req.example.json"
```

---

## 6. 方式 B：Token 三步接口

后期启用鉴权时，将 `HOST_PUBLIC_API_ENABLED=false`，改用以下流程。**宿主须自行轮询。**

### 6.1 鉴权

```http
Authorization: Bearer <Service Token>
X-Customer-Id: <Customer ID>
Content-Type: application/json
```

### 6.2 流程

| 步骤 | 接口 | 说明 |
|------|------|------|
| 1 | `POST /api/addresses/mothership-candidates` | 仅地址，返回候选 |
| 2 | `POST /api/quotes` | 确认地址 + 货物，返回 `quote_id` + `processing` |
| 3 | `GET /api/quotes/{quote_id}` | 每 2 秒轮询，直到 `done` / `failed` |

第二步 body 须含 `customer_id`、`request_id`（UUID v4）、经 MotherShip 确认的地址字段（`mothership_option_id`、`selected_from_mothership: true` 等）及货物信息。

Token 由我方在 `HOST_SERVICE_TOKENS` 中配置后发放。

---

## 7. 错误码

### 方式 A 常见错误

| code | HTTP | 说明 | 处理 |
|------|------|------|------|
| `VALIDATION_FAILED` | 400 | 参数无效 | 检查字段；message 为「请求体格式无效」时检查 JSON 格式 |
| `FORBIDDEN` | 403 | 公开接口未启用 | 确认 `HOST_PUBLIC_API_ENABLED=true` |
| `SESSION_EXPIRED` | 404 | 会话过期 | 重新调用第一步 |
| `ADDRESS_SESSION_MISMATCH` | 400 | 候选与第一步不匹配 | 使用第一步返回的候选对象 |
| `ADDRESS_NOT_SELECTABLE` | 400 | 选了不可用地址 | 换 `selectable: true` 的候选 |
| `QUOTE_TIMEOUT` | 504 | 询价超时 | 稍后重试 |
| `RATE_LIMITED` | 429 | 限流 | 降低频率 |

### 通用错误

| code | HTTP | 说明 |
|------|------|------|
| `UNAUTHORIZED` | 401 | Token 无效（方式 B） |
| `INTERNAL_ERROR` | 5xx | 服务异常 |

询价业务失败时 HTTP 可能仍为 200，`data.status === "failed"`，见 `error_message`。

---

## 8. 安全与运维

1. 方式 A 当前免 Token，**仅建议在可信内网或已控域名下使用**；上线前改方式 B。
2. 方式 B 的 Token **仅放宿主服务端**，禁止写进浏览器或公开仓库。
3. 服务**未开放 CORS**；浏览器页请勿直接 `fetch` 查价域名，应通过宿主后端或同域代理。
4. 第二步网关/Nginx 须设 `proxy_read_timeout 420s;`。
5. 报价有效期见 `valid_until`，过期需重新询价。

---

## 9. 联系与支持

| 事项 | 说明 |
|------|------|
| 开通 / 环境确认 | 联系查价系统管理员 |
| 接口故障 | 提供 `host_session_id` 或 `quote_id`、发生时间 |
| 文档版本 | 2026-06-30 |
