news 2026/10/8 16:42:39

SpringBoot+MySQL古诗词网站开发:数据建模、检索优化与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+MySQL古诗词网站开发:数据建模、检索优化与避坑指南

简介:这是一套面向高校计算机专业学生与Java Web开发初学者的古诗词学习网站课程设计源码,采用SpringBoot+MySQL技术栈,配套前端页面与完整数据库脚本,适合用于课程设计、毕业设计或全栈入门练手。压缩包共596个文件,约39.9MB,涵盖88个Java源文件、107个XML配置、26个HTML页面、26个JS脚本、12个CSS样式及183张PNG图片等,另含SQL建表脚本与项目说明文档,前后端结构清晰。系统分用户与管理员两端:用户可分类、分朝代浏览诗词,查看全文、注释与翻译,进行收藏、评论、分享、搜索及个人信息修改;管理员可管理用户、诗词、诗人、朝代、类别、通知与评论,并审核用户上传资源。已有951人学习下载,可帮助读者快速理解SpringBoot项目分层、REST接口设计与MySQL表结构,掌握从需求到实现的完整开发思路。

1. 古诗词学习网站:从 SpringBoot 到 MySQL 的落地路线图

很多 Java 工程师在面试或接私活时都会遇到一个经典命题:用 SpringBoot + MySQL 做一个垂直领域的内容型网站。古诗词学习网站就是其中极具代表性的一类——它看起来简单,但真正动手时会发现,诗词数据的结构化存储、全文检索、分页性能、前后端字段映射,每一个环节都有讲究。这个方向适合两类人:一是想通过完整项目串联 SpringBoot 整合 MyBatis-Plus、MySQL 建表与索引优化的初中级开发者;二是需要一套可复用的内容管理骨架,快速改造成成语词典、名言警句库等同类产品的从业者。核心难点不在框架本身,而在于如何把非结构化的诗词文本拆成可检索、可关联、可扩展的数据模型,并用最少的代码把增删改查和检索体验做到位。

2. 数据建模与 MySQL 表结构:诗词网站的地基怎么打

2.1 诗词数据的三个核心实体与关系

古诗词学习网站的数据模型不需要过度设计,但必须把「作者—诗词—标签」这条主线理清楚。我一般会拆成三张主表加一张关联表:poet(作者)、poem(诗词正文)、tag(标签,如“唐诗”“宋词”“思乡”“边塞”),以及poem_tag(诗词与标签的多对多关联)。这样做的理由是:诗词的朝代、体裁、情感主题这些属性天然适合用标签体系来扩展,而不是在poem表里堆一堆枚举字段。后期想加“小学必背”“中考高频”这类运营标签时,只需要插tag表数据,不用改表结构。

poem表里最关键的字段是title、content、poet_id、dynasty、genre。content用TEXT类型存储全文,title和poet_id上建联合索引,因为最常见的查询是“按作者查诗”和“按标题模糊搜”。注意 MySQL 5.7 和 8.0 在全文索引上的差异:5.7 的ngram分词器对中文支持有限,8.0 虽然改进了但仍有停用词问题。如果项目允许,我建议把全文检索交给应用层做前缀匹配,或者引入 HanLP 分词后在 Java 侧建倒排,而不是硬扛 MySQL 全文索引。

2.2 建表 SQL 与 MyBatis-Plus 实体映射

下面这套建表语句是我在多个内容型项目中反复用过的版本,字段长度和索引都经过实际数据验证。注意poem表的content字段不要用VARCHAR(65535),虽然语法允许,但行溢出会拖慢查询,用TEXT更稳妥。

