微信小程序步数排行榜开发:getWeRunData解密与后端架构
简介werun 是一个基于微信小程序的步数计数与排名源码项目主要使用 JavaScript 开发面向想入门小程序开发或希望进阶掌握微信运动数据接口的开发者。它围绕“每日步数获取—数据处理—排行榜展示”这条主线完整演示了调用微信运动开放接口、授权处理、数组排序与界面实时刷新等关键环节也覆盖了生命周期与状态管理的基础用法。压缩包共 21 个文件包含 9 个 .js 逻辑脚本、3 个 .wxml 页面结构、4 个 .wxss 样式文件、4 个 .json 配置以及 1 个 README 说明文档整体仅 27KB结构清晰、便于逐文件研读。目前已有 1873 人学习下载适合作为实战练手项目。通过学习这套源码可以掌握小程序项目目录组织方式从数据获取到排名展示的完整链路理解如何将微信运动数据接入业务逻辑并在实际编码中体会 JavaScript 模块化与数据处理的常见技巧。 这个项目的初始动机特别朴素跑步群里每天截图微信运动排名再手动统计一周总步数费时又容易漏。干脆做个微信小程序把“步数计数”和“好友排名”两件事自动化。我给它起名 werun核心功能很明确——拉取微信运动步数生成可分享的排行榜。做之前我以为最难的是 UI 和交互实际走下来才发现真正的门槛在数据链路微信运动属于用户敏感数据小程序前端拿到的不是明文步数而是经过加密的数据串。这就意味着必须有小程序后端参与前端先获取加密数据后端再结合登录态解密才能拿到真实步数并落库。整个项目的核心架构其实是在围绕“安全拿到步数并正确排序”展开。这篇文章适合两类人一是想给运动社群做步数打卡/排名工具的产品经理或独立开发者二是在学习微信小程序服务端开发、想搞懂 encryptedData 解密和 session_key 机制的前端同学。我会把从项目搭建、授权取数、后端解密、排行榜设计到真机调试踩坑的完整过程按我实际开发的顺序拆开讲附带可直接抄走的代码和排查思路。1. 数据从手机到后端微信运动步数的加密链路你可能会问小程序里不是有现成的接口能拿步数吗直接在前端读出来再传给排行榜展示不就完了实际不行。微信的规则是wx.getWeRunData 确实能拿到步数数据但返回的是一个加密字符串 encryptedData外加一个解密用的初始向量 iv。这两个字段在你看来几乎是一堆乱码必须配合小程序后端的 session_key 才能解开。session_key 相当于解密密钥只在用户登录时由微信服务器下发给你的后端前端永远拿不到。这个设计让我想起快递柜的取件逻辑你想取包裹步数快递员先给你一个取件码encryptedData 和 iv但取件码本身没有意义必须是快递柜系统后端验证你的身份后再用内部保存的开柜密钥session_key去匹配柜门才打开。小程序前端直接操作快递柜等于把开柜权限暴露出去了微信当然不允许。所以 werun 的整体流程被我设计成三步用户点击“同步步数”前端调用 wx.login 获取临时登录凭证 code同时调用 wx.getWeRunData 拿到 encryptedData 和 iv。前端把 code、encryptedData、iv 一起 POST 到自己的后端接口。后端用 code 调用微信的 code2session 接口换取 openid 和 session_key再用 session_key 解密 encryptedData得到步数列表最后写入数据库。这里有几个容易忽略的细节。code 有效期只有 5 分钟而且只能用一次所以“取登录态”和“存步数”最好放在同一个接口请求里处理不要分两次。session_key 的有效期不固定微信可能在用户主动调用 wx.login 或长时间未使用时刷新它所以后端不要长期缓存 session_key最好每次拿步数时都重新换一次。代价是请求量大了点但微信的 code2session 接口本身不收费换取频率可控不必担心。另一个坑是encryptedData 是个很长的字符串里面包含了步数时间戳数组。前端传给后端时建议用 POST 而非 GET否则 URL 长度很容易超限而且运动数据走 GET 容易留在日志里有泄露风险。2. 前端接入 getWeRunData授权、取数与异常处理前端部分第一步是让用户授权。微信把获取运动数据归为 scope.werun和获取用户信息、获取位置是同一类敏感权限需要明确的授权动作。我这里用了用户主动点击按钮触发避免一进页面就弹窗造成骚扰。// pages/sync/sync.js Page({ data: { syncing: false }, onLoad() { this.checkWeRunAuth(); }, // 检查是否已授权过微信运动 checkWeRunAuth() { wx.getSetting({ success: (res) { const auth res.authSetting[scope.werun]; if (auth false) { wx.showModal({ title: 需要微信运动权限, content: 请在设置中开启微信运动权限才能同步步数, confirmText: 去设置, success: (modalRes) { if (modalRes.confirm) { wx.openSetting(); } } }); } } }); }, // 用户点击同步按钮 handleSync() { if (this.data.syncing) return; this.setData({ syncing: true }); // 1. 先取登录 code wx.login({ success: async (loginRes) { if (!loginRes.code) { this.toast(登录失败请重试); return; } // 2. 取微信运动加密数据 wx.getWeRunData({ success: async (runRes) { if (!runRes.encryptedData || !runRes.iv) { this.toast(获取步数失败请确认已开启微信运动); return; } // 3. 提交到后端 const result await this.uploadStepData({ code: loginRes.code, encryptedData: runRes.encryptedData, iv: runRes.iv }); if (result result.stepCount 0) { this.setData({ todayStep: result.stepCount }); this.toast(同步成功今日 ${result.stepCount} 步); } else { this.toast(未获取到有效步数); } }, fail: () { this.toast(获取步数失败请检查微信运动设置); }, complete: () { this.setData({ syncing: false }); } }); }, fail: () { this.setData({ syncing: false }); this.toast(登录失败请重试); } }); }, uploadStepData(payload) { return new Promise((resolve) { wx.request({ url: https://api.yourdomain.com/api/sync-step, method: POST, data: payload, success: (res) resolve(res.data), fail: () { this.toast(网络请求失败); resolve(null); } }); }); }, toast(msg) { wx.showToast({ title: msg, icon: none }); } });这段代码里有一个我实际开发中反复踩的坑wx.getWeRunData 在开发者工具的模拟器里根本拿不到真实数据经常返回空 encryptedData 或者干脆报错。这个接口强制依赖真机的传感器和微信运动记录模拟器是没有的。所以测试时一定要用真机预览而且手机本身要在微信运动里有步数记录否则就是白测。关于授权拒后引导我的经验是不要每次进入页面都弹 wx.openSetting那会非常烦人。比较好的做法是只检测一次只有当 authSetting[scope.werun] 是 false 时才弹窗引导如果用户从未处理过授权authSetting 里没有这个 key就等用户主动点击“同步”按钮时触发授权弹窗。这样既不打扰也不会错过授权时机。还有个细节上传步数的接口返回后如果当天已经同步过后端返回的可能不是最新步数而是库里存的历史数据。所以我在后端做了个逻辑——每次同步都 upsert存在则更新不存在则插入确保返回的一定是当天最新值。前端拿到 stepCount 后立即刷新排行榜接口形成“同步完马上看到排名变化”的顺畅体验。3. 后端解密与存储werun 的核心服务端逻辑服务端我选的是 Node.js MySQL主要考虑到和微信生态的兼容性、部署成本以及排行榜查询的便利性。解密这一步Node 生态里有现成的 crypto 模块不用额外装依赖。先看 code2session 和 Aes 解密的实现这是 werun 后端最关键的一段// services/wechat.js const crypto require(crypto); const axios require(axios); const WX_APPID your_appid; const WX_SECRET your_app_secret; // code 换 session_key async function code2Session(code) { const url https://api.weixin.qq.com/sns/jscode2session; const params { appid: WX_APPID, secret: WX_SECRET, js_code: code, grant_type: authorization_code }; const resp await axios.get(url, { params }); const { openid, session_key, errcode, errmsg } resp.data; if (errcode) { throw new Error(code2session failed: ${errcode} ${errmsg}); } return { openid, session_key }; } // AES-128-CBC 解密注意 key 是 session_keyiv 是前端传来的 iv function decryptWeRunData(sessionKey, encryptedData, iv) { const key Buffer.from(sessionKey, base64); const ivBuf Buffer.from(iv, base64); const encryptedBuf Buffer.from(encryptedData, base64); const decipher crypto.createDecipheriv(aes-128-cbc, key, ivBuf); decipher.setAutoPadding(true); let decoded decipher.update(encryptedBuf, base64, utf8); decoded decipher.final(utf8); return JSON.parse(decoded); }解密后得到的 JSON 结构大致是{ watermark: { appid: your_appid, timestamp: 1712390400 }, stepInfoList: [ { timestamp: 1712332800, step: 1234 }, { timestamp: 1712419200, step: 4567 }, { timestamp: 1712505600, step: 7890 } ] }stepInfoList 数组里是按天为单位的时间戳和步数timestamp 是当天零点的时间戳形式不是精确到分秒的采集点。我只需要取出其中时间戳对应“今天”的那一项就是用户当前的最新步数。这里必须提一个安全校验点解密后一定要检查 watermark.appid 是否等于自己的小程序 appid。因为 encryptedData 是微信生成的如果 appid 对不上说明数据来源异常或者被中途篡改过直接丢弃。这是官方文档明确要求做的但很多教程没提。存储方面werun 的表结构比我预想的简单核心就一张表CREATE TABLE we_run_records ( id int(11) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 用户唯一标识, nickname varchar(64) DEFAULT 微信用户, avatar_url varchar(512) DEFAULT , step_count int(11) NOT NULL DEFAULT 0, record_date date NOT NULL COMMENT 对应步数的日期, updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid_date (openid, record_date), KEY idx_date_step (record_date, step_count) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唯一索引 uk_openid_date 是关键保证同一用户同一天只能有一条记录。写入时用 INSERT ... ON DUPLICATE KEY UPDATE这样“同步”操作天然支持幂等反复点击同步按钮也不会产生脏数据。idx_date_step 复合索引是为了排行榜查询准备的MySQL 可以直接命中record_date 今天 ORDER BY step_count DESC这个查询走索引扫描人少的时候几乎感觉不到压力。写入异步处理我也做了一层缓冲前端同步接口只负责解密和写库如果写库失败直接返回错误前端提示“稍后重试”如果写库成功再异步刷新 Redis 里当天的排行榜缓存。微信步数排行榜的用户量一般不会特别大但也别小看群里的高频刷新接口层加个 30 秒缓存能省很多数据库压力。4. 排行榜排名逻辑与接口设计排行榜是 werun 最直观的功能也是产品体验的核心。排名逻辑不复杂难点在于“今日榜”和“总榜”要同时支持且刷新频率不低。今日榜的 SQL 最简单SELECT openid, nickname, avatar_url, step_count FROM we_run_records WHERE record_date CURDATE() ORDER BY step_count DESC LIMIT 50;如果要做“近 7 天总榜”需要把同一个人 7 天的步数先求和再排序SELECT openid, nickname, avatar_url, SUM(step_count) AS total_step FROM we_run_records WHERE record_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 6 DAY) AND CURDATE() GROUP BY openid ORDER BY total_step DESC LIMIT 50;注意“近 7 天”如果用 7 个日期字符串直接拼 IN性能差且代码难看用 BETWEEN 走索引更稳。openid 不能直接返回给前端因为那是敏感信息我在接口里统一做了一层映射把 openid 替换成一个自增的用户 uid或者干脆只返回 nickname avatar_url。接口设计上我遵循“列表接口必须分页”的原则即使目前只有几十个人也做了 limit/offset 或基于游标的分页避免以后人多了接口拖垮。排行展示用到的头像昵称现在微信有新的规范wx.getUserProfile 已经逐步收紧新政策下不建议直接弹窗获取用户昵称头像。小程序生态现在推荐用“头像昵称填写能力”——用户主动点击头像昵称输入框或选择按钮微信弹出一个半屏的授权面板由用户自行选择头像和昵称。我在 werun 里做了一个简单的表单view classprofile-card button classavatar-btn open-typechooseAvatar bind:chooseavataronChooseAvatar image src{{avatarUrl}} modeaspectFill/image /button input typenickname placeholder输入昵称 bind:bluronNicknameInput / /view用户第一次进入小程序时会看到这个设置页填写昵称和头像后才能加入排行榜。如果用户跳过就用默认的“微信用户”和小程序默认头像代替。这个设计既符合平台规则也不会卡住只想看排行不想填资料的人。排行榜接口返回的结构我设计成{ rank: 3, myStep: 7890, list: [ { rank: 1, nickname: 跑者阿飞, avatarUrl: https://..., step: 15600 }, { rank: 2, nickname: 晨跑小刘, avatarUrl: https://..., step: 12300 } ] }rank 表示当前用户的名次myStep 是当前用户的当日步数list 就是排行榜前 50 名。这样前端列表页一次请求就能把“我的排名卡片”和“完整榜单”都渲染出来不需要额外的拼接逻辑。实际体验中微信小程序的 setData 性能对大列表不太友好。werun 的榜单一页只渲染 50 条每条包含头像图片 URL、昵称、步数和排名数字实测在低端安卓机上滑动依然流畅。关键点在于 image 标签一定要设置 lazy-load而且头像要做 CDN 缓存避免每次进页面都从微信头像服务器拉图。5. 真机调试中容易翻车的 5 类问题与排查链路开发 werun 的过程中我花在排错上的时间不比写代码少。这几类问题非常典型基本每个做微信小程序后端对接的人都会遇到我把排查链路整理成表方便你直接对照问题现象根因方向排查线索与解法模拟器里同步步数一直失败模拟器没有运动传感器数据必须在真机上预览调试微信开发者工具无法模拟微信运动真机上 wx.getWeRunData 报错提示没有权限用户关闭了微信运动或未授权 scope.werun检查微信运动公众号/服务里是否开启步数记录用 wx.getSetting 检查授权状态后端解密报错error:0606506Dsession_key 和 encryptedData 不匹配常见于先用旧 code 取了 session_key又用新 encryptedData 解密。确保使用同一次登录流程的 code 和 encryptedData体验版请求接口失败net::ERR_CONNECTION_RESET不满足 request 合法域名要求小程序后台配置 request 合法域名必须是 HTTPS、ICP 备案域名开发工具里“不校验合法域名”选项只对本地调试有效上线后排行榜数据不更新隐私协议未配置或用户拒绝了隐私授权需要在公众平台配置用户隐私保护指引声明收集运动数据审核时会检查隐私弹窗这里重点说两个坑。第一个是 ERR_CONNECTION_RESET。很多新手看到这个报错第一反应是服务器出问题了实际上大概率是域名白名单没配。微信小程序对 wx.request 的域名有严格校验必须是 HTTPS、必须有 ICP 备案、必须在 mp.weixin.qq.com 后台配置到“request 合法域名”列表。开发者工具里勾选“不校验合法域名”只在本地生效真机预览时域名校验是强制的。我在首次调试时就吃过这个亏——后端已经通了浏览器直接访问接口也正常但小程序真机里就是连接被重置排查到最后发现是域名没加到白名单。第二个坑是关于隐私协议。2023 年后微信小程序加强了用户隐私保护要求涉及获取用户运动数据的类目必须在“小程序管理后台 - 设置 - 服务内容声明 - 用户隐私保护指引”里如实填写收集哪些信息、用途是什么。如果没配置即使开发阶段功能正常审核阶段也会被驳回而且部分用户手机微信版本较旧可能直接导致接口调用被拦截。werun 这类和运动健康相关的项目隐私问题尤其敏感建议在开发第一天就去后台把声明提交掉别拖到最后。另外提一句体验版的体验问题开发版和体验版是两个不同的“环境”体验版成员要在公众平台后台的“成员管理”里添加。体验版二维码经常有人问在哪看其实就在 mp 后台“版本管理 - 开发版本”里点“生成体验版二维码”就行。如果你在真机调试时发现体验版和开发版数据不一致优先检查两个版本请求的后端域名是不是同一个开发时经常有人把体验版域名指到测试服务器结果看到的数据完全是两套。6. 实测的一些心得体会werun 从立项到上线跑通前后大约用了两周其中一半时间花在安全链路的理解和排错上。回头总结经验我做对了三件事第一没有在一开始追求功能全而是先打通“加密数据 - 后端解密 - 落库 - 排行榜展示”这条最小闭环闭环跑通再加功能第二把授权、解密、写入的链路画了一张数据流图贴在工位前每次改代码都对着图看数据流是否断掉这帮我避免了很多“按下葫芦浮起瓢”的 bug第三生产环境前后端联调时所有接口都加了请求日志包括 openid 之外的业务参数摘要出问题时能快速定位是前端数据问题还是后端解密问题。最后再分享一个实用的小技巧如果你想快速验证步数解密流程不需要先把整个排行榜写完。写一个极简的测试接口接收 code、encryptedData、iv解密后把 stepInfoList 原样返回在开发者工具里调用一次用 console.log 看输出。这个环节跑通了后续功能都会顺很多。werun 目前已经跑在日常运动打卡场景里下一步我打算加入“周报”和“好友邀请”能力让用户可以看到自己一周步数趋势并邀请微信好友加入同一个排行榜。数据链路不变都是基于这套 getWeRunData 后端解密 排名的架构扩展如果你想做类似工具从这个项目骨架起步会比自己从零踩坑省不少时间。本文还有配套的精品资源点击获取