# MotherShip 查询接口调用文档

> **第三方正式对接请优先阅读**：同目录 [`api对接文档.md`](./api对接文档.md)（v1.1，**四路线**：MotherShip/Flock × 免账号/登录后）。  
> 本文档保留 MotherShip 宿主两步 + 管理端接口细项，适合内部运维对照；Flock / 登录后字段以对接文档为准。

本文档说明 **MotherShip / Axel** 查询报价相关接口（偏免账号宿主两步），适合内部运维对照使用。

Base URL：`https://if.dev.51track.vip`

统一响应格式：

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

`code !== 0` 为业务/鉴权错误。询价业务失败时 HTTP 可能仍为 `200`，此时看 `data.status === "failed"`。

---

## 1. 推荐接口（第三方宿主）

当前推荐使用 **两步宿主接口**（须鉴权）：

1. `POST /api/host/quote/candidates`
2. `POST /api/host/quote/submit`

特点：

- 第一步返回提货 / 派送地址候选
- 第二步服务端内部等待询价并同步返回最终报价
- 调用方**不需要**自己轮询 `GET /api/quotes/{id}`

---

## 2. 鉴权（必传）

当前服务器已关闭公开免 Token 模式（`HOST_PUBLIC_API_ENABLED=false`），**每次请求**须携带：

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

| 字段 | 说明 |
|------|------|
| `Authorization` | `Bearer` + 我方发放的凭证 |
| `X-Customer-Id` | 客户 ID，必须与凭证绑定一致 |
| `Content-Type` | 固定 `application/json` |

### 2.1 凭证来源（二选一）

| 类型 | 形态 | 来源 |
|------|------|------|
| **客户 API Key（推荐给第三方）** | 通常以 `chj_` 开头 | 管理端「客户管理」新建客户 / 轮换 Key 后发放 |
| Service Token | 自定义字符串，如 `demo-host-token` | 服务器 `.env` 的 `HOST_SERVICE_TOKENS`（联调/内网） |

第三方正式对接请使用管理端发放的 **API Key**，不要把 Key 写进前端页面或公开仓库。

### 2.2 请求头示例

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

联调可用 env Token（若服务器已配置）：

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

### 2.3 鉴权错误

| HTTP | message | 说明 |
|------|---------|------|
| 401 | 缺少 Authorization 头 | 未带 Bearer |
| 401 | Service Token 无效 | Key/Token 错误或已轮换失效 |
| 403 | customer_id 与令牌租户不匹配 | `X-Customer-Id` 与 Key 绑定不一致 |
| 401 | 令牌无效或已过期 | 误用了管理员 JWT，应使用 API Key / Service Token |

### 2.4 Apifox 环境变量

| 变量 | 示例值 |
|------|--------|
| `baseUrl` | `https://if.dev.51track.vip` |
| `token` | 管理端发放的 `chj_...` 或联调 `demo-host-token` |
| `customerId` | 与 Token 绑定的客户 ID，如 `CUST_001` |

Headers：

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

---

## 3. 第一步：获取联想地址候选

### 接口

```http
POST /api/host/quote/candidates
```

### 请求头

```http
Authorization: Bearer <API Key>
X-Customer-Id: <Customer ID>
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` | 否 | 建议传，格式 `12345` 或 `12345-6789` |
| `cargo.weight.value` | 是 | 单托重量 |
| `cargo.weight.unit` | 是 | `lb`、`kg` 或 `t`/`tonne`（公制吨；禁止 `ton`/`tons`） |
| `cargo.dimensions.*` | 是 | 单托长宽高 |
| `cargo.dimensions.unit` | 是 | `in`、`cm` 或 `m` |
| `cargo.pallet_count` | 是 | 托盘数，1–25 |
| `cargo.cargo_type` | 是 | 见下方枚举 |

**货物类型 `cargo_type`：**

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

服务端会自动把 `lb`/`kg`/`t`、`in`/`cm`/`m` 换算为 MotherShip 需要的英制单位（小数进一）。

