---
url: /docs/api-docs/v1/device.md
---
# device —— 设备共享域（v1）

> 统一前缀：`/api/v1/device/`
> 覆盖：设备绑定读写（action 协议）+ 荣耀/Zepp/小米三厂商 OAuth + 授权中心四态
> 契约权威：`src/app/device/interface/api/device_actions.py` + `src/app/honor/interface/api/honor_routes.py` + `src/app/zepp/interface/api/zepp_routes.py`

## 子文档索引

| 文档 | 内容 |
|---|---|
| [vendors（授权中心）](/api-docs/v1/device/vendors) | 四态状态机端点 + 通用发起授权 |
| [honor（荣耀）](/api-docs/v1/device/honor) | M1 发起 / M2 回调 / 数据查询 |
| [zepp](/api-docs/v1/device/zepp) | 三端 OAuth / webhook 六事件 / legacy |
| 本页 | 共享 action 协议（myBindings / unbind / 数据查询）+ 公共约定 |

## 公共约定

### 双路由风格

| 风格 | 端点 | 说明 |
|---|---|---|
| **action 协议** | `POST /api/v1/device` + `{action, payload}` | V1 客户端契约（URL 与 V1 平行） |
| **独立 REST 路由** | `POST /honor-register` · `GET /honor-callback` · `POST /zepp-auth-url` · `GET /zepp-callback` · `POST /zepp-webhook` · `GET /xiaomi-callback` · `POST /xiaomi-webhook` · `GET /vendors` · `POST /vendors/{id}/auth-url` | 厂商 OAuth + 授权中心 |

### 统一信封

* **成功**：`{ "code": "0", "msg": "ok", "data": { ... } }`（data camelCase — V1 契约）
* **失败**：`{ "code": "4040xxxx", "msg": "..." }`

### 错误码区段（三层隔离）

| 区段 | 模块 | HTTP 映射 |
|---|---|---|
| `40401xxx` | honor | 默认 400；`USER_NOT_FOUND`/`ADOPT_SOURCE_MISSING` → 404；`UNCONFIGURED` → **200**（V1 兼容锚点） |
| `40402xxx` | zepp | 默认 400；`UNAUTHORIZED` → 401；`UNCONFIGURED` → **200**；`WEBHOOK_BODY_MISSING` → **415** |
| `40403xxx` | device 共享 | 默认 400；`NOT_FOUND`/`BINDING_NOT_FOUND` → 404；`UNAUTHORIZED` → 401 |

### device 共享错误码（40403xxx）

| code | 语义 | HTTP |
|---|---|---|
| `40403001` | 请求参数非法（payload 校验 / 未知 action） | 400 |
| `40403002` | 未登录 / token 无效 | 401 |
| `40403004` | 资源不存在 | 404 |
| `40403005` | 解绑目标绑定不存在 | 404 |
| `40403006` | **厂家审核中**（预登记占位 vendor 授权尝试 →「该设备厂家正在审核中，敬请期待」） | 400 |
| `40403007` | 厂商形态不支持该方法（Protocol v2 可选方法） | 400 |
| `40403008` | **轨迹图生成失败**（高德/COS 任一环 — 前端可重试，幂等无害） | 502 |
| `40403009` | **该记录无轨迹数据**（打卡类记录 — 前端显示「未记录轨迹」空态，勿重试） | 400 |
| `40403010` | **运动记录删除失败**（存储/COS 抖动 — 可重试幂等） | 502 |
| `40403011` | **设备同步记录不可删**（仅 App 记录可删 — 防同步复活；去设备侧删） | 400 |

***

## 共享 action（18 个）

* 设备绑定与数据查询 **5**（本节详述：myBindings / unbind / listDeviceDailyActivity / myMetrics / mySleep）
* 健康详情 **10**（sleepDetail / heartRateDetail / stressDetail / oxygenDetail / stepsDetail / caloriesDetail / strengthDetail / weightDetail / activitySummary / recordDates —— 契约见 [home-api-reference](/api-docs/v1/frontend/home-api-reference)）
* 运动记录 **3**（sportRecords / sportRecordDetail / **sportRecordShareImage** —— 见下节）

### myBindings —— 我的设备绑定

`{ "action": "myBindings", "payload": {} }`

**响应 data**：`{ "list": [BindingView] }`

`BindingView` 字段（**不含 token 载体** — 安全视图）：

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 绑定 id |
| `vendor` | string | `honor_oauth` | `zepp` | … |
| `status` | string | `active` | `expired` |
| `vendorUserId` | string | null | 厂商侧用户标识（荣耀 openId / Zepp userId） |
| `lastSyncAt` | string | null | 最近一次「数据同步成功」时刻 |
| `createdAt` | string | ISO |

### unbind —— 解绑

`{ "action": "unbind", "payload": { "vendor": "<vendor>" } }`

**payload 字段**：`vendor`（1~40 字符，必填）

**响应 data**：`{ "ok": true }`

**错误码**：`40403005`（404）绑定不存在

### listDeviceDailyActivity —— 每日活动

