微信步数兑换小程序实战:getWeRunData到后端校验全解析
2026/9/14 20:32:38 网站建设 项目流程

简介:步数宝步数换购小程序 V9.4.9 前端与后端源码是一套完整的无限多开版实施方案,面向需要搭建步数换购商城、或研究微信小程序前后端交互与微擎模块开发的工程师。压缩包共 1249 个文件、约 40.36MB,前端以 wxml、wxss、js、json 等小程序核心文件居多,后端含 php 及微擎相关配置,另有大量 png/jpeg 界面素材、html 辅助页面、css 样式、mp3 音效和 pem 证书等辅助资源;从内容预览可看到其覆盖商城、游戏、基础样式等前端模块,便于快速定位相关代码。该版本已针对流量主显示、页面浏览体验进行优化,并调整了微信审核相关功能,便于多开运营和提交审核。已有 263 人浏览学习,适合用于学习步数兑换玩法的完整前后端实现、参考其目录划分与模块结构,或在此基础上进行二次开发。注意此版本可能涉及版权问题,仅限学习交流使用。

1. 步数宝这类步数换购小程序在解决什么问题

走路换商品,听上去像噱头,但步数宝这类微信小程序项目实例在健身、保险、餐饮门店行业里是真实的用户运营工具。业务模型一句话就能讲清楚:用户在微信运动里的步数,被小程序读取后折算成步豆,步豆再拿去兑换优惠券或者小商品。下面不讨论打包下载和安装脚本,我按步数宝V9.4.9这类成熟版本都绕不开的完整链路讲:微信步数授权、步数换算、后端账户与订单、备案上线,每一步都能单独复现。适合要接手步数换购项目的前端,也想顺带搞懂后端怎么写的人。这条链路真正难的不是兑换页,而是微信侧步数数据的信任边界,我见过太多项目死在后端只信前端提交的数字。

2. 步数宝小程序前端:getWeRunData 授权、步数换步豆与兑换页实现

前端页面虽然只露出“今日步数、步豆余额、商品列表”三块,但代码上分别对应授权、存储、网络请求三个完全不同的问题。步数宝的前端第一个要搞清楚的是:微信步数接口不是一个普通的数据查询接口,它有触发条件和加密机制。

2.1 拿步数的正确姿势:getWeRunData 必须由用户点击触发

wx.getWeRunData有两条硬约束:第一,必须在用户 tap 事件的回调里调用,放在onLoadonShow里直接调会走fail,这是微信防止静默收集隐私的机制;第二,接口返回的不是明文步数,而是一段encryptedData,需要后端用session_key解密,前端拿到的只是密文。步数宝这类项目里,前端只负责把密文原样传到后端,再拿后端返回的“今日步数”做展示和换算。

