简介这是一份面向英语学习激励场景的微信小程序前端与Java后端完整项目源码适合正在准备毕业设计、课程设计或期末大作业的高校学生特别是计算机、电子信息等需要项目实战练习的开发者。项目代码均经过严格调试可直接运行有助于理解从需求分析、数据库设计到前后端交互与激励功能实现的完整流程。压缩包共含1343个文件大小约14.92MB目录结构清晰涵盖Java后端逻辑、Vue管理端页面、小程序wxml/wxss界面以及png设计素材等同时包含json/sql等配置与数据文件便于快速还原开发环境并掌握数据存储和激励规则设计。资源内还提供了项目文档及运行脚本可辅助完成部署与二次开发验证。目前已有183人学习下载适合作为信息类专业的毕设参考或项目练手素材。1. 微信小程序的英语学习激励系统把激励闭环做透才是高分点毕设题目叫微信小程序的英语学习激励系统很多人第一反应是做一个背单词工具单词列表、点开看释义、收藏生词。真按这个思路交上去答辩老师大概率只追问一句激励在哪里体现然后就冷场了。这个题的核心得分点不在页面数量而在激励逻辑是否成体系——签到奖励、连续学习天数、积分流水、排行榜、成就徽章五个模块互相咬合让用户每天愿意回来。这篇笔记按做毕设的实际路线讲先定激励规则再设计库表然后是前端激励展示和后端接口最后是五个必踩的坑。适合选了小程序方向、不想把毕设做成套模板堆页面的同学参考。2. 先写激励规则再写代码积分、签到与连续天数的数据设计写代码之前第一件事是把激励规则用文字列清楚什么行为给多少分、什么条件解锁徽章、连续打卡怎么算。规则不定清楚后面接口和页面都得返工。2.1 激励闭环的四个模块与数据流常见做法是把激励系统拆成四段行为记录、积分结算、激励展示、召回提醒。行为记录用户在小程序里背单词、学每日一句产生学习行为。积分结算后端根据行为计算积分写入积分流水表。激励展示首页看板展示连续天数与今日进度激励中心展示徽章与排行榜。召回提醒通过微信订阅消息在用户当天未学习时提醒他回来。数据流是单向闭环前端提交学习行为后端写入学习记录并计算积分前端拉取积分与徽章状态展示最后订阅消息把用户拉回。四个环节对应的就是后面要实现的接口登录、打卡、排行榜、订阅消息发送。答辩时把这个闭环画成一张图比贴十张页面截图都有说服力因为老师能看到你是在设计系统不是在堆页面。2.2 六张表把激励闭环撑起来库表是整个系统里最不该返工的部分。选 MySQL 而不是云开发文档数据库是因为答辩老师大概率会问数据库设计关系型表结构更容易讲清楚主键、唯一索引、外键和事务这些都是明确的计分点。我按最常见的 MySQL InnoDB 设计字符集用 utf8mb4因为微信昵称里会出现 emoji用 utf8 会存不进去。-- 用户表 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL UNIQUE COMMENT 微信用户唯一标识, nickname VARCHAR(64) DEFAULT , avatar VARCHAR(255) DEFAULT , consecutive_days INT DEFAULT 0 COMMENT 连续打卡天数, total_points INT DEFAULT 0 COMMENT 累计积分, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学习记录表记录每天学了哪些单词 CREATE TABLE learn_record ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, word_id INT NOT NULL, learn_date DATE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_word_date (user_id,word_id,learn_date), KEY idx_user_date (user_id,learn_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 打卡表一天一条 CREATE TABLE check_in ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, checkin_date DATE NOT NULL, points INT NOT NULL COMMENT 本次打卡获得积分, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id,checkin_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 积分流水表所有积分变动都留痕 CREATE TABLE points_log ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, change INT NOT NULL COMMENT 正数为加负数为扣, reason VARCHAR(32) NOT NULL COMMENT checkin/learn/badge, ref_id INT DEFAULT 0 COMMENT 关联业务记录ID, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id,created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成就表用户解锁徽章 CREATE TABLE achievement ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, badge VARCHAR(32) NOT NULL COMMENT 徽章编码, unlocked_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_badge (user_id,badge) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 单词表示例结构可按难度分表 CREATE TABLE word ( id INT AUTO_INCREMENT PRIMARY KEY, word VARCHAR(64) NOT NULL, meaning VARCHAR(255) NOT NULL, phonetic VARCHAR(64) DEFAULT , example_sentence TEXT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几处关键设计说明。learn_record的联合唯一索引uk_user_word_date用来防止同一个单词在同一天被反复提交刷积分check_in的uk_user_date保证一个用户一天只有一条打卡记录。积分不直接只累加在 user 表里而是每一笔变动都写进points_log这样答辩时能展示每一分从哪里来的对账能力。learn_date和checkin_date用 DATE 而不是 DATETIME因为业务上只关心是哪一天时间精度反而会带来跨天边界问题。2.3 积分规则做成配置而不是写死在 if 里积分规则如果散落在接口代码里后期改一个分值要动三处。常见做法是单独抽一个规则配置文件接口里只引用规则对象。// utils/reward-rule.js module.exports { checkinBase: 5, // 完成打卡的基础积分 streakStep: 2, // 连续天数每增加1天额外加2分 learnPerWord: 1, // 每学一个单词1分 learnDailyLimit: 20, // 每日学习积分上限防止刷分 badgeRules: [ { badge: first_checkin, name: 初次见面, condition: { type: checkin_count, min: 1 } }, { badge: streak_7, name: 一周坚持, condition: { type: streak_days, min: 7 } }, { badge: words_100, name: 百词记录, condition: { type: learn_count, min: 100 } } ] };这里把徽章条件设计成{ type, min }对象而不是一长串 if/else是为了让新增徽章变成纯配置操作。判定函数里用一个 switch 把 type 映射到用户统计数据字段比如streak_days读user.consecutive_days。答辩时当场演示把 learnDailyLimit 从 20 改成 10系统自动限流比现场翻代码改逻辑更有设计感。3. 前端把激励感做出来进度环、打卡页与徽章墙后端把数据和规则准备好前端只负责一件事让用户直观看到我今天学到了什么程度、还差多少、能拿什么奖励。三个页面是核心首页看板、学习打卡、激励中心。如果以后想跨 Android/iOS视图层可以换 uni-app 重写但库表和后端接口不用动这也是把数据设计和页面设计分开的好处。3.1 首页学习看板进度环与导航栏适配首页要展示今日学习进度和连续打卡天数。进度环不用 canvas用 CSS 的 conic-gradient 就能画代码量小低端安卓机上也不卡。view stylebackground: conic-gradient(#07c160 {{progress}}%, #f2f2f2 {{progress}}%) classring view classring-inner text classpercent{{progress}}%/text text classsub今日已学 {{learnedCount}}/{{dailyGoal}} 词/text /view /view.ring { width: 320rpx; height: 320rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .ring-inner { width: 280rpx; height: 280rpx; background: #fff; border-radius: 50%; display: flex; flex-direction: column; align-items: center; justify-content: center; }进度值在页面 onShow 里计算progress Math.round(learnedCount / dailyGoal * 100)。用户从打卡页返回首页时数据必须是最新的onLoad 只在首次进入执行一次这个细节很影响演示效果。另一个首页必踩的点是自定义导航栏。如果你想把连续天数做成导航栏下一块醒目的卡片就需要自定义导航栏此时必须动态计算状态栏高度和胶囊按钮位置这就是微信小程序顶部导航栏高度的经典适配问题。// utils/navbar.js function getNavBarInfo() { const win wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); const menu wx.getMenuButtonBoundingClientRect(); const navBarHeight (menu.top - win.statusBarHeight) * 2 menu.height; return { statusBarHeight: win.statusBarHeight, navBarHeight, menuRight: win.screenWidth - menu.right }; }wx.getMenuButtonBoundingClientRect()拿到胶囊按钮的位置navBarHeight按胶囊上下间距对称推导。不同手机上这个值不一样绝对不能写死写死的话不同机型首页头部会一高一矮演示时很尴尬。3.2 学习打卡页单词卡片与打卡状态缓存打卡页的交互用点击翻卡片就够正面是单词和音标点击翻转显示释义点击认识把单词 id 记入learnedIds数组。关键在打卡提交的时机与防重复。// pages/learn/learn.js 片段 const STORAGE_KEY learn_daily_status_v1; submitCheckin() { const today this.formatDate(new Date()); const cached wx.getStorageSync(STORAGE_KEY); // 本地缓存命中且是今天直接置为已打卡不再发请求 if (cached cached.date today cached.done) { this.setData({ checkedIn: true }); return; } wx.request({ url: ${getApp().globalData.baseUrl}/api/learn/checkin, method: POST, data: { wordIds: this.data.learnedIds }, header: { Authorization: Bearer getApp().globalData.token }, success: (res) { if (res.data.code 0) { // 写本地缓存并带上当天日期第二天自动失效 wx.setStorageSync(STORAGE_KEY, { date: today, done: true }); this.setData({ checkedIn: true, earnedPoints: res.data.earnedPoints }); } } }); }缓存 key 带了v1后缀以后改数据结构或接口路径时改个版本号就能让旧缓存自动失效。date字段用于区分是不是同一天第二天再进来时缓存命中条件失败会重新请求接口。这其实就是微信小程序设置缓存时间的常见做法不依赖 wx.setStorage 的物理过期而是把业务时间戳写进 value 自己判断。项目里 wx.request 建议统一封装到 utils/request.js自动带上 baseUrl 和 token统一处理code ! 0的错误避免每个页面重复写 header。3.3 激励中心徽章墙与排行榜激励中心用两个 Tab徽章墙和排行榜。徽章数据来自后端 achievement 列表页面只做展示和锁定态区分。view classbadge-grid view classbadge-item {{item.unlocked ? : locked}} wx:for{{badges}} wx:keybadge image src{{item.icon}}/image text{{item.name}}/text text classcond{{item.conditionText}}/text /view /view解锁态用查询接口返回的unlocked布尔值配合 WXSS 给.locked加灰色滤镜。排行榜在 onShow 里拉取切换日榜、周榜、总榜时传period参数给后端接口。我一般会在榜单顶部高亮当前用户那一行哪怕没进前三用户也能找到自己这对激励感的提升非常明显代码只是wx:if判断item.userId currentUserId。排行榜不分页后端直接返回前 50 名激励类榜单只有展示头部才有冲击力超过一屏用 scroll-view 滚动即可。4. 后端接口与微信登录从 code2session 到积分事务前端能不能拿到真实数据取决于三个接口登录、打卡、排行榜。登录解决你是谁打卡解决行为怎么记账排行榜解决激励往哪展示。4.1 登录接口wx.login 换 openid 与 token小程序端不能直接持有appid/secret必须用wx.login拿到的临时 code到后端换取 openid。code 有效期只有五分钟且只能用一次。后端用 Node.js 写只是演示方便用 Java/Spring Boot 实现时流程完全不变把 axios 换成 RestTemplate 或 OkHttp接口路径和参数保持一致前端不用动。// routes/auth.js const axios require(axios); const jwt require(jsonwebtoken); router.post(/api/login, async (req, res) { const { code } req.body; const { WX_APPID, WX_SECRET } process.env; // secret 只存在服务端 const url https://api.weixin.qq.com/sns/jscode2session; const qs { appid: WX_APPID, secret: WX_SECRET, js_code: code, grant_type: authorization_code }; const { data } await axios.get(url, { params: qs }); if (!data.openid) return res.status(401).json({ error: jscode2session failed }); let user await findUserByOpenid(data.openid); if (!user) user await createUser({ openid: data.openid }); const token jwt.sign( { userId: user.id, openid: data.openid }, process.env.JWT_SECRET, { expiresIn: 7d } ); res.json({ token, userId: user.id }); });登录成功后返回 JWT小程序端存到 Storage后续请求都带Authorization头。JWT 的好处是后端不需要维护 session 表七天过期时间在毕设里够用。findUserByOpenid和createUser是数据访问层封装实际项目放到 dao 目录单独维护。WX_SECRET必须放环境变量绝不能写进小程序代码或前端仓库。4.2 打卡接口唯一索引加事务把防重复做在后端打卡接口是整个系统最核心的接口要同时完成三件事写打卡记录、计算积分、写积分流水。这三件事必须在一个事务里任何一步失败都要整体回滚。// routes/learn.js router.post(/api/learn/checkin, requireAuth, async (req, res) { const { userId } req.user; const { wordIds } req.body || []; const today getServerDate(); // 用服务器日期不信任前端传的日期 const conn await mysql.createConnection(dbConfig); await conn.beginTransaction(); try { // 行锁 唯一索引兜底防止并发重复打卡 const existing await conn.query( SELECT id FROM check_in WHERE user_id ? AND checkin_date ? FOR UPDATE, [userId, today] ); if (existing.length 0) { await conn.rollback(); return res.json({ code: 1, msg: 今日已打卡 }); } // 本日有效学习数量按单词去重防止同一单词反复提交 const learned await conn.query( SELECT COUNT(DISTINCT word_id) AS cnt FROM learn_record WHERE user_id ? AND learn_date ?, [userId, today] ); const earn Math.min(learned[0].cnt * rewardRule.learnPerWord, rewardRule.learnDailyLimit); // 打卡记录 积分流水 用户积分累加同一事务 const insertResult await conn.query( INSERT INTO check_in (user_id, checkin_date, points) VALUES (?, ?, ?), [userId, today, earn] ); await conn.query( INSERT INTO points_log (user_id, change, reason, ref_id) VALUES (?, ?, ?, ?), [userId, earn, checkin, insertResult.insertId] ); await conn.query( UPDATE user SET total_points total_points ? WHERE id ?, [earn, userId] ); await conn.commit(); res.json({ code: 0, earnedPoints: earn }); } catch (e) { await conn.rollback(); res.status(500).json({ error: e.message }); } });FOR UPDATE是行锁两个请求同时进来时后一个会等前一个提交后再查此时前一个已插入记录后一个查出来就返回今日已打卡。COUNT(DISTINCT word_id)是防刷的第二道闸同一单词提交十次只算一次。积分上限learnDailyLimit来自规则配置接口里不写死。连续天数的计算没有放进上面代码因为主流程已经够长。实际可以抽成一个updateStreak(userId, today)函数在事务提交后调用查询用户最近一条打卡日期是昨天则连续天数加一是今天说明是重复请求不处理否则重置为一。提示打卡接口的重复提交防御不要只依赖前端按钮置灰。按唯一索引 行锁 服务端积分上限三层设计才能扛住并发请求和手动刷接口。4.3 排行榜接口先别急着上 Redis把 SQL 写对排行榜有三种区间日榜、周榜、总榜。毕设数据量在几千条时直接查points_log聚合完全够用Redis 是加分项但不必要。-- 日榜当日积分流水聚合 SELECT p.user_id, u.nickname, u.avatar, SUM(p.change) AS point FROM points_log p JOIN user u ON u.id p.user_id WHERE p.created_at CURDATE() GROUP BY p.user_id, u.nickname, u.avatar ORDER BY point DESC LIMIT 50;周榜把WHERE换成p.created_at DATE_SUB(CURDATE(), INTERVAL 7 DAY)总榜直接对user.total_points排序。这里有个易错点日榜周榜必须从points_log聚合不能查user.total_points因为后者是历史累计体现不了今天谁学得最多。当数据量到几万条而且接口被频繁查时再把聚合结果缓存 60 秒失效时间设在每天 0 点过后 30 秒避免当日榜单被整天缓存或者换 Redis ZSET 维护排名。答辩时主动说出什么时候该换 ZSET的判断依据比闷头写接口得分高。5. 毕设避坑实录微信登录、订阅消息与刷分的 5 个常见问题做这个题目最容易翻车的五个位置每一条都是现象、原因、解决的完整记录。前四条是微信小程序生态特有的平台约束平时写普通网页根本遇不到第五条是数据库层面的经典问题。真机调试时全程开着开发者工具自带的 Network 面板看请求状态很多问题一眼就能定位。5.1 真机预览登录失败appid 与 request 合法域名现象开发者工具里一切正常点真机预览后请求全部失败Network 面板显示url not in domain list登录按钮转圈后没有反应。原因开发工具调试模式下不校验合法域名选项只对当前工具会话生效手机上是真机环境所有 wx.request 的域名必须在小程序管理后台的服务器域名里配置过而且必须是 HTTPS。另一个常见原因是用了测试号 appid测试号的登录接口和正式 appid 行为不完全一致。解决注册小程序账号后在 mp.weixin.qq.com 后台把后端接口域名加入 request 合法域名。域名需要是已备案的 HTTPS 域名这一步要提前一周做。本地开发可以勾选不校验合法域名但提交体验版之前必须关掉再全量测一遍。预览和演示请用真机扫码模拟器不校验的部分平台行为真机会暴露。5.2 订阅消息收不到一次性订阅的限制现象用户点了允许订阅第二天服务端推送消息用户手机上什么都没收到。原因微信订阅消息是一次性的用户在 button 上点一次授权后端最多只能发一条模板消息。如果授权流程里只让用户点了一次而后端打算每天发提醒第二天就必然失败。模板关键词还要先在后台申请并通过审核模板 ID 对不上也会静默失败。解决在打卡成功、解锁徽章这类用户刚得到正反馈的时机引导用户再次点击订阅按钮每次授权对应一次发送配额。服务端把配额存进一张subscribe_quota表发送一条就消费一条。演示时不要承诺每天提醒改成完成七天打卡后收到学习报告这种单次触达既能展示订阅消息能力又避开频率限制。5.3 打卡积分被刷前端校验不可信现象测试人员连续点击打卡按钮或者拿到一个用户 token 后直接拼接口积分短时间内暴涨排行榜被刷穿。原因页面上的已打卡判断只存在于小程序端token 是明文存在 Storage 里的复制出来就能直接调接口。后端如果没做幂等同样请求发一百次就会加一百分。解决三层防御。第一层是check_in表唯一索引加FOR UPDATE行锁数据库层面保证一天一条第二层是learn_record的联合唯一索引同一单词同一天只能计入一次第三层是服务端积分上限learnDailyLimit即使前两层被绕过单日积分也有天花板。排查看积分流水时用GROUP BY reason看每个积分来源的分布能快速定位异常行为。5.4 连续天数计算错乱本地时间与跨天边界现象用户晚上 23:59 打卡第二天 00:01 再打卡连续天数要么断了要么没增加和页面显示的日期对不上。原因前端new Date()取的是手机本地时间用户改系统时间或时区异常都可能导致日期错误。另一个问题是后端计算连续天数时用了数据库的now()而没有用业务上的checkin_date做判断跨天瞬间的边界没处理。解决所有日期以服务器为准打卡接口里的getServerDate()返回后端当天日期前端只拿这个值做展示。连续天数判断统一写成最近一条打卡日期等于昨天则加一等于今天说明重复打卡否则重置为一。写完用一组跨天用例验证昨天没打卡、昨天已打卡、今天已打卡、连续七天打卡四种情况各跑一遍。5.5 排行榜接口越查越慢缺索引与聚合扫描现象用户量到几百、积分流水几千条后排行榜接口耗时从几十毫秒涨到一秒多首页打开明显卡顿。原因points_log表在user_id和created_at上没建索引日榜聚合WHERE created_at CURDATE()会全表扫描。更隐蔽的问题是查询里写了DATE(created_at) CURDATE()函数包裹字段会让索引失效。解决建表时保留KEY idx_user_time (user_id, created_at)查询范围直接写created_at CURDATE()这种能用索引的写法。日榜周榜加上 LIMIT 50 截断总榜直接读user.total_points。做到这一步几千条数据下毫秒级返回。如果用的云开发数据库也要在控制台给集合字段建索引否则免费配额下很容易查询超时。6. 答辩前把激励系统做成亮点验证数据闭环的三种做法毕设答辩最怕被问你怎么证明这个系统是有效的。我习惯提前做三件事让数据替系统说话。第一件事是跑对账脚本。用一条 SQL 验证每个用户的total_points与points_log累计值一致不一致就说明有积分凭空产生或被吞掉这是激励系统的黑匣子检查SELECT u.id, u.total_points, IFNULL(SUM(p.change), 0) AS log_sum FROM user u LEFT JOIN points_log p ON p.user_id u.id GROUP BY u.id HAVING u.total_points ! log_sum;第二件事是准备一条演示主线新用户扫码进入、微信登录、学十个单词、打卡拿积分、徽章解锁、排行榜上榜。演示前在库里预置三五个账号和几百条学习记录让排行榜看起来真实同时明确告诉老师哪些是构造数据、哪些是现场操作这是测试数据的标准做法不算造假。演示时每点一步切到数据库或服务端日志里指出对应记录的变化比反复点页面更有说服力。第三件事是拉一周的学习趋势SELECT checkin_date, COUNT(*) FROM check_in GROUP BY checkin_date。如果连续打卡人数随积分奖励上升说明激励规则起作用了如果曲线是平的甚至下降就解释为规则参数还需要调优这正好证明可配置规则这个设计的意义。我答辩前的习惯是导出一份points_log流水逐条确认每笔积分都有来源、能对到业务记录。激励系统最怕数据闭环对不上能经得起这笔对账就已经赢过大多数只堆页面的同类毕设。希望这些拆解对你做这个题目有帮助拿到高分。本文还有配套的精品资源点击获取