古诗词网站后端实战:SpringBoot+MySQL构建教学级诗词系统

发布时间:2026/10/30 9:38:40
古诗词网站后端实战:SpringBoot+MySQL构建教学级诗词系统

简介:本资源是一套完整的古诗词学习网站课程设计项目,基于SpringBoot后端(Java)与MySQL数据库构建,前端融合React技术栈,面向计算机专业本科生及Java Web初学者,解决传统文化类Web应用开发的全流程实践需求。压缩包共596个文件,含88个核心Java控制器类(如PoetryController、AdminController等)、26个HTML页面、72个Class编译文件、26个JS脚本及183张PNG界面截图,涵盖前后端代码、数据库脚本、静态资源与配置文件,整体体积39.9MB,结构清晰便于模块化学习与二次开发。已有951人下载学习,资源提供可直接运行的完整系统,包含用户浏览/收藏/评论/分享、管理员诗词/用户/通知/资源审核等全功能模块,附带SQL建表语句与典型业务逻辑实现细节,是理解MVC分层架构、RESTful接口设计与权限管理落地的优质教学案例。

1. 为什么古诗词网站不能只靠前端堆页面?SpringBoot + MySQL 是真正在跑业务的底座

你见过太多“古诗词网站”:首页飘着水墨动画,点开《静夜思》能朗读、带注释、有赏析——但后台一查,用户收藏夹是空的,搜索框输“月落乌啼霜满天”返回 0 条,管理员想看本周最热诗句,数据库里连访问日志表都没有。这不是设计问题,是底座没立住。这个标题里的Java(SpringBoot)+ MySQL不是凑数的技术栈组合,而是解决三个刚性问题的最小可靠方案:用户状态必须持久化(登录/收藏/错题)、诗句数据要支持结构化检索(按朝代/作者/体裁/关键字模糊匹配)、并发访问下不能丢数据(比如多人同时提交背诵记录)。它不追求炫技,但能扛住真实教学场景——中学语文老师用它布置每日一诗打卡,学生交作业、系统自动判对错、生成班级掌握热力图;高校古典文学课用它做词频统计实验,从《全唐诗》5万首中抽样分析“酒”字在李白与杜甫诗中的共现模式。如果你正卡在“前端做得像模像样,一加登录就崩”“MySQL建了10张表但不知道哪张该加索引”“SpringBoot启动报错说找不到 DataSource”,那这篇就是为你写的血泪复盘笔记。


2. 从零初始化项目:Maven 依赖、数据库建模与 SpringBoot 自动装配链路

2.1 用 Spring Initializr 选对依赖,避开版本地狱

别直接spring-boot-starter-web一把梭。古诗词网站的核心矛盾是:文本字段长(一首诗正文可能300字)、查询条件多(朝代+作者+是否含典故+是否押韵)、写操作频次低但一致性要求高(用户收藏不能重复)。所以依赖必须精准:

<!-- pom.xml 关键依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>

注意:spring-boot-starter-data-jpa是关键。它让@Entity注解的 POJO 直接映射到 MySQL 表,省去手写 JDBC 模板代码;而spring-boot-starter-validation用于校验用户输入的诗词标题长度(防 SQL 注入)、作者名非空等。thymeleaf虽然不如 Vue 前卫,但对古诗词这类内容型网站足够——模板里直接写th:text="${poem.content}",比 React 的 JSX 更贴近语文老师编辑习惯。

2.2 MySQL 建模:三张核心表的设计逻辑与字段陷阱

古诗词不是博客文章,它的元数据结构特殊:一首诗可属多个标签(如“边塞诗”“七言绝句”),一个作者写多首诗,用户收藏跨朝代。ER 图必须拆解为Poem(诗)→ Author(作者)→ Tag(标签)→ UserCollection(用户收藏)四张表。重点说三张:

表名字段设计要点为什么这么设
poemid BIGINT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(100) NOT NULL,content TEXT NOT NULL,dynasty ENUM('唐','宋','元','明','清') NOT NULL,author_id BIGINT NOT NULL,created_at DATETIME DEFAULT CURRENT_TIMESTAMPcontent用TEXT而非VARCHAR(2000):王维《山居秋暝》全文 128 字,但加上注释和赏析可能超 2000 字;dynasty用ENUM而非VARCHAR:避免用户输入“唐朝”“唐代”“唐朝(盛)”等不一致值,保证后续GROUP BY dynasty统计准确
authorid BIGINT PRIMARY KEY,name VARCHAR(50) UNIQUE NOT NULL,intro TEXT,birth_year INT,death_year INTname加UNIQUE:防止“李白”和“李太白”被当两个作者;birth_year/death_year用INT而非DATE:古人出生年份常存疑(如杜甫生卒年有公元712/713两种说法),存整数更灵活
user_collectionuser_id BIGINT NOT NULL,poem_id BIGINT NOT NULL,collected_at DATETIME DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (user_id, poem_id)复合主键(user_id, poem_id)防止同一用户重复收藏同一首诗;不设自增id,节省空间且语义清晰

