☰
SpringBoot+MySQL实战:手把手开发古诗词学习网站
2026/10/2 14:54:35 网站建设 项目流程

简介:基于Java SpringBoot与MySQL实现的古诗词学习网站完整项目源码,适合Java课程设计、毕业设计以及前后端分离开发练习。系统功能覆盖用户端与管理端:用户可按朝代、类别浏览诗词,进入详情页查看全文、注释与翻译,支持收藏、评论、分享上传,以及头部搜索栏检索诗词,还能修改个人资料并查看管理员通知;管理员则能对用户、诗词、诗人、朝代、类别、收藏、评论、通知和用户分享内容进行增删改查与审核。资源包共596个文件,压缩后约39.9MB,主要包含Java后端Controller源码、SQL数据库初始化脚本、前端HTML/CSS/JS页面、XML配置文件及项目运行素材,结构组织清晰,便于直接导入集成开发环境运行,也方便按业务需求扩展改造。目前已有951人学习,适合以此为基础快速完成古诗词主题的课程设计演示或二次开发。

1. 古诗词学习网站:SpringBoot+MySQL这个组合能解决什么问题

很多刚过完Java基础、想找一个完整课程设计的同学,最后都会落到“Java课程设计案例源码”这个搜索词上。古诗词学习网站是个很典型的题目:数据量不大但关系清楚,功能覆盖搜索、分类、详情、收藏,刚好能把SpringBoot的Controller、Service、Mapper三层和MySQL的表设计、索引、分页全部练一遍。这个标题里的“基于Java(SpringBoot)+MySQL”,不是花架子,它意味着你交付的是一个能启动、能录入数据、能对外提供诗词检索接口的Web项目,而不是一个只跑通控制台的练习。适合三类人:做毕设或课设的学生、想快速搭一个垂直内容站点的开发者、以及准备SpringBoot面试时需要一个完整项目来描述的求职者。下文我会把从建表到部署的路径完整拆开,新手能照着复现,熟手可以直接抄其中几个关键配置。

2. 先定表结构:古诗词数据模型、索引和数据库连接参数

2.1 为什么古诗词网站选SpringBoot+MySQL而不是其他栈

先回答一个很多人犹豫过的问题:古诗词网站用SpringBoot+MySQL是不是“过时”了?换成Node+MongoDB,或者直接搞一个静态站点生成器,不是更轻量吗?这个质疑有一定道理,但换到学习项目和中小型内容站场景,SpringBoot+MySQL反而是最稳的。

古诗词数据有明确的关系结构:一首诗必然属于一位作者,一个作者属于一个朝代,一首诗可以被打上多个标签,一个用户可以收藏多首诗。这种数据天然适合关系型数据库。MySQL对关系查询、事务、索引、全文检索的支持都很成熟,而且社区资料多,踩到什么坑都能搜到答案。SpringBoot这边,最大的价值不是性能,而是“约定大于配置”带来的开发节奏:你只需要写好实体类、Mapper接口和Service,剩下的自动装配、依赖注入、内嵌Tomcat、健康检查都帮你处理好了。对一个需要在一两周内出成果的课设或毕设来说,这个组合能把时间花在业务功能上,而不是折腾环境。

也有人说,那用Spring Boot Vue 前后端分离不是更时髦吗?时髦不等于省事。前后端分离意味着你要同时维护两套工程、处理跨域、考虑接口鉴权,这些对学习项目是额外负担。我见过很多同学在分离架构上花了一周联调,结果诗词查询接口还没写完。所以我一般建议:除非你的课题明确要求前后端分离,否则用Thymeleaf模板做页面,一套工程搞定,先把业务跑通。

2.2 建表SQL:作者表、诗词表、收藏表和用户表

数据模型是整个项目的底盘。我给你的建议是最多五张表:用户表、作者表、诗词表、收藏表、分类标签表。不要一开始就设计十几张表,诗词数据的关系没那么复杂,表多了只会让联表查询越来越难维护。

先看作者表和诗词表,这是核心。下面的SQL可以直接跑在MySQL 5.7或8.0上:

