四六级这三个字大概是很多大学生又爱又恨的存在。我陆陆续续做过几个学习类项目,每次调研都能看到类似的诉求:想要一个“纯粹背单词”的小工具——打开就能背、有发音、有例句、能自动安排复习,不想被社区、课程、积分商城这些功能打扰。于是就有了这个基于Spring Boot、Vue和小程序技术栈的“四六级英语单词助手APP”项目。
这篇文章我打算完整复盘一遍,从需求拆解、技术选型、词库处理、记忆算法落地,到前后端联调和实际部署踩坑。无论你是准备拿它做毕业设计,还是想练一下全栈开发手感,又或者单纯好奇一个单词类产品背后的工程细节,都应该能从里面找到可以带走的东西。
1. 一个被高频提及的需求:四六级单词助手为什么值得自己造
1.1 现有工具的痛点在哪里
市面上并非没有背单词的工具,但我在试用了一圈之后,反而越来越确定“自己做一个”是有价值的。不少主流App把功能堆得很满:社区、打卡排行、课程推荐、积分商城,打开App要先划过好几屏才能进入单词列表。对大多数备考者来说,每天真正需要的其实只有三件事——知道今天要背哪些词、快速过一遍、把容易忘的按计划复习。这个核心闭环被大量冗余功能稀释之后,反而成了使用负担。
另一个痛点在于词库更新。四六级考纲词汇在历年考试中会有微调,但很多老牌App的词表多年不更新,例句和释义也停留在很旧的版本。我做过内容抓取和校对,知道这类数据维护的工作量被严重低估:同样一个单词,四级要考哪个释义、六级要考哪个释义,其实是不同的。如果不做考纲粒度上的区分,那只是一个“词典搬运工”,解决不了备考效率问题。
还有一点是学习节奏。很多工具默认用户应该“每天固定学20个新词”,然后通过签到、排行榜逼你坚持。但真实用户往往是一周里有几天特别忙,只能完成最低限度的复习。我更想做的是一个能弹性调整、把复习队列放在优先级最高位置的系统:哪怕今天只打开五分钟,也保证“到期的旧词先复习完”,绝不欠账。
1.2 项目的功能边界:不做大而全,只做“背得进、记得住”
想清楚痛点之后,功能边界就很好画了。这个单词助手的定位是“面向四六级考试词汇的轻量学习工具”,核心模块只有四个:词库浏览、每日学习计划、间隔复习、打卡统计。词库浏览让用户按四级/六级、高频/低频筛选单词,点击查看音标、释义、例句和发音;每日学习计划由后端根据用户历史数据生成,默认每天安排固定数量的新词和当天到期的复习词;间隔复习是整套系统的引擎,保证单词按照遗忘规律出现在该出现的时间点;打卡统计用一个日历视图展示学习记录,让人一眼看到自己的连贯性。
我特意砍掉了社交、排行、课程购买这些功能。理由很简单:一旦加入社交机制,产品方向就会从“帮用户提高记忆力”滑向“帮用户消耗时间”。本阶段的目标用户是备考学生,他们的注意力本身就是最稀缺的资源,任何抢注意力的设计都是在给核心目标帮倒忙。这个取舍在后面开发中省了大量事,UI复杂度、接口设计、数据统计维度都变得非常可控。
2. 技术选型的三方组合:Spring Boot管稳定,Vue管后台,小程序贴近用户
2.1 为什么选了这套组合而不是更“流行”的跨端方案
技术选型没有银弹,关键是契合团队熟悉度和项目形态。后端用Spring Boot,几乎是Java技术栈里最稳妥的选择:生态成熟、第三方库齐全,一个可执行jar包就能部署,和MySQL、Redis、JWT这些组件配合起来非常顺。管理后台用Vue 3配合组件库,主要是因为它上手快、文档全,在一个仓库里同时维护接口和页面很顺手。用户端用小程序原生开发,而不是再套一层跨端框架,背后有几个实际考量。
很多人会问:为什么不直接用uni-app或者Taro一套代码跑小程序加H5?我的经验是,跨端框架的抽象层会带来两个隐性成本:一是平台新能力出来时,你永远在等框架适配;二是调试链路变长,出问题要先判断是框架的坑还是自己代码的坑。这个项目里小程序端要做的是大量卡片交互、左滑右滑、音频播放,这些用原生组件实现最直接,资料也最齐全,没有为了“一套代码”去承担抽象成本的必要。Vue已经在后台承担管理界面职责了,前端侧没必要为了炫技再引入一套编译链。
后端模块划分上也做了务实取舍。整个服务是一个单体应用,按照controller、service、mapper、entity、common五层分包,没有一上来就拆微服务。我们这个体量下,微服务的注册中心、配置中心、网关全是额外运维负担;单体部署一个jar包就能跑,内部方法调用还能省掉一堆远程通信开销。留好接口边界,将来真要拆也拆得动。
2.2 整体架构与部署形态
系统的访问链路是这样的:小程序端和Vue管理后台都通过HTTPS请求Nginx,Nginx做反向代理转发到Spring Boot应用,应用读写MySQL和Redis,音频文件放在对象存储服务上。给个小表格看下每个环节的职责:
| 层次 | 技术 | 职责 |
|---|---|---|
| 终端 | 小程序原生 | 用户学习交互、打卡展示 |
| 终端 | Vue 3 + Element Plus | 词库管理、用户管理、数据统计 |
| 接入 | Nginx | HTTPS终结、反向代理、静态资源托管 |
| 应用 | Spring Boot | 业务逻辑、接口暴露、定时任务 |
| 缓存 | Redis | 登录态缓存、热点词库缓存 |
| 存储 | MySQL 8 | 业务数据持久化 |
这个架构没有复杂到需要画部署拓扑图,但每一层的边界都很清晰。Redis在这里不是摆设,我后来在做计划接口的时候发现,每次都要实时计算复习队列和统计数据,数据库压力很大;把当天计划和打卡摘要做成短时缓存后,接口响应从平均一两百毫秒降到几十毫秒,体验提升明显。
3. 词库是第一生产力:四六级数据的采集、清洗与表结构设计
3.1 词库来源与清洗流程
一个背单词产品,词库就是它的内容生命线。技术方案再漂亮,词库质量不行,用户两三天就会卸载。第一步是拿到四六级词汇表,网上流传的版本很多,但格式和准确性差异极大。我最后选择以公开的考纲词汇表为基础,配合多个开源词典数据源做交叉比对,再用脚本做统一的清洗工作。
清洗过程大概分四步。第一步去重和归一化,把同一单词的大小写、连字符、中英文空格问题处理掉。第二步是词性标注和义项裁剪,一个单词可能有十几个义项,但四级语境下只考其中两三个,这一步是人工耗时最多的,需要把“考试常考义项”和“生僻义项”区分开。第三步是例句抽取,原始语料里的句子往往过长,不适合卡片展示,我按“8到15个单词以内、包含当前释义、无超纲词过多”作为筛选规则,跑完再人工抽检。第四步是给每个词打频次标签,结合历年真题统计出高频、中频、低频。
这里想说句经验之谈:词库清洗一定要做版本管理。我第一次整理时直接在原文件上改,结果某个词条的例句抽错了,全量重跑要花几个小时。后来改成“原始数据导入后被清洗脚本处理后写入临时表,人工确认后再同步到正式表”,整个流程才可控。词库这种数据,出错不是技术问题,而是流程问题。
3.2 核心数据表设计与索引规划
数据库设计是整个项目的地基。单词表、例句表、用户表、学习记录表、复习队列表这几张表的关系,直接决定后续算法落地的顺畅程度。下面是几张核心表的简化结构:
-- 单词主表 CREATE TABLE words ( id BIGINT PRIMARY KEY AUTO_INCREMENT, word VARCHAR(64) NOT NULL, phonetic VARCHAR(128), meaning TEXT, level TINYINT NOT NULL COMMENT '0-四级 1-六级', frequency TINYINT DEFAULT 2 COMMENT '0-高频 1-中频 2-低频', tags VARCHAR(128), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_word_level (word, level) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 例句表 CREATE TABLE word_examples ( id BIGINT PRIMARY KEY AUTO_INCREMENT, word_id BIGINT NOT NULL, sentence VARCHAR(512) NOT NULL, translation VARCHAR(512), source VARCHAR(64), KEY idx_word_id (word_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户学习记录表 CREATE TABLE user_words ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, word_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0-学习中 1-已掌握', familiarity TINYINT DEFAULT 0, review_count INT DEFAULT 0, last_review_date DATE, UNIQUE KEY uk_user_word (user_id, word_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 复习队列表 CREATE TABLE review_queue ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, word_id BIGINT NOT NULL, due_date DATE NOT NULL, interval_days INT DEFAULT 1, UNIQUE KEY uk_user_word_due (user_id, word_id, due_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 每日学习汇总表 CREATE TABLE study_records ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, study_date DATE NOT NULL, new_count INT DEFAULT 0, review_count INT DEFAULT 0, correct_count INT DEFAULT 0, wrong_count INT DEFAULT 0, UNIQUE KEY uk_user_date (user_id, study_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;索引设计上,最关键的是user_words的联合唯一索引和review_queue的due_date索引。查询每日复习计划时,核心SQL只有一条:按用户查到所有due_date <= 今天的记录,再关联words表取出单词信息。这个查询频率极高,索引没设计好,后期数据量上来就会出现明显卡顿。例句表按word_id建索引就够了,不用过度设计。
词库规模上,我在这个项目里首批整理出四级核心词四千多个、六级核心词两千多个,去重之后加上例句差不多有三万条数据。这个体量对MySQL来说非常轻松,真正考验数据结构的反而是用户行为数据——每个用户每天都会产生多条学习记录,几年积累下来,统计查询一定要提前考虑按日期分表或汇总表预热。
4. 记忆引擎:把遗忘曲线真正落进代码
4.1 从艾宾浩斯到间隔重复,我的简化实现
本地上最忌讳的一件事,就是把“遗忘曲线”当成玄学挂在嘴边,却拿不出可执行的调度策略。艾宾浩斯的研究给出的是一个定性的结论:记忆会随时间推移衰减,复习可以减缓遗忘。但真实用户不是实验室被试,状态波动很大,我必须设计一个能在工程上落地、又能根据用户反馈动态调整的复习间隔机制。
我最终选择了“Leitner盒子 + SM-2思想”的折中方案。每个单词对应一个familiarity等级,范围从0到5。用户第一次学习某词时,等级为0,间隔1天;如果复习时反馈“认识”,等级加1,间隔翻倍;如果反馈“忘记”,等级减半并重置间隔到1天。连续复习到等级5之后,单词进入“已掌握”状态,退出日常复习队列,但会被每月滚动复习一次,防止长期不用造成遗忘。这个策略写起来不复杂,却能让系统表现出“每个人节奏不同”的个性化特征。
核心计算逻辑用Java实现大概是这样的:
public int calculateNextInterval(int familiarity, boolean correct) { if (!correct) { return 1; } // 等级越高,间隔越长 int[] intervals = {1, 1, 2, 4, 7, 15}; int nextLevel = Math.min(familiarity + 1, 5); return intervals[nextLevel]; }间隔数组看起来简单,背后的依据是:新学单词第一天必须复习,之后如果连续答对,间隔依次拉长到2天、4天、7天、15天。一旦答错就回到1天,让它重新进入短期记忆通道。这个机制比固定“第1、2、4、7、15天复习”的刻板排程要健壮得多,因为每位用户每次复习的真实反馈都被纳入了排程计算。
4.2 每日学习计划生成逻辑
每天的学习计划不是简单把20个新词丢给用户,而是分两个队列:复习队列优先、新词队列兜底。打开首页时,后端先查询当天到期的所有复习词,如果数量大于用户设置的上限,只取上限数量并提示剩余待复习数;如果没有到期的复习词,才开始分配新词。新词的选择也不是随便取20个,而是优先选择频次标签为“高频”且用户从未学过的词。
生成计划的伪代码逻辑如下:
1. 取出 review_queue 中 due_date <= today 且未完成的记录,按 due_date 升序 2. 如果复习词数量小于今日额定复习数,从 words 表取 level=目标等级 AND frequency=高频 AND id 不在 user_words 中的单词补齐 3. 组装计划时,复习词排在前、新词排在后 4. 把整个计划接口响应缓存在 Redis,key 为 user:plan:{userId}:{today}这里有个很容易忽略的细节:新词和复习词的“认识/模糊/忘记”反馈要分开处理。新词首次学习时,只有“认识”和“不认识”两种反馈;复习阶段才使用“认识/模糊/忘记”三档。模糊的含义是“好像见过但想不起来确切释义”,它不会像“忘记”那样把间隔清零,但会把当前间隔减半,给用户一个温和的降级机制。
4.3 学习卡片的交互与状态流转
学习卡片是整个小程序里交互最重的部分。正面展示单词、音标、发音按钮,用户听发音并回忆释义,点击翻面才能看到释义和例句,再在底部选择反馈按钮。这个流程刻意不让释义和单词同屏,因为一旦同屏出现,用户会产生“我认识这个词”的错觉,实际上只是看到了答案。
状态流转上,一个单词在整个生命周期里可能经历:未学习 → 学习中(新词首次) → 复习中(进入队列) → 已掌握(等级5)。用户手动点击收藏或加入错词本,会额外生成两个辅助列表:收藏表与错词表。错词本的学习逻辑和主队列独立,但同样会进入复习排程,只是权重更高——错词会在第二天优先出现,确保短期记忆被及时强化。
5. 后端接口的设计思路:前端少请求、服务端多干活
5.1 登录态与用户体系:小程序登录态怎么设计
小程序的登录和普通Web登录有本质区别。小程序平台通过授权接口拿到用户临时凭证,后端拿这个凭证向开放平台换取用户唯一标识。业务系统自己不需要设计注册流程,用户首次登录时自动建档,后续请求通过JWT保持会话状态。
我选择JWT而不是传统Session,主要考虑三点。第一,小程序请求天然无状态,JWT放在请求头里即可,服务端不用维护会话对象;第二,后续如果增加H5、App端,认证代码可以直接复用;第三,JWT的过期时间可以独立控制,配合Redis做服务端主动失效,解决“用户注销后token仍有效”的隐患。具体做法是登录成功后在Redis里存一个userId → tokenVersion,JWT中携带版本号,每次校验都检查版本号是否一致,不一致则视为失效。
用户体系不复杂,users表只保留了openid、昵称、头像、目标等级(四级还是六级)、每日新词数配置这几个字段。目标等级字段直接影响计划生成,比如用户选了“备战四级”,系统不会在计划中混入六级专属词。
5.2 核心接口一览
整个后端接口数量控制在二十个以内,按模块划分非常清晰。下面列出核心接口:
| 方法 | 路径 | 说明 | 主要参数 |
|---|---|---|---|
| POST | /api/auth/login | 登录换token | code |
| GET | /api/words/list | 词库分页查询 | level, frequency, page |
| GET | /api/words/{id} | 单词详情 | id |
| GET | /api/plan/today | 今日学习计划 | 无 |
| POST | /api/learn/submit | 提交新词学习结果 | wordId, isKnown |
| POST | /api/review/submit | 提交复习结果 | wordId, feedback |
| GET | /api/review/today | 今日复习队列 | 无 |
| GET | /api/stats/calendar | 打卡日历 | month |
| GET | /api/stats/overview | 学习概览 | 无 |
| POST | /api/user/config | 更新学习配置 | level, dailyNewCount |
接口设计有一条原则:前端尽量少请求,能一次拿全的数据不要拆成三次。拿“今日计划”来说,如果前端分别调计划接口、复习队列接口、用户配置接口,三个请求会有先后依赖,弱网环境下体验很差。我把这三个数据合并成一个/api/plan/today返回,一次性拿到用户配置、新词列表、复习列表。代价是后端接口要做更多聚合,但换来的是前端逻辑简单、用户等待时间短。
5.3 最需要小心的接口:提交学习结果的事务与幂等
提交学习结果这个接口是整个系统最容易出问题的点。一次提交里要更新user_words的状态和复习次数、往review_queue插入或更新下一条复习记录、累加study_records的统计值。这三个操作必须在一个事务里,否则用户刷新一次页面,可能出现词表更新了、复习队列没插入的中间状态。
@Transactional public void submitReview(Long userId, Long wordId, int feedback) { // 1. 查当前用户词状态 // 2. 更新 familiarity 和 review_count // 3. 计算下一次复习时间,更新 review_queue // 4. 更新 study_records 统计 }比事务更难处理的是幂等。用户在小程序上点击反馈按钮时,如果网络抖动,前端可能会重试一次请求,导致统计数据被累加两次。我的解决方法是给user_words表加一个last_feedback_date字段,同一天内同一用户对同一单词的重复提交会被直接忽略。这种业务幂等比纯粹在接口层做Token更简单有效,因为它天然贴合“每天只能记录一次”的业务规则。
6. 双端落地:小程序学习端与Vue后台管理的协作方式
6.1 小程序端:页面结构、组件拆分与交互细节
小程序端一共有五个主页面:首页、词库、学习卡片、统计、我的。首页展示今日计划和连续打卡天数,用了环形进度条组件;词库页在顶部做四级/六级Tab切换,底部是单词列表,支持搜索;学习卡片页是全屏沉浸式设计,底部三个反馈按钮;统计页展示打卡日历和整体学习数据;我的页放用户配置、错词本和收藏夹。
组件拆分的边界我花了点心思。word-card是核心组件,负责单词正面、背面翻转动画、音频播放;progress-ring负责首页的学习进度环;calendar-view负责统计页的打卡日历,支持按月的左右滑动。这三个组件都被多个页面复用,把交互细节收敛在组件内部,页面只传数据,逻辑清晰很多。
状态管理上没有引入重量级状态库,而是通过一个全局单例对象加本地存储完成登录态和用户配置的持久化。这个项目的信息流是典型的“页面级拉取+操作后更新”,不是多页面高频共享状态,引入状态管理框架反而增加心智负担。启动时只请求“今日计划”和“概览数据”,其他页面数据进入后再按需加载,保证首屏速度。
6.2 Vue管理后台:词库维护与学情统计
管理后台是用Vue 3配合Element Plus从零搭的。它承担三块职责:词库管理、用户管理、学情统计。词库管理支持单词的增删改查、批量导入、例句编辑;用户管理可以查看每个用户的注册时间、当前等级、累计学习天数、掌握词汇量,并支持禁用异常账号;学情统计页展示三张趋势图:每日新增用户、打卡率曲线、复习完成率。
后台接口有个容易忽视的问题:它和小程序端共用同一个后端服务,但权限必须分开。我的做法是在JWT里加入userType字段,后台登录走独立的账号密码登录接口,生成包含管理员标识的token;后端的拦截器对/api/admin/**路径强制校验管理员角色。这样小程序用户就算手工拼接后台接口地址,也会被统一的权限拦截挡在外面。
词库批量导入这个功能很值得多说一句。我一开始只做了单个新增表单,结果维护几千个单词时,后台操作员差点崩溃。后来加了Excel导入,后台读取文件后按模板解析,解析成功的数据先进入预览列表,人工确认后再写入正式表。要是解析出格式错误的行,会返回具体行号和错误原因,而不是整批失败。这套流程上线后,词库更新效率提升非常明显。
7. 实际上线前后踩过的坑:从数据并发到包体积
7.1 并发提交学习记录导致的数据错乱
上线后收到的第一个稳定复现的bug,反而是我自己在自测时偶然点出来的。快速连续点击“认识”按钮两次,第二次请求会把复习间隔覆盖回旧值。原因是两个请求同时读到旧记录,后提交的请求用旧数据计算出的间隔覆盖了前一个请求刚写入的新值。
排查链路是这样的:先在日志里打印两次请求参数和实际执行的SQL,发现两次提交的due_date都不同,但后一次结果把前一次覆盖掉了。继续定位到更新语句用的是UPDATE ... SET ... WHERE user_id=? AND word_id=?,没有带数据版本条件。修复方案是把SQL改成条件更新,只有当前间隔等于更新前的值时才执行:
UPDATE review_queue SET due_date = ?, interval_days = ? WHERE user_id = ? AND word_id = ? AND interval_days = ?再加一层业务幂等,同一用户同一天对同一单词只能首次提交生效。两端防护叠加之后,这个bug再没有出现过。踩完这个坑我的体会是:涉及状态变更的更新操作,哪怕业务上看起来是单用户操作,也要考虑并发可能,条件更新永远比先查再更新稳妥。
7.2 小程序包体积与首屏速度的权衡
小程序平台对包体积有严格限制,单体包超过一定体积就会上传失败。我的项目一开始把所有页面和静态资源都放在主包,结果词库页还没写完善就到临界值了。后来做了两件事:一是分包加载,把统计、管理相关的低频页面拆到分包,主包只保留首页、词库和学习卡片这三个高频页面;二是把内置的音频资源从本地文件改为远程地址,发音音频全部上传到对象存储,URL存入数据库。
首屏速度的优化也同样关键。最早的版本首页要同时请求计划、用户信息、打卡统计三个接口,数据返回后还要逐个解析渲染,白屏时间在弱网环境下能到两三秒。改成聚合接口之后,首屏只发一个请求,同时渲染骨架屏,整体体感好了很多。事实证明,小程序这类轻端应用,网络请求次数比单次请求体量更影响感知性能。
7.3 词库内容合规与特殊字符处理
词库上线前的审核比预想的麻烦得多。释义和例句来源混杂,有些包含特殊全角符号、引号嵌套、RTL字符,直接渲染到小程序页面上会出现排版错乱。我用脚本做了全文转义,统一成标准的中英文标点格式,再跑一遍敏感词过滤。例句内容也做了人工抽检,避免出现不适合学生群体阅读的内容。
这里提醒一下做同类项目的朋友:词典数据的版权和合规问题一定要提前想。虽然音标、释义这类基础语言事实不属于著作权保护范围,但如果你直接抓取某个商业词典网站的例句,发布时就有侵权风险。我最后的做法是从开源语料和自建例句库中筛选,标注来源,避免直接搬运商业产品的内容。
7.4 部署环境与数据备份
部署用的是单机Linux服务器,一个Spring Boot jar包加MySQL和Redis,Nginx负责HTTPS和反向代理。这个体量完全不需要容器化,jar包用 systemd 托管,开机自启,日志统一输出到指定目录做切割。MySQL每天凌晨做一次全量备份,保留最近7天;Redis定期开启快照,防止缓存雪崩后无后备数据。
定时任务方面,我用Spring的@Scheduled做了两个:一个在每天凌晨统计前一天的打卡数据和用户活跃数,写入统计表;另一个定期清理长期不活跃用户的学习缓存,避免内存占用持续上涨。定时任务要和主业务接口错峰执行,我踩过一次教训,定时任务和大流量时段重叠导致接口响应变慢,后来把执行时间调到凌晨四点,问题解决。
8. 项目复盘:这套东西还能往哪个方向延伸
整个项目从需求确认、词库整理、后端开发、小程序和后台联调,到部署上线,是一个完整的全栈闭环。现在回头复盘,我认为做得最正确的决定是先把词库和复习算法想清楚再动手写业务代码。代码结构有问题可以重构,算法的排程策略有问题却会影响所有用户的长期体验,内容质量和记忆引擎才是这个产品的天花板。
这套架构的扩展空间很大。记忆引擎可以继续优化,比如引入更成熟的SM-2变体,加入用户遗忘率统计,动态调整每日新词数量;如果用户连续几天提前完成复习计划,系统可以适当提高新词数量,反之则自动降低,实现真正的自适应学习。功能层面,四六级单词可以联动听力材料、阅读真题和写作模板,把单词卡片嵌入到真题语境中,学习效率会更高。
我做这个项目最大的体会是,背单词类工具的技术难度不在界面多华丽,而在于三件琐碎的事情:词库数据是否准确、复习调度是否合理、统计反馈是否及时。框架和组件库只是下限,数据、算法和细节才是上限。希望这篇复盘能给你一个清晰的技术地图,不只是停留在“Spring Boot加Vue跑通接口”的表层,而是真正把一个学习工具做深做扎实。