我不是中介,也不搞“赠送源码”那套。但这几年帮人审过不少Spring Boot毕设项目,也带过几个徒弟,说实话,“阅享小说阅读平台”这一类题目,属于典型的中规中矩、性价比高、不容易翻车的选择。它不花哨,但覆盖的技术点足够撑起一篇像样的毕业论文,也足够在答辩时让老师问不倒你。
这篇文章就从这个项目出发,聊聊为什么推荐它、它到底要做哪些功能、核心表结构怎么设计、后端怎么搭、调试部署有哪些坑,以及最重要的——答辩时老师会盯上哪里。内容完全基于Spring Boot 2.7 + MyBatis-Plus + MySQL + Redis + Vue 3这套常见毕设组合,适合基础一般、想要稳妥过关的同学参考。
1. 为什么这类项目适合做毕设:定位与工作量分析
选毕设题目,第一原则不是“高大上”,而是“能做出来、能写清楚、能讲明白”。小说阅读平台正好卡在这个平衡点上。下面拆开说。
1.1 从技术栈看:Spring Boot相关知识点覆盖刚好够用
Spring Boot作为当前Java后端开发的主流框架,在毕设里承担的是“地基”角色。这类小说平台项目,天然要求你做这几件事:
- 用户注册登录(涉及Spring Security或JWT、拦截器、密码加密)
- 小说分类、搜索(涉及MyBatis-Plus的条件构造器、模糊查询)
- 书架收藏(涉及关联表设计、事务管理)
- 阅读记录与评论区(涉及多表联查、分页插件)
- 后台管理(涉及角色权限、文件上传、数据统计)
这一套下来,Spring Boot的自动配置、依赖注入、AOP日志、统一异常处理、参数校验等核心特性基本全用上了。老师问“Spring Boot的自动配置原理是什么”,你完全可以用项目里的实际配置来回答,这是实打实的加分项。
工作量上,如果是一个人从零开始,包含前端页面、后端接口、数据库设计和论文撰写,集中精力大概需要三到五周。这对于一个学期的毕设周期来说,不算紧张,也不至于糊弄。
1.2 从业务场景看:小说阅读的需求是真实且完整的
和那些“XX管理系统”相比,小说平台有一个天然优势:业务逻辑真实存在,用户行为链路完整。从游客浏览、用户注册、充值阅读(可选)、收藏打赏,到后台的小说上下架、章节管理、数据统计,每一环都有明确的业务含义,不像纯CRUD的管理系统,做起来索然无味,答辩时也容易被评委质疑“需求来源是什么”。
而且小说平台自带“高并发阅读”这个延伸点,即使你实现不了高并发,也可以在论文里写清楚“未来可以通过Redis缓存热点章节、使用CDN加速静态资源”,这在研究意义和展望部分是一个很好的加分项。
补充说明:如果你觉得纯小说平台有点单薄,可以在这个基础上增加一个“用户阅读时长统计”或者“基于分类的推荐列表”功能,不需要做复杂的推荐算法,系统根据分类浏览量排序推荐即可,这种轻量级亮点既能体现思考深度,又不会把实现难度拉到失控。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能模块拆解:从用户端到管理端的闭环设计
一个完整的阅读平台,一定包含用户端、管理端两个大的子系统。梳理清楚这个边界,后面的表设计和接口设计都不会乱。
2.1 用户端功能群
用户端是给读者用的,功能上要覆盖一条完整的阅读路径。
- 注册与登录:手机号或邮箱注册登录,密码用MD5加盐或BCrypt加密存储,登录后返回JWT令牌。为了防止无脑刷评论,可以加上验证码,用Hutool工具包生成即可。
- 小说浏览:首页展示轮播图推荐位、分类列表(玄幻奇幻、都市职场、悬疑灵异、历史军事等)、热销榜、新书榜。列表页需要支持关键词搜索和条件筛选(按分类、按状态如连载中/已完结、按更新时间排序)。
- 小说详情页:展示封面、作者、简介、状态、章节数量、点击量。读者可以收藏、加入书架,也可以在此页查看评论区。
- 阅读页:本章节正文按章加载,会话级记录阅读历史。这里有两个不错的细节:上一章/下一章跳转、目录侧滑栏、字体大小调节、夜间模式。别小看这些,答辩时一句“体感优化”就能聊一阵。
- 书架与历史记录:书架管理收藏的小说,支持移除操作;历史记录保存最近阅读的章节,方便一键回跳。
- 个人中心:头像上传、昵称修改、密码修改、阅读偏好设置。
2.2 管理端功能群
管理端是给网站运营人员用的,不需要太花哨,但每个功能都要能跑到。
- 数据看板:展示用户总数、小说总数、今日新增评论数、总点击量,可以用ECharts画几个简单的折线图和饼图。
- 小说管理:录入小说基本信息,上传封面,将小说关联到分类;上下架操作;批量导入章节(支持TXT章节正文粘贴提交后分章保存)。
- 章节管理:章节的增删改查,章节排序调整,已发布章节推送至Redis缓存热点数据。
- 用户管理:查看用户列表、禁用账号、重置密码。
- 评论审核:查看读者评论,进行隐藏或删除处理。
- 分类管理:对小说分类进行维护,通常一级分类就够用。
2.3 非功能性需求
这部分往往被学生忽略,但毕设论文里必须写清楚,答辩也常从这上面展开:
- 性能需求:首页接口要求响应时间小于1秒。实测可以用后端缓存(Redis缓存小说详情、轮播图数据)优化。
- 安全需求:登录接口令牌校验,管理端接口统一鉴权,防止越权操作。
- 可用性需求:关键表单有校验提示,上传文件有大小和格式限制。
模块边界清楚了,接下来就要落表。表结构设计的合理程度,直接决定你后期写SQL开不开心。
3. 数据库设计:核心表结构要这样规划才能少走弯路
我用的是MySQL 8.0。表名采用下划线命名,字段采用骆驼峰下的下划线风格,后面写MyBatis-Plus映射时会非常顺畅。下面是这个项目最核心的几张表,以及对应设计的思考过程。
3.1 用户表(t_user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 唯一,登录名 |
| password | varchar(100) | BCrypt哈希后的密码 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像地址 |
| varchar(100) | 邮箱,可空 | |
| phone | varchar(20) | 手机号,可空 |
| status | tinyint | 0正常 1禁用 |
| create_time | datetime | 创建时间 |
注意:password字段不要设成默认值,也不要做成“明文”。这里使用BCrypt不是装模作样,Spring Security自带的BCryptPasswordEncoder就能做,但如果你没接Security,用Hutool的BCrypt工具类也行,几行代码的事。答辩如果被问到“为什么不用MD5”,你回答“MD5存在彩虹表风险,BCrypt自动加盐且迭代计算成本高”就可以了。
3.2 小说表(t_novel)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(100) | 小说名 |
| author | varchar(50) | 作者名 |
| category_id | bigint | 关联分类表 |
| intro | text | 简介 |
| cover_url | varchar(255) | 封面图地址 |
| status | tinyint | 1连载中 2已完结 |
| is_publish | tinyint | 0下架 1上架 |
| click_count | int | 点击量 |
| chapter_count | int | 章节数 |
| update_time | datetime | 最后更新时间 |
一个容易犯的错:把点击量、章节数这些统计字段实时count计算。随着数据量增长,这种写法会让首页列表接口越来越慢。更合理的做法是在阅读接口里使用update语句对click_count做+1操作,chapter_count则可以在发布新章节时同步更新。这种设计思路,论文里写清楚就是亮点。
3.3 章节表(t_chapter)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| novel_id | bigint | 关联小说 |
| title | varchar(100) | 章名 |
| content | longtext | 正文 |
| sort_order | int | 排序编号 |
| words_count | int | 字数 |
| create_time | datetime | 创建时间 |
排序字段sort_order非常关键,因为它决定了“上一章/下一章”和“目录顺序”的实现方式。你后面写跳转SQL时,直接where novel_id = ? and sort_order < 当前章的sort_order order by sort_order desc limit 1,即可快速定位上一章,没必要搞链表式的前后章id字段。
正文用longtext,一个正常的网络小说章节,字数在2000到4000字之间,这个类型完全够用。存储过程不需要,也别在一张表里整成JSON数组。
3.4 书架表(t_bookshelf)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 关联用户 |
| novel_id | bigint | 关联小说 |
| create_time | datetime | 加入时间 |
书架表是典型的联合唯一索引场景,需要加上unique key (user_id, novel_id),避免重复收藏。这类表还有一个作用:作为“收藏总数”的数据源。
3.5 评论表(t_comment)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| novel_id | bigint | 关联小说 |
| user_id | bigint | 关联用户 |
| content | varchar(500) | 评论内容 |
| status | tinyint | 0待审核 1已通过 |
| create_time | datetime | 评论时间 |
评论表是后期最容易“捡了芝麻丢西瓜”的表。建议一开始就把status状态字段带上,即使你暂时不做审核功能,也要留好扩展点。
3.6 表之间的关系说明
- 小说表 与 分类表:多对一,小说表存category_id;
- 章节表 与 小说表:多对一,章节表存novel_id;
- 书架表 与 用户表、小说表:多对一;
- 评论表 与 用户表、小说表:多对一。
画ER图的时候,按这个关系画,就能生成标准的毕业设计ER图。
这里可以同步提一个开发细节:Tinyint类型的字段,在Java实体类里建议统一使用Integer接收,不要用Boolean。原因是MyBatis-Plus的默认转换里,Boolean和Tinyint能对上,但遇到一些不是0/1的状态枚举时(比如0待审核、1已通过、2拒绝),Boolean就不够用了。统一Integer,后面扩展枚举状态时你会感谢这个决定。
4. 后端技术选型与核心逻辑实现:为什么要这样搭配
4.1 为什么是Spring Boot 2.7,而不是Spring Boot 3
这一点很关键。目前很多教程已经切到Spring Boot 3了,但在毕设场景下我仍然推荐Spring Boot 2.7.x。原因很简单:
- Spring Boot 3要求JDK 17,而大部分学校机房、答辩演示环境装的是JDK 8或JDK 11;
- MyBatis-Plus当前版本对Spring Boot 2.x的兼容性非常成熟,但Spring Boot 3需要在引入时注意mybatis-plus-spring-boot3-starter版本号,新手很容易在这里掉进依赖地狱;
- 网上绝大多数现成资料和工作中的面试题都围绕Spring Boot 2.x展开,遇到问题好查到答案。
对应的依赖栈是:Spring Boot 2.7.18 + MyBatis-Plus 3.5.3.x + MySQL 8.0 + Redis 2.7.x + Hutool 5.8.x。
补充一点:Java版本就锁在JDK 8。不要觉得老,Spring Boot 2.7官方就是面向JDK 8设计的,文档、回答、资料全部顺畅。
4.2 核心接口实现亮点:登录鉴权与统一异常处理
登录这块,建议不拉全量Spring Security,而是用JWT + 拦截器(HandlerInterceptor)实现。理由是:Security虽然强,但配置复杂,毕设周期内新人学起来容易卡在配置上,而且你在论文里把JWT原理写清楚,比写“我引入了Security但没怎么自定义”更有说服力。
接口流程大致为:
- 用户提交用户名密码,后端校验通过后,用jjwt库生成token,设置7天过期时间;
- 前端把token存在localStorage中,每次请求放入header的Authorization属性;
- 后端自定义拦截器解析token,将userId写入ThreadLocal,供Service层取用;
- 放行白名单:/api/user/login、/api/user/register、/api/novel/list、/api/novel/detail/**。
统一异常处理是必写的。定义一个GlobalExceptionHandler,用@RestControllerAdvice包裹MethodArgumentNotValidException(参数校验错误)、BizException(自定义业务异常)、Exception(兜底错误),返回统一JSON结构。这个结构里,code、msg、data三个字段固定好,哪怕出错了前端也能拿到友好提示。
4.3 章节阅读与缓存策略
阅读接口是系统里调用最频繁的接口。如果每个请求都去MySQL里捞一遍长文本,后期压力大。在毕设层面,你可以做这个简单但有效的策略:
- 首次访问章节时,从数据库取出正文,写入Redis,key为chapter:detail:{id},设置1小时过期;
- 再次访问时,先查Redis,命中则直接返回,未命中则重回数据库加载;
- 每次取章时,顺便对小说表点击量做自增操作(Redis里维护一个计数器,定时或写操作时回写MySQL)。
这个方案是真正的入门级缓存策略,代码量很少,但答辩时“你如何设计缓存”这个问题基本就稳了。
4.4 前端与后端交互
前端建议用Vue 3 + Element Plus + Axios + Vite构建,页面量控制在12到15个左右。很多人会纠结是否把前后端拆成两个项目,这里建议拆。拆完了你论文里可以写“前后端分离架构”,答辩时可说的内容就又多了。
后端统一返回结构:
java复制public class Result<T> {
private Integer code;
private String msg;
private T data;
// 静态工厂方法 success(), error(String msg)
}
前端用Axios拦截器统一处理token附加和错误提示,只对接业务,不用每个页面单独写重复的请求逻辑。
4.5 文件上传逻辑
封面图上传是一个绕不开的小功能。建议在后端写一个UploadController,接收MultipartFile,限制格式(jpg/png,10MB内),存到服务端本地目录,然后给前端返回一个URL。实际部署时,绝对路径用application.yml的upload.path配置,不要写死到代码里。如果觉得本地存储low,可以换成MinIO,自托管一个对象存储,这部分能力很强,但属实是加分项,没必要为它耽误太久。
5. 从零搭建的实战步骤:源码部署调通的全流程记录
这部分比较长,但全是实际跑通之后的流程,照着走,不出意外的话一次能起来。
5.1 环境准备
- JDK 8(版本号8u202,不要装Oracle版权纠纷后的高级版本,无意义)
- Maven 3.6.3
- MySQL 8.0
- Redis 6.x(Windows用户装memurai或redis-windows版)
- Node.js 16(前端Vue项目用)
- IDEA 2023.1或2024.1
5.2 创建后端工程
用IDEA的Spring Initializr创建项目,或者直接在start.spring.io上生成,勾选以下依赖:
- Spring Web
- MyBatis Framework(生成后可以替换为MyBatis-Plus)
- MySQL Driver
- Lombok
- Validation
生成后,在pom.xml中手动追加MyBatis-Plus和JWT相关依赖:
xml复制<!-- MyBatis-Plus 启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonweb[token</groupId>](https://taotoken.net?utm_source=general)
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
5.3 配置文件踩坑清单
application.yml是毕设项目第一周反复被坑的地方。下面的配置是实测可用的:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/read_novel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 你的密码
data:
redis:
host: localhost
port: 6379
database: 0
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
# 上传文件保存路径
upload:
path: D:/upload/
注意几个坑:
- url中serverTimezone设置为Asia/Shanghai,否则插入时间时会出现时区报错;
- driver-class-name必须是com.mysql.cj.jdbc.Driver,旧版的com.mysql.jdbc.Driver在8.x驱动下已经废弃;
- MyBatis-Plus已经包含MyBatis依赖,不要再同时引入mybatis-spring-boot-starter,否则会起启动冲突;
- 千万记得在启动类上扫描Mapper包(@MapperScan("com.example.readnovel.mapper")),忘了的话会“Invalid bound statement”错误。
5.4 前端工程创建
在项目目录下执行:
bash复制npm create vite@latest read-novel-ui -- --template vue
cd read-novel-ui
npm install
npm install vue-router@4 axios element-plus
然后配置vite.config.js里的开发代理,这一步是为了绕过跨域问题:
js复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
写完这个配置,前端请求 /api/xxx 会自动转发到后端,不需要在后端写任何跨域相关的@CrossOrigin。
5.5 路由设计示例
前端路由建议这样规划:
| 路径 | 页面 | 是否需要登录 |
|---|---|---|
| / | 首页 | 否 |
| /login | 登录 | 否 |
| /register | 注册 | 否 |
| /novel/:id | 小说详情 | 否 |
| /reader/:novelId/:chapterId | 阅读页 | 否(但阅读记录需要登录) |
| /bookshelf | 我的书架 | 是 |
| /history | 阅读历史 | 是 |
| /admin | 管理端布局 | 是(管理员) |
| /admin/novel | 小说管理 | 是 |
| /admin/chapter/:novelId | 章节管理 | 是 |
5.6 分页接口的标准写法
列表页不可能一次性查全量数据,MyBatis-Plus分页是毕设必备技能。配置一个分页插件:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
然后Service层写法就非常简单:
java复制public IPage<NovelVO> getNovelPage(int pageNum, int pageSize, String categoryId) {
LambdaQueryWrapper<Novel> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StringUtils.isNotBlank(categoryId), Novel::getCategoryId, categoryId);
wrapper.orderByDesc(Novel::getUpdateTime);
return novelMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
}
注意,返回给前端的不要是Novel实体,而应该定义一个NovelVO,封装categoryName、wordCount等展示字段,避免把intro这种大字段列表也返回,拖慢页面加载。
5.7 部署与演示环境
本地开发跑通后,给你的论文附上一份部署文档。正常流程:
- 后端用IDEA的package命令打成jar包,执行
java -jar read-novel-0.0.1-SNAPSHOT.jar启动; - 前端
npm run build生成dist目录,用Nginx托管; - 数据库把建表脚本和初始化数据跑一遍。
毕设答辩演示时,建议直接在本地环境跑,不依赖云服务器。万一哪天没网或者云服务器到期,也不至于当场翻车。
6. 调试与测试:那些让人半夜抓狂的常见问题排查
这部分我想把调试过程中最常踩的坑集中说一遍。每一条都是真实发生过的。
6.1 “Invalid bound statement (not found)”但代码看着没问题
这个问题90%出在Mapper.xml的namespace写错,或者xml文件和Mapper接口所在包路径不一致。检查三步:
- 看namespace是否和Mapper接口全限定名一致;
- 看xml文件是否放到了resources/mapper目录下;
- 看application.yml里的mapper-locations配置路径是否正确。
补一个细节:IDEA工程里resources目录的xml文件,在打包时默认不复制到classes下。需要在pom.xml中配置:
xml复制<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>
否则线上跑jar包必炸。
6.2 前端接口报404但后端接口能通
直接走Vite代理时,如果前端请求地址是/api/novel/list,而后端Controller的映射是@RequestMapping("/api/novel"),就不会有这种问题。只要你保持前后端的请求路径统一,一般不会报这错。排查时先看network请求的URL,是不是真的打到了后端端口。代理没生效,大概率是vite.config.js没改完就忘了重启dev server。
6.3 图片上传成功但访问不到
这是路径映射问题。Spring Boot默认不开放本地静态资源对外访问。你在配置文件中写了upload.path,还需要加一个WebMvcConfigurer,把本地路径映射为URL路径:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${upload.path}")
private String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceHandler("file:" + uploadPath);
}
}
上面有个手滑错误,addResourceHandler写了两行,正确的写法是只写一行:
java复制registry.addResourceHandler("/upload/**")
.addResourcePattern("file:" + uploadPath);
实际后端的这个异常很典型,记得替换成正确代码。
6.4 缓存穿透:查一个不存在的章节导致Redis扛不住
如果是毕设,这个问题影响还小,但答辩老师可能会追问。简单做法是缓存空值,访问一个不存在的章节时,在Redis里也放一个空对象,过期时间短一点(比如60秒),避免每次都打库。
6.5 前端拿到的数据是字符串“0”而不是数字“0”
这个是JSON序列化导致的。Long类型在JS里精度会丢,所以在VO类中,所有id字段建议标注@JsonSerialize(using = ToStringSerializer.class),把id序列化为字符串,前端就不会出现“末尾精度丢失”或者“数字变成科学计数法”的情况。
6.6 启动时循环依赖报错
不常见的,但如果你写了A依赖B,B依赖A的Service包装,就碰到了。解决办法是重构:用@Lazy在构造器参数上加懒加载,或者把公共逻辑抽到另一个Service里。毕设阶段最优解是第二种,不要用@Lazy,因为论文里写Lazy不是好解法,抽Service才能体现你的设计意识。
7. MyBatis-Plus技巧:减少70%重复CURD代码的实践
这节重点写给刚接触MyBatis-Plus的同学。很多人在代码里一个实体类配一个ServiceImpl,然后每个方法都手写SQL——那就没用到MP的核心价值。
7.1 继承默认实现类
java复制public interface NovelService extends IService<Novel> {
PageResult<NovelVO> getNovelPage(PageQuery query);
}
@Service
public class NovelServiceImpl extends ServiceImpl<NovelMapper, Novel> implements NovelService {
// 自己只写非默认的业务方法,基础的增删改查全部继承
}
这样做,insert、deleteById、getById、updateById这些方法全部免费拿,不必自己写一行SQL。自定义查询用Wrapper即可,只有特别复杂的统计SQL才需要写在xml里。
7.2 LambdaQueryWrapper 而不是 QueryWrapper
QueryWrapper用字符串列名,容易写错。LambdaQueryWrapper直接引用方法名,编译期就能发现错误:
java复制lambdaQuery().eq(User::getUsername, username).one();
这就是为什么现在推荐用Wrappers.lambdaQuery()而不是Wrappers.query()。
7.3 批量删除别用循环
书架批量移除,比如用户勾选了10本要删,最差的做法是for循环里调removeById。应该用:
java复制bookshelfService.remove(Wrappers.<Bookshelf>lambdaQuery()
.eq(Bookshelf::getUserId, userId)
.in(Bookshelf::getNovelId, novelIdList));
一条SQL完成,性能好,事务也好管理。
7.4 条件构造器的常见误区
很多人会在Controller层直接组装Wrapper传进Service,这是反模式。Controller只做参数接收和返回,Service里自己构建Wrapper,避免上层拿到太强的数据访问能力。
另外,调用 eq 之前先判断字符串为空:
java复制wrapper.eq(StringUtils.isNotBlank(categoryId), Novel::getCategoryId, categoryId);
这种写法不仅简洁,还能自动处理“参数为空时不加条件”的情况。
7.5 使用AutoGenerator生成基础代码
代码生成器能一次性生成实体、Mapper、Service、ServiceImpl、Controller。不过生成后需要把Controller里的基础方法删除或改写,因为默认生成的Controller是一份标准CURD壳,不能直接用来当业务代码。生成器这种工具适合搭骨架,业务实现还是得手写。
8. 答辩准备:老师会盯着这几个地方问
毕设答辩问的问题,往往不是怎么用框架,而是“为什么这么设计”和“原理是什么”。提前准备下面几个问题,稳很多。
8.1 为什么使用Spring Boot而不直接用Spring
可以回答:Spring Boot基于Spring框架,简化了配置,内置了Tomcat,做到了约定大于配置。它适合快速构建独立运行的Java应用。项目中的依赖管理、自动配置、Actuator监控等都有体现。
8.2 具体说说依赖注入的两种方式
构造器注入和字段注入。在项目里尽量用构造器注入,因为字段注入容易造成隐藏的循环依赖,构造器注入符合Java Bean规范,也更利于测试。Spring官方也在推荐构造器注入。
8.3 你们项目的密码安全性如何保证
密码存储使用BCrypt加盐哈希,不是明文,也不是简单MD5。BCrypt即使两个用户密码相同,哈希值也不同,因为盐不同。即使数据库被脱库,破解代价很高。
8.4 如果首页并发访问量大,你会怎么优化
这个问题属于扩展题,但容易得分。回答思路:
- 前端:CDN静态资源、Nginx反向代理与负载均衡;
- 后端:Redis缓存热点数据(章节缓存、榜单缓存)、数据库连接池调优、接口层面限流(如Sentinel或Guava RateLimiter);
- 系统:垂直扩容或水平扩容,将服务无状态化,多实例部署。
毕设里即使实现不了所有,也要把思路说清楚,这体现知识面。
8.5 事务是怎么控制的
推荐用Spring的@Transactional注解,放置在Service层方法上。注意两点:事务方法不能被同类内部调用,否则注解失效;不要在大循环里反复提交事务,控制好事务粒度。论文中可以提一句“阅读数据不允许孤儿数据”,比如发布章节时,更新小说chapter_count和插入章节必须在一个事务里。
8.6 分页查询是怎么实现的
MyBatis-Plus分页插件本质上是在内存里生成一个LIMIT语句,配合Page对象完成物理分页,不是一次性查全量再到JVM内存里做逻辑分页。注意参数校验,页码和每页数量都应该有上限约束,比如每页最多50条。
9. 常见踩坑总结与优化方向
最后再强调这个项目最容易翻车的地方,以及后期可以怎么扩展。
9.1 数据库连接池连不上
MySQL 8.0默认驱动,URL里如果没有配置useSSL=false,可能因为SSL握手失败而报错。建议在url中加上useSSL=false&allowPublicKeyRetrieval=true。
yaml复制url: jdbc:mysql://localhost:3306/read_novel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
另外,如果你是本地装MySQL,密码为空或包含中文特殊字符,需要正确转义,否则会一直报Access denied。
9.2 前后端联调时出现跨域
使用Vite代理后一般就能解决。如果还有问题,全局在后端加一个CorsFilter,允许localhost:5173的访问。
9.3 部署后tomcat线程池耗尽
本地演示通常不会发生,但这个值得一提。后端返回慢,多半是数据库查询慢。优先为热点查询加索引,比如novel表的category_id、chapter表的novel_id和sort_order组合索引。
9.4 后续可以扩展的方向
如果你想在“完整”之外再有点亮点,可以加:
- 基于简单规则的热销榜实时计算(用Redis ZSET存储点击量排行);
- 阅读时长统计与日报(定时任务统计表);
- 基于分类的推荐列表(新用户推热度,老用户推未读);
- 使用RabbitMQ或线程池处理点赞评论等非核心操作,削峰填谷;
- 引入Spring Boot Actuator做服务健康监控。
这些方向,论文每一章都能写出一到两段,实实在在增加工作量和深度。
10. 一点个人体会
这个题目做下来,我见过太多人“卡”在不知道从哪开始。如果你看了这篇文章还是觉得无从下手,我的建议是:别急着写代码,先把数据库创建好,把六张核心表用SQL建出来,再用Postman调通“注册登录”接口。那个瞬间,项目就走起来了。后面所有功能都是在给这个骨架添肉。
这个项目的源码和文档,如果你是通过学校导师获取、或自己照着逻辑搭的,一定要搞清楚每一行代码是什么意思,不要直接拿着别人代码就上。答辩老师们看过太多“拿了源码却答不上来”的例子,问一个“你的登录逻辑是怎么走的”就能看出来。只有真正琢磨过一遍,抄来的代码也变成你自己的能力,这比什么都重要。
