简介:压缩包内提供了一款基于微信小程序的步数计数与排名应用werun的完整源码,适合具备JavaScript基础的开发者作为微信小程序实战入门项目。源码通过调用微信运动开放接口获取用户每日步数,并利用数组排序、页面渲染等机制实现数据统计和排行榜展示,覆盖了网络请求、数据处理、用户授权、界面更新等关键开发环节。压缩包共21个文件,包含9个js逻辑文件、4个wxml页面结构、4个wxss样式、4个json配置及1份md说明,整体大小约27KB,目录结构简单清晰,便于对照学习。项目特别展示了如何组织小程序utils、pages、server等模块,以及如何在用户授权后安全解析微信运动数据,对理解小程序生命周期和组件化开发也有帮助。目前已有1873人学习,对于希望快速上手微信小程序或完善步数类应用功能的人来说,是一份轻量但完整的参考实现。 去年我接手了一个很有意思的小项目,团队成员想做一个叫 werun 的微信小程序,核心功能就两个:读取微信运动步数、展示好友之间的步数排名。原本以为是个简单活儿,结果从登录态到数据解密,从榜单设计到真机调试,踩了一圈坑才算真正跑通。这篇文章把整个项目的落地过程拆开讲清楚,从思路设计到核心代码,再到坑位排查,打算做微信步数类小程序或类似社交排行场景的朋友可以直接抄作业。
1. 项目整体设计与思路拆解
1.1 核心需求解析与数据流设计
werun 这个项目的需求听上去很清晰:用户打开小程序,授权读取微信运动步数,然后能在一个排行榜里看到自己和好友的当日步数对比。但真动手做的时候,你会发现这里牵扯到三条线:微信运动数据、用户身份体系、排行榜展示逻辑。
先说数据流。整个链路是这样的:微信运动 -> wx.getWeRunData() -> 服务端解密存储 -> 排行榜查询接口 -> 前端列表渲染。这里面最容易被忽视的是“微信运动数据经过加密”这件事。wx.getWeRunData 返回的 encryptedData 是一段密文,需要配合 session_key 用 AES-128-CBC 算法解密,才能拿到真实的步数数组。而这个 session_key 只有在小程序登录(wx.login)之后,由后端调用微信接口换取的 code2Session 才能获得。这就决定了:步数功能不是纯前端能搞定的,必须配套一个后端服务。
另一个核心点是“好友”这个概念的实现。微信没有开放“读取用户好友列表”的接口,你能拿到的是用户主动分享小程序后,通过分享卡片进入的“同一个小程序使用者”。所以 werun 的排行榜本质上是一个“同小程序用户步数榜”,而不是真正的微信好友榜。如果你在小程序里看到类似“好友排名”的文案,那是基于“同一小组/同一群/同一批使用该小程序的人”做的圈子排名。这个定位想清楚,才能合理设计榜单的数据隔离。
1.2 技术选型:为什么选原生小程序 + 自建后端
关于技术栈,我和团队当时比较过三个方案:原生微信小程序、uni-app 跨端框架、Taro 跨端框架。虽然 uniapp 和 Taro 在跨端上有优势——以后还能编译成 H5 或 App,但 werun 这个项目有强烈的“微信平台耦合性”:加密数据解密、微信运动授权、订阅消息、开放能力分享,这些都和微信原生接口强绑定。用跨端框架反而要在各种 API 差异上做兼容,得不偿失。所以最终方案是:原生小程序 + 自建 Node.js 后端 + MySQL 数据库。
选 Node.js 而不是 Java 或 Go,原因也很简单:小程序后端的核心就三件事——登录换 openid、解密微信运动数据、维护排行榜。Node.js 的生态里,解密微信数据的 crypto 模块开箱即用,再加上微信官方开源的解密算法示例本身就是 JavaScript 写的,直接平移过来省了不少事。数据库用 MySQL 因为数据结构非常固定:用户表(openid、昵称、头像)、步数表(openid、日期、步数)、好友关系表(user_id、friend_id,可选)。这种结构化数据用关系型数据库最直观,完全没必要上 MongoDB 或 Redis。
这里有个注意点:小程序后端一定要配置 HTTPS 合法域名。开发模式下可以在开发者工具里勾选“不校验合法域名”,但上线后所有请求域名必须在小程序管理后台配置为 HTTPS 白名单,否则线上请求会被拦截,报request:fail url not in domain list。这一步很多人到真机调试阶段才想起来,后面排查问题会非常被动。
2. 微信步数获取与登录体系完整拆解
2.1 登录机制:code -> openid -> session_key 全流程
微信登录是这个小程序的第一道关口。用户打开 werun,前端先调wx.login()拿到一个临时的 code,然后把这个 code 发送到自己的后端;后端拿着 code 加上小程序的 appid 和 appsecret 请求微信接口https://api.weixin.qq.com/sns/jscode2session,返回的响应里就有 openid(用户唯一标识)和 session_key(用于解密敏感信息的密钥)。
这里有两个细节特别容易踩坑。
第一个是 code 的时效性。code 五分钟内有效,且只能用一次,用完了就得重新 wx.login()。所以后端拿到 code 后应立即调用微信接口换取信息,不要做缓存或延迟处理。
第二个是 session_key 的保存策略。session_key 是敏感信息,原则上不应该下发到前端,因为一旦泄露,别人就能解密你的微信运动数据。安全做法是:后端拿到 session_key 后,用你自己的业务逻辑生成一个自定义 token(比如 UUID),返回给前端做登录态。后续前端请求步数数据时,只带这个 token,后端再根据 token 找到对应的 session_key 来解密数据。这样一来,session_key 全程不出服务器,前端只是存了一个业务 token 而已。
2.2 wx.getWeRunData 调用与 encryptedData 解密
当用户完成登录并同意授权scope.werun后,前端就可以调 wx.getWeRunData 了:
wx.getWeRunData({ success: (res) => { // res.encryptedData: 加密的步数数据 // res.iv: 加密算法的初始向量 wx.request({ url: 'https://your-server.com/wechat/werun', method: 'POST', data: { encryptedData: res.encryptedData, iv: res.iv }, header: { 'Authorization': 'Bearer ' + token // 自定义登录态 } }); } });后端拿到 encryptedData 和 iv 后,用 AES-128-CBC 算法解密,密钥是 session_key 的前 16 字节,偏移量是 iv 的前 16 字节。解密后的 JSON 格式大致是这样的:
{ "stepInfoList": [ { "timestamp": 1722860405, "step": 12345 }, { "timestamp": 1722861000, "step": 12456 } ] }stepInfoList 里存的是当天的分段步数快照,不是每个用户一个值,而是每几分钟一跳的累计步数。我们要的是“当日最新步数”,直接取数组最后一条的 step 即可,然后把 openid、日期、步数写入数据库。如果同一天已有数据,就做更新,不要重复插入。
有一点必须提醒:用户授权 scope.werun 之后,不是每次 getWeRunData 都会弹框。首次访问如果拒绝授权,后续再调用会直接走 fail 回调,需要引导用户去设置页手动打开“微信运动”权限。具体的做法是调wx.openSetting()跳转到设置页,让用户重新开启授权,而不是反复弹授权框。这种交互细节处理不好,用户会直接卸载小程序。
2.3 用户昵称头像:授权接口的演进与应对
早期版本做用户信息收集很简单,调wx.getUserInfo就能一步拿到昵称和头像。但微信后来调整了规则,wx.getUserInfo不再弹出授权框,而是默认返回匿名数据(昵称变成“微信用户”,头像变成灰色默认图)。再后来微信又推出了“头像昵称填写能力”,由开发者自己设计表单让用户填写。
当时 werun 的做法是:用默认昵称和头像初始化用户,不强求用户完善资料。因为排行榜关注的是步数,不是头像多好看。如果用户想自定义头像,就提供一个“完善资料”入口,用 input 让用户输入昵称,用 button open-type="chooseAvatar" 选择头像。现在这么设计,既规避了授权接口的坑,又保持了用户体验,算是踩过坑后的经验总结。
3. 排行榜设计与关键实现
3.1 排行榜数据模型与查询策略
排行榜的核心是“当日步数排名”,数据结构再简单不过:
CREATE TABLE user_step_daily ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL, step_date DATE NOT NULL, step_count INT DEFAULT 0, update_time DATETIME, UNIQUE KEY uk_openid_date (openid, step_date) );这里在 openid 和 step_date 上建了联合唯一索引,目的是保证“一个用户同一天只有一条记录”,用INSERT ... ON DUPLICATE KEY UPDATE就能实现步数的更新。查询排行榜时,直接按当天步数倒序排:
SELECT u.nickname, u.avatar_url, s.step_count FROM user_step_daily s JOIN user u ON s.openid = u.openid WHERE s.step_date = CURDATE() ORDER BY s.step_count DESC, s.update_time ASC LIMIT 100;排序上加了一个update_time ASC的二级排序,用来处理“步数相同但先达标的人排在前面”的场景。这个细节是产品同学提的需求,意思是同样走了 10000 步,你先走到就说明你更“自律”。这个排序逻辑写进去后来大家都觉得合理。
3.2 榜单缓存与并发读优化
等用户量上来之后,每次打开排行榜都实时查 MySQL 也不算最优解。尤其是整点前后,大家都喜欢刷新看谁登顶了,数据库压力会瞬间飙升。一个低成本方案是:在服务端加一层 Redis 缓存,key 设计为rank:step:{yyyy-MM-dd},value 直接存 Top 100 的 JSON 数组,缓存过期时间 5 分钟。每次排行榜接口先查缓存,缓存不存在再查 MySQL 并回填。这样既保证了数据新鲜度(5分钟内的排名够用),又显著降低数据库负载。
也可以更进一步,用 Redis 的有序集合(Sorted Set)来做实时排名。member 是 openid,score 是步数,每次用户上报步数时直接ZADD rank:step:20250110 12345 openid123,查询排名用ZREVRANGE rank:step:20250110 0 99 WITHSCORES,性能比 MySQL 快很多。但要注意:Redis 数据要定期清理过期 key,否则 365 天下来会攒出大量无用数据。werun 的实际用户量级不大,我最后用的是“MySQL 主 + Redis 缓存”的组合,逻辑简单也够用,没必要一开始就上复杂的实时榜单架构。
3.3 前端展示的适配点:安全区、导航栏高度、自定义 tabbar
榜单页面从 UI 上不难,但有几个小细节会在真机上翻车。
一个就是自定义 tabbar。很多人习惯官方 tabbar 的简单省事,但官方自带 tabbar 不支持中间凸起按钮、不支持更复杂的样式定制。werun 的榜单页面想要“首屏展示当前用户排名卡片 + 下方滚动榜单”,这种布局用官方 tabbar 会显得很呆板,所以我改成了自定义 tabbar 组件。
自定义 tabbar 的坑在 iPhone 底部安全区:老型号机型底部没有 Home 指示条,新型号有,如果 tabbar 高度写死 50px,新款机器会显得下沉不足,按钮很贴近底部。解决办法是在 tabbar 组件里读取wx.getSystemInfoSync().safeArea或直接用env(safe-area-inset-bottom)CSS 变量,给 tabbar 底部加一个动态 padding,才能在各机型上显示均匀。顺便说一句,自定义 tabbar 必须在小程序后台的 app.json 里对应页面声明"tabBar": {},然后每个 tab 页的自定义 tabbar 组件要保持一致的selected状态,否则会出现“切换后按钮高亮不对”的诡异 bug。
另一个坑是顶部导航栏高度。werun 不需要自定义导航栏,但如果做“分享海报页”之类需要全屏沉浸的页面,就必须设置"navigationStyle": "custom",这时候顶部导航栏高度在不同机型上不是固定的。推荐用wx.getMenuButtonBoundingClientRect()获取胶囊按钮位置,然后结合wx.getSystemInfoSync().statusBarHeight计算自定义导航栏高度,这样能适配所有机型。
4. 实操过程:从零到一跑通核心链路
4.1 项目初始化与权限声明
新建一个原生微信小程序项目,AppID 填你自己的,开发工具版本建议保持最新。app.json 里核心配置如下:
{ "pages": [ "pages/index/index", "pages/rank/rank", "pages/profile/profile" ], "permission": { "scope.werun": { "desc": "用于读取你的微信运动步数以参与步数排名" } }, "requiredPrivateInfos": [], "style": "v2" }这里permission里的scope.werun并不是“主动申请”用的,而是描述“为什么要用这个权限”。真正申请权限是在代码里调wx.authorize({ scope: 'scope.werun' })的时候弹出。如果用户拒绝过,后续再调wx.authorize会直接 fail,必须通过wx.openSetting跳转设置页重新打开。
另外,在后台的“开发管理-接口设置”里,要确认wx.getWeRunData这个接口的状态是“已启用”。有些新创建的小程序默认部分接口不可用,需要自己申请开通,这个不留意的话,真机测试时调用接口就会发现根本没有权限。
4.2 步数上报接口的 Node.js 实现
先看核心的解密代码:
const crypto = require('crypto'); function decryptWeRunData(sessionKey, encryptedData, iv) { // 密钥和偏移量都是 base64 解码,且只取前 16 字节 const sessionKeyBuffer = Buffer.from(sessionKey, 'base64'); const ivBuffer = Buffer.from(iv, 'base64'); const encryptedDataBuffer = Buffer.from(encryptedData, 'base64'); const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyBuffer, ivBuffer); decipher.setAutoPadding(true); let decoded = decipher.update(encryptedDataBuffer, 'base64', 'utf8'); decoded += decipher.final('utf8'); return JSON.parse(decoded); }然后后端收到请求后,做三件事:校验 token 找到用户 -> 解密步数数据 -> 入库或更新。
app.post('/wechat/werun', async (req, res) => { const token = req.headers.authorization.replace('Bearer ', ''); const user = await getUserByToken(token); if (!user) { return res.status(401).json({ code: 401, message: '登录态失效' }); } const { encryptedData, iv } = req.body; try { const stepData = decryptWeRunData(user.sessionKey, encryptedData, iv); const stepList = stepData.stepInfoList || []; const latest = stepList[stepList.length - 1]; const today = formatDate(new Date()); await db.query( 'INSERT INTO user_step_daily (openid, step_date, step_count, update_time) VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE step_count = VALUES(step_count), update_time = NOW()', [user.openid, today, latest.step] ); res.json({ code: 0, data: { step: latest.step } }); } catch (e) { console.error('解密失败', e); res.status(500).json({ code: 500, message: '数据处理失败' }); } });注意一个细节:stepInfoList的最后一条不一定就是“当前步数”,它可能是几分钟前记录的快照。如果要拿到更准确的实时步数,可以在前端隔几秒调一次wx.getWeRunData,然后对比数据中最后的 timestamp,取最新值。但频繁调用也会触发微信的接口频率限制,实际项目里每 5 分钟同步一次就够了,如果用户停留在页面上,可以加一个下拉刷新的手势来触发手动同步。
4.3 排行榜页面与分享卡片实现
排行榜页面的实现分成接口层和视图层。接口层前端请求/wechat/rank?date=2025-01-10,返回当前 Top 100 和当前用户的排名位置。前端拿到数组后用scroll-view渲染列表即可,重点在于“当前用户排名标红”和“Top 3 特殊样式”两个视觉效果。
分享功能是 werun 拉新用户的关键路径。榜单页右上角胶囊菜单默认自带“转发”,但默认分享卡片只能带 title 和 path,朋友点开后是冷启动,体验一般。如果你想让分享卡片更吸引人,可以用wx.updateShareMenu开启withShareTicket: true,这样就能够在用户从分享卡片进入后,通过wx.getShareInfo拿到群 ID,进而做一个“同一群聊的人”的专属榜。这是微信很早开放的能力,但很多开发者不知道。
当时 werun 做了一大半发现“好友排名”这个概念很难实现,后来加了一个小功能:把排行榜按“来自同一个分享群的用户”分组,在榜上打上一个“同群”标签,互动感瞬间强了不少。这个功能的门槛在于用户必须是通过分享卡片进入且在同一群内,并且要确保分享时开了withShareTicket。
5. 消息推送与自动化更新方案
5.1 订阅消息:每日步数提醒的实现边界
用户打开小程序后,如果想做“每日步数未达标提醒”,官方现在只能走订阅消息。微信已经下架了长期模板消息,只有一次性订阅消息,也就是说:用户点一次“允许订阅”,你只能给他推送一条消息。如果想每天推,就得让用户每天来点一次,这个体验其实是反人类的。
实操中我建议,把“步数达标提醒”做成一个内嵌页面:用户勾选开启后,先调wx.requestSubscribeMessage请求一次订阅,拿到一次性订阅资格后,在特定时机推送给用户一次“昨日步数回顾”。等用户再次打开小程序时,再引导他重新订阅。哪怕不能做到每日自动推送,至少能做到“以用户打开频次为节奏”的提醒。这种设计微信生态是支持且合规的,切记不要尝试用其他手段伪造长期订阅或绕过限制,小程序一旦被封,前面所有工作量都白费。
5.2 步数数据追平:服务端定时拉取的取舍
理论上,用户的微信运动步数是微信自己维护的,werun 只能“被动”等用户打开小程序时上报数据。这就有一个问题:如果用户一整天没打开小程序,当天的步数就不会进数据库,排行榜上就不会出现他。
要不要做“服务端定时拉取”?很遗憾,微信没有开放“服务端通过 openid 直接获取用户步数”的接口,wx.getWeRunData必须由用户端发起调用。所以榜单的数据完整度,本质上取决于用户打开小程序的频率。这不仅是技术问题,也是一个产品问题:你想让用户天天来看榜单,就得在运营上设计钩子,比如“每日步数解锁勋章”“连续打卡 7 天获得徽章”,否则榜单数据永远是残缺的,用户留存也做不起来。
对于“打开过一次但没有当天步数”的情况,可以展示“今日未同步,快去走两步吧”的占位状态,引导用户再次打开更新数据。这个比看到自己在榜上消失要好得多,至少给了用户一个回来的理由。
6. 常见问题与排查技巧实录
6.1 登录态与数据解密类
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 获取登录后的微信用户失败 | code 被重复使用或已过期 | 每次登录重新调 wx.login,保证 code 是新的 |
| decrypt 解密失败,报 500 | session_key 与 encryptedData 不匹配 | 检查是否在 code2Session 后再调 getWeRunData,确保 session_key 没有过期 |
| 真机调试 failed: net::ERR_CONNECTION_RESET | 开发版未配置合法域名或后端服务未启动 | 在开发者工具中勾选“不校验合法域名”用于本地联调;线上必须配置 HTTPS 域名 |
| 步数上报成功但榜单没更新 | 前端传的日期和服务端时区不一致 | 统一用服务端 UTC 时间换算当天日期,或统一用北京时间 |
有个真实案例卡了我一整天:本地模拟器一切正常,真机测试一调wx.getWeRunData就报解析失败。排查后发现是 iPhone 系统时间和中国标准时间差了几小时,导致 timestamp 对应当天日期时数据越界。后来后端统一用Asia/Shanghai时区取日期,问题消失。
6.2 开发者工具与模拟器类
“maximum setlocal recursion level reached” 这个报错,我当时在开发者工具里看到都懵了,感觉是 Windows 脚本的报错,怎么跑到小程序项目里来了。后来查证发现,这是某个版本的开发者工具在云开发控制台初始化时偶发的 Node 脚本问题,解决办法很简单:关闭开发者工具,重新打开,或把开发者工具升级到最新版本。这个报错跟你的业务代码基本无关,不用花太多心思去追。
还有一个小程序 id 不生效的问题:你用 HBuilderX 改了 project.config.json 里的 appid,但运行到微信开发者工具模拟器时,显示的还是旧 appid。原因是微信开发者工具的项目缓存没刷新,退出工具,重新导入项目或手动清除缓存即可。这个在 wu 前端群里出现过很多次,不是什么大问题,但浪费时间。稳妥的做法是改完 appid 后确认 project.config.json 里"appid"字段改干净了,再重启开发者工具。
6.3 发布审核与体验版注意事项
上传代码后,先要生成体验版二维码,扫码测试没问题再提交审核。体验版二维码在微信开发者工具右上角“版本管理”里点击生成,不是用代码里的某个接口生成的。正式版本发布后,用户搜到的是线上版,与体验版数据隔离,别搞混了。
审核不通过最常见的坑就两个。第一个是隐私政策:小程序涉及收集用户的步数数据,后台必须配置“用户隐私保护指引”,并在小程序内提供可查看的隐私政策。第二个是类目选择:步数排行属于“工具-健康管理”类目,如果你的小程序还涉及社交互动,可能需要额外申请“社交”类目,不然审核会被拒。我当时没提前看类目要求,被拒了一次才补上,审核周期白白多花了两三个工作日。
7. 数据权限、隐私合规与一些真实的运营心得
微信小程序对用户数据的合规要求越来越严,尤其是运动步数这种敏感个人数据。2023 年之后,新提交的版本如果在收集用户信息时没有弹窗告知“为什么要获取步数”,连开发版都会受限。所以在项目上线前,一定要去小程序管理后台的“设置-服务内容声明-用户隐私保护指引”里,把“微信运动数据”这一项勾选上,并写好用途说明。
运营侧我还有一个真实的体会:werun 这类轻量工具小程序的次日留存天然不高,因为用户“看一眼榜单”就卸载,这是数据模型决定的,不是你代码写得不好。我做了一件事之后留存明显改善:把“每周步数总计排名”加入了排行榜。虽然实现上只是多了一个周榜查询,但用户的使用时长变长了,因为他会想知道这一周的努力加起来能不能冲到前三。榜单类产品,“周期感”很重要,日榜是即时反馈,周榜是沉淀,月榜是仪式感。三者叠加起来,一个看似简单的步数排行工具就变得有层次了。
从技术架构角度讲,werun 这个项目本身不算复杂,但它把微信小程序开发中最常见的几个模块全部串了一遍:登录、解密、数据库写入、榜单查询、订阅消息、分享、自定义 tabbar、合规配置、上线审核。把这个项目完整做下来,小程序基础开发的功力基本就扎实了。如果后面想扩展,可以沿着“好友分组排行、运动数据周期报表、团队步数挑战赛”这些方向继续深入,底层逻辑都是相通的,唯一变的是业务想象力。
最后再分享一个真实经验:做这种个人工具类的小程序,开发时间往往只占 30%,剩下 70% 的时间都花在“适配各种机型和版本差异”以及“和微信平台规则斗智斗勇”上。别嫌麻烦,这些都是必经之路。保持耐心,把每一个报错都录下来,你的第二个小程序会比第一个顺利一倍以上。
本文还有配套的精品资源,点击获取