更新记录
1.0.0(2026-10-19)
首次发布:三端每日步数读取与授权生命周期
- 对外 API 共 7 个:
configure配置隐私同意与远程通道、getEnvInfo同步查询通道与能力位、requestAuthorization拉起授权、getAuthorizationStatus查询授权状态、cancelAuthorization取消授权、getDailySteps读取区间日步数、resolveRemote远程通道回填。全部是 uni 风格options + success / fail / complete,返回void,不返回 Promise。 - 通道模型:Android 按
Build.MANUFACTURER命中小米 MIUI ContentProvider / 华为 HMS Health Kit / 荣耀 Health Kit + HonorId / vivo 远程通道;iOS 走 HealthKit(原生调用落在混编BearHealthKitNative.swift);HarmonyOS NEXT 走@kit.HealthServiceKit;Web 与小程序走utssdk/index.uts兜底,除getEnvInfo外回9040009。 - 通道判定、时间对齐、日步数聚合与补零、华为长区间分段重试、厂商原始码归类、授权状态归一都放在跨端纯逻辑层(
core.uts/core-state.uts),三端共用同一份实现,同一机型不会出现分端行为差异。 - 时间口径:
timestamp为本地时区当天 0 点、单位毫秒,查询区间左闭右开。 - vivo 与荣耀的服务端链路走注入式远程通道(
RemoteChannel协议函数):插件包内零域名、零密钥,client_secret与access_token都不进 App;未注入实现时数据类接口回9040008。实现方拿到结果后按请求里的requestId调resolveRemote({ requestId, data })回填,失败时也走它、把errCode写成非 0——入参对象里没有success / fail回调(uni-app 的 JS↔原生桥只传数据,原生侧构造的闭包过不到 JS)。errCode只认9040段位内的码,段位外的数一律归9040008,原值仍写进errMsg。 - 荣耀链路(静默登录、授权页结果解析、换 token、采样查询)每一跳都挂 20s 看门狗:厂商 SDK 或接入方服务端不回调时按
9040005(授权 / 查状态 / 取消)或9040006(取步数)如实返回一次,errMsg点名停在链条哪一跳并给出该跳的排查方向,不会让业务侧无限等待。同一份链路摘要拼在失败结果末尾,凭证只记「有没有取到 + 长度」。 - 取消授权按各端真实能力返回:华为 / 荣耀 / 鸿蒙走 SDK 撤销(
mode: 'sdk'),vivo 与远程通道走服务端解绑(mode: 'remote');Apple 与小米在系统层面没有撤销接口,统一回9040007并随结果给出用户自助撤销路径,不做假成功。 - 隐私合规前置:未
configure({ privacyAgreed: true })时任何数据类接口都回9040004,插件不会静默初始化厂商 SDK;privacyAgreed: false会连同既有配置与已登记的远程实现一起清空,作为用户在应用内撤回同意的落点。Android 端只声明小米通道需要的ACTIVITY_RECOGNITION与miui.permission.READ_STEPS(v1 只读不写,不声明WRITE_STEPS),不申请定位等无关权限。 - 错误码占
9040段位,序号 001–009 全部登记文案;厂商原始码(HMS50065、鸿蒙1002703001、HealthKit 错误域)不占段位,统一拼进errMsg。 - 真机验收:小米(2026-10-16)、华为 P30 Pro(2026-10-16)、HUAWEI nova 14 / HarmonyOS 6.1.0.135(2026-10-16)、iPhone 6s / iOS 15.8.8(2026-10-17)、荣耀 RMO-AN00 / MagicOS 7.1(2026-10-19,配合接入方自己的
code2token服务端)五条通道都在云端自定义调试基座上跑通四步链路并取到按天步数。vivo 通道尚未真机验收(无设备、平台资质未申请):这条通道不碰任何原生 SDK,实现全在你的远程代码里,所以案例页内置了一套可直接跑的 vivo 实现 + 零依赖模拟服务端,秒级时间戳 / 裸数组 / 未绑定50001401三种回包形状在任意机型上都能复现。 - iOS 读取授权口径:
HKAuthorizationStatus按 Apple 文档只表示写入授权,用它当读取门禁会把已允许的用户也挡在外面;本插件改用getRequestStatusForAuthorization,authorized在 iOS 端表示「授权流程已完成」而不是「一定能读到」。用户在授权页里拒绝时 Apple 不给信号,查询如实回全 0 并在detail里说明「未授权或区间内无记录不可区分」。 - 案例页覆盖环境检测 / 正常路径(配置 → 授权 → 读步数)/ 失败路径(
9040004、9040001、9040007、9040008可视化)/ 各厂商取消授权 / 生命周期五类场景,每个按钮下方直接显示本次结果;命中荣耀机型时展开「荣耀通道参数」区(App ID 与远程接口地址填进去就能跑通,不必改代码),vivo 远程实现与模拟服务端内置在案例里,onUnload停止回调写入。 - 服务端两跳的官方口径已抄录进 readme:荣耀 Code 换取 Access Token(
POST https://hnoauth-login.cloud.honor.com/oauth2/v3/token、application/x-www-form-urlencoded、code使用前去掉\转义并 urlencode、expires_in3600 秒不要长期缓存、失败只判sub_error);vivo Health Kit 云云接入接口文档(/cloud/steps无区间入参、有/cloud/oauth/unAuthorize解绑、客户端不可直连云服务)。 - 华为 Android 通道的完整操作说明(scope 申请项、
agconnect-services.json放哪、证书与基座怎么打、打完包的自检命令、故障对照表)见同目录huawei-android-setup.md。厂商资质申请材料清单不随插件包发布,需向作者索取。 - 已知限制:v1 只读取步数、只读不写(
wantWrite: true回9040001);Android 仅适配华为 / 荣耀 / vivo / 小米,其余厂商回9040002;华为通道依赖utssdk/app-android/config.json的project里 apply 的com.huawei.agconnect(agcp:1.9.1.301)——AGC 的应用身份由该插件在构建期写进安装包资源,只放agconnect-services.json而不 apply 插件时 HMS 取不到身份、授权会回原始码907135000;不用华为通道的接入方可以删掉project.plugins/project.dependencies这两项,其余通道照常工作,只是华为机型回9040002;小米通道要求宿主manifest.json的app-plus.distribute.android.targetSdkVersion≥ 29(uni-app 云打包默认 28,低于它系统不允许申请ACTIVITY_RECOGNITION)。心率、睡眠与运动记录待后续版本(各厂商为独立 scope 与独立资质审批)。
平台兼容性
uni-app(4.75)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | × | × | √ | - | 6.0 | 12 | 12 |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
uni-app x(4.75)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| - | - | 6.0 | 12 | 12 | × |
其他
| 多语言 | 暗黑模式 | 宽屏模式 | 蒸汽模式 |
|---|---|---|---|
| × | × | × | √ |
bear-health-data
统一读取设备侧健康平台的步数,并配套完整的授权生命周期(拉起授权 → 查授权状态 → 取消授权)。支持 Android(小米 / 华为 / 荣耀 / vivo)/ iOS(HealthKit)/ HarmonyOS NEXT(Health Service Kit) 三端,一份代码跑多厂商:业务侧不需要按厂商写分支,但可以随时查到「这台机器命中了哪条通道、这条通道能不能撤销」。
与官方 API 的关系:本插件不替换 uni.getWeRunData(那只服务微信小程序),而是直接驱动各端原生健康平台;对上层给出 uni 风格的 options + success / fail / complete 接口,不返回 Promise。
v1 边界(如实声明,避免按宣传语去找不存在的能力):
- 只有步数,且只读:心率、睡眠、运动记录在各厂商是独立 scope 与独立资质审批,未纳入 v1;
requestAuthorization({ wantWrite: true })会直接回9040001。 - Android 只适配华为 / 荣耀 / vivo / 小米四类机型,其余厂商回
9040002;v1 刻意不接 Google Health Connect,误判成任一厂商只会让用户看到一个弹不出来的授权页。 - vivo 与荣耀的服务端链路由接入方后端承担:插件零域名、零密钥,
client_secret与access_token都不进 App。 - 厂商资质申请材料清单(各开放平台要提交什么、多久过审、产出物放哪里)不在插件包内,需向作者索取。
- 授权档位(普通授权 / 源码授权)与权利边界见
license.txt;厂商申请材料清单只随源码授权档发送。
兼容性
| 平台 | 支持 | 实现方式 | 最低版本 | 说明 |
|---|---|---|---|---|
| Android | √ | 按 Build.MANUFACTURER 命中通道:小米 MIUI 步数 ContentProvider / 华为 HMS Health Kit / 荣耀 Health Kit + HonorId / vivo 走注入式远程通道 |
minSdk 23;小米通道另需宿主 targetSdkVersion ≥ 29 | 小米、华为、荣耀三条通道已真机实测通过(小米 / 华为 2026-10-16,荣耀 2026-10-19);华为侧还要求宿主配 agconnect-services.json 并在 config.json 里 apply AGC 插件(见「使用前提」);荣耀 SDK 由 utssdk/app-android/config.json 依赖引入,需云端打包;荣耀通道还需 configure({ honorAppId }) + 注入带 exchangeAuthorizationCode 的远程实现(回填走 resolveRemote),并在开放平台登记包名与签名证书 SHA256 指纹,四项缺一取步数就失败 |
| iOS | √ | Apple HealthKit(原生层为混编 Swift BearHealthKitNative.swift) |
deploymentTarget 12 | 已真机实测通过(2026-10-17,iPhone 6s / iOS 15.8.8);需要宿主 App ID 开启 HealthKit capability,模拟器不可用;status 口径与别的端不同,见「通道与能力」下的 iOS 专段 |
| HarmonyOS | √ | @kit.HealthServiceKit healthStore,只申请 dailyActivities 读取 |
API 12(HarmonyOS NEXT) | 已真机实测通过(2026-10-16,HUAWEI nova 14 / HarmonyOS 6.1.0.135);资质在 AppGallery Connect 侧申请(dailyActivities 数据类型需逐项开通,未开通时授权或取数会失败),另需设备已安装「华为运动健康」并同意隐私政策 |
| Web / 小程序 | × | 走 utssdk/index.uts 兜底 |
- | getEnvInfo() 照常返回且 channel 恒为 none,其余接口一律回 9040009 |
运行环境要求:HBuilderX 3.6.8+,uni-app / uni-app-x 4.75+,vue3。
Android 小米通道的硬性前置:宿主
manifest.json的app-plus.distribute.android.targetSdkVersionMUST ≥ 29(uni-app 云打包不写时默认 28)。低于 29 时系统不允许申请ACTIVITY_RECOGNITION,requestAuthorization回9040002,errMsg里会带出要改的这段配置。真机实测状态(截至 1.0.0)。下表里的通道都在 HBuilderX 云端打包的自定义调试基座上跑通了
configure→requestAuthorization→getAuthorizationStatus→getDailySteps四步,并能取到按天聚合的步数:
通道 实测机型与系统 验收日期 小米 / Redmi(MIUI 步数 Provider) 小米真机,宿主 targetSdkVersion: 302026-10-16 华为(HMS Health Kit) 华为 P30 Pro(VOG-AL10,Android 10,HMS Core 6.16.4.352,华为运动健康 17.0.7.310) 2026-10-16 荣耀(HonorId + Health Kit) 荣耀 RMO-AN00(MagicOS 7.1),配接入方自己的 code2token服务端2026-10-19 HarmonyOS NEXT(Health Service Kit) HUAWEI nova 14(TLR-AL00,HarmonyOS 6.1.0.135),已装「华为运动健康」 2026-10-16 iOS(HealthKit) iPhone 6s(iOS 15.8.8),宿主 App ID 已开 HealthKit capability 2026-10-17 vivo(云云接入) — — 每条通道要准备的资质与宿主配置见下面的「使用前提」。vivo 这条不碰任何原生 SDK,实现全在你注入的远程代码里,所以案例页内置了一套可直接跑的 vivo 实现 + 零依赖模拟服务端:四个协议函数、
web-view承接授权页、用户返回后回查状态再resolveRemote结算的时序,以及秒级时间戳 / 裸数组 / 未绑定50001401三种回包形状都能在任意机型上复现(非 vivo 机型时插件不会调用这套实现,案例改用自检按钮直接调它并打印回填入参)。口径见「vivo 通道怎么接」。表格数值与
package.json的平台矩阵、各端config.json逐项一致;uni-app-x与uni-app共用同一份原生实现,最低版本相同。
使用前提
- 必须真机 + 自定义调试基座:UTS 插件不参与标准基座;Android 端还依赖 HMS / Honor 的 Maven 依赖,只能走云端打包。没走云端打包时的典型表现是本地编译 UTS 在
utssdk/app-android/index.kt报找不到名称“hihonor”(或huawei)并附官方 known-issue error18 链接——那是本地编译不解析config.json依赖,不是代码或依赖声明写错。 - Android 宿主
targetSdkVersion必须 ≥ 29(小米通道):ACTIVITY_RECOGNITION是 Android 10(API 29)才有的运行时权限,而 uni-app 云打包不显式配置时 默认是 28,此时系统权限申请接口直接抛IllegalStateException。在宿主manifest.json写:{ "app-plus": { "distribute": { "android": { "targetSdkVersion": 30 } } } }改完 MUST 重新云端打自定义调试基座才生效。本插件在申请前会校验宿主 targetSdk,低于 29 时如实回
9040002并把上面这段配置写进errMsg,不会让原生异常逸出到 JS。 MUST NOT 把它写进插件的utssdk/app-android/config.json:该文件支持的顶层字段只有abis/dependencies/minSdkVersion/project(见官方 UTS 插件文档「Android平台原生配置」一节:https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#androidconfigjson),targetSdkVersion写进去不报错但完全无效——targetSdk 是整个 App 的打包参数,不属于插件。宿主侧配置项说明见 https://uniapp.dcloud.io/tutorial/app-android-targetsdkversion。 -
宿主侧配置由接入方提供,插件一律不内置:
- 华为:在 AGC 为宿主包名创建应用并开通「运动健康」服务、申请步数读 scope;下载
agconnect-services.json放到宿主工程的nativeResources/android/(uni-app 对这类配置文件的官方口径见 https://doc.dcloud.net.cn/uni-app-x/plugin/uts-for-android.html「其他配置文件」一节),并在 AGC「项目设置 > 常规」登记该包实际打包用的签名证书 SHA256 指纹——包名或签名指纹与基座/正式包不一致时,HMS 侧会直接拒绝授权。还差一步:AGC 的配置是华为的com.huawei.agconnectGradle 插件在构建期写进安装包资源的,本插件已在utssdk/app-android/config.json的project里 apply 它(见「实现要点」);不用华为通道的接入方可以把project.plugins/project.dependencies这两项删掉,删掉后本插件其余通道照常工作,只是华为机型会回9040002。没合入 json 或没 apply 插件时 HMS 取不到应用身份,表现为授权页拉不起来并带出 HMS 原始码907135000,或直接回9040002并在errMsg里说明要配agconnect-services.json。另外需在「华为运动健康」里同意开发者隐私政策;没同意时 HMS 会在读数据时报错,本插件翻成9040006并把 HMS 原始码带在errMsg里(与插件侧的privacyAgreed合规开关是两件事)。华为 Android 通道的完整操作说明(scope 申请项、配置文件放哪、证书与基座怎么打、打完包的 2 分钟自检、故障对照表)见同目录huawei-android-setup.md。 -
荣耀:这条通道是「原生授权 + 接入方服务端」两段式,两段都配齐才能取到步数:
- 在荣耀开放平台申请应用后拿到 App ID,通过
configure({ honorAppId })传入(只留在内存,不落盘、不打日志)。没传时任何荣耀接口都回9040002。 同一个应用里还要登记三件事:应用包名,该包实际打包用的签名证书 SHA256 指纹(调试基座与正式包是两套指纹,都要登记),以及为该应用开通「荣耀账号服务」与「运动健康 Health Kit」并申请step.readscope。这三项与 App ID 不一致时 HonorId 校验不会通过;荣耀 SDK 这些内部分支既不打日志也不回调业务,所以真机上不容易看出卡在哪,插件用 20s 看门狗把这种静默转成9040005/9040006并在errMsg里列出这三项。但「点了没反应」不一定出在登记项——远程实现没按resolveRemote契约回填时表现一模一样,见下面的第 2 条与resolveRemote一节。 - 通过
configure({ remote })注入的远程实现里必须带exchangeAuthorizationCode:插件把 HonorId 的一次性authorizationCode交给你自己的服务端,服务端用client_secret换回{ accessToken }(HTTP 非 200 或缺accessToken都算失败)。缺这个函数、或remote.channel写成了别家(只有空串或'honor'才算接管本通道),getDailySteps都会回9040008。openId不从远程接口取:荣耀构造查询账号用的openId只来自 HonorId 的SignInAccountInfo.openId(真机实测 2026-10-17 RMO-AN00 上这份有值),服务端只需要回accessToken。本地这份为空时插件直接回9040003并提示重新授权。authorizationCode是一次性的:交出去换过一次 token 就作废,所以插件在每次需要换 token 前会重新静默登录取新码,不会拿旧码去撞服务端。 请求体字段名与响应包法由接入方服务端自定:案例页同时送code与authCode(同值),响应里accessToken/access_token在顶层或包一层data都能取到,服务端回的msg会原样显示在案例页上。 回填用resolveRemote({ requestId: req.requestId, data: { accessToken } }),不是req.success(...)——远程入参对象里没有回调函数(原生闭包过不了 JS↔原生桥),失败时同样调它,把errCode写成非 0。
授权、查状态、取消授权三步只走原生 HonorId,不需要远程实现,所以「授权成功、读步数失败」是这条通道最常见的形态;
9040008是可修复的配置问题,不是机型或资质问题。案例页在命中荣耀机型时会展开「荣耀通道参数」区,App ID 与服务端地址填进去就能直接跑通取数,不必改代码。 荣耀链路的每一步(静默登录、授权页结果解析、换 token、采样查询)都挂 20s 看门狗:某一步没回调时按9040005(授权/查状态/取消)或9040006(取步数)如实回一次,errMsg里写明停在具体哪一跳以及那一跳该查什么(停在「远程换 token」先查服务端与网络,停在「采样查询」先查荣耀运动健康有没有当天数据),不会让业务侧无限等待。 这条链路同时会逐跳打关键日志(logcat 前缀[bear-health-data]),并把同一份链路摘要拼在失败结果的errMsg末尾(形如[荣耀链路]读步数发起 账号可复用=false → 静默登录通过 openId=有(36) authorizationCode=有(284) → 换 token 发起 … → 换 token 回来 accessToken=有(64) 线程=main → 采样查询失败 errorCode:xxx)。凭证一律只记「有没有取到 + 长度」,accessToken/authorizationCode/openId的明文既不进日志也不落盘。部分厂商定制系统(如 MagicOS)关闭了第三方 App 的 logcat 读取,此时链路摘要就是唯一的现场证据。 - 在荣耀开放平台申请应用后拿到 App ID,通过
- vivo:整条链路(授权页 URL、取数、解绑)都在接入方服务端,通过
configure({ remote })注入;授权页由宿主的web-view页面承接。案例页内置了这套实现与一个零依赖模拟服务端,见「vivo 通道怎么接」。
- 华为:在 AGC 为宿主包名创建应用并开通「运动健康」服务、申请步数读 scope;下载
- iOS:宿主 App ID 开启 HealthKit capability;插件随附
Info.plist的NSHealthShareUsageDescription与 HealthKit entitlement 文案,宿主可在自己的证书里保留同名项。HealthKit 在模拟器上不可用,验收 MUST 用 iOS 15+ 真机 + 云端自定义调试基座;模拟器上getDailySteps会如实回9040002。已实测机型:iPhone 6s(iOS 15.8.8),四步链路走通并能取到按天步数(2026-10-17);这是目前实测的最低系统版本,deploymentTarget声明的 iOS 12–14 尚未实测。 - HarmonyOS:在 AGC 申请 Health Service Kit 的数据类型权限;用户在「华为运动健康」内未同意隐私政策时回
9040004,插件会同时引导跳到运动健康 App。已实测机型:HUAWEI nova 14(TLR-AL00,HarmonyOS 6.1.0.135,包内只声明ohos.permission.INTERNET,不申请任何系统权限)。 - 调用顺序固定:
configure({ privacyAgreed: true })→requestAuthorization→getAuthorizationStatus/getDailySteps。未 configure 或隐私未同意时任何数据类接口都回9040004,插件不会静默初始化厂商 SDK;用户在应用内撤回同意后,再调一次configure({ privacyAgreed: false })就让插件回到未配置态(连同注入的远程实现一起注销)。 - 权限最小化:Android 端只声明小米通道读步数需要的
ACTIVITY_RECOGNITION与miui.permission.READ_STEPS(v1 只读不写,不声明WRITE_STEPS);ACTIVITY_RECOGNITION由requestAuthorization现申请,其余端与其余厂商都不申请任何系统权限,授权发生在厂商账号与运动健康 App 内。
快速开始
<script setup>
import { configure, getEnvInfo, requestAuthorization, getDailySteps } from '@/uni_modules/bear-health-data'
const env = getEnvInfo() // 同步,configure 之前也能查:channel / 三个能力位
console.log(env.channel, env.channelReason) // 'huawei' / '华为设备,走 HMS Health Kit'
configure({
privacyAgreed: true, // 必须先向用户说明用途并取得同意
honorAppId: '', // 荣耀设备必填
remote: null, // vivo / 荣耀取数时注入接入方实现
success: (res) => {
requestAuthorization({ // 用户拒绝也算正常返回,走 success 按 status 降级
success: (auth) => {
if (auth.status != 'authorized') return
getDailySteps({
days: 7,
success: (res) => console.log(res.totalStep, res.steps), // timestamp 为当天 0 点,单位 ms
fail: (err) => console.log(err.errCode, err.errMsg)
})
}
})
}
})
</script>
import 只允许写到插件根目录,MUST NOT 写
@/uni_modules/bear-health-data/utssdk/index.uts。小米/红米机型先检查宿主
manifest.json里有"app-plus": { "distribute": { "android": { "targetSdkVersion": 30 } } }(缺省是 28,走不通ACTIVITY_RECOGNITION的动态申请),改完需重新云端打自定义调试基座。详见「使用前提」第 2 条。
API
所有异步接口都是 uni 风格:一个 options 入参携带 success / fail / complete,返回 void;fail 收到的对象继承自 UniError,含 errCode / errMsg / errSubject(errSubject 恒为 bear-health-data)。
configure(options)
记录隐私同意、注入远程通道、配置荣耀 App ID 与回溯上限。未命中可用通道时 configure 依然成功,channel 会是 none 并在 channelReason 里说明原因——这样业务能先把「本机不支持」显示出来,而不是被一个异常打断。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| privacyAgreed | boolean | - | 必填。为 false 时回 9040004 并清空此前的配置与已登记的远程实现——用户在应用内撤回同意,就是再调一次 configure 并把本参数置 false |
| honorAppId | string | '' | 荣耀开放平台应用 ID,仅命中 honor 通道时必需 |
| remote | RemoteChannel | null | null | 自定义远程通道实现(vivo / 荣耀);传 null 即无远程通道 |
| maxBackfillDays | number | 90 | 步数查询最大回溯天数,超过 365 会被截断 |
| success / fail / complete | function | - | uni 风格回调 |
ConfigureResult(字段均为实际生效值,用于核对命中了哪条链路):
| 字段 | 类型 | 说明 |
|---|---|---|
| platform | string | android / ios / harmony |
| channel | string | 本次判定命中的通道,取值见下方「通道与状态取值」 |
| channelNeedsRemote | boolean | 该通道是否依赖接入方远程实现(vivo / 荣耀 / remote 为 true) |
| remoteConfigured | boolean | 远程通道是否已登记(登记不等于可用) |
| remoteHandlers | Array\<string> | 已登记的函数名列表,用来核对漏注了哪个 |
| honorAppIdSet | boolean | 荣耀 App ID 是否已配置 |
| maxBackfillDays | number | 实际生效的回溯上限 |
getEnvInfo() : EnvResult
同步返回环境与能力,不需要权限、configure 之前也可调用,用于案例页环境检测块与业务运行时自检。
| 字段 | 类型 | 说明 |
|---|---|---|
| osName / osVersion | string | 端标识与系统版本原文,如 14 / 17.5 / 5.0.1 |
| manufacturer | string | 设备厂商(Android 取 Build.MANUFACTURER,iOS / 鸿蒙为空串) |
| brand | string | 设备品牌 |
| channel | string | 判定命中的通道;无可用通道为 none |
| channelReason | string | 判定说明:无通道 / 需要远程而未注册 / 缺荣耀 App ID |
| stepsSupported | boolean | 该通道能否读取步数 |
| cancelSupported | boolean | 该通道能否在插件内取消授权(小米、iOS 为 false) |
| authSupported | boolean | 该通道能否在插件内拉起授权 |
| configured | boolean | 是否已成功 configure |
| remoteConfigured | boolean | 远程通道是否已注册 |
| healthPlatform | string | 本端依赖的原生健康平台名称,未实现端为 not-support |
| supportedPlatforms | Array\<string> | 插件声明支持的端,取自 package.json 平台矩阵 |
requestAuthorization(options)
拉起本端通道的授权。小米走系统 ACTIVITY_RECOGNITION 弹窗,华为 / 荣耀走厂商授权页(结果由 UTSAndroid.onAppActivityResult 回收),vivo / remote 交给注入实现,iOS 走 HKHealthStore.requestAuthorization,鸿蒙走 healthStore.requestAuthorizations。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| wantWrite | boolean | false | v1 只读;传 true 回 9040001,避免业务误以为已拿到写入权限 |
| success / fail / complete | function | - | 用户拒绝属正常返回(success,按 status 降级);仅链路本身失败才走 fail,如小米通道宿主 targetSdkVersion 低于 29 时回 9040002 并在 errMsg 给出要改的 manifest 配置 |
AuthStatusResult:
| 字段 | 类型 | 说明 |
|---|---|---|
| channel | string | 判定所依据的通道 |
| status | string | authorized / denied / undetermined / unsupported |
| canCancel | boolean | 该通道能否在插件内取消授权 |
| rawStatus | string | 厂商 SDK / 系统返回的原始状态文本,便于排查 |
| detail | string | 面向用户的说明,如「请先在华为运动健康 App 内同意隐私政策」 |
getAuthorizationStatus(options)
不弹任何界面地查询当前授权状态,适合进入页面时先判定再决定是否引导授权。入参只有 success / fail / complete,返回结构同上为 AuthStatusResult。查询本身失败(厂商 SDK 报错、取不到 Activity / Context)回 9040005。
cancelAuthorization(options)
撤销授权。这是各端能力差异最大的一项:Apple 与小米在系统层面就不给第三方撤销接口,插件不会假装成功。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| clearLocalData | boolean | false | 是否同时删除厂商侧已同步的数据。置 true 后用户历史步数不可恢复,业务 MUST 先二次确认 |
| success / fail / complete | function | - | 该端 / 厂商无法在插件内撤销时回 9040007,errMsg 里带用户自助撤销路径 |
CancelResult:
| 字段 | 类型 | 说明 |
|---|---|---|
| channel | string | 操作所依据的通道 |
| cancelled | boolean | 授权是否确实已撤销 |
| mode | string | sdk(厂商 SDK 撤销:华为 / 鸿蒙 / 荣耀原生)/ remote(服务端解绑)/ local-only(只清插件内存态)/ unsupported |
| guide | string | 用户自助撤销指引,mode 为 local-only / unsupported 时必然非空 |
| detail | string | 厂商返回的原始信息 |
getDailySteps(options)
读取区间内的日步数。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| startTime | number | - | 区间起点(ms),按整天取;与 endTime 都不传时按 days 取最近 N 天 |
| endTime | number | 当前时刻 | 区间终点(ms),含所在日期整天 |
| days | number | 30 | 未传 startTime 时生效的回溯天数(含今天),上限取 configure.maxBackfillDays |
| includeEmptyDays | boolean | true | 无记录的日期是否补 0 返回;关闭后区间内的空洞要业务自己识别 |
| success / fail / complete | function | - | 未授权回 9040003(小米在读取前自检系统权限;iOS 只在「还没拉起过授权页」时回,见下面的 iOS 口径说明),厂商侧读取失败回 9040006;华为的「未授权」由 HMS 自身报错,落到 9040006 并在 errMsg 带出原始码 |
DailyStepsResult:
| 字段 | 类型 | 说明 |
|---|---|---|
| channel | string | 实际使用的通道 |
| startTime / endTime | number | 生效区间(ms),起点已对齐当天 0 点,终点为末日次日 0 点(左闭右开) |
| unit | string | 恒为 ms,作为契约留位,后续版本改单位时业务可感知 |
| steps | Array\<DailyStep> | { timestamp, step },按 timestamp 升序;timestamp 是本地当天 0 点 |
| totalStep / dayCount | number | 区间合计 / 返回天数(恒等于 steps.length) |
| rawSampleCount | number | 厂商返回的原始采样条数,用来核对聚合是否丢数据 |
| chunked | boolean | 是否命中过厂商区间限制而分段重读合并(华为长区间实测报 50065) |
| detail | string | 补充说明 |
resolveRemote(options)
远程通道的回填入口,只有注入了 remote 实现(vivo / 荣耀 / remote 通道)的业务需要调它:注入的函数请求完自己的服务端后,用请求入参里的 requestId 调本接口把结果交回插件。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| requestId | string | - | 必填,取 req.requestId 原样带回。插件用它认领那次请求;不认识或已结算回 9040008 |
| data | RemoteReplyData | null | null | 成功时必填,按请求种类填对应字段(见下表) |
| errCode | number | 0 | 失败时给非 0(通常 9040008),插件把它转成业务收到的错误。段位内的码(9040001–9040009)原样透传;填段位外的数(如厂商自定义码)一律归 9040008,你原填的数仍会写进业务收到的 errMsg 里,不会丢 |
| errMsg | string | '' | 失败说明 |
| success / fail / complete | function | - | 只表示这次回填有没有被插件接收,与那次数据接口的结果无关 |
RemoteReplyData(按需填其中一个组合,字段全为可选):
| 字段 | 类型 | 用于哪个请求 |
|---|---|---|
| steps | Array\<DailyStep> | getDailySteps |
| status | string | getAuthorizationStatus / requestAuthorization,取值同 AuthStatusResult.status |
| cancelled / mode / guide | boolean / string / string | cancelAuthorization;mode 一般写 remote |
| accessToken | string | exchangeAuthorizationCode。荣耀只需要这一个字段,openId 由 HonorId 账号对象给出,不由服务端下发 |
| detail | string | 全部,实现方自带的排查说明 |
为什么回填要单独调一个接口,而不是
req.success(...):uni-app 的 JS↔原生桥只传数据,插件在原生侧构造的回调函数过不到 JS 里。真机(荣耀 RMO-AN00,2026-10-19)在 JS 侧实测注入的实现拿到的req.success是undefined,一调用就抛req.success is not a function,异常留在 webview、插件毫无感知,于是整条远程通道表现为「20s 内没有任何回调」。所以现在请求只带编号,回填走本接口,一个 requestId 只结算一次(重复回填后续调用会收到「requestId 不存在」)。
通道与状态取值
| channel | 端 | 取步数方式 | 插件内可撤销 | 需要接入方后端 |
|---|---|---|---|---|
xiaomi |
Android | MIUI 步数 ContentProvider | × | × |
huawei |
Android | HMS Health Kit readDailySummation |
√ | × |
honor |
Android | HonorId 授权 + 荣耀 Health Kit | √ | √(换 access_token) |
vivo |
Android | 全部由注入的远程实现完成 | √(远程解绑) | √ |
remote |
Android | 接入方自定义远程通道 | √(远程解绑) | √ |
healthkit |
iOS | HealthKit HKStatisticsQuery |
× | × |
harmony-health |
HarmonyOS | Health Service Kit readData |
√ | × |
none |
任意 | 无可用通道,数据类接口回 9040002 |
× | × |
status 取值:authorized(已授予)/ denied(拒绝或已撤销)/ undetermined(从未申请)/ unsupported(该通道无法表达授权状态)。
iOS(
healthkit)的status口径与别的端不同,必读:Apple 明确不向 App 暴露「读取是否被允许」——HKAuthorizationStatus只表示写入授权,对只申请读取的步数类型恒为denied(真机表现为「用户已在授权页点了允许,仍回未授权」)。本插件因此改用getRequestStatusForAuthorization,authorized在 iOS 端的含义是授权流程已完成(系统不再弹授权页),而不是「已确认允许」;undetermined表示还没拉起过授权页。用户真的在授权页里拒绝时,HealthKit 让查询返回空而不是报错,此时getDailySteps成功回全 0、rawSampleCount为 0,并在detail里写明「未授权或区间内无记录,Apple 不区分这两者」。业务侧 MUST NOT 把 iOS 的authorized当成「一定能读到数据」,正确做法是按查询结果降级。
远程通道注入协议(RemoteChannel)
插件对 vivo / 荣耀只定义协议、不带实现,五个函数按需注入,缺哪个即视为该能力不可用(回 9040008)。每个请求入参都带 channel 与 requestId,结果一律用 requestId 调 resolveRemote 回填,入参对象里没有也不该有 success / fail:
| 函数 | 入参 | 回填 data 字段 |
|---|---|---|
| getDailySteps | RemoteStepsRequest{ channel, requestId, startTime, endTime } |
steps(+ 可选 detail) |
| getAuthorizationStatus | RemoteStatusRequest{ channel, requestId } |
status(+ 可选 detail) |
| requestAuthorization | RemoteAuthRequest{ channel, requestId } |
status,用户中途放弃回填 denied |
| cancelAuthorization | RemoteCancelRequest{ channel, requestId, clearRemoteData } |
cancelled / mode / guide / detail |
| exchangeAuthorizationCode | RemoteTokenRequest{ channel, requestId, authCode } |
accessToken(荣耀链路服务端只需要回这一个) |
失败同样走 resolveRemote,把 errCode 写成非 0、errMsg 写清楚即可;errCode 只认 9040 段位内的 9 个码,段位外的数会被归成 9040008(原数仍拼在 errMsg 里),业务侧因此不需要为厂商自定义码写分支。忘记回填时插件不会让业务无限等待,荣耀通道由 20s 看门狗按阶段回 9040005 / 9040006 并点名卡在哪一跳。
exchangeAuthorizationCode 背后对接的是荣耀官方的 Code 换取 Access Token 接口(文档),你那跳服务端替 App 做的正是它,几个会影响成败的细节:
- 端点
POST https://hnoauth-login.cloud.honor.com/oauth2/v3/token,Content-Type: application/x-www-form-urlencoded(不是 JSON);表单体grant_type=authorization_code、code、redirect_uri、client_id、client_secret。 - 插件交出来的
authCode是 HonorIdsilentSignIn那份一次性授权码(官方口径:SDK 静默授权换 token 依赖silentSignIn拿到的 code)。官方明确要求使用前去掉\转义字符并做 urlencode;redirect_uri在 SDK 接入下填平台上配置的回调地址,没配回调地址时填固定值honorid://redirect_url。 - 成功回
access_token/expires_in/token_type,scope含openid时另有id_token,authorize 带access_type=offline时才有refresh_token。expires_in是 3600 秒,所以别把 token 缓存成长期状态——本插件每次取步数都会要一份新码来换。响应里没有 openId,插件用的是 HonorId 账号对象里那份,服务端不需要下发。 - 失败回
{ "error": 主码, "sub_error": 子码, "error_description": … },官方建议只判子码。和这条链路最相关的三个:20155code 过期、20156code 已被使用过(授权码是一次性的,这两种都要重新取码而不是重试)、12303/12304(client_id不存在 /client_secret不对,属平台登记问题)。入参类子码(20002、20022、20024、20152、20154、20172、20182等)官方说明是不要重试,改代码。 - 这一跳失败请回填
resolveRemote({ requestId, errCode: 9040008, errMsg: <带上 sub_error 与 error_description> }),插件会把原文拼进errMsg末尾的[荣耀链路]摘要,用户侧就能区分「卡在厂商 SDK」与「卡在换 token」。
impl.channel 传空串表示接管本端默认远程通道,写 'vivo' / 'honor' 则只服务该厂商。一份实现同时接两家时按入参的 req.channel 分派,别为了区分厂商丢掉另一家的函数。
vivo 通道怎么接(案例已内置演示)
vivo 是本插件里唯一完全不碰原生 SDK 的 Android 通道:设备命中 vivo 后,四个数据类接口全部转调注入的远程实现,exchangeAuthorizationCode 用不到(vivo 的授权码在 vivo→你的服务端回跳里就地换掉了,App 不经手,所以漏注这一个函数不影响 vivo)。
vivo 侧走的是官方 vivo Health Kit 云云接入接口文档——「云云接入」的意思就是服务端到服务端,官方 FAQ 明确写了客户端不可以直连其云服务,所以这条通道必须配你自己的后端。签名(HmacSHA256 + URL-Safe Base64)、appId/appSecret、accessToken(默认 30 天)与 refreshToken(默认 180 天,过期要用户重新授权)、服务端出口 IP 登记这些都在后端那一层,插件侧一概不经手。
| 协议函数 | 你要提供的服务端接口 | 回填 |
|---|---|---|
| requestAuthorization | 下发 vivo 侧 H5 授权页 URL(/auth?appId=&redirectUri=&state=,redirectUri 需 UrlEncode 且要先让 vivo 配置) |
打开 URL 让用户同意,返回后回查绑定状态再回填 status |
| getAuthorizationStatus | 查绑定关系(代理官方 /cloud/oauth/queryStatus,未授权回 4005) |
status:已绑定 authorized / 未绑定 undetermined |
| getDailySteps | 按区间取日步数(代理官方 /cloud/steps,官方接口没有区间入参,区间过滤得你后端自己做) |
steps(timestamp 毫秒);未绑定 / 授权失效回填 errCode: 9040003 |
| cancelAuthorization | 服务端解绑(代理官方 /cloud/oauth/unAuthorize,解绑后官方其他接口会回 5032) |
cancelled / mode: 'remote',自家后端另需清掉存的 token 与绑定关系 |
四个必须注意的点:
-
授权结果没有同步回调。流程是「服务端下发 URL → webview 承接 → 用户在页内同意 → vivo 回跳你的服务端换 token」,App 侧只能等用户返回后回查状态再结算这一次
requestId。案例用一个web-view页面承接(example/vivo-auth.vue,本地模拟页在hybrid/html/vivo-auth-mock.html),并在本页onShow里回查后回填;用户长时间不返回时由案例的 120s 定时器把这次请求收口成9040008,因为插件对远程请求不设超时。授权页路径属于宿主,插件 MUST NOT 代你navigateTo。 -
时间戳单位要自己核对。官方
StepInfo是{t: 13 位毫秒, v: double},与本插件DailyStep.timestamp的毫秒契约天然对得上,MUST NOT 除 1000;一旦回秒级值,会被折成 1970 年的某天、再被区间过滤掉,现象是「服务端明明有数据、插件回空数组」而不是报错。历史素材里踩过(原生分支除过 1000、vivo 分支没除),案例按位数补齐并留了开关复现。步数同理:官方v是小数,Math.round是实现方的活。 -
回包形状不唯一。历史素材的服务端既可能回
{data:{stepInfoList:[…]}}也可能直接回数组,两种都要能吃;业务码50001401(未绑定 / 授权失效)要转成9040003而不是给空数组,否则用户以为自己步数是 0。该码不在官方码表内(官方对应语义是4005/5032),新接入的后端建议直接透官方码。 -
当天查不到步数不一定是坏了。官方给的时延口径是「距上次上报 > 半小时 或 累计 > 1000 步才会上报」,云端同步另有延迟;非 vivo 手机与安卓版本过低都不支持 vivo Health Kit。
-
授权页路径属于宿主。案例把承接页放在
example/vivo-auth.vue(整页只有一个web-view,这是 App 端对web-view必须是页面根元素的限制),复制到宿主工程后 MUST 在宿主pages.json里登记这一页,插件 MUST NOT 假定路径存在就navigateTo。
案例页的「vivo 通道」卡片可以在任何机型上把这套实现跑一遍:勾上「内置模拟服务端」就是零依赖的假数据(四个开关分别复现秒级时间戳、裸数组、未绑定),自检按钮用案例自编的 requestId 直接调这四个函数并打印会回填的入参。本机 channel 不是 vivo 时插件不会调用这套实现——通道由「端 + Build.MANUFACTURER」判定,插件刻意不提供覆盖入口,vivo 真机上勾上「注入远程通道」再按正常路径的按钮走完整链路。vivo 通道至今未做真机验收,验收前请按上面的官方文档把服务申请、出口 IP 与步数权限(permissionId 1)先就位。
注意两级检查不同粒度:configure 的 remoteConfigured 与 getEnvInfo 的 remoteConfigured 只看有没有登记并接管该通道,getDailySteps 才会逐个函数检查——登记了 remote 却漏注 exchangeAuthorizationCode,configure 仍显示 remoteConfigured=true,取数时回 9040008。ConfigureResult.remoteHandlers 列出本次真正登记上的函数名,接进来先核对这一项最省事。
configure 只需在 App 启动或进入功能页时调一次,注入的 remote 实现会被插件保留到后续接口使用。插件侧已给 configure 加 @UTSJS.keepAlive(官方 UTS 插件文档「keepAlive」一节):UTS 默认在「收到回调的那次调用」返回后就释放 JS 传进来的函数,若不加这个装饰器,第一次取步数之后再调就会在原生侧报 exchangeAuthorizationCode回调函数已释放,不能再次执行(荣耀真机实测踩过,现象是远程通道整体失效、取步数必然失败)。重复 configure 以最后一次注入的实现为准,MUST NOT 每次取数都重新 configure。
JS→原生方向靠 @UTSJS.keepAlive 保活,原生→JS 方向的回调却根本传不过去(见上面 resolveRemote 的说明),两件事容易混为一谈:前者是「函数被回收」,后者是「函数压根没出现在入参对象里」。远程通道要跑通,两者都得按现在的实现走。
实现要点与注意事项
- 分层与唯一答案:
interface.uts只有声明;unierror.uts是错误码表 +UniError子类;core.uts放与平台无关的纯逻辑(通道判定、能力表、时间对齐、区间归一、日步数聚合、厂商原始码归类、结果装配);core-state.uts放有生命周期的配置中心ConfigHub与远程通道中心RemoteHub(与core.uts拆开只因单文件行数上限);各端index.uts只做「读设备侧事实 → 守卫 → 驱动厂商 SDK → 用 core 装配函数回结果」。通道判定与日步数聚合在三端只有一份实现,这两件事最容易写出行为差异,分端 MUST NOT 自己算一遍。 - 时间口径统一:
timestamp恒为本地时区当天 0 点、单位 ms,区间左闭右开。历史素材里 iOS / Android 返回毫秒而业务再除 1000、秒与毫秒混用的坑在本插件收口;dayStartOf只在一处实现,跨时区与夏令时的逐日推进用「日 0 点 + 整天数」而不是给原时刻加天。 - 厂商差异的处理方式:六套授权状态写法(
1/0/yes/authorized,鸿蒙与华为含义还相反)统一折叠到四个status值,原始值放rawStatus带出;厂商原始码(HMS50065、鸿蒙1002703001、HealthKit 错误域)不占段位序号,一律拼进errMsg的 detail——为每家单开一码会逼业务按厂商写分支,与「一份代码跑多端」相悖。 - 读取前的授权自检:小米通道在
getDailySteps里先查ACTIVITY_RECOGNITION是否已授予,未授予直接回9040003,不让 ContentProvider 抛错被翻成查询失败;荣耀(HonorId 未登录 / 无授权码)、iOS(getRequestStatusForAuthorization回shouldRequest,即还没拉起过授权页)、鸿蒙(getAuthorizations未含DAILY_ACTIVITIES)同样回9040003。两处例外:华为——HMS 对未授权只报查询失败,本插件如实落9040006并带出原始码,MUST NOT 为了口径统一去猜它是不是没授权;iOS——用户拒绝读取时 Apple 不给任何信号(查询回空),所以自检只能拦「没弹过授权页」,拦不了「弹了但点了不允许」,见上面的 iOS 口径说明。 - 华为长区间自动分段:
readDailySummation跨天区间实测报50065,插件按 7 天切段串行重读合并(并发读会互相触发同一个码),并如实把chunked=true告知业务;其余端不做重试,失败就直接回码。 - 荣耀判定必须在华为之前:荣耀独立后新机型仍可能上报
huawei,resolveChannel里先匹配honor/hihonor。 - iOS 混编层的原因:HealthKit 的授权与统计接口大量使用 Swift 值类型(
Set、Date、DateComponents、OptionSet),UTS 直接桥接不稳定,所以实际调用落在BearHealthKitNative.swift,app-ios/index.uts里没有任何 HealthKit import,只传毫秒数与布尔 / 字符串回调。跨这一层的数值入参与回调签名 MUST 是NSNumber而不是Double(UTS 的number在 iOS 端就编译成NSNumber,闭包类型也跟着是(NSNumber, NSNumber) -> Void)。 - iOS 读取授权只能「异步问系统要不要弹页」:
HKHealthStore.authorizationStatus(for:)看名字像授权状态,实际按 Apple 文档只表示「是否授权保存该类型」,读取被拒与被允许都看不出区别(真机实测:用户在授权页点了允许,它仍回原始值1)。所以本端判定走getRequestStatusForAuthorization(toShare:read:)(iOS 11+,异步),MUST NOT 在getDailySteps里同步读authorizationStatus当门禁——那会让所有 iOS 用户都拿不到步数。 - 撤销不假成功:
cancelSupported由通道能力表给出,不由分端自述;iOS / 小米回9040007并随guide给出系统设置或厂商 App 内的自助路径。 - 第三方原生依赖(版本与来源):Android 端全部由
utssdk/app-android/config.json声明并在云端打包时从厂商官方 Maven 仓库解析——华为com.huawei.hms:health:6.15.0.300、com.huawei.hms:hmscoreinstaller:6.6.0.300(https://developer.huawei.com/repo/)、荣耀com.hihonor.mcs:fitness-health:1.1.0.300、com.hihonor.mcs:honorid-opensdk:8.0.1.351(https://developer.hihonor.com/repo/);iOS 端只链接系统框架HealthKit.framework,鸿蒙端只用@kit.HealthServiceKit,两端都不引入第三方二进制。插件包内libs/与Frameworks/为空,也 MUST NOT 通过 npm 依赖或运行时下载的方式引入原生代码。 config.json的project为什么要 apply 华为的 Gradle 插件(动这段前必读):project.plugins: ["com.huawei.agconnect"]+project.dependencies: ["com.huawei.agconnect:agcp:1.9.1.301"],这是 DCloud 官方给的唯一注入口(plugins进云端工程 app 模块的plugins块,dependencies进项目级buildscript,见 https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#androidconfigjson)。华为侧的应用身份不是运行时读res/raw里的 json,而是由这个插件在构建期把它加密成string/agc_*资源写进安装包,agconnect-core运行时读的是这些资源——所以宿主只把agconnect-services.json放进nativeResources/android/而不 apply 插件,HMS 依然取不到应用身份(实测表现为授权页拉不起来、HMS 回原始码907135000)。版本 MUST 写死:官方文档示例给的是agcp:1.6.0.300,云端 AGP 8 上 apply 它会报API 'android.registerTransform' is removed.;1.9.1.301已实测可在同一云端链路正常出包(基座里string/agc_*66 条)。荣耀的com.hihonor.mcs.asplugin仍不注入——它的 classpath 坐标要挂在项目级 buildscript,本插件不持有这段配置;荣耀通道的 App ID 由configure({ honorAppId })在运行时传入。两个 SDK 的运行时依赖都是普通 Maven 制品,去掉 Gradle 插件后HuaweiHiHealth.getXxxController/HealthKit.getDataStoreClient照常可用,插件包内零厂商 App ID、零密钥。- 合规红线(发布审核关注点):插件不采集、不会上传任何健康数据,数据只在用户本机与其系统健康平台之间读取;
access_token/authorizationCode/openId只在内存传递,MUST NOT 落盘、MUST NOT 打日志;包内零域名、零密钥、不内置厂商 App ID;未configure或隐私未同意时不调用任何厂商 SDK。 - 回调语义:每个接口只触发一次
success或fail,并成对触发complete;远程实现重复回填同一个requestId时,RemoteHub在第一次结算就把该号摘掉,后续回填只回一句「requestId 不存在或已结算」,业务不会收到重复结果。等回填的请求最多保留 32 条,超限时淘汰最早那条,避免接入方忘记回填导致内存挂账。 - 申请系统权限前先验宿主 targetSdk:
UTSAndroid.requestSystemPermission对ACTIVITY_RECOGNITION有硬性前置——宿主targetSdkVersion必须 ≥ 29,低于它(uni-app 云打包默认 28)时接口同步抛IllegalStateException,异常穿过原生桥变成未捕获的 JS 错误,success/fail一个都不回。小米通道因此在申请前先checkSystemPermissionGranted(已授予直接成功返回,且查询本身包了 try/catch),再读applicationInfo.targetSdkVersion决定是否放行,宿主配置不当时回9040002并把要改的manifest.json路径写进errMsg。任何原生接口调用都要预置「它会抛」的收口,MUST NOT 让原生异常裸奔到 JS。 - 设备事实只用各端原生 API:
getEnvInfo的osName/osVersion/brand在 Android 取Build.BRAND/Build.VERSION.RELEASE(端标识就是常量'android',本文件只会被编译进这一端),iOS 取UIDevice.current.systemVersion(混编层出一个osVersion()函数,品牌恒为apple),鸿蒙取deviceInfo.brand/deviceInfo.osFullName。MUST NOT 换回uni.getSystemInfoSync():它是 uni ext-api 实现的 API,官方文档写明暂不支持在 UTS 插件里调用,uni-app(非 x)基座里没有io.dcloud.uniapp.extapi.*运行时类——UTS→Kotlin 编译能过,真机一点getEnvInfo/configure就抛NoClassDefFoundError: UniGetSystemInfoKt,且同步接口异常不会走 success/fail 回调,现象是「点了没反应」。只有utssdk/index.uts的 JS 兜底层可以用uni取(那一层跑在 JS 里)。 - Activity / Context 现取现用:Android 端每次调用取
UTSAndroid.getUniActivity(),不缓存成成员字段跨页面生命周期;取不到时回9040002而不是崩溃。鸿蒙端同理取UIAbilityContext。 - UTS 类型写法:数值判非法用
<= 0或!=判 NaN,不用null表达;Map只用于按天累加,遍历顺序不依赖迭代行为(改为「命中的天先落数组再 sort」),因为三端 Map 迭代语义不一致。 - Android 原生桥接的六条写法(UTS→Kotlin 编译实测,改代码前先照着做,别靠猜):
- Java 泛型回调接口 MUST 写成
implements OnSuccessListener<SampleSet>这种带实参的形式,实参取addOnSuccessListener期望的类型(HMS 的SampleSet/boolean/Void,荣耀账号 SDK 的SignInAccountInfo,荣耀取数的QueryResponse<SampleData>);写裸接口报「One type argument expected」,写any报类型不匹配。onSuccess的形参跟着写成可空。 - 异常对象取信息用
.message,Kotlin 侧java.lang.Exception没有getMessage()方法。 - 入参是 Java
String[]的接口(HMSrequestAuthorizationIntent、MIUIContentResolver.query)MUST 传arr.toTypedArray();UTS 的Array<string>直接传会报「actual type is 'UTSArray'」。 UTSAndroid.requestSystemPermission / checkSystemPermissionGranted收的就是UTSArray,不要多转一层。 java.util.List只能按下标搬:list.size判长度、list.get(i.toInt())取值,循环变量传给get前 MUST 显式.toInt();厂商返回的List也不直接交给 UTS 侧的Array<T>形参,先在监听类里搬成 UTS 数组。- UTS 的
number编译成 Kotlin 的Number,直接.toString()可能得到科学计数法串(1.76E12)。凡是要塞进 SQL 选择参数、ContentProvider 查询条件或厂商日期接口的数值,MUST 先.toLong().toString();小米通道实测这样才拿得到_begin_time的行。 - 从采集线程发回 JS 的回调 MUST 用
Handler(Looper.getMainLooper()).post(() => {...}),MUST NOT 用UTSAndroid.getDispatcher('main'):后者编译成CoroutineScopeKt.MainScope()+launch,协程内的异常没有 handler 会被静默吞掉,表现为接口调用「既不成功也不失败、日志也没有」。 另:荣耀取数链路的 Task 监听在com.hihonor.mcs.fitness.health.task包下,和账号 SDK 的com.hihonor.cloudservice.tasks不是同一套接口,混用编译期即报类型不匹配。
- Java 泛型回调接口 MUST 写成
错误码
错误码以 90 开头,本插件占 9040 段位,序号 009 固定表示平台不支持。与 utssdk/unierror.uts 逐项一致。
| errCode | 说明 |
|---|---|
| 9040001 | 参数错误(如 wantWrite: true,v1 只实现只读) |
| 9040002 | 当前设备或厂商没有可用的健康数据通道(含取不到 Activity / Context、荣耀通道缺 honorAppId、宿主 targetSdkVersion 低于 29 导致小米通道无法申请系统权限) |
| 9040003 | 未授权或授权被用户拒绝(iOS 端只表示「还没拉起过授权页」,用户在授权页里拒绝读取时 Apple 不给任何信号) |
| 9040004 | 隐私合规未通过,请先向用户说明数据用途并调用 configure(含厂商侧运动健康隐私政策未同意) |
| 9040005 | 授权状态查询失败(含荣耀 HonorId 在 20s 内没有任何回调,errMsg 会列出要核对的开放平台登记项) |
| 9040006 | 步数数据读取失败(含荣耀换 token 后采样查询在 20s 内无回调) |
| 9040007 | 当前端或厂商不支持在插件内取消授权(iOS / 小米;errMsg 带自助撤销路径) |
| 9040008 | 远程通道未注册或调用失败(vivo / 荣耀未注入 remote 实现;荣耀取步数还要求注入的实现带 exchangeAuthorizationCode,且换回的响应里有非空 accessToken——服务端超时、HTTP 非 200、包里没有 token 都算这一码,errMsg 里带远程接口回的原文与荣耀链路摘要) |
| 9040009 | 当前平台不支持(Web / 小程序等无原生实现的端) |
目录结构
bear-health-data/ # 独立可运行的 uni-app(vue3)工程,HBuilderX 导入即到案例页
├── pages.json # 首屏登记为案例页
├── manifest.json / main.js / App.vue / uni.scss
├── vendor-access-checklist.md # 厂商资质申请材料清单(不随插件包发布,需向作者索取)
├── deviation.md # 对仓库规则的偏离记录(§9),不随插件包发布
└── uni_modules/bear-health-data/ # 发布时只右键上传这个目录
├── package.json # 元数据、平台矩阵与各端最低版本
├── readme.md / changelog.md / license.txt
├── huawei-android-setup.md # 华为 Android 通道接入与自检说明
├── example/index.vue # 案例页:环境检测 / 正常路径 / 失败路径 / 各厂商取消授权 / 生命周期;每个按钮下方直接显示本次结果,荣耀机型额外给出 App ID 与远程接口地址两个输入项
├── example/vivo-auth.vue # vivo 授权页承接页(整页一个 web-view,复制到宿主后需在宿主 pages.json 登记)
└── utssdk/
├── interface.uts # 类型与错误码契约(只有声明)
├── unierror.uts # 9040 段位错误码表 + HealthDataFailImpl
├── core.uts # 通道判定与能力表、时间与区间、步数聚合、状态归一、结果装配
├── core-state.uts # ConfigHub / RemoteHub(有生命周期的跨端状态中心)
├── index.uts # Web / 小程序等无原生实现端的兜底导出
├── app-android/ # index.uts 主类 + steps-xiaomi / steps-huawei / steps-honor
│ ├── config.json # minSdk 23 + HMS / Honor Maven 依赖与仓库 + 华为 AGC Gradle 插件
│ └── AndroidManifest.xml # 只声明小米通道需要的 ACTIVITY_RECOGNITION 与 READ_STEPS
├── app-ios/ # index.uts(无 HealthKit import)
│ ├── BearHealthKitNative.swift # HealthKit 混编实现
│ ├── Info.plist # NSHealthShareUsageDescription
│ ├── UTS.entitlements # HealthKit capability
│ └── config.json # deploymentTarget 12 + HealthKit.framework
└── app-harmony/ # index.uts 驱动 @kit.HealthServiceKit + config.json(API 12)

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 10
赞赏 0
下载 12693039
赞赏 1956
赞赏
京公网安备:11010802035340号