ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微信步数兑换小程序实战:getWeRunData到后端校验全解析

2026/9/14 20:32:46 拓冰建站 浏览量
微信步数兑换小程序实战:getWeRunData到后端校验全解析 简介步数宝步数换购小程序 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 事件的回调里调用放在onLoad、onShow里直接调会走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而不是success。encryptedData和iv是必传参数二者必须配对使用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); ListStepItem 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不建议每次都调微信接口换步数宝通常把openid到session_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 0stock 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 换 tokencode2session 与登录态贯穿步数宝的登录不需要用户填账号密码前端调wx.login拿到一个临时code后端拿code换openid和session_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接口换取openid和session_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_id查exchange_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 备案备注与提审前检查小程序上线前要完成备案备案备注信息里最好把业务逻辑写明确。步数宝可以这样写该小程序用于记录用户每日运动步数经用户授权后读取微信运动数据按活动规则换算步豆并使用步豆兑换活动礼品不涉及金融、医疗等敏感信息。类目选择“生活服务-运动健身”比“工具-信息查询”更贴近业务场景审核更容易通过。提审前检查三件事隐私保护指引里声明对微信运动数据的读取和使用用户拒绝授权后不能反复弹窗强迫授权商品库存足够覆盖预期兑换量避免上线当天“已兑完”的投诉。这些检查项都过一遍再用测试号走完授权、兑换、核销全流程就可以提交审核了。本文还有配套的精品资源点击获取