做毕设或者课程设计选到这个题目的朋友,我猜你大概率是两种情况之一:SpringBoot刚学了个大概、想通过一个完整项目把全家桶串起来,或者手头已经有一套参考源码,但对着“源码+lw+部署文档+讲解”这套交付物不知道从哪下手。基于SpringBoot的中国古诗词学习平台系统,本质上是一个典型的Web信息管理类项目,用户端做诗词的浏览、搜索、收藏和学习记录,管理端做内容维护和用户管理,技术栈以SpringBoot为核心,配MySQL数据库和一套前端页面。这类项目最大的价值在于:业务逻辑不复杂,但涵盖了一个企业级Java后端项目从建表、写接口、鉴权到打包上线的完整链路,非常适合作为毕业设计或课程设计的载体。
我接触过不少拿这套题目的同学,也帮人排查过各种部署问题。这篇就把整个项目的设计思路、核心模块、实操过程和踩坑记录完整写下来,给准备做类似系统的人一条可以直接走的路线。我会尽量把每个关键点背后的“为什么”讲清楚,而不只是贴代码。
1. 项目整体设计与技术选型
1.1 业务需求拆解:这个系统到底要做什么
古诗词学习平台,这个词听起来很文化,拆开看其实就是一个“内容浏览+用户管理”的系统。先理清楚使用者是谁,再决定做哪些功能。
普通用户的需求很直接:打开网站能看到诗词列表,按朝代、作者、分类筛选,想找某首诗可以直接搜关键词,点进去看详情(原文、注释、译文、赏析),觉得好的可以收藏,学过之后可以记录自己的学习进度。管理员的需求则是另一套:登录后台,维护诗词数据(新增、修改、删除),管理分类和作者信息,必要的时候可以查看用户列表、禁用违规账号。
这里要注意一个设计原则:前台和后台的页面、接口最好分开考虑,但共用一个数据库和一套后端服务。很多初学者容易犯的错是把管理功能直接堆在用户页面里,或者干脆不做后台,只留一个数据库脚本让老师手动插数据。这两种都会在答辩时被问住。标准做法是用户端正常展示,管理端单独走一套路径。
1.2 技术栈选型:为什么是SpringBoot + MySQL + Vue
这套项目端到端的技术选型,本质上是围绕“毕业设计能讲清楚、能部署起来、代码量适中”这三个目标来定的。
- SpringBoot:不需要纠结Tomcat配置、Spring配置文件的繁琐整合,一个注解启动项目,这对时间有限的毕设党是最友好的。版本上建议用SpringBoot 2.7.x,不要盲目追最新版。热词里有个“springboot版本太高”的搜索词,说明很多人被3.x版本坑过——SpringBoot 3.x要求JDK17,很多学校的机房和服务器还停留在JDK8,你写代码用的是新语法,部署机器跑不起来,这种事在答辩前一周炸出来是真的想哭。
- MySQL:5.7或8.0均可,推荐8.0。8.0的驱动类名是
com.mysql.cj.jdbc.Driver,和5.x的写法不同,后面我在配置部分会单独说。 - MyBatis-Plus:相比纯MyBatis,它自带单表CRUD方法,分页插件也集成好了,写代码能少一半。答辩时若被问到底层原理,也答得上“它是在MyBatis基础上做增强,没有侵入原框架”。
- 前端方案“学的时候也能顺便把Vue的打包部署摸一遍”,这点在很多公司里确实是真实需求。选Vue方案就一定要学会把打包后的dist目录放进SpringBoot的
resources/static,或者用Nginx转发,否则前后端分离环境在答辩现场很容易翻车。 - 数据库连接池:默认的HikariCP就行,出题老师问起来,你就说它性能好、SpringBoot默认集成,不需要额外引入。
1.3 功能模块清单:照着这张表做不会漏
| 模块 | 功能点 | 说明 |
|---|---|---|
| 用户模块 | 注册、登录、退出 | 登录成功后返回Token或记录Session |
| 诗词浏览 | 列表展示、分页 | 按时间或热度排序 |
| 分类筛选 | 按朝代、作者、分类查询 | 可用下拉框或多条件组合查询 |
| 诗词搜索 | 标题/作者/内容模糊搜索 | 关键词非空时动态拼接SQL |
| 诗词详情 | 原文、注释、译文、赏析 | 详情页单独接口 |
| 收藏管理 | 收藏/取消收藏、收藏列表 | 关联用户ID和诗词ID |
| 学习记录 | 记录学习进度、历史列表 | 可选,但如果做了绝对是加分项 |
| 后台管理 | 诗词CRUD、分类管理、用户管理 | 管理员登录后操作 |
功能不用贪多,把上面这张表做扎实,配合文档就已经是完整度很高的系统了。关键是每张表之间的外键关系要理清楚,后面建表才不会乱。
2. 数据库设计与后端架构
2.1 数据库建模:五张表的核心设计
这套系统的核心数据模型,我用五张表就能覆盖绝大部分场景。设计的时候记住一个原则:表结构宁可稍微冗余,也别过度设计,保证CRUD顺手最重要。
用户表(sys_user)
字段包括id、username、password、nickname、avatar、role(0表示普通用户,1表示管理员)、status(是否禁用)、create_time。密码不能存明文,建议用MD5或BCrypt做加密。曾见过一个同学的源码里直接把密码明文存在数据库里,被老师质疑安全问题,现场很尴尬。
诗词表(poem)
这是核心表。字段有id、title(标题)、author(作者)、dynasty(朝代)、category_id(分类外键)、content(原文)、translation(译文)、annotation(注释)、appreciation(赏析)、create_time。创建索引时一定要给title和author加上普通索引,因为搜索功能主要就是查这两个字段,数据量上来之后全表扫描会明显变慢。
分类表(category)
id、name、description。用于管理“唐诗”“宋词”“元曲”等分类,也可以细分到“边塞诗”“田园诗”,看你自己想怎么划分。
收藏表(favorite)
id、user_id、poem_id、create_time,联合唯一索引(user_id, poem_id),防止同一个用户重复收藏同一首诗。
学习记录表(study_record)
id、user_id、poem_id、learn_time、note(用户写的笔记)。这个表是可选的,但加上之后整个项目的功能纵深就出来了,答辩时老师会觉得你有完整的业务思考。
初始化数据不要自己手打几百条,写一个SQL脚本,找一些公开的古诗词数据集转成INSERT语句,100首打底、300首更好,列表和搜索的效果才会真实。网上有不少现成的SQL数据文件,导入前记得检查编码,统一用utf8mb4,否则中文乱码会折磨你很久。
2.2 后端分层与接口规范
SpringBoot项目最忌讳把所有逻辑堆在Controller里。标准做法是四层结构:Controller接收参数——Service处理业务——Mapper操作数据库——Entity映射表结构。
统一返回结果集是一件值得一开始就做好的事情。定义一个Result类,包含code、msg、data三个字段,所有接口都返回这个对象。这样前端处理数据时只需要判断code是否为200,业务逻辑清爽很多。别让每个接口返回不同的JSON结构,后期维护的时候会想骂人。
分页接口的设计也要统一。接收pageNum和pageSize两个参数,返回total(总数)、list(当前页数据)、pages(总页数)。MyBatis-Plus的Page对象天然支持这套结构,直接用就行。
2.3 登录鉴权:Session还是Token
很多毕设项目在登录鉴权上很纠结,其实就看你的前端方案。
- 前后端分离(Vue单独跑):用JWT Token。用户登录成功后,后端生成一个Token返回给前端,前端存在localStorage里,后续每个请求在Header里带上
Authorization: Bearer token。后端用一个拦截器或过滤器统一校验。 - 前后端不分离(Thymeleaf模板):用Session就够了,登录成功后把用户信息放进session,需要登录的接口用拦截器判断session是否为空。
拦截器里要注意一个坑:必须放行登录接口、注册接口、静态资源路径(比如/css/**、/js/**、/images/**),否则前端页面会全部404或者无限重定向。放行列表用Ant表达式,写/api/user/login、/api/user/register,再放行静态资源,其余接口一律校验Token。
2.4 搜索和筛选的动态SQL处理
搜索功能看起来简单,写起来最容易出现“SQL拼接字符串漏洞”或“空条件报错”。推荐直接用MyBatis-Plus的LambdaQueryWrapper,用条件判断动态拼接。
比如用户输入了关键词才去查title和author,没输入就查全部;选了朝代才拼dynasty条件。用StringUtils.hasText()做空判断,避免模糊查询时传入空字符串导致查询结果不对。分页查询用Page对象配合IPage返回,比手写LIMIT更安全、更规范。
3. 实操过程与核心功能实现
3.1 快速搭建SpringBoot项目骨架
直接用IDEA的Spring Initializr创建项目,选好Maven和JDK版本。依赖上建议勾选:
- Spring Web
- MySQL Driver
- Lombok(如果你们允许用,能少写大量getter/setter)
- Validation(参数校验)
pom.xml里手动加MyBatis-Plus依赖(它的mybatis-plus-boot-starter没有进Initializr的可选项,需要自己填)。版本选择要和SpringBoot版本兼容,我用SpringBoot 2.7.x配的是mybatis-plus 3.5.x,实测很稳。
项目目录建议这样建:
com.example.poetry ├── controller // 接口层 ├── service // 业务层 │ └── impl ├── mapper // 数据访问层 ├── entity // 实体类 ├── config // 配置类(拦截器、跨域、MyBatis-Plus分页) ├── common // 统一返回结果、异常处理、工具类 └── PoetryApplication.java // 启动类结构清晰是给老师看的,也是给自己看的。项目拖到后期如果包结构乱成一团,改一个功能要找半天文件,心态特别容易崩。
3.2 核心配置与数据库连接
application.yml是整个项目的命脉,80%的启动失败都是这里配错了。给一个可以直接抄的模板:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/poetry?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_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: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有几个关键细节:
characterEncoding=utf8mb4,不是utf-8,MySQL的utf8mb4才完整支持emoji和所有生僻字,古诗词里繁体字和生僻字不少,用错编码会出现问号乱码。serverTimezone=Asia/Shanghai必须加,否则MySQL 8.0默认时区会让时间字段差8小时,或者直接报警告。map-underscore-to-camel-case: true开起来后,数据库的create_time字段能自动映射到Java的createTime属性,不用手写一堆@TableField的别名校验。log-impl在开发阶段打开,能直接在控制台看到每次执行的SQL,排查问题非常爽,部署上线前建议关掉或改为不输出。
3.3 诗词列表分页和搜索接口的实现
列表分页和搜索是整个项目被调用最频繁的接口,也最能体现代码功底。Controller层接收前端传的参数,Service层组装查询条件。
接口定义:
@GetMapping("/poem/page") public Result page(@RequestParam(required = false) Integer pageNum, @RequestParam(required = false) Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) String dynasty, @RequestParam(required = false) Integer categoryId) { if (pageNum == null) pageNum = 1; if (pageSize == null) pageSize = 10; return Result.success(poemService.getPoemPage(pageNum, pageSize, keyword, dynasty, categoryId)); }Service实现:
@Override public IPage<Poem> getPoemPage(int pageNum, int pageSize, String keyword, String dynasty, Integer categoryId) { Page<Poem> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Poem> wrapper = Wrappers.lambdaQuery(); // 关键词搜索:标题或作者 if (StringUtils.hasText(keyword)) { wrapper.and(w -> w.like(Poem::getTitle, keyword).or().like(Poem::getAuthor, keyword)); } if (StringUtils.hasText(dynasty)) { wrapper.eq(Poem::getDynasty, dynasty); } if (categoryId != null) { wrapper.eq(Poem::getCategoryId, categoryId); } wrapper.orderByDesc(Poem::getCreateTime); return poemMapper.selectPage(page, wrapper); }注意keyword搜索的写法,用and(w -> w.like(...).or().like(...))包起来,否则多个条件叠加时SQL拼接逻辑会出错。分页插件必须配置好MybatisPlusInterceptor,否则Page只会返回全部数据,分页根本不起作用。
3.4 收藏功能的幂等设计
收藏是个看起来简单但容易出Bug的功能:用户连续点两次“收藏”,数据库出现两条相同记录,取消收藏又取消不掉。
我的做法是:先查favorite表是否已有该用户和该诗词的记录,有则返回“已收藏”,没有则新增。删除时只删当前用户在当前诗词下的那条记录。接口层面加个校验,防止用户传别人的userId去查或删数据。
@PostMapping("/favorite/add") public Result addFavorite(@RequestBody Favorite favorite) { LambdaQueryWrapper<Favorite> wrapper = Wrappers.lambdaQuery(); wrapper.eq(Favorite::getUserId, favorite.getUserId()) .eq(Favorite::getPoemId, favorite.getPoemId()); if (favoriteMapper.selectCount(wrapper) > 0) { return Result.error("不能重复收藏"); } favoriteMapper.insert(favorite); return Result.success("收藏成功"); }批量操作时注意用户只能查自己的收藏列表,SQL里必须带上user_id条件,这是安全上的低级坑,但每年都会有人踩。
3.5 后台管理的增删改查与权限保护
后台接口建议单独用/admin前缀,并在拦截器里做角色校验——只有role=1的管理员才能访问。这是很多人忽略的细节:用户能直接调用后台删除诗词的接口,整个系统就形同虚设了。实现不复杂,写一个AdminInterceptor,在preHandle里从Token或Session解析出用户信息,判断角色即可。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (!StringUtils.hasText(token)) { throw new RuntimeException("未登录"); } // 解析token获取用户信息,这里按你自己的方案取 User user = userService.getUserFromToken(token); if (user == null || user.getRole() != 1) { response.setStatus(403); return false; } return true; }值得注意的是,/admin接口放在拦截器里后,务必注册到拦截器配置中时,排除掉登录接口和静态资源,否则后台页面也会一起被拦。实际项目中我见到过后台轮播图加载不出来的情况,最后定位到是静态资源被拦截了。
3.6 前端整合:Vue打包进SpringBoot
很多人的项目源码里,前端是单独的Vue工程,后端是SpringBoot工程,两者分开跑没问题,但交付时要求“一个jar包跑起来”,这就涉及前端静态资源整合。
Vue项目打包前,修改vue.config.js,把publicPath设为'./'而不是默认的'/',否则资源路径会找不到。打包后会生成dist目录,里面有index.html、static或assets等子目录,直接把整个dist里的内容复制到SpringBoot的src/main/resources/static/下。打包SpringBoot时,Maven会把static目录一并打进去。
启动后端后访问http://localhost:8080/,就能直接看到前端页面。注意如果你在前端用axios请求接口时写死了http://localhost:8081之类的地址,生产环境就会跨域,建议用相对路径/api/...或者配置好CORS。
3.7 本地打包与服务器部署全流程
这部分是最容易让新手崩溃的环节。完整流程走通一次,后面的问题基本都能迎刃而解。
第一步,在项目根目录执行Maven清理和打包命令:
mvn clean package -DskipTests打包成功后在target/下会生成一个xxx.jar文件。这里有个命令行的坑:如果你是在Windows下打包,拿到服务器上是Linux环境,Java路径和文件编码都可能不同。执行前确认JDK版本一致,我见过同一个人本地JDK8打包,服务器上是JDK11,运行直接报UnsupportedClassVersionError。
第二步,上传jar包到服务器,使用nohup后台运行:
nohup java -jar poetry-system.jar > log.log 2>&1 &这样退出SSH后程序还能继续跑。查看日志用tail -f log.log,启动失败时能第一时间看到堆栈信息。
第三步,配置MySQL数据库。先把SQL脚本导入到服务器MySQL中,记得检查和本地数据库的用户名、密码、端口是否一致,不一致就在application.yml里改,或者用外置配置文件覆盖。
4. 常见问题与排查技巧实录
4.1 数据库连接与编码问题
这类问题占了毕设调试过程中至少一半的比重。下面是我整理的高频问题排查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
启动报ClassNotFoundException: com.mysql.cj.jdbc.Driver | pom.xml里没有MySQL驱动,或版本不匹配 | 检查依赖,SpringBoot 2.7用8.x驱动即可 |
中文显示为?? | 数据库表不是utf8mb4编码 | ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
| 时间字段少8小时或报时区错误 | 连接URL缺serverTimezone | 在URL后加?serverTimezone=Asia/Shanghai |
| 连接超时 | 服务器防火墙未放行3306端口 | 云服务器安全组放行MySQL端口,并确认数据库配置允许远程连接 |
4.2 启动失败的常见原因
端口被占用是新手遇到的第一道坎。启动时提示Port 8080 was already in use,说明有别的程序占了8080端口。Windows下用netstat -ano | findstr 8080找到占用进程的PID,去任务管理器结束进程,或者在application.yml里换一个端口。
Maven依赖下载失败、卡住很常见。国内下载SpringBoot相关依赖,建议配置阿里云镜像仓库,在settings.xml里加:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>这能省下大量等待时间。还有那种SpringBoot版本太高导致JDK不兼容的问题,如果你是JDK8,就回退到2.7.x,别硬刚。
4.3 接口调通但页面数据不显示
这种情况往往不是后端问题,而是前端请求地址不对或跨域。打开浏览器F12看Console和Network,一目了然。
如果是跨域报错,在后端加一个全局CORS配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }加了之后前端跨域请求就能正常发出。但要注意allowCredentials(true)时allowedOrigins("*")会失效,要用allowedOriginPatterns代替,这个坑我见过不止一次。
4.4 打包后静态资源404
Vue打包放进SpringBoot后找不到JS和CSS,最常见的两个原因:
publicPath配置错误。Vue默认的publicPath是'/',打包后资源路径是/js/chunk.js,部署在非根路径下会404,改成'./'用相对路径就好。- SpringBoot拦截器放行了
/api/**但没放行静态资源,页面本身加载了,JS却被拦截器拦住了。在拦截器排除列表里加上/static/**、/css/**、/js/**、/img/**、/favicon.ico等路径。
还有个偏门的坑:IDEA中Maven打包时没有把resources/static里的资源打进去。检查pom.xml是否配置了<resources>标签且指定了src/main/resources,确认一下范围,别把资源目录漏掉。
4.5 日志定位:控制台SQL输出是神器
MyBatis-Plus配置了StdOutImpl之外,开发时可以在application.yml开启SQL日志。查数据查不到、更新失败,统一先看控制台打印的SQL语句,再倒推到参数传值问题。
排查问题的顺序建议:先看日志堆栈异常信息,再看SQL语句,最后看前端请求是否正确。很多人一报错就满屏找“Exception”,其实日志开头那几行就写清楚了。把这个问题想清楚,能少走很多弯路。
我的几个实操体会
这类带完整交付物的项目,真正考验人的往往不是写代码本身,而是把文档、部署、演示这整条链路走通。我印象里最深的一件事是有个同学本地跑得好好的,答辩前一晚部署到学校服务器,页面英文正常、中文全变成问号,后来发现是导入SQL文件时没有指定--default-character-set=utf8。所以数据初始化那一步,务必确认数据库、表、连接串三处的字符集一致。这个坑很小,但炸起来真的非常影响心态。
另外建议把项目的README写好,写明环境要求(JDK版本、MySQL版本、Node版本)、启动步骤(导入SQL、改配置、运行jar)、默认账号(admin用户和管理员账号)。这份东西既是给老师看的,也是给你自己留的“急救手册”。几个人做同一个项目的话,互相传阅时顺手很多。
最后说一句,如果时间充裕,试着在诗词详情页加一个“作者生平”或“相关推荐”,后端多一个扩展接口就行。这种小功能不会增加太多负担,但在答辩展示环节非常出彩,能让老师觉得你在做产品而不是在应付作业。