Page({ data: { steps: 0, beans: 0, loading: false }, onLoad() { // 页面加载只读缓存,避免一进来就弹授权框 const cached = wx.getStorageSync('steps_today'); if (cached) { this.setData({ steps: cached.steps, beans: cached.beans }); } }, handleSyncSteps() { if (this.data.loading) return; this.setData({ loading: true }); wx.getWeRunData({ success: (res) => { wx.request({ url: `${getApp().globalData.baseUrl}/steps/decrypt`, method: 'POST', data: { encryptedData: res.encryptedData, iv: res.iv }, success: (resp) => { const todaySteps = resp.data.todaySteps || 0; const beans = Math.floor(todaySteps / 100); wx.setStorageSync('steps_today', { steps: todaySteps, beans }); this.setData({ steps: todaySteps, beans }); }, complete: () => this.setData({ loading: false }) }); }, fail: () => { wx.showModal({ title: '需要微信运动授权', content: '步数宝需要读取微信运动步数才能累计步豆', confirmText: '去授权', success: () => wx.openSetting() }); } }); } });

这段代码里有两个容易被前端面试题拿来考的点:一个是用户点击与异步回调的时序,按钮点击后loading置为true,防止连续点击产生多个请求;另一个是complete回调无论成功失败都会执行,所以恢复loading要放在complete而不是successencryptedDataiv是必传参数,二者必须配对使用;res.cloudID只有在小程序云开发环境下才有值,普通服务端模式不需要处理。另外微信侧对getWeRunData有频率控制,日常经验是每个用户每天拉一次足够,不要每次进页面都请求,步数宝通常把结果缓存到本地存储。如果团队用 uni-app 开发,把wx.getWeRunData换成uni.getWeRunData,其余逻辑不变。

2.2 步数到步豆的换算规则:比例、每日上限与动态标题

步数宝的换算规则不建议写死在前端,因为比例调整后小程序要过审核才能生效,太被动。我一般会在后端配置中心放三组参数,前端通过一个公共配置接口读取:

配置项常见值说明
步数转步豆比例100 步 = 1 步豆后端下发的整数比例,前端只做除法
每日最大可兑换步豆20超出部分仍然记步数,但不生成步豆
步豆有效期180 天过期清零,避免账户长期累积

其中“每日最大可兑换步豆”这个参数容易被忽略。如果完全不设上限,用户一天走 5 万步就能换 500 步豆,一个月下来商品库存撑不住。步数宝的常见做法是每日换算上限固定,兑换上限按活动周期动态调整,例如“双倍步豆日”把 100 步换 1 个改成 100 步换 1.5 个。前端拿到配置后,可以做一件事:用wx.setNavigationBarTitle把当天可兑换步豆数直接放在小程序头部标题里,这也是小程序动态设置标题的一个实用场景。

wx.setNavigationBarTitle({ title: this.data.beans > 0 ? `今日可兑 ${this.data.beans} 步豆` : '步数宝' });

动态标题适合做轻量运营位,用户每次进入页面第一眼看到的就是剩余可兑换步豆。要注意的是wx.setNavigationBarTitle只能修改当前页面标题,页面onUnload之后会自动恢复默认值,所以不涉及重置逻辑。

2.3 request 封装与商城渲染:前端面试常问的异步就在这里

商品兑换页本质上是一个轻量小程序商城:左边商品列表,右边步豆余额,点击兑换走创建订单接口。步数宝的项目里,商品列表和步豆余额是两个独立接口,但页面上必须一起展示,所以要用Promise.all并行请求,而不是串行等待。这里的前端代码可以直接复用 Vue 项目里的 axios 拦截器思路,只是把XMLHttpRequest换成wx.request

// utils/request.js const request = (path, { method = 'GET', data = {} } = {}) => { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: `${getApp().globalData.baseUrl}${path}`, method, data, header: { Authorization: `Bearer ${token}` }, success: (res) => { if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/index' }); reject(new Error('未登录')); return; } if (res.data.code !== 200) { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(new Error(res.data.msg)); return; } resolve(res.data.data); }, fail: reject }); }); }; // 页面里并行请求 async loadHome() { const [productRes, userRes] = await Promise.all([ request('/products', { data: { page: 1 } }), request('/user/beans') ]); this.setData({ products: productRes.list, beans: userRes.beans }); }

商品卡片用原生view写就行,团队里如果已经有 Vant Weapp 之类的组件库也可以用,但步数宝这种页面结构简单,引入组件库反而增加包体积。request封装里几个细节和前后端分离项目实战里踩过的坑一致:业务码code要和 HTTP 状态码分开,后端返回 200 但业务失败时不能当成功处理;401 统一清 token 跳登录,避免每个页面重复写判断;GET 参数和 POST body 都走同一个data字段,wx.request会自动按 method 处理。前端异步这里其实是前端面试八股文的高频题,但步数宝里的Promise.all和拦截器是一个能落在纸面上的答案。

3. 步数宝后端:步数校验、步豆账户与兑换订单闭环

前端把密文和用户操作传回来之后,后续全部在后端完成。步数换购小程序本质是一个带积分体系的库存商品系统,比普通电商多了一个“步数换算”的信任环节。很多前端同学问后端学习计划,其实步数宝后端就是一个足够小的样本:四张业务表、一个事务方法、一个解密接口,读懂这三样就能看懂大半 Java 后端项目。这个后端可以用 Spring Boot 写,用 Node.js 或 Go 也能实现同样逻辑,核心差异只在工程习惯和部署方式。

3.1 步数校验为什么必须放在后端

步数宝的步数校验如果放前端,相当于把仓库钥匙交给用户自己。后端至少要校验三件事。第一,解密必须在后端做,因为getWeRunData返回的encryptedData要拿session_key解,session_key只有后端调微信code2session接口才能拿到,前端永远没有这个密钥。第二,步数的来源要可信,后端要核对解密后的步数列表里是否包含当天日期,以及步数数值是否落在合理区间。第三,要防多个账号刷步数,同一个手机上注册十个微信小程序账号,每个账号都能拿到同一份步数,如果前端直接把步数提交给后端,后端不核对设备维度,这个活动上线一周就会被刷空。

public TodayStepsResult decryptAndVerify(String encryptedData, String iv, String openid) { String sessionKey = sessionKeyCache.get(openid); String json = AesUtil.decrypt(encryptedData, sessionKey, iv); // 解密结果示例:{"stepInfoList":[{"timestamp":1735660800,"step":9321}]} StepInfoList list = JsonUtil.parse(json, StepInfoList.class); List<StepItem> items = list.getStepInfoList(); StepItem today = items.stream() .filter(item -> isToday(item.getTimestamp())) .findFirst() .orElseThrow(() -> new BizException("当天步数未生成")); if (today.getStep() > 30000) { // 单日超过 3 万步不直接拒绝,标记人工复核,防止误伤真实用户 riskControlService.markForReview(openid, today.getStep()); } return new TodayStepsResult(today.getStep()); }

session_key不建议每次都调微信接口换,步数宝通常把openidsession_key的映射放在 Redis 里,过期时间按微信官方建议设置,减少一次网络往返。stepInfoList返回的是最近 30 天的每日步数,每条记录的timestamp是当天零点的时间戳,不是当前时间,所以不能用“当前时间减一天”去匹配,要按日期字符串精确匹配。

3.2 四张核心表:用户、步数记录、商品、兑换订单

后端建模不用复杂,四张表够了。用户表存步豆余额,步数记录表存每天换算明细,商品表存步豆价格和库存,兑换订单表存每次消耗。这里不建外键,外键约束留在应用层的事务里控制,因为高并发扣库存时外键会影响写入性能。下面这套 DDL 可以直接落到 MySQL。

CREATE TABLE `member` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信用户唯一标识', `beans` INT NOT NULL DEFAULT 0 COMMENT '当前步豆余额', `total_steps` INT NOT NULL DEFAULT 0 COMMENT '累计步数,仅作展示', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='步数宝用户表'; CREATE TABLE `step_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信 openid', `stat_date` DATE NOT NULL COMMENT '步数归属日期', `steps` INT NOT NULL DEFAULT 0 COMMENT '当天步数', `beans` INT NOT NULL DEFAULT 0 COMMENT '按比例换算出的步豆', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid_date` (`openid`, `stat_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日步数记录'; CREATE TABLE `product` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '商品名称', `stock` INT NOT NULL DEFAULT 0 COMMENT '剩余库存', `price` INT NOT NULL DEFAULT 0 COMMENT '步豆价格', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='兑换商品表'; CREATE TABLE `exchange_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '业务订单号', `openid` VARCHAR(64) NOT NULL, `product_id` BIGINT NOT NULL, `beans_cost` INT NOT NULL COMMENT '消耗步豆数', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0已创建 1已核销 2已退回', `biz_id` VARCHAR(64) NOT NULL COMMENT '前端幂等键', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_biz_id` (`biz_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='兑换订单表';

step_record的联合唯一索引uk_openid_date是整个防重复流程的关键。如果你接手的是一个已有的 Java Spring Boot 项目,先看resources目录下的 SQL 文件,再找对应实体类,即可快速定位到这几张表;若依这类脚手架的管理后台可以直接生成单表 CRUD,步数宝的业务表挂上去就能做商品上下架和数据查询,省掉写管理端的时间。

3.3 兑换接口:事务、乐观锁与幂等键

兑换接口是步数宝后端最容易写错的地方,问题集中在并发和重复提交。用户用步豆兑换商品,点击按钮那一瞬间可能发出两次请求,前端防了双击,还有脚本重放。所以后端必须同时处理库存扣减和幂等两个问题。

@Transactional(rollbackFor = Exception.class) public ExchangeResult exchange(ExchangeRequest req) { // 1. 幂等校验:同一 bizId 只能创建一单 if (exchangeOrderMapper.countByBizId(req.getBizId()) > 0) { return ExchangeResult.fail("请勿重复提交"); } Member member = memberMapper.selectByOpenid(req.getOpenid()); Product product = productMapper.selectById(req.getProductId()); if (product.getStock() <= 0) { return ExchangeResult.fail("商品已兑完"); } if (member.getBeans() < product.getPrice()) { return ExchangeResult.fail("步豆不足"); } // 2. 乐观锁扣库存:stock 条件参与更新,防止并发超卖 int stockRows = productMapper.deductStock(req.getProductId()); if (stockRows == 0) { return ExchangeResult.fail("库存已被抢完"); } // 3. 扣步豆、建订单 memberMapper.deductBeans(req.getOpenid(), product.getPrice()); String orderNo = generateOrderNo(); exchangeOrderMapper.insert(orderNo, req); return ExchangeResult.success(orderNo); }

对应的deductStockSQL 是关键参数所在:

UPDATE product SET stock = stock - 1 WHERE id = #{productId} AND stock > 0

stock > 0这个条件让更新操作在高并发下只有一个请求能成功,返回行数为 0 说明库存已被扣完。这里不要先SELECT stock再判断,因为两个请求同时查到的库存可能都是 1,随后都执行扣减就会超卖。同一个事务里,member表和product表靠应用层协调,事务提交失败时全部回滚。biz_id来自前端,生成规则可以用openid + 时间戳 + 随机数,唯一索引兜底,即使接口被重放也不会生成第二笔订单。

3.4 步数入库的幂等与异常步数过滤

步数入库发生在用户每次同步步数的时候,后端需要把“当天步数”写进step_record,并给member表增加对应步豆。这里最常见的问题是一个用户当天同步了三次,步数记录出现三条,总额被重复累加。解决办法是用 MySQL 的INSERT ... ON DUPLICATE KEY UPDATE,配合uk_openid_date唯一索引,新数据覆盖旧数据而不是新增一行。

INSERT INTO step_record (openid, stat_date, steps, beans) VALUES (#{openid}, #{statDate}, #{steps}, #{beans}) ON DUPLICATE KEY UPDATE steps = VALUES(steps), beans = VALUES(beans)

步数算对了,步豆的更新还要处理“今天已经换过步豆”的情况。如果步数宝的产品规则是“当天步数只能当天兑换”,就要在step_record上增加一个是否已兑换的标记;如果步豆可以累积到账户,则只需判断member.beans余额,不用关心这笔步豆是来自哪天。从运营角度看,后者更容易留住用户,但每晚会多一个步豆过期任务,用定时任务把过期时间超过 180 天的步豆清零。

4. 前后端联调:域名配置、code 换 token 与真机差异排查

步数宝前后端联调阶段的问题通常分三类:接口不通、登录态失效、步数在开发者工具和真机上表现不一致。这三类问题互相纠缠,最典型的是前端说接口 401,后端说请求根本没到网关,一查是域名没配白名单。下面按联调顺序把配置和排查路径过一遍。

4.1 小程序合法域名与开发者工具的免校验开关

小程序wx.request的域名有严格限制:必须是 HTTPS,域名要完成 ICP 备案,还要在小程序管理后台的“开发管理-服务器域名”里配置 request 合法域名。开发者工具里默认开启“校验合法域名”,联调阶段可以临时关掉,但真机预览时如果关不掉就会报url not in domain list。日常联调我一般这样处理:代码里的baseUrl用一个常量统一管理,开发环境指向测试服务器 IP 加端口,同时勾选开发者工具里的“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”;联调通过后,再把baseUrl切到正式域名,提交小程序审核前最后确认后台域名和代码域名一致。需要注意的是,小程序环境没有浏览器跨域问题,若依这类管理后台用 Vue 开发才需要处理 CORS,小程序到后端不需要后端开启跨域配置。

4.2 wx.login 换 token:code2session 与登录态贯穿

步数宝的登录不需要用户填账号密码,前端调wx.login拿到一个临时code,后端拿codeopenidsession_key,再签发自家的 token 返回前端。这就是微信小程序体系里最基础的 code 换 token 流程,和 Vue 前后端分离项目里用 axios 拦截器维护 token 是同一套思路,只是换取凭证的方式不同。

// pages/login/index.js wx.login({ success: async ({ code }) => { const data = await request('/auth/login', { method: 'POST', data: { code } }); wx.setStorageSync('token', data.token); wx.navigateBack(); } });

后端接口POST /auth/login接收到code后,调微信的code2session接口换取openidsession_key,然后生成一个自签 token。自签 token 里只存openid和过期时间,不存session_key,因为session_key属于敏感凭证,前端永远不需要知道。前端后续所有请求在Authorization头里带这个 token,后端用拦截器校验。步数宝这里有一个容易被忽略的坑:session_key会过期,过期后后端解密步数会失败。所以解密接口里要捕获解密失败异常,提示前端重新调wx.login换新 code,否则用户在活动页停留超过一天后同步步数必然报错。

4.3 getWeRunData 在开发者工具和真机上的差异

开发者工具不是真实环境,步数数据靠模拟器提供。模拟器面板里的“运动数据”可以手动填步数,填多少getWeRunData就返回多少,方便前端调试界面;真机上则有三个差异。第一,真机先要确认手机里的微信运动功能已开启,并且小程序拿到了scope.werun授权,两者缺一不可。第二,刚过零点时真机可能拿不到“当天”的步数,因为微信运动当天的数据还没有生成,工具里没有这个时序问题。第三,真机返回的stepInfoList长度可能不足 30 天,新用户前几天注册可能只有 1 至 2 条记录,后端要做好空数据处理。联调时建议用真实微信号走真机预览,工具环境只能验证 UI 和交互,不能验证数据链路。

4.4 联调阶段最常踩的四个异常

现象常见原因排查方向
授权弹窗点了同意,getWeRunData 仍 fail用户在小程序设置里关闭了微信运动授权,或微信运动功能未开启wx.getSetting查看scope.werun状态
后端解密报 illegal buffer 或 bad decryptsession_key已过期,前端还在用旧登录态让用户重新走wx.login,刷新session_key缓存
真机能拿到步数,开发者工具拿不到模拟器的运动数据未设置,或工具版本过低在模拟器“运动数据”面板手动输入步数
兑换成功但商品库存没变化后端没用乐观锁,或前端重复提交了相同biz_idexchange_order是否插入成功,核对deductStockSQL 条件

所谓“亲测有效”,落到步数宝上就四件事:步数能从微信真实拿到、步豆正确累加、兑换后库存扣减、订单在管理后台可查询。这四个环节在真机上完整走一遍,就可以判断整套前后端链路是否可用。

5. 上线后的验证与运营:对账 SQL、防刷参数与备案备注

步数宝上线不是终点,上线后第三天才是真正考验后端的时候。新用户涌入,步数同步频率升高,兑换订单变多,如果库存和步豆账户对不上,运营会拿着截图来找你。所以我会提前把对账、防刷、审核三件事做在前面。

5.1 每天跑一次对账,三张表要对得上

对账是步数宝每天必须做的验证动作。核心逻辑是:当天步数换算出的步豆,减去当天兑换消耗的步豆,等于用户表步豆总额的当日增量。

SELECT (SELECT IFNULL(SUM(beans),0) FROM step_record WHERE stat_date = CURDATE()) AS today_earned_beans, (SELECT IFNULL(SUM(beans_cost),0) FROM exchange_order WHERE created_at >= CURDATE()) AS today_cost_beans, (SELECT IFNULL(SUM(beans),0) FROM member) AS total_balance;

today_earned_beans减去today_cost_beans的结果,应该等于total_balance相对于昨天的变化量。如果不等,优先看三处:有没有用户重复同步步数把step_record写多了、有没有兑换订单创建成功但扣步豆失败、有没有步豆过期任务误清了余额。实际项目里我会把这条 SQL 放进定时任务,每天早上九点跑一次,把结果推到日志系统,连续对不上就告警。

5.2 防刷先落三个参数

步数宝这类积分兑换项目的刷量风险远高于普通小程序,第一周就可能被脚本扫库存。我会在后端配置中心预置三个参数:单个设备最多注册 3 个账号,超过则拉黑设备号并拒绝登录;单日步数上限默认 30000 步,超过不拒绝但标记人工复核;同一个 openid 每天最多兑换 3 次,每次兑换间隔不小于 30 秒。这三个参数按运营活动热度调整,大促期间可以临时下调单日步数上限,防止被大量“摇步器”刷空商品池。

5.3 备案备注与提审前检查

小程序上线前要完成备案,备案备注信息里最好把业务逻辑写明确。步数宝可以这样写:该小程序用于记录用户每日运动步数,经用户授权后读取微信运动数据,按活动规则换算步豆并使用步豆兑换活动礼品,不涉及金融、医疗等敏感信息。类目选择“生活服务-运动健身”比“工具-信息查询”更贴近业务场景,审核更容易通过。提审前检查三件事:隐私保护指引里声明对微信运动数据的读取和使用;用户拒绝授权后不能反复弹窗强迫授权;商品库存足够覆盖预期兑换量,避免上线当天“已兑完”的投诉。这些检查项都过一遍,再用测试号走完授权、兑换、核销全流程,就可以提交审核了。

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

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

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

立即咨询