简介werun 是一份面向微信小程序入门与进阶开发者的实战项目源码核心围绕微信运动步数计数与好友排名展开适合想通过完整案例掌握小程序开发流程、提升 JavaScript 应用能力的学习者。压缩包共 21 个文件以 9 个 js 逻辑脚本、4 个 wxss 样式表、4 个 json 配置文件和 3 个 wxml 页面结构为主另附说明文档整体约 27KB体量轻巧便于快速通读。项目覆盖数据获取、步数统计、排行榜排序、用户界面实时更新与授权权限管理等关键环节读者可借此理解小程序生命周期、项目目录组织及开放接口调用方式。目前已有 1876 人学习下载适合作为课程设计、练手项目或面试作品参考。1. 从零做一个 werun 步数计数与排名小程序先想清楚数据从哪来微信运动每天产生几百万条步数记录但真正落到自己项目里做计数和排名第一道坎不是写页面而是数据来源。werun 这个标题拆开看核心是三件事步数怎么拿到、计数怎么算、排名怎么排。很多人一上来就打开微信开发者工具建页面结果卡在授权弹窗上——用户点了拒绝后面全白搭。这个方案适合两类人一是想练手微信小程序完整链路的前端二是需要给内部团队做步数打卡的开发者。它不依赖任何第三方硬件纯靠微信运动接口就能跑通。但要注意微信运动的步数数据是「延迟同步」的不是实时刷新这一点直接决定了你的计数逻辑和排名刷新策略。下面按我实际踩过的顺序从授权、拉数据、算排名到避坑一步步拆开讲。2. 微信运动授权与步数拉取wx.getWeRunData 的完整调用链2.1 为什么不能直接读步数加密数据与 session_key 的关系微信运动接口返回的步数不是明文。wx.getWeRunData拿到的是一个加密的cloudID或encryptedData必须传到后端用session_key解密才能看到真实的步数数组。这个设计是为了防止前端伪造步数但也意味着你没法纯前端完成计数。常见做法是小程序端调用wx.login拿code后端用code换session_key同时生成一个自定义登录态比如 token返回给前端。前端再调wx.getWeRunData把encryptedData和iv一起发给后端解密。整个链路里session_key绝对不能下发到前端否则等于把解密钥匙给了用户。我一般会在后端维护一张user_session表字段包括openid、session_key、expires_at。每次解密前先检查session_key是否过期过期就让前端重新走wx.login。这一步不做后面步数拉取会随机失败而且报错信息很模糊容易误判成接口限流。2.2 前端调用与后端解密的最小可运行代码先看小程序端的调用// pages/index/index.js Page({ data: { steps: 0, ranking: [] }, onLoad() { this.loginAndFetchSteps(); }, loginAndFetchSteps() { wx.login({ success: (res) { if (!res.code) return; // 用 code 换后端登录态 wx.request({ url: https://your-domain.com/api/login, method: POST, data: { code: res.code }, success: (loginRes) { const token loginRes.data.token; wx.setStorageSync(token, token); this.getWeRunData(token); } }); } }); }, getWeRunData(token) { wx.getWeRunData({ success: (res) { // 把加密数据发给后端解密 wx.request({ url: https://your-domain.com/api/werun/decrypt, method: POST, header: { Authorization: Bearer ${token} }, data: { encryptedData: res.encryptedData, iv: res.iv }, success: (decryptRes) { // 后端返回解密后的步数数组 this.setData({ steps: decryptRes.data.todaySteps }); this.fetchRanking(token); } }); }, fail: (err) { // 用户拒绝授权或未开启微信运动 console.error(getWeRunData fail, err); wx.showToast({ title: 请授权微信运动, icon: none }); } }); } });这段代码的关键点有三个第一wx.login必须在wx.getWeRunData之前因为后端需要session_key才能解密第二token存在storage里后续排名接口复用第三fail回调里要区分「用户拒绝」和「未开启微信运动」前者引导去设置页后者提示去微信运动公众号开启。后端解密用 Node.js 的crypto模块// server/werun.js const crypto require(crypto); function decryptWeRunData(sessionKey, encryptedData, iv) { // sessionKey 是 base64 编码的先解码 const key Buffer.from(sessionKey, base64); const ivBuffer Buffer.from(iv, base64); const encryptedBuffer Buffer.from(encryptedData, base64); // 微信用的是 AES-128-CBCPKCS7 填充 const decipher crypto.createDecipheriv(aes-128-cbc, key, ivBuffer); decipher.setAutoPadding(true); let decoded decipher.update(encryptedBuffer, binary, utf8); decoded decipher.final(utf8); // decoded 是 JSON 字符串包含 stepInfoList const data JSON.parse(decoded); // stepInfoList 是最近30天的步数每天一个对象 return data.stepInfoList; } module.exports { decryptWeRunData };参数说明sessionKey来自wx.login换取的登录态长度 24 字节 base64encryptedData是wx.getWeRunData返回的密文iv是初始向量。解密后的stepInfoList是一个数组每个元素形如{ timestamp: 1690000000, step: 8234 }时间戳是当天 0 点。注意stepInfoList最多返回最近 30 天的数据且当天步数在下午之前可能不完整。如果你要做「今日实时排名」必须接受这个延迟或者在前端做本地累加但本地累加无法防作弊。2.3 授权失败与 session_key 过期的排查顺序授权失败最常见的三种情况用户点了「拒绝」、用户没关注微信运动公众号、session_key过期。排查顺序应该是先看wx.getWeRunData的fail回调errMsg如果是auth deny就引导用户去wx.openSetting重新授权如果是getWeRunData:fail且没有明确原因大概率是没开启微信运动提示用户搜索「微信运动」公众号并启用。session_key过期不会直接报错而是解密失败返回-41001或-41003。我的习惯是在后端解密接口里捕获异常如果错误码是这两个就返回一个特定状态码给前端前端收到后重新走wx.login并重试一次。重试还失败就提示用户稍后再试不要无限循环。3. 步数计数与排名逻辑从原始数据到榜单的完整实现3.1 计数策略当日步数、周累计与月累计怎么选拿到stepInfoList后计数方式取决于你的业务目标。如果只是「今日步数排名」直接取stepInfoList最后一项的step值。但要注意微信运动的当日步数在晚上 10 点后才会稳定白天拉取可能只有几千步用户会以为你算错了。我一般会做三层计数当日步数、本周累计、本月累计。当日步数用于实时榜单周累计和月累计用于成就体系。周累计的计算方式是找到本周一的 0 点时间戳把stepInfoList里timestamp 本周一的step相加。月累计同理找当月 1 号。这里有个坑stepInfoList的时间戳是服务器时间不是用户本地时间。如果用户跨时区周累计的边界会错。常见做法是统一用北京时间UTC8计算在代码里把时间戳转成北京时间再判断。// 计算本周累计步数 function getWeekSteps(stepInfoList) { const now new Date(); // 转成北京时间 const beijingNow new Date(now.getTime() 8 * 3600 * 1000); const dayOfWeek beijingNow.getUTCDay() || 7; // 周日算第7天 const monday new Date(beijingNow); monday.setUTCDate(beijingNow.getUTCDate() - dayOfWeek 1); monday.setUTCHours(0, 0, 0, 0); const mondayTimestamp Math.floor(monday.getTime() / 1000) - 8 * 3600; return stepInfoList .filter(item item.timestamp mondayTimestamp) .reduce((sum, item) sum item.step, 0); }这段代码先把当前时间转成北京时间再算本周一的 0 点时间戳最后过滤求和。参数stepInfoList就是解密后的数组。注意mondayTimestamp减去了 8 小时因为stepInfoList的时间戳是 UTC 时间。3.2 排名接口设计Redis 有序集合与 MySQL 的取舍排名功能的核心是「谁步数多谁靠前」。数据量小的时候比如几百人直接 MySQLORDER BY steps DESC就够了。但步数排名是高频读、低频写而且需要实时刷新用 Redis 的ZSET更合适。我的做法是后端解密步数后先更新 MySQL 的user_steps表持久化同时把当日步数写入 Redis 的ZSETkey 是ranking:todaymember 是openidscore 是步数。前端请求排名时直接ZREVRANGE ranking:today 0 99 WITHSCORES拿前 100 名。// 更新 Redis 排名 const redis require(redis); const client redis.createClient(); async function updateRanking(openid, todaySteps) { // 写入当日排名 await client.zAdd(ranking:today, { score: todaySteps, value: openid }); // 设置过期时间为当天结束避免脏数据 const now new Date(); const endOfDay new Date(now); endOfDay.setHours(23, 59, 59, 999); const ttl Math.floor((endOfDay - now) / 1000); await client.expire(ranking:today, ttl); } async function getTopRanking(limit 100) { // 从高到低拿前 limit 名 const list await client.zRangeWithScores(ranking:today, 0, limit - 1, { REV: true }); return list.map((item, index) ({ rank: index 1, openid: item.value, steps: item.score })); }参数说明zAdd的score必须是数字value是用户唯一标识。expire设置 TTL 是为了每天自动清空不用手动删 key。zRangeWithScores的REV: true表示从高到低排序。注意Redis 的ZSET在成员分数相同时会按 member 的字典序排列。如果你希望同分用户按更新时间排序需要把时间戳拼进 score比如score steps * 10000000000 (9999999999 - timestamp)。这个技巧在步数相同的情况下能保证先更新的人排前面。3.3 前端排名页面的渲染与下拉刷新前端拿到排名数组后渲染成列表。每个列表项显示排名、头像、昵称、步数。头像和昵称需要后端从user_info表里关联查询或者在前端用openid批量请求。我一般会在排名接口里直接返回头像和昵称减少请求次数。下拉刷新用onPullDownRefresh重新调getWeRunData和排名接口。注意刷新频率微信运动步数更新有延迟频繁刷新没意义。我的习惯是限制 30 秒内只能刷新一次用storage记录上次刷新时间。// pages/ranking/ranking.js Page({ data: { ranking: [], lastRefresh: 0 }, onPullDownRefresh() { const now Date.now(); if (now - this.data.lastRefresh 30000) { wx.showToast({ title: 刷新太频繁, icon: none }); wx.stopPullDownRefresh(); return; } this.setData({ lastRefresh: now }); this.fetchRanking().then(() { wx.stopPullDownRefresh(); }); }, fetchRanking() { return new Promise((resolve) { wx.request({ url: https://your-domain.com/api/ranking/today, header: { Authorization: Bearer ${wx.getStorageSync(token)} }, success: (res) { this.setData({ ranking: res.data.list }); resolve(); } }); }); } });这段代码的关键是lastRefresh的时间戳判断避免用户疯狂下拉导致接口压力。fetchRanking返回 Promise 是为了在onPullDownRefresh里等数据回来再stopPullDownRefresh否则下拉动画会提前结束。4. 避坑与常见问题步数排名小程序最容易翻车的 5 个点4.1 用户拒绝授权后无法再次弹窗现象用户第一次拒绝微信运动授权后再次调用wx.getWeRunData直接走fail不会弹授权框。原因微信的授权弹窗每个 scope 只能弹一次拒绝后必须引导用户去设置页手动开启。解决在fail回调里判断errMsg包含auth deny然后调用wx.openSetting让用户手动勾选「微信运动」。注意wx.openSetting必须在用户点击事件里调用不能自动触发。4.2 解密报错 -41001 但 session_key 刚更新过现象后端解密返回-41001但前端刚调完wx.loginsession_key应该是新的。原因wx.login的code只能用一次如果后端用同一个code换了两次session_key第二次会失败并导致session_key失效。解决后端换session_key时加锁确保一个code只换一次。如果并发请求用 Redis 的SETNX做幂等。4.3 步数列表时间戳跨天导致周累计少算一天现象周一早上拉取步数周累计比实际少了几千步。原因stepInfoList的时间戳是 UTC 时间周一 0 点北京时间对应 UTC 周日 16 点。如果代码里直接用timestamp 周一0点UTC会把周一凌晨的步数漏掉。解决统一用北京时间计算边界时间戳转换时加减 8 小时。上面getWeekSteps函数已经处理了这个问题。4.4 Redis 排名数据与 MySQL 不一致现象前端看到的排名和数据库里的步数对不上。原因更新 MySQL 和 Redis 不是原子操作如果 Redis 写入失败但 MySQL 成功或者反过来就会不一致。解决以 MySQL 为准Redis 只做缓存。每次排名接口返回前如果发现 Redis 里某个用户步数低于 MySQL就用 MySQL 的值覆盖 Redis。或者干脆每分钟从 MySQL 全量同步一次到 Redis。4.5 用户修改手机时间导致步数异常现象某个用户步数突然变成 10 万明显异常。原因微信运动的步数依赖手机传感器用户修改系统时间可能影响stepInfoList的时间戳导致同一天出现多条记录。解决后端解密后按timestamp去重同一天只保留step最大的那条。同时加一个阈值单日步数超过 5 万就标记为异常不参与排名。5. 进阶技巧用云开发绕过服务器以及步数排名的防作弊思路如果你不想维护服务器微信云开发可以省掉后端解密这一步。云函数里直接调cloud.getWXContext()拿openid再用cloud.getWeRunData解密。但云开发的session_key管理是黑盒解密失败时排查困难我一般只在原型阶段用。防作弊方面纯前端无法阻止用户伪造步数但后端可以做几件事第一校验stepInfoList的时间戳连续性如果同一天出现多条记录且步数差异巨大标记异常第二限制单日步数上限比如 5 万步第三对比用户历史步数如果某天突然是历史均值的 10 倍进入人工审核队列。我自己的习惯是排名只展示前 100 名后面的用户只显示「第 100 名」这样即使有少量作弊用户也不会影响大多数人的体验。另外每周清零一次排名给新用户追赶的机会。// 云函数解密示例 const cloud require(wx-server-sdk); cloud.init(); exports.main async (event) { const { encryptedData, iv } event; const wxContext cloud.getWXContext(); // 云开发自动管理 session_key直接解密 const result await cloud.getWeRunData({ encryptedData, iv }); return result.stepInfoList; };这段云函数代码省去了手动换session_key的步骤但注意cloud.getWeRunData需要在云函数里调用且用户必须已授权。云开发的免费额度对小型项目够用但排名数据量大了之后Redis 还是得自己搭。希望帮到你。本文还有配套的精品资源点击获取