# 美美与共查价系统 · 宿主 OpenAPI 技术对接文档

**版本**：1.2  
**更新日期**：2026-07-22  
**适用对象**：第三方宿主系统（服务端对接 / 网页嵌入）

---

## 1. 文档说明

本文档供第三方宿主系统对接「美美与共查价中台」使用。

中台对接 **两个官网**，每个官网各有 **免账号 / 登录后** 两种查价方式，合计 **四条互斥路线**：

| 路线 ID | 官网 | 账密 | 说明 |
|---------|------|------|------|
| `MS_GUEST` | MotherShip | 无 | 免登录 Widget / Direct 路径 |
| `MS_LOGGED_IN` | MotherShip | 有（该客户已绑定 MotherShip 账密） | 官网 Create shipment 登录态路径 |
| `FLOCK_GUEST` | Flock Freight | 无 | 免登录 Direct（失败可回退 DOM） |
| `FLOCK_LOGGED_IN` | Flock Freight | 有（该客户已绑定 Flock 账密） | 强制官网登录 DOM，禁止 Direct |

**核心规则**：根据「用户/宿主上传的承运商账密」与「本票货物/地址参数是否符合某官网硬限」判定路线后，**整票只走一条路线**，禁止混用上限、混用接口、混用结果字段。

本文档仅列出对调用方有意义且可依赖的字段。响应中如出现未在本文档定义的其他字段，均属于平台保留字段，调用方不应依赖。

**对接形态**：

| 形态 | 说明 | 见章节 |
|------|------|--------|
| OpenAPI 服务端调用 | 宿主后端带 API Key 调两步查价接口 | §2～§6 |
| 网页嵌入（iframe） | 宿主页面嵌查价 UI；**须直传类型+Key，跳过登录页** | §2.5 |

> **安全要求**：API Key、承运商官网账密仅允许保存在宿主服务端或经我方管理端加密存储；禁止写入宿主公开前端仓库、移动端安装包。OpenAPI 调用禁止把 Key 放进 URL；iframe 嵌入仅允许由**宿主服务端**在生成 `iframe.src` 时短时拼入查询串（见 §2.5）。

---

## 2. 接入信息

### 2.1 基础地址

| 环境 | Base URL |
|------|----------|
| 当前联调 / 演示 | `https://if.dev.51track.vip` |

> 公网访问**不要**追加端口（如 `:30325`）。`:30325` 仅用于服务器本机调试。

嵌入页路径：`{Base URL}/embed-demo`

### 2.2 请求格式

- 请求方法：以各接口定义为准
- 请求体：JSON
- 字符编码：UTF-8
- Content-Type：`application/json`

### 2.3 鉴权请求头（OpenAPI）

| 请求头 | 必填 | 说明 |
|--------|------|------|
| Authorization | 是 | `Bearer <API Key>`；Key 通常以 `chj_` 开头，由我方管理端发放 |
| X-Customer-Id | 是 | 客户 ID，必须与 API Key 绑定一致 |
| Content-Type | 是 | 固定为 `application/json` |

本次联调凭证（示例，正式值由我方单独发放）：

| 配置项 | 值 | 说明 |
|--------|-----|------|
| API Key | `<由我方发放>` | 放入 `Authorization: Bearer ...` |
| Customer ID | `<由我方发放>` | 放入 `X-Customer-Id` |

> **注意（OpenAPI）**：API Key 不得放入 URL、请求体或业务日志明文。

**请求头示例**：

```http
Authorization: Bearer chj_xxxxxxxxxxxxxxxxxxxxx
X-Customer-Id: CUST_001
Content-Type: application/json
```

内网联调若使用环境 Token（非正式第三方形态）：

```http
Authorization: Bearer demo-host-token
X-Customer-Id: CUST_001
Content-Type: application/json
```

### 2.4 通用响应结构

| 字段 | 类型 | 说明 |
|------|------|------|
| code | Number / String | `0` 表示接口调用成功；非 `0` 表示鉴权、参数或业务处理失败 |
| message | String | 提示信息；成功时通常为 `ok` |
| data | Object / null | 成功时的业务数据；失败时多为 `null` |

### 2.5 网页嵌入（iframe / CC 等宿主）— 跳过登录页

#### 2.5.1 目标

宿主将查价系统嵌进自有页面时：

1. **用户不应再看到**嵌入页「客户登录」表单。  
2. 宿主传入 **登录类型 + 凭证** 后，直接进入 **MotherShip / Flock Freight** 两模块门户。  
3. 模块内可再选「免账号 / 登录后」；登录后路线需已绑定承运商官网账密（可在嵌入页内配置）。

#### 2.5.2 URL 查询参数

