---
url: /docs/api-docs/v1/frontend/device.md
---
# 前端联调 · 设备线（device.ts）

> 归属：[前端联调 API 手册](./) → `src/api/device.ts`
> 端点：**4 个**（授权中心 2 + 设备绑定 action 2）
> 错误码段：`40403xxx`（全表见 [错误码注册表](/ai-contexts/error-code-registry)）
> 错误契约：**HTTP 400 / 401 / 404 + `{code, msg, data}`**
> 取值语义：**[取值速查 · device](./values#device)** —— 12 家厂商 `meta.id` 清单、`authType` 四值各自的交互形态、`status` 四态的判定优先级、`platform` 的静默归一（`iOS` → `web`）
> 深入说明：[Device 设备线 API Docs](/api-docs/v1/device/) · [授权中心四态](/api-docs/v1/device/vendors)

## 本页 curl 变量

```bash
BASE_URL=https://api.qingmulife.cn   # 本地：http://localhost:3000
ACCESS=<access_token>
ENV=Prd                              # Dev | Prd —— 全程必须一致
VENDOR=honor_oauth                   # meta.id —— 全表固定值，勿硬编码中文名
```

> 全部端点需 `Bearer access` + `X-Env`。

## 端点一览

| # | 方法 | 路径 | 场景 |
|---|---|---|---|
| [1](#listvendors) | GET | `/api/v1/device/vendors` | 授权中心列表（12 平台 + 四态） |
| [2](#getauthurl) | POST | `/api/v1/device/vendors/{id}/auth-url` | 发起厂商授权（拿 authUrl 给 webview） |
| [3](#mybindings) | POST | `/api/v1/device` `myBindings` | 我的绑定设备列表 |
| [4](#unbind) | POST | `/api/v1/device` `unbind` | 解绑设备 |

***

## listVendors

**一句话**：拉厂商清单 + 每家的健康状态与四态绑定状态 —— 授权中心页（`mine/data/device-list`）进入时。

### curl

```bash
curl -s "$BASE_URL/api/v1/device/vendors" \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS"
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `Authorization` | header | ✔ | `Bearer <access>` |
| `X-Env` | header | ✔ | 双库路由（**厂商清单与健康状态也按库算**） |

> 无 body / query。

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `vendors[].meta.id` | string | 否 | **机器键**：`honor_oauth` / `zepp` / `xiaomi` / `suunto` + 9 家占位 —— `unbind` 与 `auth-url` 都传这个值 |
| `vendors[].meta.displayName.zh` / `.en` | string | 否 | 展示名 |
| `vendors[].meta.logoUrl` | string | null | 是 | 厂商 logo（CDN） |
| `vendors[].meta.authType` | string | 否 | `oauth` | `ble` | `import` | `sync` |
| `vendors[].meta.dataWindowDays` | number | 否 | 可回溯数据天数 |
| `vendors[].meta.capabilities.sportStat` / `sampleSets` / `healthRecord` / `realtime` | boolean ×4 | 否 | 能力位 |
| `vendors[].health.ok` | boolean | 否 | 厂商服务是否可用 |
| `vendors[].health.detail` | string | null | undefined | — | 🔴 **后端不存在此键！对应物是 `health.message`** |
| `vendors[].health.configured` / `health.serviceReady` | boolean | 否 | 后端返回、前端未声明（不消费无影响） |
| `vendors[].health.message` | string | null | 是 | 健康状态说明 —— **要展示文案就用这个，不是 `detail`** |
| `vendors[].status` | string | 否 | 四态：`'bound'` | `'available'` | `'reviewing'` | `'error'`（**优先级 bound > available > error > reviewing**） |

> 🔴 **坑位 · `health.detail`**：前端 `VendorEntry` 声明了 `health.detail?`（optional），但后端**没有这个键** —— 展示时恒 `undefined`。
> 因 optional 且当前 UI 只消费 `meta` / `status`，**运行时不炸**；要用健康文案请改读 `health.message`（或让后端补 `detail` 别名）。

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40403002` | 401 | 未登录 / token 无效 | 走拦截器 401 分档 |

### `status` 四态 → UI 建议

| 状态 | 含义 | 前端展示 |
|---|---|---|
| `bound` | 已绑定 | 「已连接」+ 可解绑 |
| `available` | 可授权 | 「去授权」按钮可点 |
| `reviewing` | 厂家审核中（占位厂家） | ⚠️ **12 平台全部展示**（2026-09-19 决策**回滚**了更早的「隐藏」口径）—— 整行可点，点击 toast 引导预期，见 `device-list.vue:207` |
| `error` | 服务异常 / 未配置 | 置灰 + 提示稍后再试 |

***

## getAuthUrl

**一句话**：发起厂商授权 —— 拿 `authUrl` 丢给 webview 加载（授权中心点「去授权」）。

### curl

```bash
# body 传空对象即可（zepp 三端才需要 platform）
curl -s "$BASE_URL/api/v1/device/vendors/honor_oauth/auth-url" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{}'

# zepp 三端（platform 可选）
curl -s "$BASE_URL/api/v1/device/vendors/zepp/auth-url" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"platform":"android"}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `{vendor_id}` (path) | string | ✔ | 传 `vendors[].meta.id`（如 `honor_oauth`）—— **不是中文名** |
| `platform` | string | ✖ | 仅 zepp 三端需要；缺省合法 |
| `Authorization` / `X-Env` | header | ✔ | |

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `authUrl` | string | 否 | 厂商授权页 URL → **webview 加载** |
| `sessionKey` | string | null | 是 | 授权会话键（部分厂商返回） |
| `requireProfileCompletion` | boolean | 否 | 后端**恒返回第三键**，前端未声明（不消费，无影响） |

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40403004` | 404 | 未知 vendor id（registry 查不到） | 用 `meta.id`，别用中文名拼 |
| `40403002` | 401 | 未登录 | 走拦截器 401 分档 |
| `40403006` | 400 | 厂家审核中（占位厂家） | 「该设备正在接入中」置灰 |
| `40403007` | 400 | 厂商形态不支持该方法 | 按 `authType` 隐藏入口 |

***

## myBindings

**一句话**：我的绑定设备列表 —— 设备页显示已连接设备。

### curl

```bash
curl -s "$BASE_URL/api/v1/device" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"myBindings","payload":{}}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `action` | string | ✔ | 固定 `"myBindings"` |
| `payload` | object | ✖ | 忽略 |

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `list[].id` | string | 否 | 绑定记录 id（前端类型写成 `number \| string`，更宽 ✓） |
| `list[].vendor` | string | 否 | 厂商键（`honor_oauth` / `zepp` / `xiaomi` / `suunto`…） |
| `list[].status` | string | 否 | `active` | `revoked`… |
| `list[].vendorUserId` | string | null | 是 | 厂商侧用户 id |
| `list[].lastSyncAt` | string ISO | null | 是 | 最后同步时刻 |
| `list[].createdAt` | string ISO | **否** | 非空（前端类型声明 `string \| null`，更宽 ✓） |

> ✅ **契约干净**。注意 `list[].vendor` 与 `vendors[].meta.id` **同值域** —— `unbind` 直接传它即可精确匹配。

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40403002` | 401 | 未登录 | 走拦截器 401 分档 |

***

## unbind

**一句话**：解绑设备 —— 设备列表点「解除绑定」。

### curl

```bash
curl -s "$BASE_URL/api/v1/device" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"unbind","payload":{"vendor":"honor_oauth"}}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `action` | string | ✔ | 固定 `"unbind"` |
| `payload.vendor` | string | ✔ | **1–40 字符**；传 `meta.id` / `binding.vendor`（页面传 `device.id`）—— 精确匹配，无模糊 |

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `ok` | boolean | 否 | 恒 `true` |

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40403005` | 404 | 该用户未绑定此厂商（BINDING\_NOT\_FOUND） | toast「未找到绑定记录」+ **刷新列表** |
| `40403001` | 400 | `vendor` 缺失 / 超 40 字符 / 未知 action | 检查调用点 |
| `40403002` | 401 | 未登录 | 走拦截器 401 分档 |

#### ⚠️ `40403005` 实测记录（2026-09-21 真机联调）

**实测报文**（`18569401013` / 无任何 IoT 绑定的账号）：

```json
{"code":"40403005","msg":"binding not found","data":null}
```

**两个必须知道的点**：

1. **`msg` 是英文技术黑话，且会直接弹给用户。**
   `utils/request.ts` 在业务码非 0 时统一 `toast(body.msg)` —— 所以用户看到的就是
   **`binding not found`** 这句英文。**本手册的「前端建议处置」列并不生效**：
   前端若不自造文案，透传的就是后端原文。
   → **契约层面应要求后端返回中文用户文案**（如「该设备尚未绑定」），
   而不是让每个客户端各写一份映射（那就是 DBG-009 那类「改一处漏一处」）。

2. **「刷新列表」这一列不是可选建议，是必需。**
   本码的语义是\*\*「你的本地状态已过期」\*\*（TOCTOU：下拉列表是 `onShow` 的快照，
   用户读到点击之间状态可能已变）。**不刷新 = 幽灵状态持续存在**，用户会重复撞同一个错。

**边界说明**：本码的**存在**是刻意的 —— 后端 `DeviceErrorCode.BINDING_NOT_FOUND → HTTP 404`
是 **V1 兼容**（注释自陈「V1 Errors.notFound」），**不是缺陷**。缺陷只在 `msg` 与前端处置。