`{ "action": "listDeviceDailyActivity", "payload": { "vendor"?, "days"? } }`

**payload 字段**：

| 字段 | 类型 | 必填 | 默认 | 约束 |
|---|---|---|---|---|
| `vendor` | string | null | ⬜ | — | 按厂商过滤 |
| `days` | int | ⬜ | 30 | 1~180 |

**响应 data**：`{ "list": [{id, vendor, date, step, distanceM, caloriesKcal, sleepMin, activeMin, source}], "days": <n> }`（最多 200 条，date 倒序）

### myMetrics —— 健康指标

`{ "action": "myMetrics", "payload": { "metricType", "start"?, "end"? } }`

**payload 字段**：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `metricType` | string | ✅ | 1~40 字符（如 `HR` / `STRESS` / `VO2MAX` / `HRV` / `REST_HR`） |
| `start` / `end` | string | null | ⬜ | `YYYY-MM-DD` 区间 |

**响应 data**：`{ "list": [{id, userId, metricType, calendarDate, value, sport, level, ingestedAt}], "total": <n> }`（最多 500 条，desc）

### mySleep —— 睡眠记录

`{ "action": "mySleep", "payload": { "start"?, "end"? } }`

**响应 data**：`{ "list": [{id, userId, calendarDate, sleepStartGMT, sleepEndGMT, deepSleepSeconds, lightSleepSeconds, remSleepSeconds, awakeSleepSeconds, ingestedAt}], "total": <n> }`（最多 500 条，desc）

### 运动记录三 action（2026-09-23 立项 / 2026-10-09 轨迹图扩展）

#### sportRecords —— 运动记录列表

`{ "action": "sportRecords", "payload": {} }`

**响应 data**：`{ "total", "summary": { totalDistanceKm, totalCalories, totalCount, **totalDurationSec**, **bySource** }, "groups": [{ month, summary, records: [{ id, kind, type, label, date, time, distanceKm, durationSec, pace, calories, avgHr, ..., **vendor**, **sourceLabel** }] }] }`

> `totalDurationSec`（秒）2026-10-09 补 —— 前端「累计运动用时」此前恒 `--` 因该字段缺失。
> `vendor` + `sourceLabel`（2026-10-10 运动数据闭环 Q23）：每条记录透出数据来源
> （`app_run` = "App 记录" / `qingmu_band` = "青沐手环" / ...；后端统一中文标签）。
> `summary.bySource`（Q24）：`{ app: {totalCount,totalDistanceKm,totalDurationSec,totalCalories}, device: {...} }`
> 分类小计（混合主口径不变，纯增量 — 旧前端零感知）。App 跑步记录（`startRun`/`submitRun`
> 提交归一）与手环记录同列表出现。

#### sportRecordDetail —— 单条运动详情（含轨迹 + 富曲线）

`{ "action": "sportRecordDetail", "payload": { "externalId": "<列表项 id>" } }`

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `externalId` | string | ✅ | 记录 id（`sportRecords` 列表项 `id` 回传） |

**响应 data 全字段**（2026-10-10 v3 实测样例 — app\_run 记录）：

```jsonc
{
  "record": {
    "id": "run-3af1dec50c20",
    "type": "户外跑步", "symbol": "跑",
    "time": "10/10 下午3:40",
    "durationText": "07:39", "durationSec": 459,
    "distanceKm": 0.74, "calories": null, "avgHr": null,
    "vendor": "app_run", "sourceLabel": "App 记录"    // ← 2026-10-10 来源标识（Q23）
  },
  "track": [[28.17319, 112.92432, 1791618005700], …],  // 清洗后轨迹点 [lat,lng,ts]；打卡类 → null
  "trackCenter": { "latitude": 28.1732, "longitude": 112.9243 },
  "vendor": "app_run",
  "imageUrl": "https://cos-cdn.qingmulife.cn/share/sport/run-3af1dec50c20.png",  // ← 轨迹图地址（2026-10-10）
  "imageReady": true,                              // ← 就绪探测（未生成=false，地址仍给）
  "location": {                                        // ← v3：开跑位置（app_run 有；旧行 null）
    "province": "湖南省", "city": "长沙市", "area": "岳麓区",
    "address": "…西二环辅路 靠近湘江科创基地一期", "lng": "112.92…", "lat": "28.17…"
  },
  "detail": {
    "metrics": {                        // 汇总指标（app_run 清洗产出 + 设备侧聚合）
      "reportedDistanceM": 743.7,       // 上报口径（列表/卡片主口径 Q18）
      "cleanedDistanceM": 554.8,        // 去漂移后（披露）
      "removedWildPoints": 3, "simplifiedFrom": 223, "simplifiedTo": 18,   // 清洗管线自述
      "avgHr": 104, "maxHr": 125, "steps": 2807,        // 以下设备侧聚合（app_run 无源 → 缺省）
      "avgStride": 73, "maxStride": 78, "avgCadence": 88, "maxCadence": 94,
      "avgPaceSec": 426, "maxPaceSec": 369,
      "totalAscent": 78, "totalDescent": 78, "maxElevation": 70, "minElevation": 44,
      "totalCalories": 530, "hrRecoveryStart": 12, "hrRecoveryEnd": 15, "hrRecovery": -3.0
    },
    "series": {                         // 六条逐分钟曲线（与 track 点数非一一对应，分开画）
      "pace": [437, 393, …],            // 秒/公里（Y 轴反向：越小越快）
      "heart": [27, 55, …], "stride": […], "cadence": […], "recovery": […], "elevation": […]
    },
    "zones": [{ "time": "00:07:09", "label": "热身", "percent": 22 }, …],   // 心率区间
    "laps":  [{ "label": "<1", "value": "7'49\"", "percent": 79 }, …]       // 每公里分段
  }
}
```

