ARTICLE DETAIL

建站实战干货

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

SpringBoot英语知识网站毕设:MyBatis-Plus与Redis实战

2026/9/17 17:28:28 拓冰建站 浏览量
SpringBoot英语知识网站毕设:MyBatis-Plus与Redis实战 简介面向计算机相关专业毕业设计选题的学生与指导教师这份 JavaSpringBoot 英语知识应用网站论文文档提供了一套完整的网站开发方案与写作参考。文档围绕英语知识应用场景梳理了从可行性分析、需求整理到系统功能设计与数据库设计的全过程区分管理员与普通用户两类角色管理员可管理用户、在线学习、学习技巧、培训信息、培训报名、英语角、试卷、试题与考试等模块用户则能在线学习、查看培训信息、报名、留言并参与考试。技术层面采用 B/S 模式以 Spring Boot 与 MYSQL 为核心强调界面清晰、操作简单与功能齐全。资源共 1 个 doc 文件压缩包约 2.35MB已有 50 人学习。读者可将其作为毕设论文结构、系统分析章节、模块划分与数据库设计表达的参考资料也可借其摘要、目录与关键词组织思路辅助完成开题、撰写与答辩准备。1. 从一份英语知识应用网站的毕设文档说起每年三四月总有一批人拿着「毕业设计论文 Java SpringBoot 英语知识应用网站」这个题目开工然后卡在同一个地方词条、题库、学习记录这三类数据看着简单真动手才发现单词要不要拆释义表、错题本怎么和答题记录对齐、按难度分页时索引为什么没走。这个题目的技术骨架其实非常固定——Java 做后端语言SpringBoot 做 Web 框架MySQL 存业务数据Redis 扛背单词模块的高频读取前端用 Vue 前后端分离或者 Thymeleaf 服务端渲染都能收尾。它能解决的问题很具体把散落在 Excel 和纸质本子上的单词表、语法题、错题整理成一套带账号体系、能做每日任务、能看学习统计的小工具。适合正在做基于 SpringBoot 的 Java 毕设、需要一份能跑起来、能当场演示、答辩时讲得清楚的项目的人也适合想拿一个完整小工程练手 MyBatis-Plus 和 Redis 的后端新手。2. SpringBoot 英语知识网站的技术选型与工程骨架2.1 选 SpringBoot 而不是把 SpringMVC 老工程改造过来毕设周期通常只有两三个月时间要花在业务上而不是环境上。SpringMVC 那套需要自己写web.xml、注册DispatcherServlet、逐个对 jar 包版本改一个 Spring 版本就可能连带 MyBatis 报NoClassDefFoundError。SpringBoot 把依赖版本统一锁在父 POM 里内嵌 Tomcatmvn package出来是一个可以直接java -jar跑的胖 jar答辩现场不用预装 Tomcat插上 U 盘就能演示。SpringBootApplication拆开看是三个注解的合体SpringBootConfiguration声明配置类ComponentScan扫描当前包及子包EnableAutoConfiguration负责自动装配。自动装配的原理是启动时读取 classpath 下META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里登记的配置类再配合ConditionalOnClass、ConditionalOnMissingBean这类条件注解决定装不装。答辩被追问为什么引入 starter 就能用答这一句就够了。有一个坑必须提前说联网热词里常出现的「springboot 版本太高」说的就是 Spring Boot 3 这条线。Boot 3 要求 JDK 17 起步并且把javax.*全部换成了jakarta.*从旧教程抄javax.servlet.http.HttpServletRequest会直接编译不过。老机器还装着 JDK 8 的话要么升 JDK要么老老实实用 Boot 2.7 系列别一边用 JDK 8 一边拉 Boot 3。对比项传统 SpringMVC 工程SpringBoot 工程依赖管理手动指定每个 jar 版本父 POM 统一锁定Web 容器外置 Tomcat需部署 war内嵌 Tomcat直接跑 jar配置方式多个 XML 文件一个application.yml启动方式装容器 部署java -jar xxx.jar答辩演示依赖现场环境一条命令起服务2.2 用 IDEA 创建 SpringBoot 项目与依赖清单在 IDEA 里走File → New → Project → Spring InitializrJDK 选 17Type 选 Maven然后勾 Spring Web、MySQL Driver、Spring Data Redis、Lombok。MyBatis-Plus 在 Initializr 里没有需要手动加。生成后检查pom.xml重点看下面几项parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId !-- 示例版本建议直接用 Initializr 按你本机 JDK 给出的默认值 -- version3.2.5/version /parent properties java.version17/java.version /properties dependencies !-- Web 层内嵌 Tomcat SpringMVC Jackson -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-PlusBoot 3 必须用 boot3 专用 starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies参数说明mybatis-plus-spring-boot3-starter是给 Boot 3 用的坐标如果写成老的mybatis-plus-boot-starter启动时会因为javax.annotation找不到而失败这是从网上抄依赖最容易踩的一个坑。spring-boot-starter-validation用来做注册、登录、新增词条时的参数校验配合实体上的NotBlank、Size使用比在 Controller 里写一堆 if 干净得多。mysql-connector-j的scope设成runtime编译期不需要它运行时才加载驱动。Lombok 设optional是为了它只参与编译不打进最终 jar。2.3 英语知识网站的包结构怎么切毕设项目最怕所有类堆在一个包里答辩时老师翻代码第一眼看的就是结构。按职责分层领域对象单独放一个包方便后面接 MyBatis-Plus 的type-aliases-package包名职责典型类controller接收请求、参数校验、返回统一结果WordController、QuizControllerservice/service.impl业务逻辑、事务边界WordService、QuizServiceImplmapper数据访问接口WordMapper、AnswerRecordMapperdomain与数据库表对应的实体Word、Questiondto前端传入、后端返回的传输对象AnswerDTO、AnswerResultcommon统一返回体、异常、常量Result、BizExceptionconfig配置类MybatisPlusConfig、RedisConfigutils工具JwtUtil、PasswordUtil2.4 application.yml 里必须改的几项配置生成的application.properties建议直接删掉换成 yml层级更清楚。下面这几项是英语知识网站一定会用到的server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/english_knowledge?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root hikari: maximum-pool-size: 10 # 毕设量级 10 条连接足够别照抄 100 minimum-idle: 2 data: redis: host: 127.0.0.1 port: 6379 database: 0 timeout: 3000ms lettuce: pool: max-active: 8 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.english.domain configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0几处说明spring.data.redis是 Boot 2.7 之后的新路径老教程写的spring.redis已废弃抄错会连不上 Redis 但报错信息很含糊只说找不到连接工厂。serverTimezoneAsia/Shanghai不加插入时间会整体偏 8 小时学习记录的今日打卡判断就全乱了。logic-delete-field开启逻辑删除后所有selectById查询会自动带上deleted 0词条被删除后不参与出题但历史答题记录还能关联到它这比物理删除安全。log-impl打开 SQL 打印只用于开发交付前记得关掉否则控制台刷屏影响演示。3. 英语知识库的表结构设计与 MyBatis-Plus 落地3.1 词条、题库、学习记录三张核心表怎么拆先想清楚业务会发出哪些查询再决定表。英语知识网站最高频的动作是「按词书 难度翻页看词」和「随机抽一批词出题」其次是「提交答案写记录」和「查错题本」。据此拆成五张表就够word存词条本体word_category存词书四级核心、考研高频之类question存选择题answer_record存每次作答明细wrong_book存错题汇总。不建议把单词和释义拆成两张表。毕设里的词条只要求展示一条中文释义和一个例句拆表只会让查询多一次 join答辩时也解释不出拆分的必要性。只有一个字段确实撑不下时才拆——比如一个单词要挂多个词性、多个义项那才值得建word_sense。表名作用关键字段word词条本体word、meaning、difficulty、category_idword_category词书分类name、cover、word_countquestion题目stem、option_a~d、answer、analysisanswer_record作答明细user_id、question_id、user_answer、is_correctwrong_book错题本user_id、question_id、wrong_times3.2 建表 SQL 与索引选择CREATE DATABASE IF NOT EXISTS english_knowledge DEFAULT CHARSET utf8mb4; CREATE TABLE word ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, word VARCHAR(64) NOT NULL COMMENT 单词原型, phonetic VARCHAR(64) DEFAULT NULL COMMENT 音标, meaning VARCHAR(255) NOT NULL COMMENT 中文释义, example VARCHAR(512) DEFAULT NULL COMMENT 例句, difficulty TINYINT NOT NULL DEFAULT 1 COMMENT 1易 2中 3难, category_id BIGINT UNSIGNED DEFAULT NULL COMMENT 所属词书, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_word (word), KEY idx_cat_diff (category_id, difficulty) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT词条表; CREATE TABLE answer_record ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, question_id BIGINT UNSIGNED NOT NULL, user_answer VARCHAR(16) NOT NULL, is_correct TINYINT NOT NULL DEFAULT 0, cost_ms INT NOT NULL DEFAULT 0 COMMENT 作答耗时毫秒, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_time (user_id, create_time), KEY idx_question (question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT答题明细表;说明两点。uk_word唯一索引防的是导入词表时重复录入同一个单词出现两条出题时会连着考同一个词体验很差。idx_cat_diff是联合索引字段顺序不能反查询条件是category_id ? AND difficulty ?把区分度高的category_id放左边difficulty只有三个取值放右边这样EXPLAIN出来的type是ref而不是ALL。answer_record上建idx_user_time是为了支撑最近七天学习曲线这类按用户 时间范围的统计查询。3.3 MyBatis-Plus 实体、Mapper 与分页插件实体字段名用驼峰配合map-underscore-to-camel-case: true自动映射下划线列名Data TableName(word) public class Word { TableId(type IdType.AUTO) private Long id; private String word; private String phonetic; private String meaning; private String example; private Integer difficulty; private Long categoryId; TableLogic // 逻辑删除字段查询自动追加 deleted 0 private Integer deleted; }Mapper 只要继承BaseMapper就自带增删改查不用写一行 XMLpublic interface WordMapper extends BaseMapperWord { }分页插件必须手动注册否则Page对象查出来永远是全量数据Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor page new PaginationInnerInterceptor(DbType.MYSQL); page.setMaxLimit(100L); // 单页上限防止前端传 size99999 把库拖垮 page.setOverflow(false); // 页码超出总页数时不回到首页直接返回空 interceptor.addInnerInterceptor(page); return interceptor; } }setMaxLimit(100L)是必须加的防护。毕设演示时有人手滑把size传成几千一次查询几千条词条MySQL 直接卡住页面上转圈答辩现场很难看。setOverflow(false)决定页码越界的行为设成true会自动跳回第一页设成false返回空列表前端能显示没有更多了。3.4 词条分页与按难度筛选的接口实现Controller 只做参数接收和返回包装业务条件拼装放在 ServiceRestController RequestMapping(/api/word) public class WordController { Resource private WordService wordService; GetMapping(/page) public ResultIPageWord page( RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size, RequestParam(required false) Long categoryId, RequestParam(required false) Integer difficulty, RequestParam(required false) String keyword) { return Result.ok(wordService.pageQuery(current, size, categoryId, difficulty, keyword)); } }Service public class WordServiceImpl extends ServiceImplWordMapper, Word implements WordService { Override public IPageWord pageQuery(long current, long size, Long categoryId, Integer difficulty, String keyword) { LambdaQueryWrapperWord qw new LambdaQueryWrapper(); // 条件为 null 时不拼接该条件避免生成 category_id null 这种无效 SQL qw.eq(categoryId ! null, Word::getCategoryId, categoryId) .eq(difficulty ! null, Word::getDifficulty, difficulty) .like(StringUtils.hasText(keyword), Word::getWord, keyword) .orderByAsc(Word::getDifficulty) .orderByDesc(Word::getId); return this.page(new Page(current, size), qw); } }参数说明eq(boolean condition, ...)的第一个布尔参数是关键条件不成立时整段条件不拼进 SQL这样同一个接口能同时服务全部词条按词书筛选按难度筛选模糊搜索四种前端场景不用写四个方法。orderByAsc(Word::getDifficulty)让简单词排在前面符合学习顺序。模糊搜索用的是like走不了索引词表上千条以内感受不到量再大就该上全文检索这一点被问到时能说出来是加分项。4. 背单词与在线测验Redis 缓存和答题判分的具体实现4.1 哪些数据值得进 Redis不是所有查询都值得缓存。词条列表在一个用户连续背单词的十几分钟里会被反复请求而且几乎不变这一类放缓存收益最高。反例是答题记录每次都在写缓存了反而要处理一致性问题。判断标准很简单读多写少、能容忍短暂延迟、数据量不大。英语知识网站里适合进 Redis 的有三类一是按词书 难度聚合的词条列表二是首页的每日一词和平台统计数字总词条数、总题数三是登录态里的 token 黑名单。TTL 要加随机抖动避免同一时刻大量 key 一起过期把请求全打到 MySQL 上。提示本地开发环境 Redis 默认无密码且监听 6379装完先确认能不能连上再往下写代码redis-cli ping返回 PONG 才算通。4.2 RedisTemplate 的序列化配置默认的JdkSerializationRedisSerializer会把 key 写成带二进制前缀的乱码用redis-cli查的时候完全看不懂。换成字符串 key JSON valueConfiguration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object tpl new RedisTemplate(); tpl.setConnectionFactory(factory); StringRedisSerializer keySer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valSer new GenericJackson2JsonRedisSerializer(); tpl.setKeySerializer(keySer); tpl.setHashKeySerializer(keySer); tpl.setValueSerializer(valSer); tpl.setHashValueSerializer(valSer); tpl.afterPropertiesSet(); // 手动构建后必须调用否则序列化器不生效 return tpl; } }关键点在最后一行。手动new出来的RedisTemplate不会自动走afterPropertiesSet()漏掉它序列化配置就是不生效的现象是 key 一切正常、value 存进去变成乱码。GenericJackson2JsonRedisSerializer会在 JSON 里额外写入class字段用于反序列化还原类型这也意味着实体类必须有默认无参构造Lombok 的Data已经带上了。4.3 词书列表缓存的读写与过期策略Override SuppressWarnings(unchecked) public ListWord listByCategory(Long categoryId) { String key word:cat: categoryId; Object cached redisTemplate.opsForValue().get(key); if (cached ! null) { return (ListWord) cached; } ListWord list this.list(new LambdaQueryWrapperWord() .eq(Word::getCategoryId, categoryId) .orderByAsc(Word::getDifficulty)); // 基础 30 分钟 0~10 分钟随机打散过期时间防止缓存雪崩 long ttl 30 ThreadLocalRandom.current().nextInt(10); redisTemplate.opsForValue().set(key, list, ttl, TimeUnit.MINUTES); return list; }如果某个词书一条词都没有上面这段会每次请求都查库也就是常说的缓存穿透。处理办法是查不到时也写一个空集合进缓存TTL 设短一点比如两分钟if (list.isEmpty()) { redisTemplate.opsForValue().set(key, list, 2, TimeUnit.MINUTES); return list; }数据一致性靠改词条时删缓存保证。在新增、修改、删除词条的方法里加一行redisTemplate.delete(word:cat: categoryId)下次读请求自然重建。不要写成先更新库再更新缓存两个并发请求下容易把旧值写回去。4.4 答题判分、错题落库与统计刷新答题是这个项目唯一带写操作的业务事务边界要划清楚Override Transactional(rollbackFor Exception.class) public AnswerResult submit(Long userId, AnswerDTO dto) { Question q questionMapper.selectById(dto.getQuestionId()); if (q null) { throw new BizException(题目不存在); } boolean correct q.getAnswer().equalsIgnoreCase(dto.getAnswer().trim()); AnswerRecord record new AnswerRecord(); record.setUserId(userId); record.setQuestionId(dto.getQuestionId()); record.setUserAnswer(dto.getAnswer()); record.setIsCorrect(correct ? 1 : 0); record.setCostMs(dto.getCostMs()); answerRecordMapper.insert(record); if (!correct) { // 错题本用 upsert有记录就次数 1没有就插一条 wrongBookMapper.upsertWrong(userId, dto.getQuestionId()); } // 统计缓存直接删下次查询时重建避免在事务里同步更新缓存 redisTemplate.delete(stat:user: userId); return new AnswerResult(correct, q.getAnswer(), q.getAnalysis()); }rollbackFor Exception.class不能省。默认事务只对RuntimeException回滚如果业务里抛的是受检异常答题记录已经插进去了、错题本没更新数据就对不上。错题本的 upsert 需要自己写 SQLMySQL 语法如下INSERT INTO wrong_book (user_id, question_id, wrong_times, last_wrong_time) VALUES (#{userId}, #{questionId}, 1, NOW()) ON DUPLICATE KEY UPDATE wrong_times wrong_times 1, last_wrong_time NOW();这条语句依赖wrong_book表上(user_id, question_id)的唯一索引没有唯一索引ON DUPLICATE KEY UPDATE不会触发每次都会插新行错题本就会越来越长。缓存删除放在方法末尾、事务提交前存在极短的一致性窗口毕设量级完全可接受真要较真就用TransactionSynchronizationManager.registerSynchronization把删除动作挪到事务提交之后。5. 英语知识网站联调、打包与答辩演示的收尾细节前后端分离时跨域是最先撞上的问题。前端跑在 5173 端口后端 8080浏览器直接拦截。两种做法开发阶段在前端vite.config.js里配server.proxy把/api转发到 8080生产阶段在后端加全局 CORS 配置。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) // 用 patterns 而非 origins允许携带凭证时不能写 * .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }注意allowedOrigins(*)和allowCredentials(true)同时写会报错必须换成allowedOriginPatterns这个报错信息很不直观很多人会卡半天。打包上线走一条命令# 跳过测试打包产出的 jar 在 target 目录下 mvn clean package -DskipTests # 启动时用外置配置覆盖数据库密码不要把生产密码写死在 jar 里 java -jar target/english-knowledge-0.0.1-SNAPSHOT.jar --spring.datasource.password你的密码演示数据要提前造好别现场手输。写一个data.sql或用CommandLineRunner在启动时判断词条表为空就灌入两三百条词、五十道题这样换台机器也能一键还原演示环境。答题记录和错题本反而要清空免得统计页面显示一堆脏数据。答辩追问基本集中在三处。第一处Transactional加在同类内部调用的方法上不生效因为不走代理被问到就直接答自调用绕过了代理把方法拆到另一个 Service 里或者注入自身。第二处EXPLAIN里type是ALL说明没走索引常见原因是查询条件上加了函数或者最左前缀没对上可以先EXPLAIN再决定加不加索引。第三处自动装配为什么能生效答AutoConfiguration.imports加条件注解这套机制即可。最后一个实用技巧把词条的模糊搜索从like %关键词%改成like 关键词%前导百分号会让索引失效去掉前导百分号后uk_word就能被用上上千条词表的搜索响应会明显变快这个改动只需要一行代码。本文还有配套的精品资源点击获取