-- 作者表 CREATE TABLE `poet` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `name` VARCHAR(64) NOT NULL COMMENT '作者姓名', `dynasty` VARCHAR(32) DEFAULT NULL COMMENT '朝代', `bio` VARCHAR(512) DEFAULT NULL COMMENT '简介', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_name_dynasty` (`name`, `dynasty`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作者表'; -- 诗词表 CREATE TABLE `poem` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `title` VARCHAR(128) NOT NULL COMMENT '标题', `content` TEXT NOT NULL COMMENT '正文', `poet_id` BIGINT NOT NULL COMMENT '作者ID', `dynasty` VARCHAR(32) DEFAULT NULL, `genre` VARCHAR(32) DEFAULT NULL COMMENT '体裁:诗/词/曲', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_poet_id` (`poet_id`), KEY `idx_title` (`title`(32)), KEY `idx_dynasty_genre` (`dynasty`, `genre`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诗词表'; -- 标签表与关联表 CREATE TABLE `tag` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(32) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `poem_tag` ( `poem_id` BIGINT NOT NULL, `tag_id` BIGINT NOT NULL, PRIMARY KEY (`poem_id`, `tag_id`), KEY `idx_tag_id` (`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

对应的 MyBatis-Plus 实体类,重点在@TableField的映射和@TableName的指定。如果你用的是 SpringBoot 3.x,注意 MyBatis-Plus 需要 3.5.3 以上版本才兼容 Jakarta EE,否则启动时会报javax.persistence找不到的错。

@Data @TableName("poem") public class Poem { @TableId(type = IdType.AUTO) private Long id; private String title; private String content; private Long poetId; private String dynasty; private String genre; @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; }

参数说明:IdType.AUTO依赖数据库自增,不要和雪花算法混用;FieldFill.INSERT需要配合MetaObjectHandler实现类才能自动填充时间,否则字段为 null。idx_title用了前缀索引title(32),因为诗词标题很少超过 32 个字符,前缀索引能显著减小索引文件体积。

2.3 分页查询与作者关联的两种写法

诗词列表页通常需要同时展示标题、作者名和朝代。最直接的做法是用LEFT JOIN一次查出,但在数据量超过 10 万条时,JOIN配合LIMIT的深分页会明显变慢。我的经验是:列表页只查poem表,拿到poet_id列表后再用IN查询作者,利用 MyBatis-Plus 的selectBatchIds做二次组装。这样虽然多一次查询,但每次查询都走主键索引,整体响应更稳定。

// 分页查诗词 Page<Poem> page = new Page<>(pageNo, pageSize); LambdaQueryWrapper<Poem> wrapper = new LambdaQueryWrapper<>(); wrapper.orderByDesc(Poem::getId); poemMapper.selectPage(page, wrapper); // 批量查作者 Set<Long> poetIds = page.getRecords().stream() .map(Poem::getPoetId).collect(Collectors.toSet()); Map<Long, Poet> poetMap = poetMapper.selectBatchIds(poetIds) .stream().collect(Collectors.toMap(Poet::getId, p -> p));

这里有个容易翻车的点:selectBatchIds传入空集合会抛异常,务必先判空。另外orderByDesc(Poem::getId)在分页时比按created_at排序更可靠,因为自增 ID 天然有序且无重复。

3. SpringBoot 工程结构与接口实现:把 CRUD 写出生产级味道

3.1 项目分层与依赖选型

一个能直接改造成其他内容站点的 SpringBoot 工程,分层不需要太花哨,但 controller、service、mapper、entity、dto 这五层要清晰。我一般会把 DTO 和 VO 分开:DTO 接收入参,VO 返回给前端,entity 只和数据库打交道。这样后期加字段脱敏、格式化输出时不会污染实体类。

依赖方面,SpringBoot 2.7.x 和 3.x 的选型要看你的 JDK 版本。如果团队还在用 JDK 8,就锁 2.7.x;如果上了 JDK 17,直接 3.x。MyBatis-Plus 用 3.5.5 以上,MySQL 驱动用mysql-connector-j8.0.33。注意 SpringBoot 3.x 默认不再包含spring-boot-starter-web里的 Jackson 对LocalDateTime的序列化配置,需要在application.yml里显式指定时区,否则前端拿到的时间会差 8 小时。

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: url: jdbc:mysql://localhost:3306/poem_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto

serverTimezone必须显式设置,否则 MySQL 8.0 在部分 Windows 环境下会报The server time zone value '?D1ú±ê×?ê±??' is unrecognized。这个坑我在 Windows 10 上装 MySQL 8.0.46 时踩过,改成Asia/Shanghai即可。

3.2 诗词检索接口:从关键词到分页结果

检索接口是古诗词网站最核心的对外能力。需求通常是:用户输入“李白”或“静夜思”,返回匹配的诗词列表,支持分页。我的做法是用LambdaQueryWrapper的like条件同时匹配title和作者名,但作者名在poet表,所以要么先查作者 ID 再查诗词,要么用自定义 SQL 做JOIN。为了代码可读性,我选前者。

@GetMapping("/search") public Result<Page<PoemVO>> search( @RequestParam String keyword, @RequestParam(defaultValue = "1") Integer pageNo, @RequestParam(defaultValue = "10") Integer pageSize) { // 先按作者名模糊查 List<Long> poetIds = poetMapper.selectList( new LambdaQueryWrapper<Poet>().like(Poet::getName, keyword)) .stream().map(Poet::getId).collect(Collectors.toList()); LambdaQueryWrapper<Poem> wrapper = new LambdaQueryWrapper<>(); wrapper.like(Poem::getTitle, keyword); if (!poetIds.isEmpty()) { wrapper.or().in(Poem::getPoetId, poetIds); } wrapper.orderByDesc(Poem::getId); Page<Poem> page = poemMapper.selectPage(new Page<>(pageNo, pageSize), wrapper); // 组装 VO,填充作者名 Page<PoemVO> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); voPage.setRecords(page.getRecords().stream().map(this::toVO).collect(Collectors.toList())); return Result.ok(voPage); }

逻辑说明:wrapper.like(Poem::getTitle, keyword)会生成title LIKE '%keyword%',在前缀索引上无法走索引,所以数据量超过 5 万条后要改成前缀匹配likeRight,或者引入搜索引擎。or()的使用要小心,如果前面没有条件,or()会导致全表扫描,这里因为like一定存在,所以安全。参数pageSize建议在全局配置里限制最大值,防止前端传 10000 导致内存溢出。

3.3 诗词详情与标签关联的组装

详情页需要返回诗词正文、作者信息、标签列表。标签通过poem_tag关联,查询时先用poem_id查tag_id列表,再批量查tag表。这里不要用嵌套子查询,MySQL 对IN (SELECT ...)的优化在 5.7 上并不理想。

public PoemDetailVO getDetail(Long poemId) { Poem poem = poemMapper.selectById(poemId); if (poem == null) { throw new BizException("诗词不存在"); } Poet poet = poetMapper.selectById(poem.getPoetId()); List<Long> tagIds = poemTagMapper.selectList( new LambdaQueryWrapper<PoemTag>().eq(PoemTag::getPoemId, poemId)) .stream().map(PoemTag::getTagId).collect(Collectors.toList()); List<Tag> tags = tagIds.isEmpty() ? Collections.emptyList() : tagMapper.selectBatchIds(tagIds); PoemDetailVO vo = new PoemDetailVO(); vo.setPoem(poem); vo.setPoet(poet); vo.setTags(tags); return vo; }

tagIds.isEmpty()的判断是血泪经验——selectBatchIds传空集合在 MyBatis-Plus 3.5.3 之前会直接抛IllegalArgumentException,升级到 3.5.5 后虽然内部做了判空,但显式判断更稳妥。

4. 避坑与排查:古诗词网站开发中最容易翻车的 5 个点

4.1 中文乱码:从数据库到前端的三层排查

现象:诗词正文在页面上显示为??????或乱码方块。原因通常有三层:数据库连接串没加characterEncoding=utf8mb4、表字符集是latin1、或者 SpringBoot 的HttpMessageConverter默认用了 ISO-8859-1。解决顺序是:先SHOW CREATE TABLE poem确认字符集,再检查application.yml的 JDBC URL,最后在WebMvcConfigurer里显式配置StringHttpMessageConverter的默认编码为 UTF-8。三层都对齐后,乱码问题基本绝迹。

4.2 分页插件失效:PageHelper 与 MyBatis-Plus 的冲突

现象:selectPage返回的total始终为 0,或者分页不生效。原因是你同时引入了 PageHelper 和 MyBatis-Plus 的分页插件,两者拦截器冲突。解决方法是二选一,如果坚持用 MyBatis-Plus,就在配置类里注册MybatisPlusInterceptor并添加PaginationInnerInterceptor,同时排除 PageHelper 依赖。

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

4.3 SpringBoot 版本过高导致 MyBatis-Plus 启动失败

现象:启动时报java.lang.NoClassDefFoundError: javax/persistence/Table。原因是 SpringBoot 3.x 把javax.persistence换成了jakarta.persistence,而低版本 MyBatis-Plus 还在引用旧包。解决方法是升级 MyBatis-Plus 到 3.5.3+,或者把 SpringBoot 降到 2.7.x。这个坑在springboot版本太高这个热搜词下被反复讨论,本质是 Jakarta EE 迁移的连带影响。

4.4 MySQL 8.0 在 Windows 10 上服务启动失败

现象:net start mysql报“服务正在启动,但无法启动”。原因通常是my.ini里datadir路径含中文或空格,或者初始化时--initialize没有生成data目录。解决步骤:先mysqld --remove mysql卸载服务,清空data目录,用mysqld --initialize-insecure --user=mysql重新初始化,再mysqld --install安装服务。注意--initialize-insecure生成空密码 root,生产环境要用--initialize并保存临时密码。

4.5 诗词正文换行丢失:TEXT 字段与前端渲染的配合

现象:数据库里存的诗词有换行,但前端展示成一行。原因是 MySQL 的TEXT字段保留了\n,但前端用{{ }}插值时 HTML 会折叠空白。解决方法是在前端用white-space: pre-wrap样式,或者后端返回时把\n替换成<br/>并用v-html渲染。我倾向于前者,因为后者有 XSS 风险,诗词内容虽然可控,但养成习惯更重要。

5. 进阶技巧:用 HanLP 分词提升检索命中率

当诗词数据超过 3 万条后,LIKE '%keyword%'的查询延迟会从几十毫秒涨到几百毫秒。这时候可以考虑在 SpringBoot 里集成 HanLP 做分词,把每首诗词的标题和正文切词后存入一张poem_index表,检索时先对关键词分词,再用MATCH AGAINST或IN查询索引表。HanLP 的HanLP.segment()对古诗词的切分效果比标准分词器好,因为它能识别“明月”“故乡”这类固定搭配。

// 建索引时调用 List<Term> terms = HanLP.segment(poem.getTitle() + " " + poem.getContent()); String indexWords = terms.stream() .map(Term::getWord) .filter(w -> w.length() > 1) .distinct() .collect(Collectors.joining(" ")); // 存入 poem_index 表的 words 字段

检索时对用户输入做同样分词,然后用WHERE words LIKE '%词1%' AND words LIKE '%词2%'做交集匹配。这套方案在 10 万条数据下能把平均响应控制在 80ms 以内,比全表LIKE快一个数量级。注意 HanLP 的词典加载会占用约 200MB 堆内存,启动时加-Xmx512m以上。

验证方法很简单:准备 100 条包含“明月”“思乡”“边塞”的测试查询,对比优化前后的EXPLAIN结果和实际耗时。如果type从ALL变成ref或range,说明索引生效了。

我自己的习惯是:每加一个检索维度,就先写三条边界查询——空关键词、单字关键词、超长关键词,确保接口不崩、不超时、不返回全表。这个习惯帮我省掉了至少三次线上事故。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 16:42:38

AI智能体技能(Skills)工程化构建范式

1. 项目概述&#xff1a;这不是一个工具&#xff0c;而是一套可复用的智能体能力构建范式 “skills”这个词在当前AI开发语境里&#xff0c;早已不是简历上那行轻描淡写的“熟悉JavaScript”——它正迅速演变为智能体&#xff08;Agent&#xff09;系统中 最小可验证、可组合、…

作者头像 李华
网站建设 2026/10/8 16:41:46

Claude深夜放大招!原生接管谷歌全家桶,文档、PPT、表格全拿下

今天凌晨1点20&#xff0c;Claude整了个大活&#xff0c;宣布原生接入谷歌全家桶&#xff0c;文档、表格、PPT都能使用了&#xff0c;可以在侧边栏原生编辑了。说实话&#xff0c;看完这个消息我怎么觉得都别扭&#xff0c;就觉得哪里怪怪的。突然醒悟了&#xff0c;谷歌和Anth…

作者头像 李华
网站建设 2026/10/8 16:40:11

嵌入式以太网驱动开发全解析:从硬件原理到调试实战

嵌入式驱动开发做到一定阶段&#xff0c;很多人都会遇到一个绕不开的关卡&#xff1a;Ethernet。我在前九期内容里陆续聊过GPIO、中断、定时器、DMA这些基础外设&#xff0c;但真正把驱动复杂度拉高一个量级的&#xff0c;恰恰是以太网。这期就专门把Ethernet驱动开发这件事拆开…

作者头像 李华
网站建设 2026/10/8 16:39:30

微信小程序+SSM学生资助管理系统设计与实现全解析

前阵子刚帮一个师弟把他的毕设项目过了一遍整体逻辑&#xff0c;就是他那个编号为 weixin229 的学生资助在线管理软件&#xff0c;后端用的 SSM&#xff0c;前端是微信小程序&#xff0c;还配了完整的文档和源码。这个项目我从数据库设计看到接口实现&#xff0c;再到小程序端联…

作者头像 李华
网站建设 2026/10/8 16:39:18

AI日报信息筛选与工具链实践:Codex接入DeepSeek与昇腾部署

1. 一份AI日报背后的信息筛选逻辑做AI日报这件事&#xff0c;我从2024年就开始折腾了。最开始只是自己每天早上花半小时刷一圈信息源&#xff0c;把值得关注的内容记在备忘录里&#xff0c;后来发现身边不少朋友也有类似需求&#xff0c;就慢慢做成了一份固定输出的日报。到202…

作者头像 李华
网站建设 2026/10/8 16:39:04

Text-to-SQL Agent三大核心环节:执行前、中、后管控

Text-to-SQL Agent&#xff1a;除了生成 SQL&#xff0c;还得管这三件事 做 Text-to-SQL 做了两年多&#xff0c;从最早纯靠提示词调大模型&#xff0c;到后面自己搭完整的 Agent 链路&#xff0c;最大的感受是&#xff1a; SQL 生成只是入场券 。今天大部分团队卡住的不是“…

作者头像 李华