| 顶层 | 说明 |
|---|---|
| `record.vendor` / `record.sourceLabel` | 数据来源（`app_run`="App 记录" / `qingmu_band`="青沐手环" / …，后端统一文案） |
| `track` | 清洗后轨迹（DP 抽稀；`[lat, lng, epoch_ms]`）— 地图 polyline 直接用 |
| `location` | 开跑位置（v3 — 详情地址条数据源；同时是轨迹图地址行兜底） |
| `imageUrl` + `imageReady` | **轨迹图字段**（2026-10-10 增 — 前端详情页直显用）：`imageUrl` = COS 确定性地址（`share/sport/{externalId}.png`，未生成也照给）；`imageReady` = 就绪探测（COS HeadObject；未生成/探测异常 → `false`——探测失败不拖垮详情）。未配置 COS 的环境 `imageUrl: null` |
| `detail.metrics` | app\_run：双口径距离 + 清洗自述 + （无源的设备指标缺省）；设备记录：设备侧聚合全量 |
| `detail.series` | 逐分钟六曲线（`pace`/`heart`/`stride`/`cadence`/`recovery`/`elevation` — **六键恒存在，缺源给空数组**）；app\_run 现算 `pace`，无源五键诚实空数组 |
| `detail.zones` / `laps` | 心率区间（Karvonen）/ 每公里分段；app\_run 现算 laps |

⚠️ **v1 旧数据 / 空态**：`detail` 四键恒存在，但可能为空态（`metrics: {}` / `series: {}` / `zones: []` / `laps: []`）——前端画图前判空。打卡类记录 `track: null`。

**错误**：`40403004` 记录不存在（含跨用户 — 归属校验内建）。

#### sportRecordShareImage —— 运动轨迹图（高德地图）

`{ "action": "sportRecordShareImage", "payload": { "externalId", "name"?, "location"? } }`

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `externalId` | string | ✅ | 记录 id（sportRecords 列表项 `id` 回传） |
| `name` | string | ⬜ | 记录名（如「周日夜跑 · 麓松路沿线」）→ 卡片首行显示 |
| `location` | object | ⬜ | `{provice, city, area, address, lng, lat}` → 卡片地址行（`provice` 沿前端拼写，后端兼容 `province`） |

**响应 data**：`{ "imageUrl": "<COS/CDN PNG URL>", "pointCount": <int> }`

**图片形态**（1200×1600 PNG）：高德底图 + 红色轨迹线 + 起终点圆点（后端 Web Mercator 投影自画）+ 底部**九参数数据卡片**（记录名/距离大字/时长/配速/类型/平均心率/卡路里/地址/日期+品牌）。**配速后端现算**（distance÷duration — 记录顶层无该字段）。幂等：COS key `share/sport/{externalId}.png` 覆盖写；前端直接用 `<image :src>` 展示（细节见 [前端对接页](/guide/uniapp/api/sport-record-share)）。

**错误**：`40403004` 记录不存在 / `40403008` 生成失败（可重试）/ `40403009` 无轨迹（打卡类空态）。

### 厂商注入 action（同 URL）

| action | 归属 | 说明 |
|---|---|---|
| `honorAdoptBinding` | honor | 荣耀绑定切换（见 [honor](/api-docs/v1/device/honor#honoradoptbinding)） |
| `zeppAuthUrl` | zepp | 生成 Zepp 授权 URL（未配置不抛错 → `{url:'', configured:false}`，见 [zepp](/api-docs/v1/device/zepp)） |

**未知 action** → `40403001`（`unknown action: <x>`）。

## 数据落库映射（共享表）

| 表 | 写入方 | 关键幂等键 |
|---|---|---|
| `DeviceBinding` | honor/zepp 绑定 | (user\_id, vendor) |
| `DeviceDailyActivity` | honor `vendor='honor'` / zepp `vendor='zepp'` | (user\_id, vendor, date) upsert |
| `HealthMetricRecord` | 双厂商 | (user\_id, metricType, calendarDate) |
| `HealthSleepRecord` | honor | (user\_id, calendar\_date) 存在即跳过（不做同日修正） |
| `SleepSummaryRecord` | zepp legacy | (user\_id, date) upsert |
| `BodyCompositionRecord` | 双厂商体重 | (user\_id, timestamp) 查重 |
| `DeviceRawEvent` | zepp webhook 暂存 | track\_id / summaryId |
