ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

SpringBoot+MySQL古诗词网站开发:数据建模、检索优化与避坑指南 简介这是一套面向高校计算机专业学生与Java Web开发初学者的古诗词学习网站课程设计源码采用SpringBootMySQL技术栈配套前端页面与完整数据库脚本适合用于课程设计、毕业设计或全栈入门练手。压缩包共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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT诗词表; -- 标签表与关联表 CREATE TABLE tag ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(32) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;对应的 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做二次组装。这样虽然多一次查询但每次查询都走主键索引整体响应更稳定。// 分页查诗词 PagePoem page new Page(pageNo, pageSize); LambdaQueryWrapperPoem wrapper new LambdaQueryWrapper(); wrapper.orderByDesc(Poem::getId); poemMapper.selectPage(page, wrapper); // 批量查作者 SetLong poetIds page.getRecords().stream() .map(Poem::getPoetId).collect(Collectors.toSet()); MapLong, 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: GMT8 datasource: url: jdbc:mysql://localhost:3306/poem_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/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: autoserverTimezone必须显式设置否则 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 ResultPagePoemVO search( RequestParam String keyword, RequestParam(defaultValue 1) Integer pageNo, RequestParam(defaultValue 10) Integer pageSize) { // 先按作者名模糊查 ListLong poetIds poetMapper.selectList( new LambdaQueryWrapperPoet().like(Poet::getName, keyword)) .stream().map(Poet::getId).collect(Collectors.toList()); LambdaQueryWrapperPoem wrapper new LambdaQueryWrapper(); wrapper.like(Poem::getTitle, keyword); if (!poetIds.isEmpty()) { wrapper.or().in(Poem::getPoetId, poetIds); } wrapper.orderByDesc(Poem::getId); PagePoem page poemMapper.selectPage(new Page(pageNo, pageSize), wrapper); // 组装 VO填充作者名 PagePoemVO 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()); ListLong tagIds poemTagMapper.selectList( new LambdaQueryWrapperPoemTag().eq(PoemTag::getPoemId, poemId)) .stream().map(PoemTag::getTagId).collect(Collectors.toList()); ListTag 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 中文乱码从数据库到前端的三层排查现象诗词正文在页面上显示为??????或乱码方块。原因通常有三层数据库连接串没加characterEncodingutf8mb4、表字符集是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 --usermysql重新初始化再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()对古诗词的切分效果比标准分词器好因为它能识别“明月”“故乡”这类固定搭配。// 建索引时调用 ListTerm 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说明索引生效了。我自己的习惯是每加一个检索维度就先写三条边界查询——空关键词、单字关键词、超长关键词确保接口不崩、不超时、不返回全表。这个习惯帮我省掉了至少三次线上事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表