# 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 X-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 X-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 " \ -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 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」一次后再查看