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, status | content存全文,用TEXT类型;notes(注释)、translation(译文)、appreciation(赏析)单独列存储,这样详情页可以用Tab切换展示;views记录浏览量,用于热门排序 |
| poem_comment 评论表 | id, poem_id, user_id, parent_id, content, status, create_time | parent_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 Initializr,Group填com.example之类,Artifact填poetry-platform,Java版本选8。需要手动确认的依赖有三个:Spring Web(必选)、MySQL Driver(必选)、Lombok(强烈建议选上,能省掉大量getter/setter代码)。Thymeleaf如果最终选了Vue方案就不需要加。
pom.xml里除了IDEA生成的官方starter,还需要手动加入这几个关键依赖:
<!-- MyBatis-Plus 需要手动指定版本,官方starter不包含它 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <!-- JWT 用户认证 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <!-- Hutool 工具类库:脱敏、随机数、日期处理等 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autoURL里的characterEncoding=utf8和serverTimezone=Asia/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 Page<PoemVO> queryPoems(PoemQuery query, int page, int size) { LambdaQueryWrapper<Poem> 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的顶级评论,再批量查出这些评论下的子评论,组装成两级树形结构返回前端。前端里子评论在父评论下方缩进展示,这个视觉结构很直观。
这里有个性能优化点值得写进论文:不要用N+1查询,也就是不要循环查子评论。批量操作的方式是,先拿到顶级评论ID列表,然后用IN查询一次拿回所有子评论,在内存里按parent_id分组组装。代码量相差不大,但查一次跟查N次的差距,在数据量上来之后是非常明显的。
5. 我实测踩过的坑:版本、乱码、热更新、打包后的404
5.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热更新
中文乱码这个坑其实有两层。第一层是数据库层面的,连接字符串没加characterEncoding=utf8。第二层是响应层面的,排查方法是在浏览器控制台看响应头里的Content-Type。如果是text/html;charset=ISO-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做全文搜索、加一个每日推荐模块……这些想法如果在毕设阶段时间不够,完全可以留到后续慢慢迭代。一个你真正亲手做完并且还在持续维护的项目,才是毕业设计给你留下的最值钱的东西。