| 参数 | 必填 | 说明 |
|------|------|------|
| `login_type` | 建议 | `api_key` 或 `password`；别名：`type`、`lt` |
| `api_key` | Key 登录必填 | 客户 API Key；别名：`key`、`token` |
| `customer_id` | 密码登录必填 | 客户编号；别名：`customerId` |
| `password` | 密码登录必填 | 嵌入页客户密码；别名：`pwd` |
| `embed` | 可选 | `1` / `true`：宿主托管壳（隐藏「退出登录」）；别名：`hosted`、`from_cc`、`from_host` |

仅带 `api_key`（或 `key`）且未写 `login_type` 时，按 **`api_key` 登录**处理。

#### 2.5.3 推荐写法（CC：API Key）

```text
https://if.dev.51track.vip/embed-demo?login_type=api_key&api_key=<API_Key>&embed=1
```

短写：

```text
https://if.dev.51track.vip/embed-demo?type=api_key&key=<API_Key>&embed=1
```

**iframe 示例（Key 须由宿主后端渲染进 HTML，禁止写死在公开前端仓库）**：

```html
<iframe
  title="查价"
  src="https://if.dev.51track.vip/embed-demo?login_type=api_key&api_key=chj_xxxxxxxx&embed=1"
  style="width:100%;height:900px;border:0;"
  allow="clipboard-read; clipboard-write"
></iframe>
```

账号密码直登（较少用）：

```text
https://if.dev.51track.vip/embed-demo?login_type=password&customer_id=CUST_001&password=<密码>&embed=1
```

#### 2.5.4 行为与安全

| 步骤 | 行为 |
|------|------|
| 1 | 打开嵌入页 → 读取查询串 → 调用 `POST /api/embed-demo/login` |
| 2 | 成功 → 写入嵌入会话 Cookie → 进入两模块门户（**不展示登录页**） |
| 3 | 前端 `history.replaceState` 去掉地址栏中的 Key/密码等敏感参数 |
| 4 | 失败 → 展示错误文案，并可回落手动登录表单 |

安全约定：

- Key **只允许**宿主服务端拼进 `iframe.src`；用户浏览器地址栏会短暂可见，登录成功后会被剥离。  
- 生产环境嵌入 Cookie 使用 `SameSite=None; Secure`，以支持跨站 iframe。  
- 勿在公开仓库、前端构建产物、CDN 静态配置中明文提交 API Key。  
- OpenAPI 仍须走 `Authorization` 头（§2.3），与嵌入 URL 直传互不替代。

#### 2.5.5 常见错误

| 现象 | 可能原因 | 处理 |
|------|----------|------|
| 仍停在登录页 | 未传 `api_key` / `login_type`，或 Key 为空 | 检查 iframe `src` |
| 提示 API Key 无效 | Key 错误、已轮换、或客户停用 | 管理端核对 Key 与客户状态 |
| 提示缺少 Authorization | 宿主在调 OpenAPI 时未带鉴权头 | 与嵌入无关；按 §2.3 补头 |
| 登录成功但刷新掉登录 | 跨站 Cookie 被拦 / 非 HTTPS | 确认生产 HTTPS；Cookie 为 None+Secure |

**成功响应示例**（OpenAPI 通用）：

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

**失败响应示例**：

```json
{
  "code": "VALIDATION_FAILED",
  "message": "请填写 2 位州码",
  "data": null
}
```

> 调用方不能仅根据 HTTP 状态码判断业务成功。仅当 `code = 0` 时，才能判定本次接口调用成功。  
> 异步询价业务失败时，轮询接口可能仍返回 HTTP 200 且 `code = 0`，此时须检查 `data.status`：若为 `failed`，按失败处理并展示 `data.error_message`。

---

## 3. 路线判定（必读）

宿主在发起询价前，按下列顺序 **确定唯一路线**，然后只调用该路线对应接口与字段。

### 3.1 判定流程

```
1) 宿主明确选择官网，或按参数硬限自动匹配：
   - 符合 MotherShip 硬限且业务选 MotherShip → 进入 MS_*
   - 符合 Flock 硬限且业务选 Flock → 进入 FLOCK_*
   - 两网站都不符合 → 拒绝询价，提示用户改货改地址
   - 禁止：同一票并行打两家再拼结果（除非宿主产品明确要求「比价」且各自独立展示）

2) 查该客户在我方是否已绑定对应官网账密（管理端「查价网站账密」）：
   - MotherShip 已绑账密 → 必须走 MS_LOGGED_IN（服务端强制登录态，禁止匿名 Direct/缓存价）
   - MotherShip 未绑账密 → 走 MS_GUEST
   - Flock 已绑账密 → 必须走 FLOCK_LOGGED_IN（服务端强制 DOM 登录，禁止 Direct）
   - Flock 未绑账密 → 走 FLOCK_GUEST

3) 账密与参数必须同属一条路线：
   - 有 Flock 账密但货物只符合 MotherShip → 只能走 MS_*（Flock 账密本票不用）
   - 有 MotherShip 账密但货物只符合 Flock → 只能走 FLOCK_*
   - 有账密却按免账号字段凑合提交：服务端仍可能强制登录态，价格口径与免账号不同，宿主须按登录后字段规范传参
```