2.3 SpringBoot 启动时如何自动建表?application.yml 的生死配置

JPA 默认hibernate.hbm2ddl.auto=none,即不碰数据库。开发阶段必须显式开启自动建表,但绝不能在生产环境用create:

# application-dev.yml(仅开发环境) spring: datasource: url: jdbc:mysql://localhost:3306/tangsong?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password jpa: hibernate: ddl-auto: update # 关键!update 会对比实体类与表结构,只新增字段/索引,不删数据 show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect

逻辑说明:ddl-auto: update是安全底线。假设你给Poem实体加了@Column(name = "is_famous")字段,JPA 会自动执行ALTER TABLE poem ADD COLUMN is_famous TINYINT(1) DEFAULT 0;但如果把@Column(name = "title")改成@Column(name = "poem_title"),它不会重命名字段,而是新建poem_title并留着旧title字段——这就是为什么上线前必须用schema.sql手动执行 DDL。MySQL8Dialect必须指定:MySQL 8.0+ 默认启用caching_sha2_password认证插件,老版方言不兼容。


3. 诗句检索功能落地:从 LIKE 模糊查到全文索引的渐进式优化

3.1 初期用 JPQL 写搜索,但必须规避 N+1 查询陷阱

用户在搜索框输入“春风”,后端 Controller 接收参数后,常见错误写法是:

// ❌ 错误:触发 N+1 查询 List<Poem> poems = poemRepository.findByTitleContainingOrContentContaining(keyword, keyword); for (Poem p : poems) { System.out.println(p.getAuthor().getName()); // 每次 getAuthor() 都发一次 SQL }

正确做法是在 Repository 层预加载关联对象:

// ✅ 正确:用 @Query + JOIN FETCH 一次性查出诗+作者 @Repository public interface PoemRepository extends JpaRepository<Poem, Long> { @Query("SELECT p FROM Poem p JOIN FETCH p.author WHERE p.title LIKE %:keyword% OR p.content LIKE %:keyword%") List<Poem> searchByKeyword(@Param("keyword") String keyword); }

参数说明:JOIN FETCH p.author告诉 JPA 在查poem表时,用LEFT JOIN author ON poem.author_id = author.id一起把作者信息捞出来,避免循环中反复查库。%:keyword%中的%符号必须写在 Java 代码里传参,不能写在 JPQL 字符串中(否则会被当成字面量)。

3.2 当搜索变慢时,MySQL 全文索引是唯一出路

测试发现:当poem.content表达量超 10 万行,LIKE '%春风%'查询耗时从 50ms 暴涨到 2s。此时必须放弃LIKE,改用 MySQL 原生全文索引:

-- 在 poem 表的 title 和 content 字段上建 FULLTEXT 索引 ALTER TABLE poem ADD FULLTEXT(title, content); -- 查询语句改为 MATCH AGAINST SELECT * FROM poem WHERE MATCH(title, content) AGAINST('春风' IN NATURAL LANGUAGE MODE);

为什么不用 Elasticsearch?古诗词网站的搜索需求极窄:用户只搜关键词(“落花”“孤帆”),不要相关度排序、不要拼音纠错、不要同义词扩展。MySQL 8.0 的NATURAL LANGUAGE MODE已足够——它会自动忽略停用词(如“之”“乎”“者”),并按词频加权。而引入 ES 会增加运维复杂度:需要维护独立集群、同步数据、处理分词器(古汉语分词和现代汉语不同,HanLP 在 SpringBoot 中整合反而更重)。

3.3 高频搜索词缓存:用 Spring Cache + Caffeine 防止 DB 被打爆

用户搜“月”字,可能每秒 100 次请求。即使有全文索引,MySQL 连接池也会吃紧。加一层本地缓存:

// 在 Service 方法上加注解 @Cacheable(value = "poemSearch", key = "#keyword", unless = "#result.size() == 0") public List<Poem> searchPoems(String keyword) { return poemRepository.searchByKeyword(keyword); }
// 配置 Caffeine 缓存策略(application.yml) spring: cache: type: caffeine caffeine: spec: maximumSize=1000,expireAfterWrite=10m

参数说明:maximumSize=1000限制缓存最多存 1000 个搜索结果;expireAfterWrite=10m表示结果 10 分钟后自动失效——避免用户搜“李白”后,缓存长期不更新新录入的李白冷门诗。unless = "#result.size() == 0"是关键:空结果不缓存,防止恶意刷search?keyword=asdkjfhaksdjf耗尽内存。


4. 用户学习行为闭环:收藏、背诵记录与错题本的事务一致性保障

4.1 收藏功能必须用 @Transactional,否则出现“已收藏却查不到”

用户点击“收藏”按钮,后端需同时做两件事:插入user_collection记录、更新poem表的collect_count字段。若不用事务,可能出现:

  • 插入user_collection成功,但UPDATE poem SET collect_count = collect_count + 1失败 → 数据库显示“已收藏”,但排行榜里这首诗的收藏数没变;
  • 或反之 → 排行榜数字涨了,但用户个人收藏夹里没有。
@Service @Transactional // 整个方法在一个事务里 public class CollectionService { @Autowired private UserCollectionRepository collectionRepo; @Autowired private PoemRepository poemRepo; public void collect(Long userId, Long poemId) { // 先检查是否已收藏(幂等性) if (collectionRepo.existsByUserIdAndPoemId(userId, poemId)) { throw new RuntimeException("Already collected"); } // 插入收藏记录 UserCollection collection = new UserCollection(); collection.setUserId(userId); collection.setPoemId(poemId); collectionRepo.save(collection); // 更新诗的收藏数 poemRepo.incrementCollectCount(poemId); // 自定义 JPQL UPDATE } }
// PoemRepository.java 中定义原子更新 @Modifying @Query("UPDATE Poem p SET p.collectCount = p.collectCount + 1 WHERE p.id = :poemId") void incrementCollectCount(@Param("poemId") Long poemId);

为什么用@Modifying?JPA 默认@Query是只读的。@Modifying告诉 Spring 这是个写操作,会清空一级缓存,确保后续查询拿到最新值。incrementCollectCount方法名比updateCollectCount更精准——它只做+1,不覆盖原值,避免并发时丢失计数。

4.2 错题本设计:用 JSON 字段存用户作答详情,而非硬拆表

学生做默写题:“默写《春晓》后两句”,系统需记录:

  • 用户填的原文(可能错字:“花落知多少”写成“花洛知多少”)
  • 系统标准答案
  • 错误位置(第3字“落”错写为“洛”)
  • 错误类型(形近字)

如果为每个字段建单独列,表会膨胀。MySQL 5.7+ 支持JSON类型,直接存结构化数据:

ALTER TABLE user_mistake ADD COLUMN answer_detail JSON;
// Java 实体类用 @Convert 转换 @Entity public class UserMistake { @Convert(converter = AnswerDetailConverter.class) private AnswerDetail answerDetail; } // 自定义转换器 public class AnswerDetailConverter implements AttributeConverter<AnswerDetail, String> { @Override public String convertToDatabaseColumn(AnswerDetail attribute) { return new Gson().toJson(attribute); // 序列化为 JSON 字符串 } @Override public AnswerDetail convertToEntityAttribute(String dbData) { return new Gson().fromJson(dbData, AnswerDetail.class); } }

优势:查询时仍可走 MySQL JSON 函数,如SELECT * FROM user_mistake WHERE JSON_CONTAINS(answer_detail, '"形近字"');未来加新字段(如“语音识别置信度”)无需改表结构。


5. 避坑指南:SpringBoot + MySQL 在古诗词项目中最常踩的 5 个深坑

5.1 现象:SpringBoot 启动报错Failed to configure a DataSource

原因:application.yml中spring.datasource.url漏了?useSSL=false参数,MySQL 8.0 默认强制 SSL 连接,而本地开发环境通常没配证书。
解决:URL 末尾必须加?useSSL=false&serverTimezone=Asia/Shanghai。serverTimezone也不能少,否则created_at字段存的时间比实际晚 8 小时(MySQL 服务端时区和 JVM 时区不一致)。

5.2 现象:搜索“山”字,返回包含“火山”“江山”的诗,但用户只想找描写自然山峦的