-- 作者表:存放姓名、朝代、简介 CREATE TABLE author ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '作者ID', name VARCHAR(50) NOT NULL COMMENT '作者姓名', dynasty VARCHAR(20) NOT NULL COMMENT '朝代', birth_year SMALLINT DEFAULT NULL COMMENT '出生年份,存四位数,如 701', death_year SMALLINT DEFAULT NULL COMMENT '去世年份', intro TEXT COMMENT '作者简介', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作者表'; -- 诗词表:核心业务表 CREATE TABLE poem ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '诗词ID', title VARCHAR(100) NOT NULL COMMENT '标题', author_id INT UNSIGNED NOT NULL COMMENT '作者ID,关联author.id', content TEXT NOT NULL COMMENT '正文', translation TEXT COMMENT '译文', appreciation TEXT COMMENT '赏析', tags VARCHAR(255) DEFAULT '' COMMENT '标签,逗号分隔,如:思乡,送别', view_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '浏览量', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1显示,0隐藏', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', KEY idx_author_id (author_id), KEY idx_view_count (view_count), FULLTEXT KEY ft_content (title, content) WITH PARSER ngram ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='诗词表';

这段SQL里有几个值得注意的参数。作者表的年代字段我用SMALLINT而不是VARCHAR,是为了以后能做“检索某朝代的诗人”这种范围查询;LATIN1或者utf8在建表时千万别用,古诗词里大量生僻字、注音符号,必须用utf8mb4,配utf8mb4_unicode_ci这种排序规则。诗词表里我显式加了idx_author_id,因为列表页按作者查诗是很高频的操作,不加索引的话,数据量过万之后就会明显变慢。

收藏表往往是课设里被忽略的一张表,但它关系到多对多关系能不能讲清楚。一个用户可以收藏很多首诗,一首诗可以被很多用户收藏,这就是典型的多对多,需要一张中间表来拆:

CREATE TABLE favorite ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL COMMENT '用户ID', poem_id INT UNSIGNED NOT NULL COMMENT '诗词ID', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', UNIQUE KEY uk_user_poem (user_id, poem_id), KEY idx_poem_id (poem_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表';

UNIQUE KEY uk_user_poem是必要的,它能防止同一个用户对同一首诗产生重复收藏记录;同时这个唯一索引也会作为查询用户收藏列表的底层加速。如果你还想要“当前用户是否已收藏”这种标记,只要查这张表就行,不需要额外字段。

2.3 检索字段设计:全文索引、LIKE模糊查询和分词

古诗词网站最常用的功能就是搜索:按标题搜、按内容搜、按作者搜。多数课设代码会直接写LIKE '%关键词%',这样写没毛病,数据只有几百条时感受不到差异,但一旦导入几千首全唐诗,每次搜索都要全表扫描,MySQL的CPU占用会明显升高。

更好的方案是MySQL 5.7开始支持的ngram全文索引,专门解决中文分词问题。我上面建表SQL里写的FULLTEXT KEY ft_content (title, content) WITH PARSER ngram,就是为标题和正文建立中文全文索引。查询时这样写:

SELECT id, title, author_id, view_count FROM poem WHERE MATCH(title, content) AGAINST('春江花月夜' IN NATURAL LANGUAGE MODE) LIMIT 20;

ngram解析器默认分词粒度是2,也就是会把“春江花月夜”切成“春江”“江花”“花月”“月夜”这些二元组。对古诗词这种短文本来说,2元切分基本够用。如果你发现长词匹配效果不好,可以调整全局参数ngram_token_size,但要注意这需要重启MySQL并重建索引,我建议先用默认值。