### 3.2 两官网硬限对照（换算后英制）

| 项 | MotherShip（MS_*） | Flock（FLOCK_*） |
|----|--------------------|------------------|
| 地址形态 | 街道 + 城市 + 州码（建议邮编）；须经候选确认 | **仅邮编**（5 位）；提货邮编 ≠ 派送邮编 |
| 托盘 / 件数 | 1～25 | 免账号 **4～20**；登录后 `form_mode=logged_in_quick` 时 1～999 |
| 单托重量 | 0.01～9999 lb；登录后 Weight each ≤ **5000 lb** | 按**整票总重**，≤ **45000 lb** |
| 尺寸 | L≤999 in，W/H≤99 in | L≤636 in，W≤102 in，H≤108 in |
| 其它 | 整票总重 ≤ 249975 lb | 免账号须满足拖车线性英尺（约 53'） |

重量 / 尺寸支持 `lb`/`kg`/`t`（公制吨）、`in`/`cm`/`m` 入参，服务端统一换算为 lb/in（小数进一取整）。宿主应在提交前按目标官网硬限拦截。

### 3.3 接口总览

| 路线 | 推荐调用链 | 等待方式 | 典型耗时 |
|------|------------|----------|----------|
| `MS_GUEST` | `POST /api/host/quote/candidates` → `POST /api/host/quote/submit` | **同步**（submit 内等待） | 10～420 s |
| `MS_LOGGED_IN` | `POST /api/addresses/mothership-suggest`（可选）→ 组地址 → `POST /api/quotes` → `GET /api/quotes/{quote_id}` | **异步轮询** | 轮询总窗口建议 ≥ 420 s |
| `FLOCK_GUEST` | `POST /api/flock/quotes` → `GET /api/quotes/{quote_id}` | **异步轮询** | 建议 ≥ 210 s（可配置） |
| `FLOCK_LOGGED_IN` | 同上；`flock_input.form_mode = "logged_in_quick"` | **异步轮询** | 建议 ≥ 210 s |

> `MS_GUEST` 也可改用异步三步（`POST /api/quotes` + 轮询），但第三方默认同步对接请用宿主两步接口。  
> 已绑定 MotherShip 账密时，即便误调宿主两步接口，服务端仍会强制登录态 RPA；宿主两步**不支持**登录后扩展字段（`cargo_lines` / `mothership_details` 等），完整登录后能力请走 `MS_LOGGED_IN` 异步接口。

---

## 4. 承运商账密（路线开关）

账密由我方管理端按客户绑定，RPA 运行时解密使用，**不进入询价请求体**。

### 4.1 管理端接口（内部运维，非宿主日常调用）

```http
GET  /api/admin/customers/{customer_id}/provider-credentials
PUT  /api/admin/customers/{customer_id}/provider-credentials
```

需管理员 JWT。PUT 示例：

```json
{
  "provider": "mothership",
  "email": "user@example.com",
  "password": "********"
}
```

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| provider | String | 是 | `mothership` 或 `flock` | 对应官网 |
| email | String | 是 | 非空 | 官网登录邮箱 |
| password | String | 是 | 非空 | 官网登录密码 |

**影响**：

- 绑定成功后，该客户对该 `provider` **自动切换到对应 `*_LOGGED_IN` 路线**。
- 账密错误时询价可能返回 `PROVIDER_LOGIN_FAILED`（或业务失败 `error_message` 提示确认账密）。
- 更换账密须按管理端策略操作；勿在业务日志打印明文密码。

宿主侧若自行收集用户官网账密，应通过约定安全通道交给我方写入管理端，或由双方约定的 BFF 代写；**询价 OpenAPI 请求体中不要传官网密码**。

---

## 5. 路线 A：MotherShip 免账号（`MS_GUEST`）

适用：客户**未**绑定 MotherShip 账密；参数符合 MotherShip 硬限。

### 5.1 调用流程

1. 带鉴权头调用「获取地址候选」。
2. 保存 `host_session_id`（约 30 分钟有效）。
3. 用户从 `pickup_candidates`、`delivery_candidates` 各选 1 条 `selectable = true`。
4. 将**完整候选对象**与 `host_session_id` 提交「提交并获取报价」。
5. 第二步可能持续 **10～420 秒**；客户端与网关超时建议 **≥ 420 秒**。
6. `status = done` 展示 `quotes[]`；`failed` 展示中文 `error_message`。

> 第二步期间展示 Loading，并禁止重复提交。

### 5.2 第一步：获取地址候选

#### 接口地址

`POST /api/host/quote/candidates`

#### 请求参数

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| pickup_address | Object | 是 | 见下表 | 提货草稿地址 |
| delivery_address | Object | 是 | 见下表 | 派送草稿地址 |
| cargo | Object | 是 | 见下表 | 货物信息 |