### 成功响应示例

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "host_session_id": "2dd352e6-9a1d-4563-8f13-ebff18aabdb9",
    "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
  }
}
```

### 第一步调用后处理

1. 保存 `data.host_session_id`（约 **30 分钟**有效）
2. 从 `pickup_candidates` 选 1 条 `selectable: true`
3. 从 `delivery_candidates` 选 1 条 `selectable: true`
4. 第二步提交时传回选中的**完整候选对象**（不要只传 `option_id`）

不要使用 `selectable: false` 的候选（可能带 `unavailable_reason`）。

---

## 4. 第二步：提交候选并获取最终报价

### 接口

```http
POST /api/host/quote/submit
```

### 请求头

与第一步相同。

### 请求体

```json
{
  "host_session_id": "2dd352e6-9a1d-4563-8f13-ebff18aabdb9",
  "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` 必须来自第一步
- 候选对象必须是第一步列表中的完整对象（`option_id` 须匹配）
- HTTP 连接可能保持 **10–420 秒**；客户端 / Nginx 超时建议 ≥ **420 秒**
- **不会**返回 `processing` 让你继续轮询；服务端内部完成等待

### 成功响应示例

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

| 字段 | 说明 |
|------|------|
| `status` | `done` 成功；`failed` 失败（看 `error_message`） |
| `quotes[].final_total` | **对客户展示价（USD，含加价）** |
| `quotes[].carrier` | 承运商 |
| `quotes[].transit_days` | 时效 |
| `valid_until` | 报价有效期 |

---

## 5. 调用步骤总结

1. 带鉴权头调 `POST /api/host/quote/candidates`
2. 保存 `host_session_id`，让用户各选一条可用地址
3. 带鉴权头调 `POST /api/host/quote/submit`
4. 直接展示最终报价

### 第三方需要做

- API Key 放服务端，不暴露前端
- 每次请求带 `Authorization` + `X-Customer-Id`
- 第一步做地址选择 UI；第二步 Loading（超时 ≥ 420s）

### 第三方不需要做

- 不需要 `GET /api/quotes/{id}` 轮询
- 不需要自己做公制 → 英制换算（支持 `kg`/`t`/`cm`/`m`，服务端统一进一取整为 lb/in）

---

## 6. Apifox / cURL

### Apifox

| 步骤 | 方法 | URL |
|------|------|-----|
| 1 | POST | `{{baseUrl}}/api/host/quote/candidates` |
| 2 | POST | `{{baseUrl}}/api/host/quote/submit` |

第二步超时建议：`420000` ms。

### cURL（第一步）

```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 @host-candidates-req.json
```

PowerShell（本机测公网，不要写 `127.0.0.1`，不要加 `:30325`）：

```powershell
curl.exe -s -w "`nHTTP: %{http_code}`n" `
  -X POST "https://if.dev.51track.vip/api/host/quote/candidates" `
  -H "Authorization: Bearer demo-host-token" `
  -H "X-Customer-Id: CUST_001" `
  -H "Content-Type: application/json" `
  --data-binary "@deploy/host-candidates-req.example.json"
```

---

## 7. 常见错误

| code | HTTP | 说明 | 处理 |
|------|------|------|------|
| `UNAUTHORIZED` | 401 | 缺少或无效 Token/API Key | 检查 Bearer 与客户绑定 |
| `FORBIDDEN` | 403 | 客户 ID 与令牌不匹配 | 核对 `X-Customer-Id` |
| `VALIDATION_FAILED` | 400 | 参数不合法 / JSON 无效 | 检查字段、单位、州码 |
| `SESSION_EXPIRED` | 404 | `host_session_id` 过期 | 重新调第一步 |
| `ADDRESS_SESSION_MISMATCH` | 400 | 候选不在本次联想结果中 | 使用第一步返回的完整对象 |
| `ADDRESS_NOT_SELECTABLE` | 400 | 选了不可用候选 | 改用 `selectable: true` |
| `QUOTE_TIMEOUT` | 504 | 服务端等待超时 | 稍后重试 |
| `RATE_LIMITED` | 429 | 限流 | 降低频率 |

---

## 8. 客户管理接口（管理员）

供**管理端 / 内部运维**使用，不提供给第三方宿主直接调用。

### 8.1 鉴权

```http
Authorization: Bearer <Admin JWT>
Content-Type: application/json
```

管理员 JWT 通过 `POST /api/auth/login` 获取（账号密码登录）。

### 8.2 获取客户列表

```http
GET /api/admin/customers?page=1&size=10&keyword=CUST_001
```

| 参数 | 必填 | 说明 |
|------|------|------|
| `page` | 是 | 页码，从 1 开始 |
| `size` | 是 | 每页 1–100 |
| `keyword` | 否 | 按客户 ID / 名称模糊搜索 |

成功时 `data.list[]` 主要字段：

| 字段 | 说明 |
|------|------|
| `customer_id` | 客户 ID |
| `status` | `active` / `disabled` |
| `embed_password_set` | 是否已设 `/embed-demo` 密码 |
| `active_api_key` | 当前有效 API Key；历史未保存原文时可能为 `null` |
| `api_keys[]` | Key 摘要（不含完整原文，仅前缀等） |

### 8.3 新建客户

```http
POST /api/admin/customers
```

```json
{
  "name": "演示客户 003",
  "remark": "重点客户",
  "embed_password": "123456"
}
```

| 字段 | 必填 | 说明 |
|------|------|------|
| `name` | 是 | 客户名称 |
| `remark` | 否 | 备注 |
| `embed_password` | 否 | `/embed-demo` 登录密码；不传默认 `123456` |

响应中 `api_key` **仅在新建时额外返回一次**；同时写入 `active_api_key`。请立即保存发给第三方。

### 8.4 更新客户

```http
PATCH /api/admin/customers/{customer_id}
```

可更新：`name`、`remark`、`status`（`active`/`disabled`）、`embed_password`（至少 6 位）。

### 8.5 轮换 API Key

```http
POST /api/admin/customers/{customer_id}/rotate-key
```

- 原有效 Key 立即失效
- 响应中的 `api_key` 与 `active_api_key` 为新 Key
- 轮换后须通知第三方更换凭证

### 8.6 承运商官网账密（管理端）

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

用于绑定客户在 MotherShip / Flock 官网的登录账密（RPA 使用），**非第三方宿主对接接口**。

PUT 请求体示例：

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

| 约束 | 说明 |
|------|------|
| `provider` | 仅 `mothership` 或 `flock` |
| 写入策略 | 仅允许首次绑定；已绑定后不可修改 / 清除 |
| 清除 | 不支持 `clear`；更换需新建客户 |

---

## 9. 嵌入演示登录

供 `/embed-demo` 演示页使用，不是正式宿主 API。

### 9.1 登录

```http
POST /api/embed-demo/login
```

```json
{
  "customer_id": "CUST_003",
  "password": "cust003@2026"
}
```

- 密码以管理端「客户管理」中为该客户设置的为准
- 成功后写入 HttpOnly Cookie：`embed_demo_session`

### 9.2 当前会话 / 退出

```http
GET  /api/embed-demo/me
POST /api/embed-demo/logout
```

`me` 依赖登录 Cookie，返回当前客户及加价配置。

---

## 10. 其它渠道说明（非推荐宿主对接）

以下接口同样需要 Service Token / API Key，但**返回 `processing`，需客户端轮询**，不作为第三方默认同步对接方案：

| 渠道 | 提交 | 轮询 |
|------|------|------|
| MotherShip 旧三步 | `POST /api/quotes` 等 | `GET /api/quotes/{quote_id}` |
| Priority1 | `POST /api/priority1/quotes` | 同上 |
| Flock | `POST /api/flock/quotes` | 同上 |

第三方正式对接请优先使用本文档第 1–4 节的两步宿主接口。

---

## 11. 联系与支持

- Token / API Key 失效或需开通新客户：联系我方运维 / 管理端操作
- `X-Customer-Id` 必须与发放凭证绑定一致，不可串用
- 老客户若 `active_api_key` 为空：在管理端「轮换 Key」一次后再查看