原因:MySQL 全文索引的NATURAL LANGUAGE MODE对单字敏感度低,且无法排除特定词组。
解决:前端加筛选项——让用户先选“主题”(山水/边塞/咏物),再搜关键词;后端用WHERE dynasty IN ('唐','宋') AND MATCH(content) AGAINST('山' IN BOOLEAN MODE),BOOLEAN MODE支持+山 -火山语法。

5.3 现象:用户收藏 100 首诗后,个人页面加载变慢

原因:UserCollection表没建复合索引,SELECT * FROM user_collection WHERE user_id = ?全表扫描。
解决:立即执行CREATE INDEX idx_user_poem ON user_collection(user_id, poem_id)。注意顺序:user_id在前,因为查询总是以用户为维度。

5.4 现象:Poem.content字段存入带换行的古诗,网页显示成一团(无<br>)

原因:Thymeleaf 默认转义 HTML 字符,\n被当成普通字符渲染。
解决:模板中用th:utext替代th:text:<div th:utext="${poem.content}"></div>,它会把\n自动转为<br>,且不转义<>符号(需确保 content 字段不含恶意脚本)。

5.5 现象:部署到 Linux 服务器后,MySQL 连接偶尔超时断开

原因:MySQL 服务端wait_timeout默认 8 小时,SpringBoot 连接池未配置心跳检测,空闲连接被服务端主动 kill。
解决:在application-prod.yml中加连接池配置:

spring: datasource: hikari: connection-timeout: 30000 validation-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000 keepalive-time: 30000 # 每30秒发一次 SELECT 1 检测连接存活

6. 进阶技巧:用 MySQL 窗口函数生成“诗人影响力排行榜”,不写一行 Java 代码

古诗词网站常需展示“李白写了多少首诗”“杜甫诗作被收藏次数 Top10”。传统做法是 Java 代码查出所有诗人,再用Map<String, Integer>统计——但数据量大时内存溢出。更好的方案是把聚合逻辑下推到 MySQL,用窗口函数一步到位。

6.1 用ROW_NUMBER()生成实时排行榜

假设要查“各朝代收藏数 Top3 的诗人”,SQL 如下:

SELECT dynasty, author_name, collect_count, rank_in_dyna FROM ( SELECT a.dynasty, a.name AS author_name, COUNT(uc.poem_id) AS collect_count, ROW_NUMBER() OVER (PARTITION BY a.dynasty ORDER BY COUNT(uc.poem_id) DESC) AS rank_in_dyna FROM author a JOIN poem p ON a.id = p.author_id JOIN user_collection uc ON p.id = uc.poem_id GROUP BY a.dynasty, a.name ) ranked WHERE rank_in_dyna <= 3;

6.2 在 SpringBoot 中调用原生 SQL 获取结果集

@Repository public class RankingRepository { @PersistenceContext private EntityManager entityManager; public List<AuthorRanking> getTopAuthorsByDynasty() { String sql = """ SELECT ... -- 上面那段 SQL """; Query query = entityManager.createNativeQuery(sql); List<Object[]> results = query.getResultList(); return results.stream() .map(row -> new AuthorRanking( (String) row[0], // dynasty (String) row[1], // author_name ((BigInteger) row[2]).longValue(), // collect_count ((BigInteger) row[3]).intValue() // rank_in_dyna )) .collect(Collectors.toList()); } }

为什么不用 JPQL?JPQL 不支持ROW_NUMBER()等窗口函数,必须用原生 SQL。entityManager.createNativeQuery是安全的——它不拼接字符串,参数用?1占位符,防注入。BigInteger转long是 MySQL 驱动对大整数的默认映射,别用Long.parseLong()强转,会抛异常。

6.3 把排行榜做成定时任务,每天凌晨更新缓存

@Component @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public class RankingScheduler { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private RankingRepository rankingRepo; public void refreshRankingCache() { List<AuthorRanking> rankings = rankingRepo.getTopAuthorsByDynasty(); redisTemplate.opsForValue().set("ranking:author:top3", rankings, Duration.ofHours(24)); } }

教训:我曾在线上环境把排行榜查询放在用户请求链路里,结果某天语文老师布置全班搜“李白”,瞬间 2000+ 并发触发排行榜计算,MySQL CPU 100%。后来改成定时任务+Redis 缓存,接口响应从 1.2s 降到 80ms。永远把重计算、重 IO 的逻辑从用户请求流里剥离开——这是 SpringBoot 项目从能跑走向能扛的分水岭。
希望帮到你。

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