**地址对象（pickup_address / delivery_address）**

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| street | String | 是 | 非空 | 街道地址 |
| city | String | 是 | 非空 | 城市 |
| state | String | 是 | 固定 2 位 | 美国州二字码，如 `CA`、`TX` |
| zip | String | 否 | `12345` 或 `12345-6789`；可空 | 邮编；建议传 |

**货物对象（cargo）**

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| weight.value | Number | 是 | 正数 | 单托重量数值 |
| weight.unit | String | 是 | `lb`、`kg` 或 `t`（公制吨）；别名 `lbs`/`pound(s)`/`tonne(s)`；**禁止** `ton`/`tons`（美制短吨歧义） | 重量单位 |
| dimensions.length / width / height | Number | 是 | 正数 | 单托尺寸 |
| dimensions.unit | String | 是 | `in`、`cm` 或 `m`；别名 `inch`/`meter` 亦可 | 尺寸单位 |
| pallet_count | Number | 是 | 整数 1～25 | 托盘数 |
| cargo_type | String | 是 | 见 §12.1 | 货物类型枚举 |

**请求示例**：

```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"
  }
}
```

#### 响应数据

| 字段 | 类型 | 说明 |
|------|------|------|
| data.host_session_id | String (UUID) | 第二步必传；约 30 分钟有效 |
| data.pickup_candidates | Array\<Object\> | 提货候选 |
| data.delivery_candidates | Array\<Object\> | 派送候选 |
| data.requires_selection | Boolean | 是否需要用户选择 |

**候选对象字段**

| 字段 | 类型 | 说明 |
|------|------|------|
| option_id | String | 候选唯一标识 |
| display_label | String | 展示文案 |
| formatted_address | String | 格式化地址 |
| street / city / state / zip | String | 结构化地址 |
| selectable | Boolean | 仅可选 `true` |
| unavailable_reason | String | 不可选原因（可能出现） |

#### 调用后处理规则

1. 必须保存 `host_session_id`。
2. 提货、派送各选 1 条 `selectable = true`。
3. 第二步必须回传第一步返回的**完整候选对象**，禁止只传 `option_id`，禁止自行拼装。
4. 会话过期后须重新调用本接口。

### 5.3 第二步：提交候选并获取报价

#### 接口地址

`POST /api/host/quote/submit`

#### 请求参数

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| host_session_id | String | 是 | UUID；来自第一步 | 宿主会话 ID |
| pickup_candidate | Object | 是 | 第一步完整候选 | 提货地址 |
| delivery_candidate | Object | 是 | 第一步完整候选 | 派送地址 |

#### 超时与重试

- 客户端 HTTP 超时：`420000` ms。
- 网关 `proxy_read_timeout` ≥ 420 秒。
- `SESSION_EXPIRED`：重新走第一步。
- `QUOTE_TIMEOUT`：可稍后重试；建议重新获取候选。

#### 响应数据

| 字段 | 类型 | 说明 |
|------|------|------|
| data.quote_id | String | 报价单号 |
| data.request_id | String | 请求标识 |
| data.status | String | `done` / `failed` |
| data.currency | String | 固定 `USD` |
| data.valid_until | String | ISO 有效期 |
| data.source_type | String | `rpa` / `cache` / `stale` 等 |
| data.quotes | Array\<Object\> | 档位列表 |
| data.error_code / error_message | String | 失败时 |

**quotes[] 字段**

| 字段 | 类型 | 说明 |
|------|------|------|
| service_level | String | 如 `standard`、`guaranteed` |
| rate_option | String | 如 `lowest`、`fastest`、`bestValue` |
| carrier | String | 承运商 |
| transit_days | String | 时效描述 |
| final_total | Number | **对客户展示价（USD，已含加价）** |

> 档位数量不固定，按实际列表展示。

**成功响应示例**：

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

---

## 6. 路线 B：MotherShip 登录后（`MS_LOGGED_IN`）

适用：客户**已**绑定 MotherShip 账密；参数符合 MotherShip 硬限（含登录后 Weight each ≤ 5000 lb）。

服务端检测到账密后会强制登录态查价，禁止命中匿名 Direct / L2 缓存价。

### 6.1 调用流程

1. （推荐）用 `POST /api/addresses/mothership-suggest` 按关键字联想，用户点选候选。
2. 组装完整询价体（含地址确认字段 + 可选登录后字段）。
3. `POST /api/quotes` → 得到 `quote_id`，`status` 多为 `processing`。
4. 轮询 `GET /api/quotes/{quote_id}` 至 `done` / `failed`（间隔建议 2 s，总窗口 ≥ 420 s）。
5. 成功时读 `data.quotes[]`（结构同 §5.3）。

### 6.2 地址联想（可选但推荐）

#### 接口地址

`POST /api/addresses/mothership-suggest`

#### 请求参数

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| customer_id | String | 是 | 与鉴权客户一致 | 客户 ID |
| query | String | 是 | 去空白后 ≥ 3 字符 | 用户键入的地址关键字 |

**请求示例**：

