简介:这是一份可直接运行的小程序源码包,面向需要在小程序中接入微信运动步数功能的开发者,覆盖从用户登录态获取到运动数据解密展示的完整链路,适合前端入门者学习接口串联,也适合有基础者快速移植复用。资源共三个文件,含页面文件、环境配置与Git忽略规则,压缩包仅9KB,导入开发工具后即可查看运行效果,已有一百五十八人学习下载。源码清晰演示了wx.login获取openid与session_key、wx.getWeRunData读取加密运动数据、WXBizDataCrypt解密得出步数的具体流程,同时给出wx.getSetting、wx.openSetting、wx.authorize等权限引导用法,便于在用户开启微信运动的前提下优化授权体验;整个流程对用户运动开关状态、接口返回异常与解密失败场景均有可参考的应对思路。读者可对照源码拆解接口调用顺序、参数含义与异常处理细节,直接二次开发或嵌入自身小程序项目,有效节省查阅文档与踩坑时间。
1. 小程序获取微信运动步数:这个需求为什么卡住了不少人
做微信小程序的人,应该都遇到过这类需求:做个运动打卡、步数排行榜、健康周报,第一步就是拿用户的微信运动步数。听起来不就是调用一个 API 吗?真做起来会发现,坑比想象的多——授权弹窗要写对、步数只能在特定时间段读、解密验签要在后端做、模拟器上永远拿不到真实数据。这一套走通,新手容易在某个环节卡上一两天。
这篇东西要解决的就是「从零到跑通」这件事。我会把获取微信运动步数的完整链路拆开:前端怎么发起授权、后端怎么验签解密、数据要落在什么结构里,以及每一步最容易踩的坑。最后会给出一套前后端可运行的思路和关键代码,拿到手改改 appid 和密钥就能跑。适合刚接触微信小程序、或者被运动步数卡过的同学照着做。我们直接进正题。
2. 微信运动步数获取原理:先搞懂这个能力的前提与边界
2.1 这背后的接口与授权逻辑
微信小程序获取运动步数,靠的是wx.getWechatMovementStep这个接口。它不是普通 API,而是与微信运动深度联动的能力。用户必须已经在微信运动里授权过数据,小程序端还要再次拉起授权弹窗,两层授权都过了,才有机会拿到步数。
接口的行为逻辑可以这样理解:每次拉起这个接口,微信会返回一段加密数据(encryptedData)和对应的初始向量iv,外加一个校验用的signature。步数数据并不直接出现在接口返回值里,而是包在加密字符串里。也就是说,你拿到那串密文,才是刚刚开始。
这是我建议你先在开发者工具里跑通的探测代码:
wx.getWechatMovementStep({ timeRange: { startTime: Date.now() - 86400000, // 昨天 endTime: Date.now() }, success(res) { console.log('encryptedData:', res.encryptedData) console.log('iv:', res.iv) console.log('signature:', res.signature) }, fail(err) { console.error('获取失败', err) } })这里的timeRange是必传参数,起始时间用毫秒时间戳。上面这段代码取的是「昨天」到「现在」的范围,实际开发里通常也是这个区间——微信运动步数接口对跨天数据的支持并不友好,后面避坑章节我会专门说。你先跑这段,能在 console 里打出三样东西,说明授权链路没问题,接下来才谈得上解密。
2.2 为什么必须后端解密,而不是前端直接解
这里有一个很常见的误区。很多新手看到encryptedData会想:前端拿到之后,直接用小程序里存的session_key解密不就行了?代码写起来好像也不难,几分钟就能跑通。但真要在项目里这么干,你就是把用户的步数数据暴露在了一个完全不受控的环境里。
session_key是通过wx.login获取的会话密钥,它在官方设计里就只能存在服务器端。一旦把这个值下发到小程序前端,任何人都可以通过抓包或者调试工具把它捞出来。步数属于用户隐私数据,后续微信审核时也会重点关注:如果发现小程序在本地拿着session_key做用户数据解密,审核被打回是大概率事件。
所以整个正确的链路是:
小程序端 wx.login 获取 code 小程序端将 code 发送到自有后端 后端用 code 换 session_key (需要 appid + secret) 后端用 session_key 解密 encryptedData 后端把解密后的步数存入自己的数据库或缓存 小程序端只拿到解密结果关键就在于:session_key不落地前端,后端解密完成后直接落库,连返回给前端都可以只给「步数值」而不再回传密文。这既是安全要求,也是审核要求的常规姿势。常见做法是后端加一个缓存层,把session_key按用户维度存起来,避免每次拿步数都重新登录一次,减少接口往返。
3. 可运行源码的落地:前端调用与授权握手完整实现
3.1 授权链路的最小可运行代码
小程序端先要引导用户完成微信运动的授权。注意,这里的授权分两段:第一段是微信运动在微信 App 层面的授权,第二段是小程序内部的授权弹窗。第一段不是你能主动拉起的,用户没开,小程序端只能提示去开。
我一般会把这两段状态合并成一个前置检查函数,放在首页的onLoad或进入步数页面前调用:
// pages/steps/index.js const authService = require('../../services/authService') Page({ data: { hasAuthorized: false, stepList: [], todaySteps: 0 }, async onLoad() { const authResult = await authService.checkMovementAuth() if (!authResult.authorized) { // 还没授权,用 wx.getSetting 尝试拉起授权弹窗 await this.requestMovementAuth() } else { this.setData({ hasAuthorized: true }) await this.fetchStepData() } }, async requestMovementAuth() { wx.getSetting({ success: async (res) => { if (res.authSetting['scope.werun']) { // 之前拒绝过,需要引导去设置页打开 wx.showModal({ title: '提示', content: '需要微信运动授权才能读取步数,是否前往设置开启?', success: (modalRes) => { if (modalRes.confirm) { wx.openSetting() } } }) } else { // 首次申请权限 wx.authorize({ scope: 'scope.werun', success: () => { this.setData({ hasAuthorized: true }) this.fetchStepData() }, fail: () => { wx.showToast({ title: '授权失败', icon: 'none' }) } }) } } }) }, async fetchStepData() { wx.login({ success: async (loginRes) => { const code = loginRes.code const stepsRes = await this.getEncryptedStepData() console.log('步数查询原始数据:', stepsRes) // code 和加密数据一起发给后端解 wx.request({ url: 'https://your-domain.com/api/steps/decrypt', method: 'POST', data: { code, encryptedData: stepsRes.encryptedData, iv: stepsRes.iv, signature: stepsRes.signature }, success: (res) => { const steps = res.data.steps this.setData({ todaySteps: steps[steps.length - 1]?.stepCount || 0 }) } }) } }) } })这段代码的核心逻辑有几个地方值得说明。首先scope.werun是小程序里微信运动授权的固定 scope 名,不能写错。其次,wx.authorize只适合首次弹窗,如果用户之前已经点过拒绝,再调它不会弹窗而是直接走 fail,所以要用wx.openSetting引导用户去设置页手动打开。最后,wx.login要放在每次获取步数之前,因为后端需要新的code去换session_key。
3.2 后端接密与解密的完整代码参考
后端选型上,用 Node.js 的开发者比较多,因为小程序生态里 Node 的库最齐。这里给一份基于 Node.js + Koa2 的解密接口实现。session_key的获取依赖小程序官方的code2Session接口,你需要把 appid 和 secret 配置在环境变量里,不要写死在代码里。
// routes/steps.js const Router = require('koa-router') const crypto = require('crypto') const axios = require('axios') const router = new Router() const APPID = process.env.WX_APPID const SECRET = process.env.WX_SECRET async function code2Session(code) { const url = 'https://api.weixin.qq.com/sns/jscode2session' const { data } = await axios.get(url, { params: { appid: APPID, secret: SECRET, js_code: code, grant_type: 'authorization_code' } }) return data } function decryptWechatData(encryptedData, sessionKey, iv) { // 微信官方推荐方式是 AES-128-CBC 解密,iv 是十六位 const decodedKey = Buffer.from(sessionKey, 'base64') const decodedIV = Buffer.from(iv, 'base64') const decipher = crypto.createDecipheriv('aes-128-cbc', decodedKey, decodedIV) decipher.setAutoPadding(true) let decoded = decipher.update(encryptedData, 'base64', 'utf8') decoded += decipher.final('utf8') const data = JSON.parse(decoded) if (data.watermark.appid !== APPID) { throw new Error('水印校验失败:appid 不匹配') } return data } router.post('/decrypt', async (ctx) => { const { code, encryptedData, iv, signature } = ctx.request.body // 1. code 换 session_key const sessionRes = await code2Session(code) if (!sessionRes.session_key) { ctx.status = 400 ctx.body = { message: 'code 失效或非法,错误码: ' + sessionRes.errcode } return } // 2. 签名校验(可选但建议做,多一层防御) const signText = `${encryptedData}${sessionRes.session_key}` const localSign = crypto.createHash('sha1').update(signText).digest('hex') if (localSign !== signature) { ctx.status = 400 ctx.body = { message: '签名校验失败' } return } // 3. 解密 try { const stepData = decryptWechatData(encryptedData, sessionRes.session_key, iv) // stepData.stepInfoList 就是步数数组 ctx.body = { steps: stepData.stepInfoList } } catch (err) { ctx.status = 500 ctx.body = { message: '解密失败: ' + err.message } } }) module.exports = router解密之后的stepInfoList长这样:数组里每个元素包含timestamp和stepCount,时间戳是秒级。一天之内的数据是整点切分的,比如分成 24 个段。你前端拿到的步数可以取数组最后一个元素作为“到目前为止”的步数。
这里我特别加了签名校验,很多人会忽略这一步。虽然它不是解密流程的必需步骤,但它能帮你提前发现请求被篡改或者 session_key 不匹配的问题。如果signature校验失败,几乎可以肯定前端传的encryptedData和你后端手里换到的session_key对应不上。
3.3 数据字段与存储建议
解密出来的 JSON 结构其实是固定的,你只需要关注一个顶层字段和两个子字段:
| 字段 | 类型 | 说明 |
|---|---|---|
stepInfoList | Array | 步数列表,按时间戳升序排列 |
stepInfoList[].timestamp | Number | 步数对应的时间戳,单位是秒 |
stepInfoList[].stepCount | Number | 该时间点的累计步数 |
注意timestamp是秒,不是毫秒。前端Date.now()出来是毫秒,后端解析时千万换算一下,否则你把数据直接扔给前端渲染,会出现所有步数都指向 1970 年附近的情况,这也是个容易闹笑话的翻车点。
存储上我建议直接按天存储,不要按小时存储。因为每天的stepInfoList会在你下一次调用时被微信重新生成,历史数据不保留。如果你要做一个 7 天步数趋势图,那就要每天定时拉一次并存起来。一个简单的表结构大概是:uid, stat_date, step_value, source, update_time,每天一行即可。常见做法是用户在当天第一次打开小程序时拉取并 Upsert,同一天重复拉取就覆盖,这样既省存储也避免重复数据干扰统计。
4. 微信运动步数的完整演示流:从登录到拉取一次搞定
4.1 登录态与步数拉取的先后关系
很多第一次做这个功能的开发者会把顺序搞反。他们会先调wx.getWechatMovementStep拿到加密数据,再调wx.login拿code,然后把两个数据装一个包里发给后端。这个顺序不是不行,但如果你把两步反过来,先wx.login再拉步数,整体流程会更顺:因为后端必须先有session_key才能解密,而code优先到达后端,可以减少一次等待。
更值得注意的一点是:wx.login生成的code有效期只有五分钟,用一次就作废。如果你的后端解密失败,前端重新发起wx.login生成新code,再重新拉encryptedData,而不是拿旧的encryptedData去复用,否则新旧session_key不匹配,解密必然失败。
这里给一个典型的前后端交互时序图——不是流程图,是文字版:
小程序端 -> wx.login 获取 code 小程序端 -> wx.getWechatMovementStep 获取 encryptedData / iv 小程序端 -> 发送 {code, encryptedData, iv} 到自有后端 后端 -> code 换 session_key 后端 -> 校验 signature 与 watermark.appid 后端 -> AES-128-CBC 解密 后端 -> 返回 stepInfoList 小程序端 -> 渲染步数图表这一步如果失败,建议先用后端code2Session的返回结果做排查点。只要session_key能正常换到,剩下的解密就是纯算法问题,问题定位范围会小很多。
4.2 微信运动步数的完整前端处理示例
把上面的代码整理成一个可以直接跑通的页面,是很多同学想要的东西。我把它整合成一个完整的Page逻辑,页面里只做三件事:检查授权、拉取步数、展示步数列表。
const app = getApp() const DOMAIN = 'https://api.your-domain.com' Page({ data: { steps: [], total: 0, loading: true, authDenied: false }, onShow() { this.init() }, async init() { const res = await this.checkAuth() if (!res) return await this.loadSteps() }, checkAuth() { return new Promise((resolve) => { wx.getSetting({ success: (res) => { const auth = res.authSetting['scope.werun'] if (auth === undefined) { wx.authorize({ scope: 'scope.werun', success: () => resolve(true), fail: () => { this.setData({ authDenied: true }) resolve(false) } }) } else if (auth === true) { resolve(true) } else { this.setData({ authDenied: true }) resolve(false) } } }) }) }, loadSteps() { wx.login({ success: (loginRes) => { wx.getWechatMovementStep({ timeRange: this.getTimeRange(), success: (stepRes) => { wx.request({ url: `${DOMAIN}/api/steps/decrypt`, method: 'POST', data: { code: loginRes.code, encryptedData: stepRes.encryptedData, iv: stepRes.iv }, success: (resp) => { const list = resp.data.steps || [] const total = list.length ? list[list.length - 1].stepCount : 0 this.setData({ steps: list, total, loading: false }) }, fail: () => { this.setData({ loading: false }) wx.showToast({ title: '步数解析失败', icon: 'none' }) } }) }, fail: (err) => { console.error('getWechatMovementStep fail', err) this.setData({ loading: false }) } }) } }) }, getTimeRange() { const start = new Date() start.setHours(0, 0, 0, 0) return { startTime: start.getTime(), endTime: Date.now() } } })这段代码在原有基础上做了两个调整。第一个调整是把timeRange的起点改为「今天零点」,而不是昨天相同时间——因为用户打开页面时最关注的是今天走到多少步,而且微信运动接口对跨天数据的返回粒度不稳定,容易出现昨天残留数据干扰今天的统计。第二个调整是把onLoad改成了onShow,因为用户从设置页开完授权返回小程序时,onShow会重新触发,体验上更顺滑。
4.3 开发者工具里拿到的测试数据特征
在小程序开发者工具里运行上面的代码,你会发现在模拟器环境里wx.getWechatMovementStep经常返回固定的模拟数据,或者直接报错。这是正常现象,不代表代码有问题。真实设备上,微信运动会返回过去一段时间的真实步数,而且接口在开发者工具里的表现和真机上差异很大。
模拟器常见现象是:encryptedData往往很短,或者stepCount一直为 0。这背后是因为开发者工具没有真实的运动传感器数据源,只能模拟一个默认值。真正的验证只能靠真机预览,在开发者工具右上角点「预览」扫码,拿到真机上报的数据后才知道代码靠不靠谱。这也是为什么很多同学会在这一步反复怀疑自己代码写错了——别慌,换个方式验证就好。
# 小程序开发者工具上传代码后, 用预览码在真机上调试 # 真机调试模式下 vConsole 可以直接看到 wx.request 返回结果真机调试时,timeRange.endTime不要传未来时间,否则接口会报参数错误。另外,startTime和endTime的最大跨度不要超过 30 天,这是接口的隐藏限制。
5. 获取微信运动步数的 5 个典型坑与排查手段
5.1 开发者工具里永远报错 no permission
现象:在开发者工具里调用wx.getWechatMovementStep,返回fail: no permission或者错误码 63001。
原因:开发者工具默认没有开启微信运动权限,需要手动在工具的「详情 - 本地设置」里勾选「模拟微信运动」,或者你的 appid 没有在主账号下完成微信运动功能的开通。
解决:先在「微信公众平台 - 设置 - 接口设置」里确认已开通「微信运动数据」权限。代码层面,在开发者工具右上角「详情」里找到「本地设置」,勾选「模拟微信运动」,然后把appid换成自己的测试号,重新编译。如果用的还是测试号,注意测试号并不支持全部接口能力。
5.2 真机上步数一直为 0
现象:真机调试时,stepCount全部是 0,但接口本身返回成功,没有报错。
原因:微信运动步数接口有一个特性——用户在微信运动里没有打开「数据同步」,或者手机系统没有将运动健康数据同步给微信 App。这会导致微信侧拿到的原始步数就是 0,代码层面再正确也没用。
解决:让用户先在微信 App 里搜索「微信运动」并打开公众号,进入后开启「记录运动数据」。iOS 用户还要检查「健康 App」里是否授权给了微信。这一步属于用户侧配置,小程序里发现步数始终为 0,要引导用户检查微信运动公众号设置,而不是反复调接口。我一般在stepCount全为 0 时加一个引导弹窗,让用户知道不是程序坏了,而是系统数据源没开。
5.3 signature 校验永远失败
现象:后端在本地用sha1(encryptedData + session_key)计算出的签名,和前端传过来的signature不一致。
原因:通常是session_key不是当前encryptedData对应的密钥。常见的前端错误是:用户在小程序里登录过一次,后来重新调了wx.login获取了新code,但encryptedData还是旧登录态下拿到的,新旧不匹配,签名必然对不上。
解决:每次调用wx.getWechatMovementStep并拿到encryptedData之后,立刻执行wx.login获取新code,然后把两个数据放在同一个请求里发给后端。不要缓存encryptedData,也不要复用之前的code。当你怀疑session_key过期时,强制重新执行一次wx.login即可。
5.4 跨天时间戳导致图表数据乱跳
现象:前端拿到stepInfoList后直接渲染,发现步数曲线在凌晨 0 点时突然跳回很低的数值,或者曲线出现重复段。
原因:stepInfoList返回的是整个时间范围内的所有分段点。如果你传的是「昨天 + 今天」的跨度,列表里就会包含两天的数据,且第二天的数据会从 0 重新开始累计,直接渲染就在图表上出现断崖式回落。
解决:前端拿到列表后先做一次过滤,只保留timestamp属于「今天」范围的分段数据。比较稳的做法是后端在返回前也做一次过滤,避免前端逻辑被绕开。我一般会让前端先拿到完整数据用于调试,但渲染层面过滤一次,这样定位问题更可控。对stepInfoList的解析,推荐把timestamp统一转成HH:mm格式再渲染,免得不同端显示偏差搞混排查思路。
5.5 审核被拒:小程序涉及收集用户健康数据
现象:提审时被驳回,理由通常是「小程序存在收集用户微信运动数据的行为,但未在隐私保护指引中明确说明用途」。
原因:微信运动步数属于用户敏感信息,平台对于这类数据的收集是有硬性审查机制的。你的小程序如果要在「用户隐私保护指引」里声明收集「运动数据」这一项,并且说明用途,但很多开发者最初做这个功能时没有及时去后台更新隐私配置。
解决:登录公众平台,在「设置 - 服务内容声明 - 用户隐私保护指引」中添加收集的信息类型为「运动数据」,用途写清楚「用于展示用户每日运动步数和运动趋势」。同时在代码里做弹窗提示,告知用户步数会被用于哪些功能,让用户知情。这两步都做到位,审核正常就能通过。
6. 验证这套方案是否真的跑通:三个关键检查点
整条链路跑通之后,怎么确定每一步没有踩坑?我建议按顺序检查三个点。
第一个检查点是signature是否校验通过。你在后端把sha1(encryptedData + session_key)的结果拿出来对比一下前端传的signature,一致说明code与encryptedData用的是同一个会话,链路的前半段没问题。如果这一步失败,不要去查后面的解密逻辑,先把登录和会话关系理清。
第二个检查点是解密后的watermark.appid是否正确。解密成功不代表安全,你要在代码里明确校验data.watermark.appid === 你的 appid。这个字段是官方用来防止数据被放到其他环境解密的保护机制。有些开发者忽略这一步,遇到数据异常时排查起来会非常痛苦,因为错误往往不在算法层面,而是密文本身就来自另一个小程序环境。检查点放这里,相当于给解密过程上了一道保险。
第三个检查点是stepInfoList的完整度。拿到的列表长度,对应你传入时间跨度的小时数加一。比如你传startTime是今天 0 点,endTime是下午 4 点,正常情况下应该拿到 17 个数据点,也就是 0 点到 16 点每小时一个点。如果点数严重低于预期,可能是手机系统对微信运动数据同步的频率不够,或者你的startTime和endTime跨度太大被接口截断。我会在页面上埋一个日志,把「实际拿到的分段数」和「预期分段数」都记录下来,对比后快速定位。
最后提一个我自己的使用习惯:生产环境里不要在小程序前端打印encryptedData和iv的完整内容,只打印步骤数结果。之前 Debug 时把完整密文打到了 vConsole 里,后来发现这些日志会直接暴露在用户调试面板中,等于是把用户数据半公开了。从那以后,我要求所有加密数据相关日志在后端打,前端只打最终渲染值。这个习惯坚持下来,省了很多不必要的隐私争议。
如果读完这篇,你还是觉得步数数据链路复杂,那也不用灰心——它属于「全网都能搜到概念、但正确跑通的人并不多」的能力类型。收藏这份流程,找个小项目先落地一次,把授权、换密钥、解密、渲染走一圈,你会发现第二次做运动健康类功能时,整条链路会快很多。希望这篇笔记帮到你上手微信运动步数功能。
本文还有配套的精品资源,点击获取