Skip to content

honor —— 荣耀健康(v1) ​

前缀:/api/v1/device/(URL 与 V1 平行) 代码:src/app/honor/interface/api/honor_routes.py + application/usecase/honor_*.py 鉴权:两路由均公开(V1 config.public: true — 荣耀授权页在外部浏览器/webview 打开,无法带 token)

M1 · POST /api/v1/device/honor-register —— 发起荣耀授权 ​

生成会话(10min TTL 一次性)→ 返回荣耀 OAuth 授权 URL。

请求 body(全可选 —— 画像透传,用于回调后补资料):

字段类型必填默认校验规则
userIdstring⬜—仅 ^[a-z0-9]{20,30}$ 才写会话(精确绑定 — demo 场景)
nicknamestring⬜荣耀用户trim 后空则用默认
genderstring⬜unknown白名单 male / female / unknown,否则 unknown
ageint⬜—5~120 越界置 None
heightint⬜—50~300 越界置 None
weightint⬜—10~500 越界置 None
redirectSchemestring⬜—安全白名单:仅 muhe:// 或 https://(防开放重定向)

响应 data:

字段类型说明
sessionKeystring会话标识(服务端 10min TTL)
authUrlstring荣耀授权页 URL(携带 state=sessionKey)
requireProfileCompletionbool恒 true(回调只解出 5 字段,几乎必走补资料)

错误码:

codeHTTP场景
40401005200HONOR_CLIENT_ID 未配置(V1 {code:500} 兼容锚点)
40401001400参数非法

M2 · GET /api/v1/device/honor-callback —— 荣耀 OAuth 回调 ​

Query:code(授权码)+ state(sessionKey)

流程:会话校验(一次性消费 — 换 token 成功后才消费,失败可重试)→ code 换 token + getTokenInfo 取权威 openId → 用户 upsert(openid='honor:<openId>' or session.userId 精确绑定)→ DeviceBinding upsert(vendor='honor_oauth', status=active)→ 结果分流。

结果分流:

条件响应
会话带 redirectScheme(App 场景)302 deep link:`{scheme}?status=success
否则(Web 场景)HTML 结果页(✅/❌ + 文案 + 2s 自动关窗 + postMessage 通知父窗口 + AndroidBridge.onAuthResult)

错误分支文案(⚠️ 回调路径不抛 JSON 错误码 —— 一律渲染为 HTML/302 的 msg):

场景msg
缺 code/state缺少参数提示
会话过期/缺失(10min TTL)「会话已过期(10 分钟),请重新填写信息授权」
code 换 token 失败「Token 交换失败,请检查 Client Secret 或重试」
session.userId 精确绑定目标用户不存在「session.userId=… 不存在」

注意:session.userId 精确绑定分支更新 nickname/age/height/weight(不更新 gender — V1 语义)。

action:honorAdoptBinding —— 绑定切换 ​

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

把后台已有用户的荣耀授权切换给当前 APP 用户:

场景响应 data
当前用户已有 honor_oauth 绑定{ "adopted": false, "alreadyBound": true, "binding": {id, status, lastSyncAt} }
成功切换(取最近同步的他人绑定改挂){ "adopted": true, "fromNickname": "…", "binding": {id, status, lastSyncAt} }

错误码:40401009(404)暂无后台荣耀授权可切换。

数据通道(三层窗口) ​

接口窗口约束
sportStat7 天(API 硬约束 — 超窗永久丢数据)STEPS / CALORIES / STRENGTH / DISTANCE
sampleSets180 天历史,每次 ≤7 天分页单次只能一种 dataType(10050):HEART_RATE / WEIGHT / STRESS / OXYGEN_MAX
healthRecord30 天睡眠

认证(每请求 7 参数):accessToken + secretData(HmacSHA256(secretId, openId) → Base64)+ openId + appId + startTime / endTime / dataTypes。

落库映射:sportStat → DeviceDailyActivity(vendor='honor');sampleSets → HealthMetricRecord + BodyCompositionRecord(体重);healthRecord → HealthSleepRecord。

时间口径:东八区切日(容器 UTC 直接 toISOString 会偏一天);唯一例外 strengthData 用 UTC 切日(V1 历史行为保持)。

双层同步 job(授权有效性状态机) ​

beat 触发器:

模式频率范围
hourly1h 高频sportStat + 心率(sampleSets HEART_RATE)
daily24h 全量兜底sportStat 7 天窗必保 + sampleSets 30 天 + healthRecord 30 天

token 状态机(token JSON 内嵌 expiresAt / failCount — 零迁移):

事件动作
未临期(>30min)直接复用(抗频控)
临期(≤30min)才 refresh成功 → 回写新 token + failCount 清零
refresh invalid(400/401)binding status='expired' + 站内信引导重绑(不再空转)
refresh network 抖动failCount+1 回写;连续 3 次才判失效(防单次误杀)
refresh unconfigured仅告警(环境问题不判失效,旧 token 继续用)

honor 错误码(40401xxx) ​

code语义HTTP
40401001请求参数非法400
40401005HONOR_CLIENT_ID 未配置200(V1 兼容)
40401006M1 会话过期/缺失(10min 一次性)400
40401007精确绑定目标用户不存在404
40401008code 换 token 失败400
40401009无后台荣耀授权可切换404

配置(env) ​

HONOR_CLIENT_ID / HONOR_CLIENT_SECRET / HONOR_APP_ID / HONOR_SECRET_ID / HONOR_REDIRECT_URI / HONOR_API_BASE(可选覆盖);V2_ENV=production 切 prod base。

实现参考 ​

  • 模块结构:src/app/honor/CLAUDE.md
  • 协议知识(scope / 7 参数 / 实测键名坑):src/app/honor/domain/services/honor_protocol.py