```json
{
  "customer_id": "CUST_001",
  "query": "1234 Warehouse"
}
```

#### 响应数据

| 字段 | 类型 | 说明 |
|------|------|------|
| data.candidates | Array\<Object\> | 候选列表（字段同 §5.2 候选对象） |

用户选定后，须把候选映射进 `pickup_address` / `delivery_address`（见下节必填确认字段）。

### 6.3 提交询价

#### 接口地址

`POST /api/quotes`

#### 请求参数（核心）

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| request_id | String | 是 | 非空；建议 UUID | 幂等 / 追踪 |
| customer_id | String | 是 | 与鉴权一致 | 客户 ID |
| pickup_address | Object | 是 | 见下表 | 提货地址（须已确认） |
| delivery_address | Object | 是 | 见下表 | 派送地址（须已确认） |
| weight | Object | 是 | value 正数；unit `lb`/`kg`/`t` | 单托重量（兼容字段） |
| dimensions | Object | 是 | unit `in`/`cm`/`m` | 单托尺寸（兼容字段） |
| pallet_count | Number | 是 | 1～25 | 托盘数；多行时建议为各行 quantity 合计 |
| cargo_type | String | 是 | 见 §12.1 | 货物类型 |
| service_level | String | 否 | `standard` / `guaranteed` | 服务等级 |
| rate_option | String | 否 | `lowest` / `fastest` | 价格偏好 |
| ready_date | String | 否 | `YYYY-MM-DD`；周末会拨到下一工作日 | 可提货日 |
| ready_time | String | 否 | ≤32 字符 | 可提货时间 |
| pickup_accessorials | Array\<String\> | 否 | 每项 ≤64；最多 20 | 提货附加服务 id |
| delivery_accessorials | Array\<String\> | 否 | 同上 | 派送附加服务 id |
| cargo_lines | Array\<Object\> | 否 | 1～20 行 | 登录后多行货物（RPA 按行填）；**字段已为英制，不做 kg/cm/m/t 换算** |
| mothership_details | Object | 否 | 见下 | 二级 Details（联系人/FBA 等） |

**地址对象必填确认字段（与免账号草稿不同）**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| street / city / state | String | 是 | 结构化地址；state 2 位 |
| zip | String | 否 | 可空字符串 |
| place_id | String | 是 | 通常用候选 `option_id` |
| formatted_address | String | 是 | 完整展示地址 |
| selected_from_suggestions | Boolean | 是 | 必须为 `true` |
| mothership_option_id | String | 是 | 候选 `option_id` |
| mothership_display_label | String | 是 | 候选 `display_label` |
| selected_from_mothership | Boolean | 是 | 必须为 `true` |

**cargo_lines[]**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| cargo_type | String | 是 | 行货物类型文案/枚举 |
| quantity | Number | 是 | 正数，≤999 |
| weight_lb | Number | 是 | 该行单件重量 **lb**（须已换算；不接受 kg/t）建议 ≤5000 |
| length_in / width_in / height_in | Number | 是 | 该行尺寸 **英寸**（须已换算；不接受 cm/m） |

**mothership_details（节选）**

| 字段 | 类型 | 说明 |
|------|------|------|
| pickup / delivery | Object | `company_name`、`suite`、联系人姓名邮箱电话、`reference`、`notes`、`opens_at`、`closes_at` |
| request_delivery_appointment | Boolean | 是否预约送货 |
| fba_number / fba_po_number | String | FBA 相关 |
| cargo[] | Array | `description`、`nmfc`、`hazmat`、`alcohol`、`tobacco`、件数类型与数量等 |

**请求示例（登录后最小可用）**：

```json
{
  "request_id": "2dd352e6-9a1d-4563-8f13-ebff18aabdb9",
  "customer_id": "CUST_001",
  "pickup_address": {
    "street": "1234 Warehouse Street",
    "city": "Los Angeles",
    "state": "CA",
    "zip": "90001",
    "place_id": "Eisx...",
    "formatted_address": "1234 Warehouse Street, Los Angeles, CA, USA",
    "selected_from_suggestions": true,
    "mothership_option_id": "Eisx...",
    "mothership_display_label": "1234 Warehouse Street, Los Angeles, CA, USA",
    "selected_from_mothership": true
  },
  "delivery_address": {
    "street": "5678 Distribution Dr, Wilmer",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "place_id": "EihE...",
    "formatted_address": "Distribution Dr, Wilmer, Dallas, TX, USA",
    "selected_from_suggestions": true,
    "mothership_option_id": "EihE...",
    "mothership_display_label": "Distribution Dr, Wilmer, Dallas, TX, USA",
    "selected_from_mothership": true
  },
  "weight": { "value": 1103, "unit": "lb" },
  "dimensions": { "length": 48, "width": 40, "height": 60, "unit": "in" },
  "pallet_count": 2,
  "cargo_type": "general_freight",
  "ready_date": "2026-07-22",
  "cargo_lines": [
    {
      "cargo_type": "general_freight",
      "quantity": 2,
      "weight_lb": 1103,
      "length_in": 48,
      "width_in": 40,
      "height_in": 60
    }
  ]
}
```

