
简介werun小程序源码包是一份基于微信小程序实现的步数计数与排名项目主要面向微信小程序初学者和希望深入理解JavaScript数据处理逻辑的开发者。该项目围绕微信运动数据展开完整演示了从调用微信开放接口获取每日步数到利用JavaScript进行统计、排序以及排行榜展示的过程并且涉及用户授权、页面交互、生命周期管理等关键知识点。压缩包中共包含21个文件其中9个js文件负责核心逻辑与数据加工4个wxml文件定义页面结构4个wxss文件控制样式4个json文件配置页面与项目信息另有1个md文档辅助说明整体体积仅27KB结构紧凑、易于阅读。已有1873人学习下载适合通过源码实践掌握小程序API调用、数组排序和状态管理的具体写法。读者借助这份资源可以快速搭建起自己的步数统计小程序并从中学习到wx.getWeRunData等接口的使用方法以及排行榜功能的实现思路。 有段时间我拉了一个“每日走步打卡”的微信群约定每天晚上在群里报步数月底输的人请客。结果第三天就有人开始漏报第七天群里只剩三个人报数最后变成“今天走了八千晚上补个宵夜”的聊天群。手动报数这件事本质上就是在给用户的自律添麻烦早晚坚持不下去。所以我把这个需求做成了一个小程序取名 werun用户授权微信运动后自动读取步数写入数据库生成当天的小程序内排名。打开就能看“今天谁走得最多”全程不需要任何人手动填一个数字。这篇文章我会从需求拆解、技术选型、步数获取与解密、排行榜数据设计一路讲到上线前踩过的几个真实大坑。适合刚接触微信小程序的开发者或者手头想做一个类似“数据采集 排行展示”工具项目的人。它不复杂但里面的授权链路、数据写入策略和隐私合规细节够你少走不少弯路。1. 从步数打卡群到小程序项目需求与整体设计1.1 需求拆解一个“最少可用版本”应该包含什么当时我给自己定的目标是最快速度做出一个能跑通的版本然后再慢慢加东西。于是 werun 的最小功能集被拆成三块微信授权登录识别用户是谁拿到头像昵称用于排行榜展示。步数读取通过微信运动接口拿到用户当天步数。排行榜按当天步数倒序展示所有使用过小程序的用户。这三块听起来少但每一块背后都有不少细节。比如“微信授权登录”不是拉起一个 wx.login 就完了还得处理用户拒绝授权、授权状态变化、session_key 过期再比如“步数读取”前端拿到的是经过 AES 加密的密文数据必须在后端解密不能在前端直接解。至于排行榜看起来只是“查询数据库按步数倒序”但写入频率、日期边界、分页策略都会直接影响产品体验。做完最小版本之后我又补了两个非核心但很有用的功能首页展示“今日步数 目标进度环”以及“最近 7 天步数趋势”。这两个功能都不需要额外接口因为微信运动接口一次会返回最近 30 天的步数数据前端直接取就行属于性价比极高的体验加分项。1.2 技术选型为什么选了原生小程序加微信云开发当时我手头没有任何服务器也不打算为了这个小项目去备案域名、买服务器和配 HTTPS因为微信小程序要求所有请求域名必须是 HTTPS 且完成 ICP 备案这套流程下来最快也要几天。于是我把目光放在了微信云开发上免服务器、免域名、自带数据库和云函数且云函数天然带着小程序的调用身份不用单独做签名校验。原生小程序 vs uni-app 我也简单权衡过。uni-app 的优势是未来可以一套代码跑 H5、App、小程序但代价是框架的抽象层会增加调试成本尤其是 wx.getWeRunData 这类强微信生态接口在跨端框架里还多一层条件编译。werun 是一个纯微信小程序工具没有跨端需求所以直接选原生代码量少、调试路径短、官方文档对应关系清晰。如果你已经有自建后端也可以不用云开发登录态换取和步数解密放到自己的服务上即可。整体思路一样只是把“云函数”换成普通服务端接口把“云数据库”换成 MySQL 或 MongoDB。2. 步数从哪来授权、接口调用与解密的完整链路2.1 scope.werun 授权的正确流程微信运动步数属于用户敏感数据接口调用前必须获得用户授权对应的 scope 是scope.werun。很多新手在这里最容易犯的错是在 onLoad 或 onShow 里直接调 wx.getWeRunData结果授权弹窗根本没弹出来接口直接报错。原因很简单首次请求这类隐私接口时必须在用户点击事件中触发。也就是说你要在页面上放一个“授权同步步数”按钮用户点下去之后才能调 wx.authorize 或者直接调 wx.getWeRunData。单纯由页面生命周期自动调用会被微信判定为未获得用户手势授权流程会中断。我采用的方案是做一个独立的授权引导页逻辑大致如下wx.getSetting({ success(res) { if (res.authSetting[scope.werun]) { // 已授权直接进入主页 wx.switchTab({ url: /pages/index/index }) } else { // 未授权或曾拒绝显示授权按钮 this.setData({ needAuth: true }) } } })用户点击授权按钮后再调用 wx.authorizewx.authorize({ scope: scope.werun, success() { // 授权成功进入主流程 }, fail() { // 用户拒绝提示需要授权才能使用 } })需要特别注意的是如果用户之前拒绝过授权再调用 wx.authorize 不会再次弹窗而是直接进入 fail 回调。这时候只能引导用户去设置页手动打开权限wx.openSetting({ success(res) { if (res.authSetting[scope.werun]) { // 用户在设置页打开了权限 } } })2.2 前端获取加密步数授权通过后前端就可以调用 wx.getWeRunData 了。接口名容易和“微信运动”这个功能混淆但它返回的并不是一组纯粹的 JSON 明文而是一个加密数据块。wx.getWeRunData({ success(res) { // res.encryptedData 是加密后的步数数据 // res.iv 是 AES 解密时需要的初始向量 wx.cloud.callFunction({ name: decryptWeRunData, data: { encryptedData: res.encryptedData, iv: res.iv }, success(res) { const stepInfoList res.result.stepInfoList // stepInfoList 是最近 30 天的步数数组 } }) } })这里的 encryptedData 理论上只应在后端解密因为解密需要 session_key而 session_key 绝对不能下发到前端否则任何人都能伪造或窃取用户数据。云开发环境下我们的解密逻辑放在云函数里正好符合安全要求。2.3 云函数里的登录态与解密逻辑要在云函数里解密首先得有 session_key。流程是这样的小程序端 wx.login 拿到临时 code。把 code 传给云函数云函数通过cloud.openapi.auth.code2Session换取 openid 和 session_key。把 session_key 与 openid 关联保存到数据库或者 Redis 这类缓存中。之后每次拿到 getWeRunData 的 encryptedData 时再根据 openid 取出对应的 session_key 解密。登录云函数简化为这样// cloudfunctions/login/index.js const cloud require(wx-server-sdk) cloud.init() const db cloud.database() exports.main async (event) { const { code } event const res await cloud.openapi.auth.code2Session({ code }) const { openid, session_key } res await db.collection(users).doc(openid).set({ data: { _openid: openid, sessionKey: session_key, updatedAt: db.serverDate() } }) return { openid } }注意云函数里创建的记录不像小程序端那样会自动带上_openid字段所以这里手动写入。session_key 有效期内如果用户重新登录旧的 session_key 会失效所以每次都覆盖更新没问题。解密云函数的核心代码如下用 Node.js 自带的 crypto 模块即可不需要额外依赖// cloudfunctions/decryptWeRunData/index.js const cloud require(wx-server-sdk) const crypto require(crypto) cloud.init() const db cloud.database() function decrypt(sessionKey, encryptedData, iv) { const key Buffer.from(sessionKey, base64) const ivBuf Buffer.from(iv, base64) const decipher crypto.createDecipheriv(aes-128-cbc, key, ivBuf) decipher.setAutoPadding(true) let decoded decipher.update(encryptedData, base64, utf8) decoded decipher.final(utf8) return JSON.parse(decoded) } exports.main async (event) { const wxContext cloud.getWXContext() const userRes await db.collection(users).doc(wxContext.OPENID).get() const { sessionKey } userRes.data const realData decrypt(sessionKey, event.encryptedData, event.iv) return realData }这里用的算法是 AES-128-CBC对应微信官方文档的说明。解密后的数据结构是{ stepInfoList: [ { timestamp: 1723564800, step: 7453 }, { timestamp: 1723651200, step: 12003 } ] }每个元素的 timestamp 是当天 0 点的时间戳step 是当天步数。列表返回最近 30 天数据但不保证数组顺序一定按日期递增或递减所以我建议取“最近一天”时不要写死下标而是遍历比较 timestampconst todayItem stepInfoList.reduce((prev, cur) cur.timestamp prev.timestamp ? cur : prev )2.4 解密结果与前端展示拿到 stepInfoList 之后首页“今天走多少步”、目标进度环和 7 日趋势图都可以从这同一份数据里取。我的做法是取今天步数显示在首页大数字上目标进度环用 CSS conic-gradient 根据“今日步数 / 目标步数”动态画7 日趋势则直接截取 stepInfoList 排序后最近 7 条用简单的柱状图样式渲染不引入任何图表库。这里有一个信息差很多人在首页单独调一次步数接口在趋势页又调一次浪费请求。实际上一次 getWeRunData 已经把 30 天数据都给你了完全可以在前端缓存下来按需取用。3. 排行榜的数据设计集合、写入与查询3.1 集合设计与复合主键排行榜本质上是对“当日所有用户的步数”做排序所以核心集合只需要一个字段类型说明_idstring业务主键由日期 openid 拼接比如2024-08-13_oXxxxdatestring日期格式YYYY-MM-DD用东八区日期openidstring用户唯一标识nickNamestring用户昵称avatarUrlstring用户头像stepsnumber当天步数updatedAtdate更新时间为什么把_id设成“日期 openid 拼接”这是为了让同一天同一个用户只有一条记录。下单更新时直接用 doc(_id).set天然幂等不用先查再判再插既省一次查询又避免并发时插入重复数据。与此同时还需要在数据库控制台给steps集合建一个组合索引date升序steps降序。不建索引的话排行榜查询大概率会报“需创建索引”的错误别问我怎么知道的。3.2 当日步数如何写入且不重复每次用户打开首页前端拿到今天的步数后会通过云函数写入排行榜集合。写入逻辑如下// cloudfunctions/updateSteps/index.js const cloud require(wx-server-sdk) cloud.init() const db cloud.database() exports.main async (event) { const wxContext cloud.getWXContext() const openid wxContext.OPENID const { date, steps, nickName, avatarUrl } event const _id ${date}_${openid} await db.collection(steps).doc(_id).set({ data: { date, openid, steps, nickName, avatarUrl, updatedAt: db.serverDate() } }) return { _id, steps } }前端调用时把从 getWeRunData 解密出的“当天步数”传进来。这里要提醒一下前端一定要把 date 作为参数传进来而不是在云函数里用服务器时间生成。原因我会在下一章的“时区大坑”里详细说。3.3 排行榜查询与前端渲染排行榜查询本身很简单难点在分页和数据量控制。小程序端直接在数据库查询时一次最多拿 20 条记录。对于日活几百的小程序这个量级够用但如果你预期用户量会上千上万建议按以下两种方式之一用小程序端直接 where orderBy limit适合数据量在千级以内代码最简单。用云函数 聚合接口适合数据量大、需要筛选和二次统计的场景。我当时先用的小程序端直接查询const db wx.cloud.database() const col db.collection(steps) col.where({ date: today }) .orderBy(steps, desc) .limit(20) .get() .then(res { this.setData({ rankList: res.data }) })页面往下滚动时做下一页用 skipcol.where({ date: today }) .orderBy(steps, desc) .skip(this.data.page * 20) .limit(20) .get()skip 分页在数据量大以后会有性能问题所以后期如果要扩容应该改成基于游标的分页方式用上一页最后一条的 steps 和 _id 作为条件继续查询。但就我目前这个项目的规模skip 完全够用前期没必要为了“想象中十万用户”去过度设计。3.4 好友排行到底能不能做很多人看到“排行榜”就会想到微信运动里那个好友排名这里我必须泼一盆冷水小程序无法直接读取用户的微信好友步数除非好友授权了同一款小程序并授权步数。换句话说werun 的排行榜是小程序内用户之间的排名不是微信好友关系链排名。如果想做“好友榜”现实一点的做法是通过“邀请一起走”的方式让用户主动把小程序分享给好友好友打开后进入同一组榜。或者基于手机号匹配好友关系但手机号也需要用户授权合规成本和用户抵触情绪都很高。所以我在第一版只做了全国总榜页面顶部留了一个“邀请好友”按钮分享时带参数落地后依然是总榜但会高亮显示分享者勉强算是给社交关系留了一个口子。4. 从开发到上线我踩过的三个真坑4.1 授权按钮的“用户手势”陷阱第一个坑在首次开发时就踩到了。我在首页 onShow 里直接调 wx.getWeRunData想着这样用户一进来就能自动看到步数。结果真机测试时第一次进入页面毫无反应控制台报错提示“require permission scope.werun”但页面根本没有弹授权框。后来去查文档才确认首次调用 wx.authorize 或 wx.getWeRunData 这类隐私接口必须在用户点击事件的回调里执行。我把授权调用挪到按钮绑定事件里之后授权弹窗才正常出现。而且要注意如果授权被拒绝过wx.authorize 不会再弹窗只能走 wx.openSetting 引导去设置页打开权限。这个流程必须提前设计好否则用户一旦误点拒绝就再也回不来了。我的经验是在授权引导页放两个按钮一个是“同意并同步步数”另一个是“查看已授权状态”。后者点击时调 wx.getSetting 检查 authSetting 里的 scope.werun如果是 false 就直接 wx.openSetting。这样能覆盖绝大多数误拒绝场景。4.2 云函数时区把“今天”变成了“昨天”第二个坑藏得很深也是排行榜曾经“少一天数据”的元凶。云开发环境默认使用 UTC 时区而微信运动的 timestamp 是按北京时间自然日计算的。最初我在云函数里写const today new Date().toISOString().slice(0, 10)这个写法在服务器时区为 UTC 时晚上 8 点之前得到的日期是当天晚上 8 点之后得到的日期就是“明天”了。比如北京时间 2024-08-13 23:30UTC 时间还是 2024-08-13 15:30toISOString 一切正常但北京时间 2024-08-14 00:30 时UTC 时间变成了 2024-08-13 16:30toISOString 出来的是 2024-08-13等于把新一天的记录写到了前一天。解决方法是不要在云函数里动态生成“今天”而是统一由前端计算好日期字符串作为参数传入云函数。前端的运行环境是用户手机时区通常就是用户本地时间在国内基本等于北京时间。// 前端计算 const now new Date() const year now.getFullYear() const month String(now.getMonth() 1).padStart(2, 0) const day String(now.getDate()).padStart(2, 0) const today ${year}-${month}-${day}如果确实需要在云函数里生成日期也要手动加上 8 小时时区偏移再取 UTC 日期const d new Date(Date.now() 8 * 3600 * 1000) const today d.toISOString().slice(0, 10)这个细节直接影响榜单归属日期不修正的话晚上 8 点之后的步数会全部记到前一天用户在凌晨打开小程序就会看到自己当天的步数变成 0。4.3 隐私协议、审核与体验版那些事上线前另一件容易被忽略的事是用户隐私保护指引。这个小程序会收集用户的头像昵称、微信运动步数属于敏感信息必须在微信公众平台后台配置“用户隐私保护指引”声明收集这些信息的目的和方式。如果没配置调用 getWeRunData 时前端会直接报“隐私协议未同意”之类的错误体验版也不例外。配置好隐私指引后还需要在代码里接入隐私弹窗逻辑调用wx.requirePrivacyAuthorize或者在首次进入时引导用户同意隐私协议。我用的方式是在小程序启动时通过云函数返回一个“协议状态”若未同意则弹出一个自定义模态框说明收集哪些数据用户点击同意后继续流程。注意隐私弹窗的按钮点击也需要用户手势不能自动弹出后自动同意。头像昵称这里还有一个新规不能用 wx.getUserProfile 直接拉取头像和昵称了必须通过button open-typechooseAvatar和input typenickname让用户自己选择填写。所以我在排行榜的个人信息编辑弹窗里用这两个原生组件实现头像和昵称的设置。用户不填也无所谓系统会给一个默认头像占位。真机调试时如果遇到net::ERR_CONNECTION_RESET大概率是你真机上没有开启调试模式或者小程序后台没有把你当前的开发者微信号加入体验成员。这个错误和网络本身关系不大先检查这两处能省一晚上的排查时间。5. 最后再讲两个实用的小优化项目跑通之后我还做了两个很小但很影响体验的优化分享给你参考。第一个是“当天步数缓存”。getWeRunData 一次返回 30 天数据这些数据在一天之内变化不大但每次进入首页都重新解密一次既慢又浪费云函数调用次数。我的做法是把解密后的 stepInfoList 存入本地缓存30 分钟过期。进入页面时先读缓存后台再静默拉取一次新数据并更新。这样首页几乎可以秒开用户感知会好很多。第二个是“低步数保护”。有一次我自己测试发现当天只走了 200 步也成功写进了排行榜导致榜单底部全是 0 步用户观感很差。后来在写入云函数里加了一个判断当天步数小于 100 步就不写入数据库保持榜单整洁。这个阈值可以根据产品定位调整比如“每日最低 1000 步才上榜”也能刺激用户多走动。werun 从有想法到第一版上线总共花了两周左右绝大部分时间都花在授权链路、步数解密和时区问题这三个点上。如果你也要做类似的小程序我建议先画清楚“数据从哪里来、存在哪里、怎么展示”这三条线再动手写代码能少踩一半的坑。本文还有配套的精品资源点击获取