微信小程序云开发实战:微信运动步数排行榜从0到1
2026/9/16 21:37:20 网站建设 项目流程

简介: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.authorize:

wx.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。流程是这样的:

  1. 小程序端 wx.login 拿到临时 code。
  2. 把 code 传给云函数,云函数通过cloud.openapi.auth.code2Session换取 openid 和 session_key。
  3. 把 session_key 与 openid 关联保存到数据库(或者 Redis 这类缓存中)。
  4. 之后每次拿到 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 天数据,但不保证数组顺序一定按日期递增或递减,所以我建议取“最近一天”时不要写死下标,而是遍历比较 timestamp:

const 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_oXxxx
datestring日期,格式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 }) })

页面往下滚动时做下一页,用 skip:

col.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:30,UTC 时间还是 2024-08-13 15:30,toISOString 一切正常;但北京时间 2024-08-14 00:30 时,UTC 时间变成了 2024-08-13 16:30,toISOString 出来的是 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-type="chooseAvatar"><input type="nickname">让用户自己选择填写。所以我在排行榜的个人信息编辑弹窗里,用这两个原生组件实现头像和昵称的设置。用户不填也无所谓,系统会给一个默认头像占位。

真机调试时如果遇到net::ERR_CONNECTION_RESET,大概率是你真机上没有开启调试模式,或者小程序后台没有把你当前的开发者微信号加入体验成员。这个错误和网络本身关系不大,先检查这两处,能省一晚上的排查时间。

5. 最后再讲两个实用的小优化

项目跑通之后,我还做了两个很小但很影响体验的优化,分享给你参考。

第一个是“当天步数缓存”。getWeRunData 一次返回 30 天数据,这些数据在一天之内变化不大,但每次进入首页都重新解密一次,既慢又浪费云函数调用次数。我的做法是:把解密后的 stepInfoList 存入本地缓存,30 分钟过期。进入页面时先读缓存,后台再静默拉取一次新数据并更新。这样首页几乎可以秒开,用户感知会好很多。

第二个是“低步数保护”。有一次我自己测试,发现当天只走了 200 步也成功写进了排行榜,导致榜单底部全是 0 步用户,观感很差。后来在写入云函数里加了一个判断:当天步数小于 100 步就不写入数据库,保持榜单整洁。这个阈值可以根据产品定位调整,比如“每日最低 1000 步才上榜”也能刺激用户多走动。

werun 从有想法到第一版上线,总共花了两周左右,绝大部分时间都花在授权链路、步数解密和时区问题这三个点上。如果你也要做类似的小程序,我建议先画清楚“数据从哪里来、存在哪里、怎么展示”这三条线,再动手写代码,能少踩一半的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询