#### 提交成功响应

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "quote_id": "QTE_20260721_0012",
    "status": "processing"
  }
}
```

随后进入 §9 轮询。

---

## 7. 路线 C：Flock 免账号（`FLOCK_GUEST`）

适用：客户**未**绑定 Flock 账密；参数符合 Flock 免账号硬限（托盘 4～20、总重 ≤45000 lb、线性英尺等）。

服务端优先走 Direct JSON；失败时按环境配置可回退 DOM 填表（可能弱注册）。

### 7.1 调用流程

1. 校验邮编、取货日、件数、总重、尺寸与线性英尺。
2. `POST /api/flock/quotes` → `quote_id` + `processing`。
3. 轮询 `GET /api/quotes/{quote_id}`（总窗口建议 ≥ 210 s）。
4. 成功时读 `data.flock.lines[]`（**不是** MotherShip 的 `quotes[]` 形态）。

### 7.2 提交询价

#### 接口地址

`POST /api/flock/quotes`

> 若服务端关闭 Flock 能力，可能返回业务不可用（如 `QUOTE_UNAVAILABLE`）。

#### 请求参数

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| request_id | String | 是 | 非空 | 请求标识 |
| customer_id | String | 是 | 与鉴权一致 | 客户 ID |
| flock_input | Object | 是 | 见下表 | Flock 表单参数 |

**flock_input（免账号）**

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| pickup_date | String | 是 | `MM/DD/YYYY`；不可早于今天；不可周末 | 提货日 |
| pickup_zip | String | 是 | 5 位邮编 | 提货邮编 |
| delivery_zip | String | 是 | 5 位；≠ pickup_zip | 派送邮编 |
| pallet_count | Number | 是 | 整数 **4～20** | 托盘件数 |
| total_weight | Object | 是* | value 正数；unit `lb`/`kg`/`t` | 整票总重；也可用 `total_weight_lb` |
| dimensions | Object | 否 | 缺省 48×40×48 in；unit `in`/`cm`/`m` | 单托尺寸 |
| form_mode | String | 否 | 免账号**不要**传 `logged_in_quick` | 省略即为匿名 |
| registration | Object | 否 | 见附录 | DOM 回退时可选自填弱注册信息 |

\* `total_weight` 与 `total_weight_lb` 二选一即可。

**硬限（换算后）**

| 项 | 限制 |
|----|------|
| 总重 | ≤ 45000 lb |
| 尺寸 | L≤636，W≤102，H≤108（in） |
| 线性英尺 | 整票占用 ≤ 约 53'（否则校验失败） |

**请求示例**：

```json
{
  "request_id": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "customer_id": "CUST_001",
  "flock_input": {
    "pickup_date": "07/22/2026",
    "pickup_zip": "90001",
    "delivery_zip": "75201",
    "pallet_count": 6,
    "total_weight": { "value": 12000, "unit": "lb" },
    "dimensions": { "length": 48, "width": 40, "height": 48, "unit": "in" }
  }
}
```

#### 提交成功响应

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "quote_id": "QTE_20260721_0201",
    "status": "processing"
  }
}
```

### 7.3 结果字段（轮询成功后）

| 字段 | 类型 | 说明 |
|------|------|------|
| data.flock.reference | String | Flock 参考号（可能为空） |
| data.flock.lines[] | Array | 报价行 |
| data.flock.lines[].tier | String | `flock_direct` / `standard` |
| data.flock.lines[].label | String | 展示名 |
| data.flock.lines[].transit_days / transitDays | String | 时效 |
| data.flock.lines[].final_total_usd | Number | **对客户展示价（USD，已含加价）** |
| data.flock.shipment_meta | Object | 提货日、邮编、重量等摘要 |

> 一档即可能成功；勿假设固定两档。展示价优先 `final_total_usd`。

---

## 8. 路线 D：Flock 登录后（`FLOCK_LOGGED_IN`）

适用：客户**已**绑定 Flock 账密；参数按登录后 Quick 表单填写。

服务端强制官网登录 DOM，**禁止**匿名 Direct。宿主须传 `form_mode: "logged_in_quick"`，并尽量补齐登录后字段，否则 RPA 可能填表失败。

### 8.1 调用流程

与 §7.1 相同（同一对接口），差异仅在请求体与服务端执行路径。

### 8.2 提交询价

#### 接口地址

`POST /api/flock/quotes`（同路线 C）

