SpringBoot实战:古诗词鉴赏交流平台的设计与实现 1. 为什么我把毕业设计押在古诗词鉴赏与交流平台上1.1 选题的三个困境每年毕业设计选题的时候我身边几乎都是同一批哀嚎不想做那种烂大街的学生管理系统但算法方向又怕数学底子撑不住前后端分离的电商项目又卷到飞起。最后我选了这个基于SpringBoot的古诗词鉴赏与交流平台说实话做了大半个学期之后回头看这个题目帮我把所有麻烦都规避掉了而且它几乎覆盖了SpringBoot体系里所有值得写进简历和答辩PPT的技术点。毕设选题本质上是在回答三个问题第一这个项目能不能让别人一眼看懂是做什么的第二技术栈能不能体现出你确实掌握了主流开发能力第三工作量能不能做到不太水也不至于做不完。古诗词鉴赏与交流平台恰好三点全中。古诗词是大众认知度极高的内容领域评阅老师不需要任何背景知识就能理解你在做什么而鉴赏交流这个组合意味着系统不是简单的增删改查它有内容展示、有检索、有用户行为、有互动关系天然就能把SpringBoot的全链路能力串起来。我见过太多人选了基于SpringBoot的XX管理系统做完发现所有页面都是同一套表格加弹窗答辩的时候连自己都讲不出特色。但古诗词平台不一样它的业务形态天然贴近真实产品首页要像内容门户详情页要有阅读感用户之间要有互动痕迹。这种项目做出来演示效果比十个管理系统都强。1.2 这个题目的评分点藏在交流两个字里很多同学看这个题目第一反应是不就是把诗词存到数据库里然后做个列表页嘛。如果真这么做那确实只是个中等水平的CRUD项目。主题里的鉴赏与交流平台才是拉开差距的地方——鉴赏意味着你要处理富文本内容、分类浏览、多维度检索、详情展示交流意味着你要实现用户体系、评论回复、点赞收藏、个人中心。这两个词合起来实际上把一个完整的内容社区拆成了两条业务线内容生产与消费线、用户互动线。从评分角度说一条业务线只能证明你会写接口两条业务线并行才能证明你会做系统设计。我当时规划的功能清单是这样的前台门户诗词列表、朝代分类、诗人主页、诗词详情含注释、译文、赏析、关键词搜索用户中心注册登录、个人信息维护、我的收藏、我的点赞、我的评论记录互动模块评论发布与回复、诗词点赞、收藏管理后台管理诗词管理、诗人管理、用户管理、评论审核与管理、数据统计这个清单的好处在于每一块都有独立的接口设计和数据表支撑答辩时你可以按模块逐个讲设计方案不会有这个功能是硬凑的的感觉。而且它天然支持你做技术深化——比如检索模块可以用上MySQL全文索引也可以升级成Elasticsearch用户模块可以用Session也可以换JWT评论审核可以做状态机。这些都是答辩时的加分话术。2. 功能清单与数据库设计先想清楚要做什么再动手写代码2.1 前后台功能模块的划分逻辑我见过不少同学一上来就建表结果做到一半发现字段不够用或者表结构设计得过于复杂导致接口写起来痛苦。我的建议是先做功能清单再做模块划分最后才落到数据库设计。这个项目的功能模块我最终收敛成了四块内容展示模块、用户交互模块、个人中心模块、后台管理模块。内容展示模块是门面负责把古诗词的内容、作者、时代背景完整呈现给访客用户交互模块是灵魂如果只有浏览功能这个平台是没有粘性的——但有了评论和点赞之后用户之间就产生了数据关联系统就从资料库变成了社区个人中心模块是用户行为数据的汇总出口后台管理模块则让系统具备了运营能力诗词数据可以维护、用户评论可以审核。划分模块时有个很实用的判断标准每个模块必须对应至少两张数据表且表之间的关联关系要能讲清楚。比如用户交互模块对应评论表、点赞表、收藏表这三张表都跟用户表、诗词表产生外键关联个人中心模块本身不新增表但它是对用户相关数据的聚合查询。这样划分之后每个模块的工作量是均等的不会出现一个模块写了三百行代码、另一个模块只有一句SQL的情况。2.2 核心表结构与字段取舍数据库是这个项目的根基我最终设计了六张核心表这里把字段和设计理由一起列出来表名核心字段设计说明user 用户表id, username, password, nickname, avatar, email, role, status, create_time密码字段必须存加密后的密文role区分普通用户和管理员用int类型而不是字符串避免魔法值散落在代码里poet 诗人表id, name, dynasty, birth_year, death_year, biography, avatar把诗人单独建表而不是在诗词表中直接存作者名字是为了诗人主页功能——按诗人聚合作品时需要连表查询poem 诗词表id, title, poet_id, dynasty, content, notes, translation, appreciation, tags, cover, views, statuscontent存全文用TEXT类型notes注释、translation译文、appreciation赏析单独列存储这样详情页可以用Tab切换展示views记录浏览量用于热门排序poem_comment 评论表id, poem_id, user_id, parent_id, content, status, create_timeparent_id字段支持楼中楼回复为0表示顶级评论status控制审核状态0待审核、1已通过、2已驳回poem_favorite 收藏表id, user_id, poem_id, create_time必须加唯一索引(user_id, poem_id)否则用户能对同一首诗词收藏无数次poem_like 点赞表id, user_id, poem_id, create_time点赞和收藏是两种不同行为必须分表。点赞表同样要加唯一索引防止重复点赞这里有一个容易被忽略的设计点诗词表为什么不直接用VARCHAR存全文而是用TEXT类型。古诗词的正文虽然一般只有几十到几百字但你在做数据库设计的时候不能只看当前数据量——如果你后续想收录宋词全集的几万首词VARCHAR的65535字节上限很容易被突破。TEXT类型最大支持65535字节MEDIUMTEXT最大支持16777215字节对于绝大多数诗词内容来说TEXT完全够用。2.3 我在字段设计上做错过的两个决定第一个错误是朝代字段的设计。一开始我在poem表里直接用varchar存唐代宋代这样的字符串后来做朝代分类统计时发现排序很痛苦——数据库按字符串排序会得到唐代宋代元代这种字典序而不是时间序。后来我把朝代改成了int类型的代号前台上显示的时候再通过枚举映射成字符串。第二个错误是封面图字段。我最初没有给诗词表设计cover字段导致前端列表页全是空白图片整个页面视觉效果惨不忍睹。古诗词虽然以文字为主但列表页和详情页完全可以放意境图——山水画、书法作品、诗人肖像这些素材网上都能找到免费资源。加了cover字段之后整个项目的UI质感提升了一个档次答辩演示的时候视觉效果好很多。3. 技术栈到底怎么选一版能过答辩的组合3.1 后端选型真的别去追最新版本后端我选用的是SpringBoot 2.7.x JDK 8 MyBatis-Plus 3.5.x MySQL 8.0的组合。为什么不用SpringBoot 3.x因为SpringBoot版本太高这个问题我在网上看过太多人踩坑SpringBoot 3.0开始强制要求JDK 17并且javax包名改成了jakarta很多第三方组件还没有完全适配。我的目标是稳定跑通而不是尝鲜2.7.x是目前生态最成熟、资料最多、遇到问题最容易搜到解决方案的版本线。选MyBatis-Plus而不是MyBatis的原因更直接MyBatis-Plus提供内置的BaseMapper接口和QueryWrapper条件构造器单表CRUD不需要写SQL。这个项目大概有几十个数据访问接口如果用原生MyBatis每个接口都要写对应的XML文件或者注解SQL代码量至少翻一倍。毕业设计的时间本来就紧把时间花在重复的SQL编写上不值得。而且MyBatis-Plus的LambdaQueryWrapper写出来的代码可读性很好答辩时展示代码也拿得出手。3.2 前端两条路线Thymeleaf还是Vue前端是我犹豫最久的部分。服务端渲染用Thymeleaf优点是项目结构简单、不需要跨域、打包部署方便、教程极多缺点是你没法用上组件化开发和前后端分离的架构简历上少了一个可写的技术点。前后端分离用Vue Vite Element Plus优点是架构更现代、页面可以做得更美观缺点是多一套Node环境、联调跨域、打包配置整体工作量至少增加三分之一。我最后的选择是核心页面用Vue 3 Vite Element Plus做单页应用构建产物直接放进SpringBoot的static目录下。这样做既保留了前后端分离的代码结构和开发体验又避开了独立部署Nginx的复杂度。打包后的Vue项目本质上是一堆静态文件SpringBoot默认把classpath:/static/目录作为静态资源根目录直接把dist目录的内容复制进去就能运行。网上关于vue打包放进springboot有很多现成的方案我实测下来最稳妥的做法是在pom.xml里配置maven-resources-plugin把前端dist目录在打包阶段自动拷贝到static目录下这样就不用手动复制了。3.3 项目搭建的关键步骤和依赖配置用IDEA创建SpringBoot项目这一步很简单选Spring InitializrGroup填com.example之类Artifact填poetry-platformJava版本选8。需要手动确认的依赖有三个Spring Web必选、MySQL Driver必选、Lombok强烈建议选上能省掉大量getter/setter代码。Thymeleaf如果最终选了Vue方案就不需要加。pom.xml里除了IDEA生成的官方starter还需要手动加入这几个关键依赖!-- MyBatis-Plus 需要手动指定版本官方starter不包含它 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency !-- JWT 用户认证 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency !-- Hutool 工具类库脱敏、随机数、日期处理等 -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency配置好依赖之后还要在application.yml里设置数据源、MyBatis-Plus的日志和驼峰映射、Jackson的日期格式。这套组合全部跑通之后项目骨架就算搭完了。这里给一个数据源配置的参考里面有一个细节很多人会漏掉spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/poetry_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autoURL里的characterEncodingutf8和serverTimezoneAsia/Shanghai是我用血泪换来的——前者不配全是中文乱码重灾区后者不配你会看到所有时间字段比北京时间少8小时。4. 核心功能实现从注册登录到诗词检索再到评论互动4.1 用户模块JWT登录和密码加密用户模块是所有交流功能的前置条件评论点赞收藏都得知道是谁在操作。登录方案我在Session和JWT之间选了JWT原因很简单前后端分离的项目里Session要处理跨域携带Cookie的问题比较麻烦而JWT只需要前端把token存在localStorage里每次请求在请求头带上Authorization字段即可。密码加密用的是Spring Security里的BCryptPasswordEncoder虽然整个项目我没有引入Spring Security只单独拿了这个类来做加密。千万不能用MD5或SHA直接存密码——这个在答辩时被老师问起来简直就是送分题。我当时的实现是用一个PasswordUtils工具类封装了加密和校验方法所有注册和登录接口都调用它。登录接口的逻辑流程是这样的接收用户名和密码根据用户名查出用户记录校验密码查询用户状态字段是否正常将用户ID和角色放进JWT载荷生成token返回前端。前端拿到token后存入localStorage后续请求在axios拦截器里统一添加请求头。后端再写一个拦截器拦截所有/api/**下的请求校验token有效性并解析出当前用户信息存入ThreadLocal。这里有一个需要提前设计好的点哪些接口需要登录才能访问、哪些接口匿名就能访问。拦截器要做白名单配置比如诗词列表、诗词详情这些浏览类接口必须放行否则游客就什么都看不到了。4.2 诗词模块多条件检索怎么写得优雅诗词检索是内容展示模块的核心能力也是技术上比较好展示的一个点。用户在前台可以按几个维度筛选关键词匹配标题或内容、朝代、作者、标签。如果用原生SQL一条一条拼接WHERE条件代码会非常丑陋且容易漏条件用MyBatis-Plus的LambdaQueryWrapper就能很优雅地解决。我当时的实现思路是把检索条件封装成一个PoemQuery对象Service层用LambdaQueryWrapper按条件动态拼接public PagePoemVO queryPoems(PoemQuery query, int page, int size) { LambdaQueryWrapperPoem wrapper Wrappers.lambdaQuery(); // 关键词标题或内容模糊匹配 if (StrUtil.isNotBlank(query.getKeyword())) { wrapper.and(w - w .like(Poem::getTitle, query.getKeyword()) .or() .like(Poem::getContent, query.getKeyword())); } // 朝代筛选 if (query.getDynasty() ! null) { wrapper.eq(Poem::getDynasty, query.getDynasty()); } // 作者筛选 if (query.getPoetId() ! null) { wrapper.eq(Poem::getPoetId, query.getPoetId()); } // 按浏览量排序实现热门诗词功能 wrapper.orderByDesc(Poem::getViews); // ...分页查询 }这里的wrapper.and(...)是个很容易写错的地方——如果你直接把两个like条件用or()拼到外层wrapper上前一个条件比如朝代过滤和关键词条件会变成并列的OR关系导致查询结果完全不对。用and()把or条件包成一组才能保证关键词组内是OR、关键词组与其他条件是AND的正确逻辑。这种细节问题在答辩时讲出来老老师会认为你真的在认真写代码。4.3 交流模块收藏、点赞、评论的实现细节交流模块是整个平台区别于普通资料库的核心部分。收藏功能的实现核心是唯一索引兜底。在poem_favorite表创建的时候我专门加了UNIQUE KEY uk_user_poem (user_id, poem_id)这样即便代码里忘记判断用户是否已经收藏数据库层面也能拦住重复收藏。点赞表同理。对应的Service方法只需要关注当前状态如果用户传的操作类型是收藏先查是否存在记录存在则删除取消收藏不存在则插入添加收藏。评论模块要比收藏点赞复杂一些因为要支持楼中楼回复。我的实现方案是评论表带一个parent_id字段顶级评论的parent_id为0回复顶级评论时parent_id指向那条评论的ID。查询某首诗词的评论列表时先查出所有parent_id为0的顶级评论再批量查出这些评论下的子评论组装成两级树形结构返回前端。前端里子评论在父评论下方缩进展示这个视觉结构很直观。这里有个性能优化点值得写进论文不要用N1查询也就是不要循环查子评论。批量操作的方式是先拿到顶级评论ID列表然后用IN查询一次拿回所有子评论在内存里按parent_id分组组装。代码量相差不大但查一次跟查N次的差距在数据量上来之后是非常明显的。5. 我实测踩过的坑版本、乱码、热更新、打包后的4045.1 SpringBoot版本和MyBatis-Plus的兼容性陷阱这个坑我愿称之为开局第一课。我的SpringBoot版本一开始选的是3.0.2自带的JDK版本要求17而MyBatis-Plus官方当时还没完全适配SpringBoot 3导致项目启动直接报错Failed to introspect Class。我查了半天资料得到的结论是springboot版本太高的兼容性问题不只是MyBatis-Plus包括其他很多第三方库都没有跟上。解决方案是退回到SpringBoot 2.7.x JDK 8的组合。这里提醒一下如果你在IDEA里创建项目时选择了SpringBoot 3.x就不要再往下硬扛了直接重新生成一个2.7.x版本的项目省时间。JDK 8还是Java生态里兼容性最好的版本所有框架都保证支持。5.2 中文乱码和Thymeleaf热更新中文乱码这个坑其实有两层。第一层是数据库层面的连接字符串没加characterEncodingutf8。第二层是响应层面的排查方法是在浏览器控制台看响应头里的Content-Type。如果是text/html;charsetISO-8859-1八成是漏了配置强制编码的过滤器。SpringBoot里最简单的解法是在application.yml里加server: servlet: encoding: charset: UTF-8 enabled: true force: true设置force: true表示强制所有请求和响应都使用UTF-8这一行配置能解决绝大多数乱码问题。如果你用了Thymeleaf做模板引擎还会遇到一个热更新的问题改了HTML页面刷新浏览器却不生效。这是Thymeleaf默认开启了缓存导致的。解决方案是在application.yml里关掉模板缓存spring: thymeleaf: cache: false并且配合spring-boot-devtools依赖按快捷键重新编译后页面才会刷新。我当时第一次遇到这个情况时以为代码写错了排查了快一个小时最后发现只是缓存问题。5.3 前端打包后放进SpringBoot的404前后端分离开发完成后Vue项目执行npm run build会在dist目录生成一堆静态文件把这些文件复制到SpringBoot的src/main/resources/static目录下启动SpringBoot就能直接访问首页。但此时最经典的问题出现了进入首页没问题但刷新某个子路由页面时SpringBoot返回404。原因很好理解Vue用history模式管理路由是纯前端的URL路径在服务器上没有对应的物理文件。刷新http://localhost:8080/poem/detail/12时SpringBoot找不到名为poem/detail/12的静态资源自然就404了。解决方案是要把SpringBoot的404错误转发到index.html让Vue前端接管路由。我知道网上最常用的做法是实现一个ErrorPageRegistrar把404转发到forward:/index.html但这里有一个大坑——这样做会连后端接口不存在的404也一起转发到首页导致接口调用方拿到的是HTML而不是JSON排查起来很痛苦。我在实际项目里采用了一个更精细的方案写一个拦截器只有请求路径不是/api开头且不存在对应静态资源时才回退到index.html。这样前端路由刷新就正常了而后端API的错误响应还能保持JSON格式。5.4 Docker部署时的时间与初始化问题我最后是用Docker把项目部署起来做的演示。Dockerfile本身不复杂基于openjdk:8-jdk-alpine镜像把jar复制进去暴露8080端口就行。但有个坑让我折腾了一晚上容器运行后接口返回的时间全部多了8小时。查了半天发现是容器内默认时区是UTC不是东八区。解决方案是在Dockerfile里加一条命令FROM openjdk:8-jdk-alpine RUN apk add --no-cache tzdata \ cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ echo Asia/Shanghai /etc/timezone COPY target/poetry-platform.jar /app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]顺便说一句如果你数据库也打算用Docker跑MySQL容器初始化SQL脚本需要在第一次启动时挂载到/docker-entrypoint-initdb.d/目录下。这个机制只会首次启动时生效后面再挂载也不会重新执行。所以数据库初始化的SQL脚本最好在项目文档里单独说明方便别人手动导入。6. 源码交付和答辩演示别让项目死在最后一公里6.1 README和初始化脚本要写成什么样附源码是这个项目标题里很关键的一部分。我发现很多同学到答辩之前才想起来源码还没整理一堆代码里塞满了测试文件、临时日志、注释掉的废弃代码这种交付物的印象分很不好。我的做法是单独开一个项目说明文档按顺序写清楚这几件事项目简介和技术栈、环境要求、快速启动步骤建库、导入SQL、改配置文件、启动、默认管理员账号、功能清单和界面截图。SQL脚本我单独放在sql目录下文件命名要带版本号比如v1.0-init.sql这样别人也好知道这是初始版本而不是改了半截的残留。源码目录结构也要做整理。Controller、Service、Mapper的分层结构要保持清晰删掉系统生成的无用测试类和数据文件。如果是前后端分离的项目前端源码和后端源码要放两个目录并且各自带独立的README。我以前见过把node_modules整个传给老师或者把target目录一起打进压缩包的——这种低级错误特别影响印象分。压缩包之前先看一遍文件大小target目录和node_modules加起来动不动几百兆全清掉之后一般就剩两三兆这样才是一个干净的源码包。6.2 演示路径的编排答辩演示是最终成品的高光时刻但大多数人都是上台之后才开始想先点哪里。我的建议是提前规划一条讲故事的演示路径先以游客身份打开首页展示热门诗词列表和朝代分类点击进入一首诗词的详情页展示注释、译文、赏析三个内容块的展示效果此时游客想评论触发登录流程展示注册登录功能登录后完成收藏、点赞、发表评论的操作再进入个人中心验证数据确实是我的最后切到管理员账号进入后台演示诗词的增删改查和评论审核。这条路径顺着业务逻辑走每个环节都能解释得清。特别提醒一个细节演示前把数据库里的脏数据清理掉评论内容不要有测试输入的乱码诗词数据尽量导入一些真实且完整的古代作品。我当时把《将进酒》《水调歌头》这些名篇的注释和译文认认真真补齐了演示的时候打开页面那一瞬间内容完整度本身就是一种说服力。6.3 老师常问的问题和我的回答思路实时答辩质询环节问题基本集中在几个方向一是为什么用SpringBoot——回答落在自动配置、起步依赖、内嵌容器让部署更简单这几个点上springboot自动装配原理这个知识点要能讲得清SpringBoot通过EnableAutoConfiguration配合spring.factories文件里的自动配置类按条件注解ConditionalOnClass判断依赖是否存在再注入Bean这个原理是被问的高频点二是JWT和Session的区别——回答落在无状态和不可扩展的差异上面JWT的token不依赖服务端存储分布式环境下天然支持水平扩展三是数据库为什么这样设计索引——回答落在unique联合索引防止重复数据和text字段存储长文本上。答题思路可以围绕设计到实现的链路展开不用背文档让老师感受到你确实在动手写的就稳了。还有一个小经验主动给老师看代码里自己写得比较满意的部分比如LambdaQueryWrapper的封装逻辑、评论子查询的组装方法。主动展示比被动防御更能掌握对话节奏。把项目从毕设变成真正自己的东西整个项目做下来我最大的体会是毕业设计的意义在过程而不是结果。从选题开始到最终演示我等于从头到尾走了一遍真实项目的完整流程需求分析、数据库设计、接口设计、前端联调、部署上线。GitHub上那些教程只会告诉你怎么做但只有自己踩过版本冲突、乱码、404之后你才会真正理解为什么这么做。最后分享一个小建议别在毕设结束后就把代码扔到硬盘角落吃灰。花半天时间把项目重新整理一下去掉学校相关信息写一份干净的README推到代码托管平台上。答辩之后我把它顺手更新了一下把封面图和配色调整过业界的人看到项目时第一印象好了很多。这个项目后续可以扩展的地方还有很多接入大模型API做诗歌智能赏析、用Elasticsearch替换MySQL做全文搜索、加一个每日推荐模块……这些想法如果在毕设阶段时间不够完全可以留到后续慢慢迭代。一个你真正亲手做完并且还在持续维护的项目才是毕业设计给你留下的最值钱的东西。