Appearance
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(授权中心) | 四态状态机端点 + 通用发起授权 |
| honor(荣耀) | M1 发起 / M2 回调 / 数据查询 |
| 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)
- 运动记录 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> 展示(细节见 前端对接页)。
错误:40403004 记录不存在 / 40403008 生成失败(可重试)/ 40403009 无轨迹(打卡类空态)。
厂商注入 action(同 URL)
| action | 归属 | 说明 |
|---|---|---|
honorAdoptBinding | honor | 荣耀绑定切换(见 honor) |
zeppAuthUrl | zepp | 生成 Zepp 授权 URL(未配置不抛错 → {url:'', configured:false},见 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 |