#### flock_input 相对免账号的差异

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| form_mode | String | **是（本路线）** | 固定 `logged_in_quick` | 切换登录后校验与 RPA 表单 |
| pallet_count | Number | 是 | **1～999** | 登录后件数上限放宽 |
| pickup_location_type | String | 建议 | 官网地点类型 | 如商业/住宅等 id/文案 |
| delivery_location_type | String | 建议 | 同上 | 派送地点类型 |
| packaging_type | String | 建议 | 包装类型 | 登录后 Quick 字段 |
| freight_class | String | 建议 | 货运等级 | 登录后 Quick 字段 |
| description | String | 建议 | 货物描述 | 登录后 Quick 字段 |
| stackable / turnable | Boolean | 否 | | 可堆叠 / 可翻转 |
| additional_services | Array\<String\> | 否 | | 附加服务 id |
| vehicle_types | Array\<String\> | 否 | | 允许车型 id |
| pickup_liftgate / pickup_inside / pickup_pallet_jack | Boolean | 否 | | 提货附属 |
| delivery_liftgate / delivery_inside / delivery_pallet_jack | Boolean | 否 | | 派送附属 |
| call_before_pickup / call_before_delivery | Boolean | 否 | | 提货/派送前电话 |
| pickup_service / delivery_service 及窗口时间字段 | String | 否 | 总重 >5000 lb 时官网才开放高级调度 | 见实现字段名 |

> 登录后**不再强制**免账号的 4～20 托盘与线性英尺校验；尺寸/总重上限仍适用 Flock 官网硬限。

**请求示例**：

```json
{
  "request_id": "b2c3d4e5-f6a7-4890-b123-456789abcdef",
  "customer_id": "CUST_001",
  "flock_input": {
    "form_mode": "logged_in_quick",
    "pickup_date": "07/22/2026",
    "pickup_zip": "90001",
    "delivery_zip": "75201",
    "pallet_count": 2,
    "total_weight": { "value": 2200, "unit": "lb" },
    "dimensions": { "length": 48, "width": 40, "height": 48, "unit": "in" },
    "packaging_type": "Pallets",
    "freight_class": "70",
    "description": "General freight",
    "pickup_location_type": "Business with a dock or forklift",
    "delivery_location_type": "Business with a dock or forklift"
  }
}
```

结果字段同 §7.3。

---

## 9. 通用：异步轮询

适用于 `MS_LOGGED_IN`、`FLOCK_GUEST`、`FLOCK_LOGGED_IN`（以及非推荐的 MotherShip 异步三步）。

### 9.1 接口地址

`GET /api/quotes/{quote_id}`

### 9.2 请求

- 方法：GET  
- 路径参数：`quote_id`（提交接口返回）  
- 鉴权头：同 §2.3（须为报价所属客户）

### 9.3 响应数据

| 字段 | 类型 | 说明 |
|------|------|------|
| data.quote_id | String | 报价单号 |
| data.request_id | String | 请求标识 |
| data.status | String | `processing` / `done` / `failed` / `expired` |
| data.currency | String | `USD` |
| data.quotes | Array | MotherShip 档位（MS_* 成功时） |
| data.flock | Object | Flock 结果（FLOCK_* 成功时） |
| data.error_code / error_message | String | 失败时 |
| data.rpa_stage / rpa_stage_label | String | processing 时可能出现的进度 |
| data.valid_until | String | 成功时有效期 |

### 9.4 轮询建议

| 项 | 建议 |
|----|------|
| 间隔 | 2 s；10 s 后可加密至约 0.8 s |
| MotherShip 总窗口 | ≥ `420000` ms |
| Flock 总窗口 | ≥ `210000` ms（以联调环境配置为准） |
| 终止条件 | `done` / `failed` / `expired` / 超时 |
| 并发 | 同一用户操作防重复提交 |

---

## 10. 错误码

### 10.1 接口层错误（`code !== 0`）

| code | HTTP | 说明 | 处理建议 |
|------|------|------|----------|
| UNAUTHORIZED | 401 | 缺少或无效 API Key | 检查 Bearer / 轮换 |
| FORBIDDEN | 403 | 客户与 Key 不匹配，或无权访问报价 | 核对 `X-Customer-Id` / `quote_id` |
| VALIDATION_FAILED | 400 | 字段不合法 | 按**当前路线**硬限与必填项检查 |
| SESSION_EXPIRED | 404 | `host_session_id` 过期 | 仅 MS_GUEST：重走候选 |
| ADDRESS_SESSION_MISMATCH | 400 | 候选不属于本次会话 | 使用返回的完整对象 |
| ADDRESS_NOT_SELECTABLE | 400 | 提交了不可选候选 | 改选 `selectable: true` |
| QUOTE_NOT_FOUND | 404 | 报价不存在 | 检查 quote_id |
| QUOTE_TIMEOUT | 504 | 同步等待超时 | 稍后重试 |
| RATE_LIMITED | 429 | 限流 | 降频 |
| QUOTE_UNAVAILABLE | 503 | 渠道不可用（如 Flock 关闭/占用） | 稍后重试或换路线 |
| INTERNAL_ERROR | 5xx | 系统异常 | 稍后重试 |

### 10.2 询价业务失败（`code = 0` 且 `data.status = failed`）

