Skip to content

zepp —— Zepp 健康(v1) ​

前缀:/api/v1/device/(URL 与 V1 平行) 代码:src/app/zepp/interface/api/zepp_routes.py + application/usecase/zepp_*.py 鉴权:zepp-auth-url 需 Bearer;zepp-callback / zepp-webhook 公开(官方回调,无 token)

三端 OAuth(web / android / ios) ​

Zepp 在 console.zepp.com 注册三个应用(web/android/ios 各一套凭据):

  1. 前端按端调 POST /api/v1/device/zepp-auth-url 传入 platform
  2. 服务端用发起端凭据生成授权 URL,并把 {userId, platform} 写入会话(callback 换 token 必须用发起端凭据)
  3. GET /zepp-callback 按会话记录的平台完成 exchange

POST /api/v1/device/zepp-auth-url —— 生成授权 URL ​

请求 body:

字段类型必填说明
platformstring | null⬜web(默认)| android | ios(白名单外归一为 web)

响应 data:{ "url": "<授权页>", "platform": "<归一后平台>" }

错误码:

codeHTTP场景
40402005200ZEPP_CLIENT_ID/SECRET 未配置(V1 {code:500} 兼容锚点;「console.zepp.com 三应用待批」)
40402002401未登录

GET /api/v1/device/zepp-callback —— OAuth 回调 ​

Query:code / state / error

流程:error 参数 → 失败 → 会话反查 {userId, platform} → 用发起端凭据 exchange token → zeppUserId(token 响应自带,无则调 profile 补)→ DeviceBinding upsert(vendor='zepp')→ 消费会话 → HTML 结果页(✅/❌ + 2s 自动关窗)。

绑定成功后的两个 fire-and-forget 动作(失败静默不阻塞绑定 — V1 语义):

  1. backfill 近 29 天(activities + sleeps 走官方 backfill 通道)
  2. Legacy 直拉近 29 天(明文通道兜底 — 实测 backfill 推送为密文,legacy 保证当下可达)

错误分支:一律 HTML 渲染(msg 内文案)——「授权失败:…」/「缺少 code/state 参数」/「会话已过期,请重新发起授权」/「换取 token 失败:…」。

POST /api/v1/device/zepp-webhook —— 事件推送(六事件) ​

Content-Type:text/plain(官方约定);响应恒 204(解析失败也吞错 — 防官方重试风暴)。

密文双模(2026-08-27 破案):官方文档只写明文双层 JSON,实测 body 为 base64(AES-128-ECB(双层JSON))(key = base64decode(CLIENT_SECRET),必须 16 字节)—— 解析器双模兼容:明文直传 / 密文 base64。

双层 JSON:先 JSON.parse 得 string[],再逐条 JSON.parse(单层解析必炸)。

空 body → 40402010 + HTTP 415(Content-Type 须为 text/plain)。

六事件分发与落库 ​

事件落库幂等键
authorizationChange / authorizationDeleteDeviceBinding status='expired'—
sportChange暂存 DeviceRawEvent + 批后防抖触发 legacy 近 3 天增量(整批一次)track_id
activityFiles(ping-pull)暂存 DeviceRawEvent + fire-and-forget 拉 FITsummaryId
sleeps(push)HealthSleepRecord(user, calendarDate, startGMT) 三维 — 同日多段(午睡+夜睡)
hrv(push)HealthMetricRecord metricType=HRV(同日重推=修正更新)—
dailies(push)DeviceDailyActivity(vendor='zepp')+ REST_HR 指标—
bodyComps(push)BodyCompositionRecord(克→kg 换算)(user, timestamp)

单条失败不阻塞整批(skipped++,官方重试机制兜底 — V1 语义)。

action:zeppAuthUrl —— 生成授权 URL(action 版) ​

{ "action": "zeppAuthUrl", "payload": { "platform"? } }(POST /api/v1/device,需 Bearer)

与独立路由的差异:未配置不抛错 → { "url": "", "configured": false };platform 恒 web。

Legacy 拉取通道(数据同步) ​

GET /users/-/sleep + GET /users/-/activities(Bearer token,明文通道,interval=daily 必填):

项口径
窗口近 29 天(clamp [1, 29] — 压官方 30 天上限内)
睡眠字段date / deepSleepTime / shallowSleepTime / wakeTime / rem / sleepScore / start / stop / rhr / sleepHrv(时长单位分钟 ×60 转秒)
活动字段date / steps / distance(米)/ calories
落库sleep → SleepSummaryRecord upsert + rhr/sleepHrv → REST_HR/HRV 指标;activity → DeviceDailyActivity upsert
收尾lastSyncAt 刷新(授权中心绑定卡「最近同步时间」)

每日 job ​

beat:daily(24h Legacy 全量 29 天幂等 upsert — 与 honor daily 同款节奏)。 sportChange 密文链已通(运动变更 → 增量拉取),但睡眠/静息心率无推送不更新 —— 每日全量保新鲜度。

token 刷新:1h 短命 token 临期(<30min)先 refresh 换新回写 DeviceBinding(「临期才刷」抗风控);refresh 失败降级旧 token(简化版 — 不做 failCount 计数,V1 zepp job 语义如此)。

zepp 错误码(40402xxx) ​

code语义HTTP
40402001请求参数非法400
40402002未登录 / token 无效401
40402005ZEPP_CLIENT_ID/SECRET 未配置200(V1 兼容)
40402006OAuth 会话过期/缺失400
40402010webhook body 缺失415(V1 兼容)

配置(env) ​

ZEPP_CLIENT_ID / ZEPP_CLIENT_SECRET(web 主凭据)+ android / ios 三端凭据;webhook 解密 key = ZEPP_CLIENT_SECRET。

实现参考 ​

  • 模块结构:src/app/zepp/CLAUDE.md
  • 密文编解码:src/app/zepp/domain/services/zepp_webhook_codec.py
  • FIT 拉取现状:下载 + 暂存(FitIngestPort),FIT 解析链未迁移(V2 差异记录在迁移报告)