---
url: /docs/api-docs/v1/device/vendors.md
---
# vendors —— 授权中心（四态状态机）

> 前缀：`/api/v1/device/vendors`
> 鉴权：Bearer access（C 端用户）
> 代码：`src/app/device/interface/api/device_actions.py`（`cz_build_device_vendors_router`）+ `application/usecase/vendor_center.py`
> 产品需求落档：`docs/internal-wiki/architecture/device-cloud-integration-research.md`「授权中心状态机」

## 四态语义（bound / available / reviewing / error）

C 端授权页列出全部目标平台。每个厂家的 `status` 字段**四态之一**：

| 状态 | 语义 | 前端渲染 | 交互 |
|---|---|---|---|
| `bound` | 已连接（当前用户有 active 绑定） | 绿色徽标「已连接」+ 最近同步时间 | 点击 → 详情/解绑 |
| `available` | 可授权（凭据已配置、未绑定） | 品牌色 + 「去连接」 | 点击 → OAuth 跳转 |
| `reviewing` | **厂家审核中**（预登记占位，未对接完成） | 灰色徽标「正在审核中」 | 点击 → toast「该设备厂家正在审核中，敬请期待」 |
| `error` | 通道异常（凭据已配置但健康检查未就绪） | 黄色徽标「暂时不可用」 | 点击 → 重试 |

**状态映射规则**（服务端硬编码顺序：`bound > available > error > reviewing`）：

| 条件 | status |
|---|---|
| 用户有该 vendor 的 `active` 绑定 | `bound` |
| `configured=true` 且健康检查 `ok=true` | `available` |
| `configured=true` 但 `ok=false` | `error` |
| `configured=false` 且 `planned=true`（预登记占位） | `reviewing` |
| `configured=false` 且非预登记（真实厂商未配凭据） | **不出现**（过滤，前端无感） |

### C 端消费现状（2026-09-18 真机实证 — qm-frontend `pages/mine/data/device-list.vue`）

| status | 实际渲染 | 交互 |
|---|---|---|
| `bound` | 绿字「已连接」+ 红描边「解除连接」按钮；顶部「N 个已连接」徽章 | 点击 → showModal 确认 → `unbind` action → onShow 刷新 |
| `available` | 深绿字「未连接」+ 绿描边「去连接」按钮 | 点击 → `POST vendors/{id}/auth-url` → webview 打开 `authUrl`；回调落绑定后返回页 onShow 自动变 `bound` |
| `reviewing` | 灰字「正在审核中」 | 点击 → 前端 toast「该设备厂家正在审核中，敬请期待」（**不发请求** — 后端 40403006 为兜底） |
| `error` | 红字「暂时不可用」 | 点击 → toast 提示稍后再试 |

* 厂商清单为动态拉取（12 平台：honor\_oauth / zepp / xiaomi / suunto / apple\_health / garmin\_cn / garmin\_intl / coros / huawei / oppo / polar / werun），中文名取 `meta.displayName.zh`
* 请求带 `X-Env: Prd`（qm-frontend `config/index.ts` X\_ENV — 双库路由）
* 真机 bound 闭环已实证：插绑定 → 已连接+解除连接 → 确认解绑 → 行删除 + 状态回流（详见 `docs/internal-wiki/operations/device-binding-live.md`）

## GET /api/v1/device/vendors —— 厂家清单

**响应 data**：

```json
{
  "vendors": [
    {
      "meta": {
        "id": "honor_oauth",
        "displayName": { "zh": "荣耀运动健康", "en": "HONOR Health" },
        "logoUrl": "https://…",
        "authType": "oauth",
        "dataWindowDays": 30,
        "capabilities": { "sportStat": true, "sampleSets": true, "healthRecord": true, "realtime": false }
      },
      "health": { "ok": true, "configured": true, "serviceReady": true, "message": null },
      "status": "bound"
    }
  ]
}
```

**`status` 枚举**：`bound` · `available` · `reviewing` · `error`（上表）。

## POST /api/v1/device/vendors/{vendor\_id}/auth-url —— 通用发起授权

registry 分发（非硬编码厂商列表）—— 同一端点服务全部厂商。

**请求 body**：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `platform` | string | null | ⬜ | 平台（Zepp 三端用：`web` | `android` | `ios`；其他厂商忽略） |

**响应 data**：

| 字段 | 类型 | 说明 |
|---|---|---|
| `authUrl` | string | 厂商授权页 URL |
| `sessionKey` | string | null | 会话标识（服务端已存） |
| `requireProfileCompletion` | bool | 是否需补资料（荣耀恒 true） |

**错误码**：

| code | HTTP | 场景 |
|---|---|---|
| `40403004` | 404 | vendor 不存在 |
| `40403006` | 400 | **厂家审核中**（预登记占位 vendor）— msg=「该设备厂家正在审核中，敬请期待」+ `data.vendor` 回传 vendor id |

## 厂家清单（registry 当前注册）

| vendor id | 类型 | 状态 |
|---|---|---|
| `honor_oauth` | 真实实现（荣耀） | ✅ 已迁移 |
| `zepp` | 真实实现（Zepp） | ✅ 已迁移 |
| `xiaomi` | 真实实现（小米 — S3）| 🔶 未配 env 显示审核中；配置 XIAOMI\_CLIENT\_ID/SECRET 即 available |
| `apple_health` | 预登记占位 | ⏳ 审核中 |
| `garmin_cn` / `garmin_intl` | 预登记占位 | ⏳ 审核中 |
| `coros` | 预登记占位 | ⏳ 审核中 |
| `huawei` | 预登记占位 | ⏳ 审核中 |
| `suunto` | 预登记占位 | ⏳ 审核中 |
| `oppo` | 预登记占位 | ⏳ 审核中 |
| `polar` | 预登记占位 | ⏳ 审核中 |
| `werun` | 预登记占位 | ⏳ 审核中 |

> 占位清单 = 设备云对接研究文档 12 平台矩阵（除已迁移荣耀 / Zepp / 小米外 9 家）。
> 接入完成一家即从占位升为真实实现（registry 覆盖注册 — 同 id 替换，端点契约不变）。

## 实现参考

* `IDeviceVendor` 统一抽象（v2 七方法）：`src/app/device/domain/vendor.py`
* 注册中心：`src/app/device/application/vendor_registry.py`
* 占位实现：`src/app/device/application/planned_vendor.py`