那是不是说LIKE就没用了?不是。LIKE模糊查询适合“作者名搜索”这种场景,因为作者名字大多是两个字,全文索引对单字和双字匹配反而没有优势。我实际项目里的策略是:搜作者名走LIKE,因为走的是author.name LIKE CONCAT('%',#{name},'%'),配合数据量小,性能没问题;搜诗词标题和正文走全文索引。你甚至可以两条路都保留,界面上做一个下拉选择搜索范围。这里要提醒一句:全文索引和LIKE的结果集排序规则完全不同,全文索引会按相关度排序,LIKE只能按你指定的字段排序,这个差异在需求分析时要提前确认。

2.4 数据源配置:连接池参数和utf8mb4缺一不可

表结构定了,接下来是SpringBoot连MySQL的配置。这块出问题最多的地方,不是代码逻辑,而是配置文件里那些“玄学参数”。下面这个application.yml片段是我在课设项目里常用的基础配置:

spring: datasource: url: jdbc:mysql://localhost:3306/poetry?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 15 idle-timeout: 300000 connection-timeout: 30000

先说字符集。URL里的characterEncoding=utf8mb4,要和数据库表、客户端连接三方保持一致。注意,MySQL驱动8.0版本里,你写成characterEncoding=utf8mb4没问题,但老驱动只认utf8,如果你用的驱动是5.x,这里写成characterEncoding=utf8即可,否则会启动报错。

再说allowPublicKeyRetrieval=true。这个参数是给MySQL 8.0的caching_sha2_password认证插件准备的。MySQL 8.0默认认证方式改了,有些版本下JDBC第一次连接会报Public Key Retrieval is not allowed,加上这个参数就是允许客户端向服务器取公钥加密密码。测试环境加它没问题,生产环境建议用sha256_password或把参数关掉,这里不过度展开。

连接池我用的HikariCP,它是SpringBoot默认的,不需要额外引入依赖。最大连接数15、最小空闲5对一个诗词学习网站足够。经常有人问“MySQL的数据库连接池到底设多大”,回答是:不要照抄网上的200、300,你这种项目扛的并发量不大,连接池开太大反而浪费MySQL的资源,一般最大不超过20。最后,idle-timeout和connection-timeout这两个超时参数直接关系到一个经典故障:晚上挂机第二天早上打开网站第一次请求特别慢,甚至报连接超时。原因就是池里的空闲连接被MySQL踢断了,但连接池不知道。解决方法是把idle-timeout设得比MySQL的wait_timeout短,或者加一个连接存活检查,HikariCP会自动做,但你至少要把参数配置对。

3. 后端实现:搜索诗词、作者详情和收藏功能的SpringBoot代码

3.1 工程结构、Maven依赖和MyBatis分页插件

拿到这个项目标题,你第一件事是建一个标准SpringBoot工程。包名我习惯用com.poetry,内部结构分成controller、service、mapper、entity、common五层。这个分层不是硬性规定,但课设答辩时老师问到“为什么分层”,你能说出“Controller负责参数校验,Service负责业务逻辑,Mapper负责SQL操作”就及格了。

pom.xml里的关键依赖下面这个够用:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

这里有个版本搭配的坑,我专门在第5章展开,但现在先给你结论:如果你的JDK是8,SpringBoot老老实实用2.7.x,不要追新上3.x。MyBatis官方启动器mybatis-spring-boot-starter的2.3.x版本对SpringBoot 2.x支持最好。PageHelper的分页插件在这个组合里是顺滑的,等下看代码就知道。

3.2 诗词搜索接口:从Mapper到Service再到Controller

搜索接口是网站的门面。我先写Mapper层的XML,SpringBoot里既可以用注解写SQL,也可以用XML,我建议用XML,因为SQL一长,注解里拼接字符串很难维护。

<!-- PoemMapper.xml --> <mapper namespace="com.poetry.mapper.PoemMapper"> <!-- 搜索结果包含作者名 --> <select id="searchPoems" resultType="com.poetry.entity.PoemVO"> SELECT p.id, p.title, p.content, p.view_count, a.name AS authorName, a.dynasty FROM poem p LEFT JOIN author a ON p.author_id = a.id <where> <if test="keyword != null and keyword != ''"> AND (p.title LIKE CONCAT('%', #{keyword}, '%') OR p.content LIKE CONCAT('%', #{keyword}, '%') OR a.name LIKE CONCAT('%', #{keyword}, '%')) </if> AND p.status = 1 </where> ORDER BY p.view_count DESC, p.id DESC </select> </mapper>

这段SQL我先说逻辑:用LEFT JOIN把作者表的名和朝代带出来,这样前端列表页不用再查一次作者表。<where>标签是MyBatis的动态SQL,只有keyword非空时才拼接LIKE条件。排序按浏览量倒序,这能让高人气诗词排在前面,体验上比按ID排序舒服。

接着是Service层。Service不做SQL拼接,它只负责补充分页和组装结果:

@Service public class PoemService { @Autowired private PoemMapper poemMapper; public PageInfo<PoemVO> search(String keyword, int pageNum, int pageSize) { // PageHelper.startPage 必须在 Mapper 查询之前调用 PageHelper.startPage(pageNum, pageSize); List<PoemVO> list = poemMapper.searchPoems(keyword); return new PageInfo<>(list); } }

PageHelper的使用要点就一句话:PageHelper.startPage(pageNum, pageSize)必须放在Mapper方法调用之前,而且中间不能夹其他的SQL操作,否则分页会作用到错误的查询上。很多人第一次用MyBatis的分页插件,把startPage写在Service方法第一行,结果前面还有一个查询用户信息的SQL,分页就加到那个SQL上去了。新版PageHelper会通过PageInterceptor拦截,但拦截的是它后遇到的第一个查询,顺序不能乱。

Controller层负责参数接收:

@RestController @RequestMapping("/api/poem") public class PoemController { @Autowired private PoemService poemService; @GetMapping("/search") public Result<PageInfo<PoemVO>> search( @RequestParam(defaultValue = "") String keyword, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return Result.success(poemService.search(keyword, page, size)); } }

Result是一个统一响应体的包装类,里面包含code、message、data三个字段。这个类在课设里几乎是标配,别嫌它多余——没有统一返回结构,前端处理异常时每个接口都要写一遍判空逻辑,有它之后至少能保证“接口挂了也能返回一个JSON对象”而不是直接抛字符串。@RequestParam(defaultValue = "1")这行是给页码设默认值的,用户不传page也能正常访问,避免怪异的空指针问题。

3.3 诗词详情和作者详情:列表联表一次,详情再拆两次

搜索列表我们已经联表了,那点击某首诗进详情页,要不要继续用JOIN?我的答案是不用。详情页除了诗本身,还要展示作者的完整生平、同作者的其它作品,甚至相关推荐。这些数据如果全塞进一个大JOIN,SQL会变得很长,而且会查出很多重复字段。

我一般这样做:先按poem_id查出诗词本身,带上作者ID和作者名:

@GetMapping("/detail/{id}") public String detail(@PathVariable Integer id, Model model) { PoemVO poem = poemMapper.getPoemById(id); Author author = authorMapper.getAuthorById(poem.getAuthorId()); List<Poem> sameAuthorPoems = poemMapper.listByAuthorId(poem.getAuthorId(), 5); model.addAttribute("poem", poem); model.addAttribute("author", author); model.addAttribute("sameAuthorPoems", sameAuthorPoems); return "poem/detail"; }

三次查询,各干各的事。getPoemById只查诗词主表,顺便把浏览量加一,可以用一条UPDATE poem SET view_count = view_count + 1 WHERE id = #{id}完成,避免读出再写回的竞态问题。listByAuthorId用来做“本作者更多作品”推荐,LIMIT 5就够了。这样做的理由很实际:SQL可读性强,而且缓存好做。如果将来想去掉数据库压力,你可以在Service层给getPoemById加本地缓存,一次查询一个键,方便命中。

3.4 收藏功能:先判重再插入,用@Transactional避免脏数据

收藏功能关系到MySQL事务,是面试时能拿出来讲的点。接口逻辑很直白:用户点击收藏,后端检查该用户是否已收藏过,没有就插入一条记录,并给一个收藏成功的标志。

@Service public class FavoriteService { @Autowired private FavoriteMapper favoriteMapper; @Transactional(rollbackFor = Exception.class) public boolean favorite(Integer userId, Integer poemId) { int count = favoriteMapper.countByUserAndPoem(userId, poemId); if (count > 0) { return false; // 已收藏,直接返回 } int rows = favoriteMapper.insert(userId, poemId); return rows > 0; } }

@Transactional在这里作用很大:假设以后你还要维护一个“用户的收藏数量”字段,那么插入收藏表和更新用户收藏计数这两个操作必须是一个原子操作,要么全成功,要么全回滚。rollbackFor = Exception.class的意思是任何异常都回滚,包括自定义的业务异常和RuntimeException,不加这个参数的话,只有RuntimeException才回滚,检查异常不会触发回滚,这是新手最容易误解的一点。

注意,我上面这种“先查count再insert”的写法在并发下有可能重复插入,稳妥的做法是直接用数据库层的唯一索引兜底,也就是第2章建表时加的UNIQUE KEY uk_user_poem,配合INSERT IGNORE INTO或ON DUPLICATE KEY UPDATE,从源头杜绝重复。

4. 把网站跑起来:页面模板、JDK环境变量和MySQL启动步骤

4.1 前后端一体还是分离?学习项目选Thymeleaf更省事

标题里只写了Java(SpringBoot)+MySQL,没有提前端框架,说明这个项目的前端可以很自由。我的建议是:如果你不是为了练Vue,就老老实实用Thymeleaf做服务端渲染。理由有三个。

第一,工程简单。Thymeleaf模板直接放在src/main/resources/templates下面,不需要额外启动一个Node服务,不需要解决跨域,也没有axios配置这一层。启动SpringBoot就是启动整个网站,这对新手来说少了一整条故障链路。第二,数据传递自然。Controller里往Model塞什么,页面上就能直接用${poem.title}取出来,不会有JSON序列化和字段名对不上的问题。第三,SEO友好。古诗词网站的流量主要靠搜索引擎,服务端渲染的页面,文章内容直接在HTML源码里,爬虫抓得到。

当然,如果你课题要求是前后端分离,那就用REST接口配合Vue或React,这个不冲突。第3章我写的是@RestController,已经把接口层做好了;第4章的Thymeleaf是为一体化部署准备的,两者可以共存,只改一个注解的事。

4.2 首页、列表页、知识卡片用模板引擎怎么写

Thymeleaf模板的核心是“在HTML标签里写表达式”。以列表页为例,搜索结果的每一行诗词用th:each循环渲染:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>搜索结果</title> </head> <body> <div class="poem-list"> <div th:each="poem : ${pageInfo.list}" class="poem-card"> <h3 th:text="${poem.title}">标题</h3> <p class="author" th:text="${poem.authorName} + ' · ' + ${poem.dynasty}">作者</p> <p class="content" th:text="${poem.content}">正文</p> <a th:href="@{/api/poem/detail/{id}(id=${poem.id})}">查看赏析</a> </div> </div> </body> </html>

th:text会自动转义HTML字符,对内容安全是有好处的;th:href和@{...}结合可以拼出带路径参数的URL,比字符串拼接优雅得多。这里接的是第3章里/api/poem/detail/{id}这个路径。注意,如果你用的是@RestController,返回的是JSON,页面跳转要用@Controller,或单独加一个页面Controller。很多新手栽在这:两个注解只能返回到Thymeleaf模板,@RestController返回的是字符串本身,不是页面。

页面里还有一个隐藏技巧:用th:if="${pageInfo.hasPreviousPage}"来控制“上一页”“下一页”按钮是否显示。PageInfo对象已经封装了这些状态,直接复用即可,没必要自己在Controller里算当前页是不是第一页。

4.3 本地从零启动:装JDK、装MySQL、导入SQL、跑SpringBoot

这一步我按顺序给你,中间任何一步失败都按这个顺序排错。

# 1. 检查Java环境,java环境变量配置不对时,下面命令会报 command not found java -version # 2. 配置JAVA_HOME(macOS/Linux示例,Windows在系统环境变量里设置) export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home export PATH=$JAVA_HOME/bin:$PATH # 3. 启动本机MySQL(macOS通过brew安装的服务) brew services start mysql # 或者直接执行mysqld_safe;Linux则用 systemctl start mysqld # 4. 连接MySQL并建立数据库 mysql -uroot -p CREATE DATABASE poetry DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE poetry; SOURCE /path/to/init.sql; # 5. 回到SpringBoot项目根目录,执行Maven启动 mvn spring-boot:run

每条命令背后都有坑。第1步如果提示java not found,十有八九是JAVA_HOME环境变量没配,或者配了没生效,重启终端试试。第3步在macOS上最容易报Can't connect to local MySQL server through socket '/tmp/mysql.sock',这是MySQL服务没起来,而不是密码错,别急着改配置。第4步导入SQL文件时,文件路径不要有中文和空格,否则SOURCE命令会读不到。第5步用mvn spring-boot:run会占用当前终端,想后台运行就打包成Jar:

mvn clean package -DskipTests java -jar target/poetry-0.0.1-SNAPSHOT.jar

如果打包慢,先确认Maven镜像用的是国内仓库,不然下载依赖能卡到你怀疑人生。

5. 避坑记录:MySQL连接、中文乱码、版本冲突和分页失效

5.1 MySQL连接报错error 2002:服务没起来,不是密码问题

现象:启动SpringBoot时,控制台报error 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。

原因:这个报错的意思是客户端尝试通过Unix套接字连接MySQL,但套接字文件不存在。最常见原因是MySQL服务根本没启动;其次是MySQL装在Docker容器里或自定义了socket路径,和客户端默认的/tmp/mysql.sock不一致。

解决:先去服务管理里确认MySQL运行状态,macOS用brew services list,Linux用systemctl status mysqld。如果服务确实没起来,启动它;如果起来了还报错,在JDBC URL里把localhost改成127.0.0.1,强制走TCP连接而不是Unix套接字,这招能绕开大部分socket路径问题。

5.2 古诗词中文乱码:数据库字符集、JDBC参数、页面编码三层一起查

现象:页面出现“鍝ュ攱”“????”之类的乱码,或者数据库中诗文内容正确、查询出来却是问号。

原因:三层字符集不一致。第一层是数据库表没设成utf8mb4,默认用了latin1;第二层是JDBC URL没带characterEncoding=utf8mb4;第三层是HTML页面没声明UTF-8,浏览器按系统默认编码解析。

解决:一条路走通。建库时就用CREATE DATABASE poetry DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,建表时也显式写上字符集;JDBC URL里加上useUnicode=true&characterEncoding=utf8mb4;Thymeleaf模板<head>里加<meta charset="UTF-8">。如果导入已有SQL文件后数据乱码,别在代码里折腾,先把库里数据清掉重建再导入,因为错误字符集写入的数据已经是坏数据了。顺便提一句,MySQL 8.0客户端默认字符集是utf8mb4,5.7老版本可能需要你在mysql命令行里先执行SET NAMES utf8mb4;。

5.3 SpringBoot版本太高引发的连锁不兼容

现象:用SpringBoot 3.2.x创建项目,启动时报ClassNotFound: javax.servlet.Filter,或者mybatis-spring-boot-starter和pagehelper-spring-boot-starter大量冲突。

原因:SpringBoot 3.x从javax包迁移到了jakarta包,并且最低要求JDK 17。如果你本机还装着JDK 8,跑3.x项目编译阶段就会报错。很多农历课设教程是2020年左右写的,依赖版本是基于SpringBoot 2.x,你直接套到3.x上,自然翻车。

解决:做课设首选SpringBoot 2.7.x配JDK 8,这个组合最成熟。如果非要上3.x,那把JDK换成17及以上,同时MyBatis启动器换成mybatis-spring-boot-starter的3.0.x版本,PageHelper换成pagehelper-spring-boot-starter的2.0.x版本。注意,升级版本不是只改parent版本号就完事,所有用到javax.servlet的代码都要改成jakarta.servlet。我建议别在这上面浪费时间,2.7已经足够你答辩了。

5.4 PageHelper分页失效:startPage作用到了错误的查询上

现象:点击搜索第二页,返回的数据还是第一页的内容,或者LIMIT加到了author表查询上,导致数据错乱。

原因:PageHelper拦截的是它后面第一条SQL。如果你在同一个Service方法里先调用了authorMapper.selectById(userId),再调用poemMapper.searchPoems(),分页插件会作用在第一个查询上。还有一种情况是Mapper方法返回类型不是List,而是PageInfo,插件会报类型错误。

解决:把startPage紧贴在目标查询前面,中间不要插入任何其他Mapper调用。如果用了@Transactional,要注意分页查询在事务内执行和事务外执行,结果是一样的,但查询顺序不要被绕过。还有一个低频坑:MyBatis的二级缓存开启后,分页查询如果命中了缓存,LIMIT不会生效,这就需要关闭该Mapper的二级缓存,或者在Cache配置上加上readOnly属性。

5.5 MySQL 8.0的认证插件导致连接失败

现象:本地用Navicat能连,SpringBoot启动却报Unable to load authentication plugin 'caching_sha2_password'或Public Key Retrieval is not allowed。

原因:MySQL 8.0默认新用户的认证插件是caching_sha2_password,一些老驱动或非标准客户端不支持该插件。

解决:在JDBC URL加allowPublicKeyRetrieval=true&useSSL=false,这是最省事的办法。更底层一点的方案是登录MySQL执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';,把该用户的认证方式改回mysql_native_password。如果你是在生产环境,我不建议改认证方式,而是升级驱动版本到8.0以上。

6. 进阶:给诗词搜索加分词、缓存和一组验收用例

6.1 把LIKE搜索升级成HanLP分词搜索

当诗词库到了几万首,用户输入“春江花月夜”这种长句,LIKE模糊匹配其实也能搜出来,但搜索“月”这种单字,结果集大到没法看。这时候可以在Service层接入HanLP分词,把一句话拆成多个词,再分别匹配。做法不复杂,引入HanLP的依赖后,写一个分词工具类:

public List<String> segment(String keyword) { // 使用HanLP标准分词,停用词过滤 List<Term> termList = HanLP.segment(keyword); return termList.stream() .map(term -> term.word) .filter(word -> word.length() > 1) .collect(Collectors.toList()); }

前端搜索“思乡明月夜”,后端先分词得到“思乡”“明月”几个词,然后组装成动态SQL的多个LIKE条件,排序时给完全命中的结果加权。这比单条LIKE更合理,也让你的项目在答辩时多一个技术亮点。注意HanLP的词典默认是面向现代汉语的,对“浔阳江头夜送客”这种句子可能切得不够准,测试阶段要人工核对Top结果。

6.2 热点诗词缓存:先上本地缓存,再考虑Redis

每次刷新首页都查一次数据库,是不划算的。我先用Spring Cache搭配一个本地ConcurrentHashMap做缓存:

@Cacheable(value = "poemDetail", key = "#id") public PoemVO getPoemDetail(Integer id) { return poemMapper.getPoemById(id); }

加好之后,同一个ID的详情查询,在缓存有效期内不会再穿透到数据库。对这个项目来说,Redis不是必需品,等真出现并发瓶颈再加。很多课设最大的毛病不是没用Redis,而是为了写Redis而写Redis,最后缓存和数据库数据对不上,反而成了扣分项。本地缓存不加额外依赖,切Redis也只需要改一下缓存管理器,这个演进思路要讲清楚。

6.3 验收用例:一台干净电脑上的自检清单

交付项目前,我习惯跑一组固定用例,确认“是不是真能干活”。你可以把下面这个表格当成你的验收清单:

测试场景操作预期结果
首次启动mvn spring-boot:run控制台无异常,端口8080被监听
首页浏览器访问 /index展示推荐诗词,无乱码
关键词搜索输入“春江花月夜”能返回该诗,作者、朝代显示正常
作者搜索输入“李白”返回李白名下多首诗,无重复数据
详情页点击一首诗浏览量+1,同作者作品列表能加载
收藏同一用户对同一首诗点两次第二次提示已收藏,数据库无重复记录
分页第2页,每页5条返回5条且与第1页不重复
异常输入搜索空字符串、特殊字符<script>页面不报错,无XSS注入风险

最后一列关于XSS的用例,你在模板渲染时用th:text就已经自动转义了,这是一体化模板的一大优势。如果走了前后端分离,全局过滤器还要单独处理,这也是我推荐Thymeleaf的附加理由。

这个项目做完之后,我自己的习惯是把init.sql、启动说明、JDK版本要求写在一个README里,因为两个月后你自己都会忘掉密码和版本;教材上不会教你这个,但实际交付时这是最值钱的一部分。希望你做完之后不只是跑通了代码,还能把每个表为什么这么建、每个参数为什么这么设讲给别人听,那我这篇文章的目的就达到了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询