| error_code | 说明 |
|------------|------|
| QUOTE_UNAVAILABLE | 暂时无法获取报价 |
| QUOTE_TIMEOUT | 询价超时 |
| QUOTE_ENTRY_UNAVAILABLE | 报价入口暂时无法打开 |
| ADDRESS_SUGGESTION_NOT_FOUND | 地址无法匹配 |
| CARRIER_NO_CAPACITY | 该线路暂无可用运力 |
| ADDRESS_NOT_SUPPORTED | 地址不在服务范围 |
| RPA_CAPTCHA | 触发验证码 |
| SESSION_EXPIRED | 承运商登录会话过期 |
| PROVIDER_LOGIN_FAILED | 承运商账密不正确或未即时更新 |
| RPA_DATA_INVALID | 未能解析完整报价 |
| PAGE_LOAD_TIMEOUT | 承运商页面加载超时 |

> 对外展示优先使用中文 `error_message`。

---

## 11. 对接建议

### 11.1 第三方需要做

1. 服务端保存 API Key 与 Customer ID，后端代理调用。
2. 实现路线判定（§3），**一票一路**。
3. 账密由管理端绑定；有账密则按登录后字段规范传参。
4. `MS_GUEST`：地址选择 UI + submit Loading（≥420 s）。
5. 异步路线：实现轮询与超时；Flock 读 `flock.lines`，MotherShip 读 `quotes`。
6. 成功后注意 `valid_until`。

### 11.2 第三方不需要做

1. 不需要在请求体传官网密码。
2. 不需要自行做单位换算（支持 `lb`/`kg`/`t`、`in`/`cm`/`m`，服务端统一进一取整为 lb/in）。
3. 不需要调用管理端客户 CRUD / embed-demo 登录作为正式对接。
4. 不需要把 Priority1 实验接口当作本期正式四路线之一。

### 11.3 cURL 示例

**MS_GUEST 第一步**：

```bash
curl -X POST "https://if.dev.51track.vip/api/host/quote/candidates" \
  -H "Authorization: Bearer <API Key>" \
  -H "X-Customer-Id: CUST_001" \
  -H "Content-Type: application/json" \
  --data-binary @ms-guest-candidates.json
```

**FLOCK_GUEST 提交**：

```bash
curl -X POST "https://if.dev.51track.vip/api/flock/quotes" \
  -H "Authorization: Bearer <API Key>" \
  -H "X-Customer-Id: CUST_001" \
  -H "Content-Type: application/json" \
  --data-binary @flock-guest.json
```

**轮询**：

```bash
curl -X GET "https://if.dev.51track.vip/api/quotes/QTE_20260721_0201" \
  -H "Authorization: Bearer <API Key>" \
  -H "X-Customer-Id: CUST_001"
```

### 11.4 Apifox 环境变量建议

| 变量 | 示例值 |
|------|--------|
| baseUrl | `https://if.dev.51track.vip` |
| token | `chj_...` |
| customerId | 与 Token 绑定的客户 ID |

Headers：`Authorization: Bearer {{token}}`，`X-Customer-Id: {{customerId}}`，`Content-Type: application/json`。

---

## 12. 附录

### 12.1 货物类型枚举（cargo_type，MotherShip）

| 值 | 说明 |
|----|------|
| general_freight | 普通货物（默认） |
| machinery | 机械设备 |
| furniture | 家具 |
| electronics | 电子产品 |
| building_materials | 建材 |
| auto_parts | 汽车配件 |
| food_nonperishable | 非生鲜食品 |
| apparel | 服装 |
| other | 其他 |

### 12.2 报价状态（status）

| 值 | 说明 |
|----|------|
| processing | 询价进行中（须继续轮询） |
| done | 成功 |
| failed | 失败，看 error_message |
| expired | 报价已过期 |

### 12.3 Flock registration（可选，免账号 DOM 回退）

| 字段 | 类型 | 说明 |
|------|------|------|
| first_name / last_name | String | 姓名 |
| company | String | 公司 |
| email | String | 工作邮箱 |
| phone | String | 10 位美国电话 |
| shipments_per_month | String | `1-25` / `26-50` / `51-100` / `101+` |

### 12.4 非推荐 / 不在本期四路线

| 说明 | 接口 |
|------|------|
| MotherShip 免账号异步三步（可用，非默认同步方案） | `POST /api/quotes` + 轮询 |
| Priority1（实验） | `POST /api/priority1/quotes` |
| `/embed-demo` 演示门户 | Cookie 登录，非正式 OpenAPI |
| 管理端客户 / 预警等 | 仅运维 |

---

## 13. 联系与支持

| 事项 | 说明 |
|------|------|
| 开通客户 / API Key / 绑定官网账密 | 联系我方运维或管理端操作 |
| Key 失效 | 轮换后立即通知宿主更换 |
| 故障排查 | 提供时间、路线 ID、`quote_id` 或 `host_session_id`、请求摘要（勿传完整 Key/密码） |
| 文档版本 | 1.1（2026-07-21） |
