十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序健步走源码深度解析:步数解密、积分防刷与排行榜设计

微信小程序健步走源码深度解析:步数解密、积分防刷与排行榜设计 简介这是一份基于Java与Vue前后端分离架构的云上健步走微信小程序设计源码面向需要学习微信小程序开发、运动健康类项目或红色文化主题应用的学生与开发者。项目围绕微信步数兑换里程积分、关卡解锁、红色文化知识学习及个人/队伍/团队排行榜、勋章机制等核心功能展开适合作为课程设计、毕业设计或实际项目参考。压缩包共831个文件包含322个Java后端源文件、140个XML配置、102个SVG图形、101个Vue组件、88个JavaScript文件以及jpg、vm、scss、html等辅助资源整体约9.27MB。资源目录结构清晰涵盖前后端分离的开发环境配置、构建脚本和静态资源便于快速导入与二次开发。已有545人学习下载。通过阅读源码可掌握Java后端接口设计、Vue组件化开发、微信小程序交互逻辑及项目工程化组织方式并借鉴其融合运动健康与红色文化传播的产品设计思路。1. 为什么云上健步走先从 wx.getWeRunData 看起拿到这套源码我第一反应不是看 Java 写了多少而是先找 wx.getWeRunData 的调用位置。云上健步走这类微信小程序技术难点从来不在页面而在把微信步数安全地变成后端可信任的业务数据。项目里 322 个 Java 源文件、101 个 Vue 组件、102 个 SVG 图标说穿了都在服务一件事用步数换里程、换关卡、换排行。这套源码适合两类人一类是准备做运动健康类小程序但不知道怎么处理步数授权与积分的 Java 工程师另一类是接手了带管理端和用户端的多角色项目想研究前后端分离工程怎么拆分的人。接下来我按源码骨架、步数链路、排行模型、Vue 联调四条线拆着讲。2. 833 个文件里的分工Java、Vue、SVG 和 XML 的源码骨架2.1 先看统计这 833 个文件是怎么分配的文件类型数量在项目里承担的角色Java 源文件322后端接口、业务服务、数据访问、定时任务XML 配置文件140Spring 配置、MyBatis 映射、Maven 依赖SVG 图形文件102图标、勋章、关卡插画的矢量资源Vue 组件文件101管理端页面和可复用 UI 组件JavaScript 文件88小程序页面逻辑、请求封装、路由工具HTML 文件5管理端宿主页与静态资源入口PNG 图片5启动图、分享位图等少量点阵资源一眼能看出这是个典型的「小程序原生端 Vue 管理后台 Java 接口层」三件套。322 个 Java 源文件说明后端不是简单把 CRUD 堆在一起而是按controller / service / mapper / model做了分包XML 里会有相当一部分是 MyBatis 的 SQL 映射而不是纯 Spring 配置。140 个 XML 配 322 个 Java 文件这个比例意味着很多查询是手写 SQL 而不是 JPA 自动生成的手写 SQL 的好处是排行榜和里程汇总这类聚合查询能精确控制索引和临时表行为。102 个 SVG 是个容易被忽略的信号。项目没有把图标做成雪碧图或 PNG 切片而是走矢量图标方案。前端把 SVG 作为组件引入主题色替换只改 fill 属性不同分辨率下也不会模糊。缺点是如果某张 SVG 是直接从设计稿导出的路径里可能会带stylefill:#f00这种内联样式组件里想覆盖颜色时就失效了。后面接手的同学如果发现勋章颜色改不动先查 SVG 文件的根节点里有没有写死 fill。2.2 启动脚本 package.bat / build.bat / run-web.bat 里的门道项目正文里列了三个 Windows 批处理文件它们的分工很清楚package.bat 做全量打包build.bat 只编后端run-web.bat 管前端本地开发。常见做法是长这样echo off chcp 65001 nul rem package.bat先打后端 jar再打前端 dist call mvn -f pom.xml clean package -DskipTests -q if errorlevel 1 ( echo [package] backend build failed exit /b 1 ) cd web call npm install if errorlevel 1 goto :frontend_failed call npm run build cd .. echo [package] all done exit /b 0 :frontend_failed cd .. echo [package] frontend build failed exit /b 1这个脚本里最有用的参数是-DskipTests。云上健步走这类项目里通常有单元测试但如果是接手别人的代码仓测试用例往往依赖本地数据库直接mvn clean package大概率会挂在测试环境连不上。-DskipTests跳过测试执行但保留编译适合先验证代码能不能跑通。注意它不是-Dmaven.test.skiptrue后者连测试代码编译都跳过在需要跑测试时才用。build.bat 我只留一行核心逻辑的话会是call mvn clean package -pl . -DskipTests它的作用是快速验证后端改动。而 run-web.bat 会做两件事先cd web然后npm install npm run dev。这里有个 Windows 下常见坑如果 web 目录下已经存在 node_modules 且是旧版本npm install 会按 package-lock.json 增量更新但偶尔会有 package-lock.json 与 package.json 不一致导致依赖版本错乱。我一般出问题就直接删掉 node_modules 和 lock 文件重装比逐层排查快。2.3 .gitignore 与 .editorconfig 在多人协作里的实际作用.gitignore 被列了三次说明项目在根目录、后端目录、前端目录各放了一份。前端那份通常长这样node_modules/ dist/ *.log .env.local .env.*.local.env.development这类文件最容易踩坑如果它没被忽略本地联调的接口地址就会被提交到代码库。云上健步走管理后端开发时开发机上用的是http://127.0.0.1:8080一旦被提交其他人拉下来直接 npm run dev请求全打到这台开发机内网地址上表现就是页面 401 或者请求超时。所以.env.development可以提交但.env.development.local必须忽略后者才是本地私有配置的落点。.editorconfig 则是给团队省事的root true [*] charset utf-8 indent_style space indent_size 2 end_of_line lf insert_final_newline trueJava 项目多数用 4 空格缩进前端 Vue 项目习惯 2 空格这个文件让 IDE 在跨目录时自动切换缩进风格。如果发现 Vue 文件里缩进一会儿两格一会儿四格先看.editorconfig 是不是被 IDE 忽略了再检查是不是有人用 Tab 提交过代码。云上健步走这类多模块项目这一项直接影响代码 diff 的干净程度。3. 步数换里程的完整数据链路wx.getWeRunData、解密与积分防刷3.1 小程序端授权与步数读取微信步数读取的前置条件是用户授权 scope.werun这一步要放在用户进入首页时做。如果用户拒绝授权后期几乎不可能再通过wx.authorize直接弹窗只能引导到设置页手动打开。所以在健步走这类项目里我会把授权弹窗放在「点击开始健步走」之后而不是一进小程序就弹。// pages/index/index.js Page({ onShow() { wx.getSetting({ success: (res) { if (res.authSetting[scope.werun]) { this.fetchStepData() } } }) }, handleStartWalking() { wx.getSetting({ success: (res) { if (res.authSetting[scope.werun] false) { wx.openSetting({ success: () this.fetchStepData() }) } else { wx.authorize({ scope: scope.werun, success: () this.fetchStepData(), fail: () wx.showToast({ title: 需要微信运动授权, icon: none }) }) } } }) }, fetchStepData() { wx.getWeRunData({ success: ({ encryptedData, iv }) { wx.request({ url: https://api.example.com/walk/convert, method: POST, data: { encryptedData, iv }, success: (res) this.refreshRankAndMileage(res.data) }) } }) } })这段代码里最关键的决策是把 encryptedData 原样交给后端。因为解密需要微信的 session_key而 session_key 只能通过 code 换 session_key 的接口拿到前端拿到 session_key 本身就是安全隐患。所以正确链条是wx.login 拿 code后端用 code 换 session_key再接住 encryptedData 和 iv 做解密。wx.getWeRunData返回的 encryptedData 里包含步数列表和水印信息watermark 里的 timestamp 可以校验数据是否为最近时段生成防止旧数据重放。3.2 Java 后端解密与今日步数提取后端收到 encryptedData 和 iv 之后先查 openId再取 sessionKey 解密。解密算法是 AES-128-CBCPKCS5Padding密钥和 IV 都是 sessionKey 和 iv 的 Base64 解码结果public WeRunData decryptWeRunData(String sessionKey, String encryptedData, String iv) throws Exception { byte[] keyBytes Base64.getDecoder().decode(sessionKey); byte[] ivBytes Base64.getDecoder().decode(iv); byte[] encryptedBytes Base64.getDecoder().decode(encryptedData); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, AES), new IvParameterSpec(ivBytes)); String json new String(cipher.doFinal(encryptedBytes), StandardCharsets.UTF_8); return JSON.parseObject(json, WeRunData.class); }解密之后拿到的结构里stepInfoList是一个按时间排序的数组每个元素包含timestamp和stepstep 是到该时间点为止的累计步数不是增量步数。很多新手在这里会写sum把同一用户一天内的四条记录全加起来得到的结果是真实步数的三到四倍。正确做法是取当天时间戳对应条目里 step 最大值也就是最后一次上报的快照public int getTodaySteps(WeRunData data) { long startOfDay LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond(); return data.getStepInfoList().stream() .filter(item - item.getTimestamp() startOfDay) .mapToInt(StepInfo::getStep) .max() .orElse(0); }至于步数怎么换算里程和积分我建议把比率参数放进配置表而不是写死在代码里。常见做法是 2 步折算 1 米10 米折算 1 积分这两个数在运营配置里随时调。如果写死在 service 里每次调整都要重新发布后端而且团队榜和个人榜的换算口径还可能被不小心改成两套逻辑。3.3 幂等键与 Redis 的 increment 陷阱步数数据的核心门槛是防重复兑换。用户每次进小程序都会触发一次 getWeRunData同一份 encryptedData 如果不做幂等校验积分会被重复发放。最简单的方案是把 encryptedData 做 SHA-256 取哈希用 Redis setIfAbsent 写入十分钟或一小时过期String digest DigestUtils.sha256Hex(encryptedData); String key walk:dedup: digest; Boolean first redisTemplate.opsForValue().setIfAbsent(key, 1, Duration.ofHours(1)); if (!Boolean.TRUE.equals(first)) { throw new BizException(重复提交请稍后再试); }这里不建议用redisTemplate.opsForValue().increment(key)做计数器原因很具体当 key 不存在时 increment 会返回 1这个行为看起来没问题但如果你先 set 了字符串类型的值再执行 incrementSpring Data Redis 会抛RedisCallbackReturnTypeMismatchException错误信息就是那句很常见的 value is not an integer or out of range。类似这种排查在 java 面试和日常开发里都属于高频问题本质都是没搞清楚 Redis 底层 value 编码。幂等场景下 setIfAbsent 一次写入就够了不需要 increment。里程兑换走的是「先校验幂等再写流水再更新日汇总」三步。日汇总表应该对 (open_id, stat_date) 建唯一索引每天的步数和里程只保留一条记录更新的 SQL 用INSERT ... ON DUPLICATE KEY UPDATE完成INSERT INTO user_step_daily (open_id, stat_date, steps, mileage, points, update_time) VALUES (?, CURRENT_DATE, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE steps VALUES(steps), mileage VALUES(mileage), points VALUES(points), update_time NOW();这样保证一天只有一行排行榜直接 sum 这张表不需要再靠明细表实时聚合。4. 关卡解锁、三项排行榜与勋章机制的表结构设计4.1 关卡表累计里程解锁而不是碎步数解锁云上健步走的关卡设计和普通答题小程序不一样它强调的是「走到一定里程才能进入下一个学习节点」。这里我建议用累计里程做门槛而不是当日步数。如果按当日步数解锁用户只要某天暴走一万步就能把所有关卡打通运营设计的节奏感就没了按累计里程用户必须连续参与。关卡表最小集合是这几个字段CREATE TABLE walk_stage ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stage_no INT NOT NULL COMMENT 关卡序号, stage_name VARCHAR(64) NOT NULL COMMENT 关卡名称, need_mileage INT NOT NULL COMMENT 解锁所需累计里程单位米, knowledge_id BIGINT COMMENT 绑定的知识卡片ID, reward_points INT DEFAULT 0 COMMENT 首次通关奖励积分, sort_order INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stage_no (stage_no) );用户进度则单独建表CREATE TABLE walk_user_stage ( id BIGINT PRIMARY KEY AUTO_INCREMENT, open_id VARCHAR(64) NOT NULL, stage_no INT NOT NULL, unlocked_at DATETIME NOT NULL, finished_at DATETIME DEFAULT NULL COMMENT 知识学习完成时间, UNIQUE KEY uk_openid_stage (open_id, stage_no) );解锁判断可以等步数兑换成功后在事务里做先 sum user_step_daily 里该用户的累计里程再查出当前最大已解锁关卡如果里程达到下一关门槛就插入 walk_user_stage。这个逻辑放在 Java service 里比在存储过程里好维护因为解锁的同时可能还要发消息、记录勋章Java 侧能直接编排。4.2 个人、队伍、团队三个排行榜维度不同存储口径也不同项目里三个排行榜别搞成一张表三个字段打天下。个人榜关心的是单个 open_id 的里程队伍榜关心的是一个小队合起来的里程团队榜则是按组织维度聚合时间窗口都不一样。排行维度统计对象分组键时间范围数据来源个人榜单用户open_id日榜 / 月榜user_step_daily队伍榜用户自由组队team_id周榜user_team_member user_step_daily团队榜单位 / 行政组织org_id月榜user/org 关联表 user_step_daily个人榜 SQL 是最直接的教学样本SELECT open_id, SUM(mileage) AS total_mileage, ROW_NUMBER() OVER (ORDER BY SUM(mileage) DESC) AS rank_no FROM user_step_daily WHERE stat_date BETWEEN :startDate AND :endDate GROUP BY open_id ORDER BY total_mileage DESC LIMIT 100;MySQL 8.0 以上支持窗口函数ROW_NUMBER 可以稳定地给相同里程排出先后顺序。如果后端用的是 5.7就只能用rownum : rownum 1这种变量写法但并发和分页时容易出现排名错位建议直接升 8.0。队伍榜和团队榜不要在 SQL 里实时 join因为数据量上来之后几次联表sum 会把接口拖慢。更常见的做法是每天凌晨跑一个统计任务把队伍、团队的日里程写入一张 rank_daily 汇总表页面只查汇总表。个人榜如果也要支撑大流量同样可以把 TOP 100 结果缓存到 Redis用 ZSET 存储score 是里程数member 是 open_id 或 team_id每次步数兑换成功后 ZADD 一下查询走 ZREVRANGE。redis-cli ZADD walk:rank:team:202506 12800 team_01 redis-cli ZREVRANGE walk:rank:team:202506 0 99 WITHSCORESZADD 在同一个 key 上反复执行不会重复插入而是更新分数天然适配里程叠加的场景。这就是为什么排行榜场景很少直接查 MySQL。4.3 勋章解锁规则引擎还是定时扫描勋章机制本质上是一组条件判断。条件类型可能是「累计里程达到 X」「连续打卡 7 天」「队伍周榜进入前 10」。在代码里用 if else 写会越写越臭后期加一个勋章就要重新改 Service。我倾向于把勋章规则也做成一张表CREATE TABLE walk_medal_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, medal_code VARCHAR(32) NOT NULL, medal_name VARCHAR(64) NOT NULL, condition_type VARCHAR(32) NOT NULL COMMENT mileage / consecutive_days / rank_top, condition_value VARCHAR(64) NOT NULL COMMENT 条件数值或配置JSON, svg_path VARCHAR(255) COMMENT 勋章图标SVG路径, UNIQUE KEY uk_medal_code (medal_code) );判断发放的时机有两种一种是事件驱动步数兑换完成后立刻检查另一种是每日定时全量扫描。事件驱动响应快但耦合重每个业务动作里都得插一段勋章检查代码定时扫描实现简单但勋章发放会延迟一天。我一般会折中主要结算逻辑走定时任务特殊勋章走事件驱动。核心判断方法写成一个 MedalEngine不直接依赖具体业务表Component public class MedalEngine { private final MedalRuleMapper ruleMapper; private final UserMedalMapper userMedalMapper; Transactional public void checkAndUnlock(String openId, WalkSummary summary) { ListMedalRule rules ruleMapper.selectAllEnabled(); for (MedalRule rule : rules) { if (userMedalMapper.exists(openId, rule.getMedalCode())) { continue; } boolean matched switch (rule.getConditionType()) { case mileage - summary.getTotalMileage() Integer.parseInt(rule.getConditionValue()); case consecutive_days - summary.getConsecutiveDays() Integer.parseInt(rule.getConditionValue()); case rank_top - summary.getBestRank() Integer.parseInt(rule.getConditionValue()); default - false; }; if (matched) { userMedalMapper.insert(new UserMedal(openId, rule.getMedalCode())); } } } }这套写法的好处是新加勋章只需要在 walk_medal_rule 插一条记录代码不用改。102 个 SVG 里很大一部分就是给这些勋章准备的图标路径存 SVG 的路径文件名前端通过svg_path拼出图片地址。4.4 小程序端首屏渲染与顶部导航栏高度适配健步走小程序使用自定义导航栏时要么用navigationStyle: custom全权接管要么只调节页面布局的 padding-top。最常见的做法是把状态栏高度和右上角胶囊按钮高度算出来动态给顶部占位视图设高度避免刚进入的小程序页面顶栏把「微信步数」主按钮顶下去// 兼容基础库版本获取顶部安全距离 const win wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() const statusBarHeight win.statusBarHeight || 20 const capsule wx.getMenuButtonBoundingClientRect() const navBarHeight capsule.height (capsule.top - statusBarHeight) * 2 statusBarHeight Page({ data: { navBarHeight }, onLoad() { // 先从本地缓存渲染步数再异步刷新避免首屏白屏 const cached wx.getStorageSync(lastStepCount) this.setData({ stepCount: cached || -- }) } })首屏加载策略是先读本地缓存渲染一次同步请求后端拿最新数据后再覆盖。这样即使接口慢用户刚进入时也不会看到空白加载层。也顺手把「修改刚进入的加载页面」这类需求局限在 index 页自己的生命周期里而不是去改小程序根配置的全局 loading 行为。5. run-web.bat 起 Vue 管理端环境变量、转发规则与联调避坑5.1 .env.development 决定 Vue 管理端往哪里发请求Vue 管理端是独立的 web 工程它和后端 Java 服务之间通过 HTTP 接口通信。本地开发时最常出问题的就是.env.development里的接口基地址配错。Vite 项目里常见配置是# web/.env.development VITE_APP_BASE_URL/api VITE_APP_UPLOAD_URL/upload VITE_DEV_SERVER_PORT5173如果管理端和后端都在本机VITE_APP_BASE_URL写成/api再配合 vite.config.js 里的转发规则把/api开头的请求转发到 Java 服务的 8080 端口// web/vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true } } } })这里把VITE_APP_BASE_URL设置为绝对地址http://127.0.0.1:8080也能跑但上线前必须改成相对路径。我见过不少项目本地联调没问题打包后接口 404就是因为.env.production 里还留着 localhost。用相对路径 转发规则是更稳的做法生产环境由 Nginx 统一转发。run-web.bat 里的核心命令就几行cd /d %~dp0web npm install npm run dev%~dp0取的是批处理文件所在目录这样不管在哪个路径下执行都不会跑错目录。5.2 联调验证顺序与常见失败点跑起来之后别急着点页面先按这个顺序验证链路:: 1. 确认后端健康检查 curl http://127.0.0.1:8080/health :: 2. 确认 Vue 转发是否生效能看到登录接口 JSON 返回即是通 curl http://127.0.0.1:5173/api/health :: 3. 静态资源配置是否正常 curl -I http://127.0.0.1:5173/favicon.ico第一条通了说明 Java 服务正常第二条通了说明转发链路正常。如果第一步 200、第二步 404基本是 vite 转发规则里的 target 端口写错或者后端接口前缀不是 /api。第二步如果返回 HTML 而不是 JSON检查是不是后端返回了 404 页面转发逻辑本身没有错。微信小程序端的联调不要依赖外部抓包工具微信开发者工具自带的 Network 面板已经能看到每个请求的域名、header、返回体。真正要注意的是 request 合法域名校验开发环境下要关掉 URL 校验或者把接口域名加到开发白名单。如果小程序里wx.request用的是 IP 加端口开发者工具会拦截真机预览更严格只能填 app.json 里配置过的域名。跑管理端和跑小程序端是两套环境别混着看小程序接口出问题先看 Network 面板里是errno还是invalid url前者是后端逻辑后者是域名配置。本文还有配套的精品资源点击获取
返回列表