每年到了毕设选题季,总会有同学拿着同一个方向来问我:基于SpringBoot的小说阅读平台能不能做。我的回答通常很直接——能做,而且比很多所谓的“高难度课题”更适合作为毕业设计,但前提是你不要把它做成一个只会往数据库里塞数据和查数据的“控制台程序”。这个题目看起来常规,其实从用户端到管理端,从书籍展示到阅读行为记录,能延伸出的模块非常多,SpringBoot在中间承担的也不只是写几个接口那么单薄。
这篇内容我按一个完整的项目演进顺序来写,从为什么要选这个题、SpringBoot的选型逻辑,到数据库设计、核心接口实现、开发中容易翻的坑,最后聊到打包部署和毕业答辩准备。所有内容都会基于实际做这类项目时最常见的场景展开,不需要你有经验,照着一步步搭就能落地。
1. 为什么“小说阅读平台”是毕设里值得选的项目
1.1 双端业务闭环:它不像表面看起来那么“轻”
一个刚接触这个题的人,最容易把它想简单:不就是后台维护几本书,前台把书列出来给人看吗?这种理解会直接导致项目做完只有几张表、十几个接口,答辩时拿不出能讲的业务深度。
实际上,正经的小说阅读平台需要同时服务两种角色。普通用户端包含注册登录、书城首页、分类浏览、关键词搜索、书籍详情、分章节阅读、加入书架、阅读进度续读、评论互动这些完整流程;管理端则要处理小说信息录入、章节目录管理、文本内容导入、分类维护、用户状态管理、核心数据统计等。两个端不是孤立的,它们通过同一套后端服务和数据库联动,用户的阅读行为会反过来影响管理端的数据统计,书籍的上下架状态会实时影响前台展示。这种双端配合的逻辑本身就是系统设计能力的体现。
从复杂度控制来看,小说平台比电商系统好做很多——没有支付、库存、物流这些重业务域,又比简单博客系统多了“分章节阅读”和“阅读进度追踪”这类有辨析度的场景。你可以在上面把很多通用技术点讲清楚,却不会被复杂的业务规则拖垮时间。
1.2 一套主线能带出十几个高频考查点
做过一阵子毕设指导后我发现,评委问的问题翻来覆去就那么几类:SpringBoot自动装配是怎么起作用的、登录的token怎么做到服务端校验、数据库表为什么这么拆分、高访问量页面怎么做压力缓解、上传文件时内存为什么没有爆、部署时的环境差异怎么处理。
这些点在小说阅读平台上几乎都能自然对号入座。用户登录对应带JWT或者Token的接口鉴权;章节内容属于大文本字段,天然适合讨论查询效率;书架和阅读记录的写入频率高,天然适合分析唯一索引和缓存的使用;小说TXT文件导入会涉及大文件解析与事务控制;书籍列表的点击量、收藏量这类数字,又能引出Redis缓存。
也就是说,这个项目不是只能做出增删改查,而是看你愿不愿意往深处走一层。走上去,它就是一个能够覆盖SpringBoot、持久层框架、数据库设计、缓存、前后端交互、部署运维的综合型项目。
1.3 在“太简单”和“太难收尾”之间,它是平衡点
有的学生会去选人工智能、区块链、推荐算法方向的题目,听着高端,结果用到最后只是调了几个现成的库。也有人选图书管理系统这种,功能太少,写到中期自己都觉得无聊,凑不够页数也撑不起答辩时间。小说阅读平台的好处在于,它的主链路非常清晰,你不需要在一个点上死磕算法,而是在横向模块上展开,每个模块都有一定的深度,但深度都在一个本科生能够掌控的范围内。
从安排时间的角度看,前端实现一套差不多的界面,后端把主业务接口写完,中间再匀出时间处理异常场景和测试数据,节奏是舒服的。就算中间出了岔子,砍模块也容易——砍掉评论、砍掉后台统计,主流程仍然是完整的,不会影响项目的自洽性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SpringBoot项目底子怎么打:选型不是越多越好
2.1 先弄懂SpringBoot自动装配,后面才好答辩
很多人用SpringBoot写了一年项目,被问到“为什么你引入一个starter就能直接用里面的功能”时,回答永远只有一句话:“框架自动配置好了。”这句话不能用在答辩里,你至少要懂它背后的基本流程。
SpringBoot把传统的Spring配置过程大大简化,主要靠的是@SpringBootApplication这个组合注解。它里面包含了@EnableAutoConfiguration,这个注解会触发SpringFactories机制或AutoConfiguration.imports机制,让Spring容器去加载所有符合条件配置类。这些配置类通过@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnBean等条件注解做判断,只有当你的classpath下存在某个类,或者你在配置文件中设置了对应属性时,对应的Bean才会被创建。
放到小说阅读平台里讲,你引入spring-boot-starter-web,SpringBoot发现classpath里有SpringMVC相关的类,就会自动配置DispatcherServlet、内置Tomcat、JSON消息转换器。你引入mybatis-plus-boot-starter,它会自动读取数据源配置,创建SqlSessionFactory和Mapper扫描器。你引入spring-boot-starter-data-redis,它自动帮你创建RedisConnectionFactory和RedisTemplate。
写项目时你不需要手动写这些配置类,但心里要有数:SpringBoot启动时到底加载了哪些自动配置、你的配置和自动配置是怎么做的覆盖与叠加。这个底层认知会直接影响你排查问题的速度,比如后面讲到的分页插件失效、时间格式不对,很多都是“你自定义的东西和自动配置发生冲突”导致的。
2.2 技术选型不是赶时髦,而是看项目匹配度
针对小说阅读平台,我一般建议下面的组合,这也是目前最主流、资料最全的Java毕设技术栈:
| 模块 | 推荐选型 | 选型理由 |
|---|---|---|
| 后端框架 | SpringBoot 2.7.18 | 2.x时代的长期维护版本,兼容JDK8,教程和踩坑记录最丰富 |
| 持久层 | MyBatis-Plus 3.5.x | 既保留MyBatis的原生SQL能力,又有IService、BaseMapper等封装,还能自动生成代码 |
| 数据库 | MySQL 8.0 | 主流关系型数据库,文档多,对中文全文搜索和JSON类型支持都不错 |
| 缓存 | Redis | 处理缓存热点数据、登录态、首页列表等场景 |
| 认证方案 | JWT | 解决前后端分离场景下的会话保持问题 |
| 前端 | Vue 3 + Element Plus | 组件库成熟,适合快速搭建管理后台和前台页面 |
我特别想说一下为什么没有把Spring Security列为必选项。很多同学看到“登录”两个字就想到Spring Security,但在毕设里引入它会引入一套复杂到让人崩溃的过滤器链和权限模型。你用它可以,可是你用不好,被问到底层逻辑时讲不清晰,反而比不用还扣分。小说平台的用户登录只需要密码加密存储、JWT生成校验、拦截器统一处理,代码量很小,自己写一遍反而更能锻炼对请求处理流程的理解。
2.3 环境配置要留一手:JDK版本和配置文件分离
这两年SpringBoot 3.x很火热,我也看到有同学直接上了SpringBoot 3.2加JDK 17。在这里我要给大多数人一个建议:如果时间不够充裕,请选择SpringBoot 2.7.18。原因很简单,3.x从JDK17起步,和很多基于JDK8的老教程、老依赖多多少少存在兼容问题,比如MyBatis-Plus的旧版本不支持、部分工具包要换javax为jakarta命名空间。你用最新版遇到报错时,搜索引擎里能查到的有效答案远少于2.x版本。用一个成熟稳定的版本把项目写完,远比花时间调试版本冲突值。
配置上用Maven管理,项目里至少保留三份配置文件:application.yml放公共配置,application-dev.yml放本地开发环境数据库地址和日志级别,application-prod.yml放演示服务器的参数。切换环境不要手动改文件里的数据库连接,而是启动时通过--spring.profiles.active=prod参数指定,或者打包后在jar包同级目录放一份外部配置文件覆盖内置配置。
数据库连接串里的时区问题也提醒一下。MySQL 8.0之后,JDBC连接串里最好显式加上serverTimezone=Asia/Shanghai和useUnicode=true&characterEncoding=utf8。不加的话,有时数据库连接能通,但时间字段插入后和本地时间差了八个小时,排查起来你会误以为是Java的Date类型转换问题,实际上就是连接串没写对。
2.4 包结构一开始就要分好,别等项目大了再拆
小说阅读平台的业务量虽然不大,但多人协作或者答辩展示时,一个清晰的后端包结构会直接影响别人阅读你代码的第一印象。我见过不少项目把Controller、Service、Mapper全部堆在两个包里面,代码量少的时候无所谓,一旦加上后台管理功能,类文件变多,查找和维护就会痛苦。
一个可以直接照搬的划分方式是这样的:
code复制com.example.novel
├── NovelApplication.java
├── common // 通用返回结果、异常处理、常量
├── config // 配置类,跨域、拦截器、MyBatisPlus分页配置
├── controller // 控制层:admin、front分开
│ ├── admin
│ └── front
├── dto // 入参和出参对象
├── entity // 数据库实体
├── interceptor // JWT拦截器
├── mapper // 数据访问层
├── service // 业务接口和实现
└── utils // JWT工具、文本解析工具等
核心原则是Controller层只做参数接收、调用服务和结果封装,不要在里面写SQL拼接逻辑;Service层负责事务、缓存和业务规则;Mapper层只负责数据库交互。项目答辩时,如果评委让你在共享屏幕上找一个“查询用户书架列表”的代码位置,你能快速定位到service里对应的方法、mapper里对应的SQL,这本身就说明你分层没白分。
3. 数据库是平台的地基:一本书是如何入库的
3.1 先想清楚数据从哪里来、到哪里去
开发前我习惯画一张数据流转图:一部小说TXT文件进入系统后,解析出书名、作者、简介、章节标题和正文,之后生成book记录和chapter记录;用户在前台看到书籍列表,点进去查看章节内容;用户阅读时更新阅读历史和书架,阅读行为又反过来改变书籍的阅读量、收藏量。顺着这张图走,数据库的表基本就定下来了。
按照这个流程,最核心的几类数据是:用户、书籍分类、书籍、章节、阅读历史、书架、评论和后台管理员。它们之间的主外键关系并不复杂,事务边界也很清楚,插入一章需要同时更新书籍的最新章节信息,删除一本书要把它的所有章节一并清理。
3.2 核心表拆分与关键字段清单
下面这些表结构是我做这类项目时验证过比较合理的方案,表名字段名可以按自己习惯调整,但关系思路是通用的。
| 表名称 | 核心作用 | 关键字段说明 |
|---|---|---|
user |
前台用户表 | username唯一,password存BCrypt加密串 |
admin_user |
后台管理员表 | 和前台用户分开,避免权限边界混乱 |
book_category |
书籍分类表 | 分类名称与排序字段 |
book |
小说信息总表 | title、author、intro、cover_url、category_id、latest_chapter_id、latest_chapter_name、click_count、favorite_count、status |
chapter |
章节目录表 | book_id、chapter_no、chapter_name、content、word_count |
user_shelf |
书架表 | user_id、book_id、唯一索引(user_id, book_id) |
user_read_history |
阅读历史表 | user_id、book_id、chapter_id、update_time |
book_comment |
章节评论/书评表 | book_id、chapter_id、user_id、content、create_time |
需要注意,book表里的latest_chapter_id和latest_chapter_name是冗余字段。设计上你完全可以每次通过MAX(chapter_no)去查最新章节,但书籍列表页往往几十本书同时展示,每本都去执行一次聚合查询,性能压力立刻上来了。保留冗余字段,在更新章节的时候同时维护书籍表,属于一种典型的“以空间换时间”的优化。
我在最开始设计表结构的时候犯过一个错误,就是把书籍简介也设成了大文本字段,连表查询时习惯性用select *查询所有列,导致章节列表里返回的数据给前端时带着一堆根本用不到的字段。这个问题在第5章会专门说,现在你先记住一个原则:文本内容较大的字段一定要单独拆表,或者至少在列表页查询时手动排除。
3.3 长文本内容设计的取舍
章正文是小说平台里最特殊的一类数据。单章内容少则几千字,多则几万字,如果未来做大,一部小说上百万字,可能会产生上千条章节记录。按单章一条记录来存,比整本书塞进一个字段要合理得多,这样分页、查询指定章节、修改某一章都非常方便。
MySQL中对应字段一般使用LONGTEXT类型,它可以存储最大4GB的字符内容。单纯从类型容量看,长文本不是问题,真正的问题是查询时机。默认情况下,MyBatis-Plus用select *查表会把content字段也加载到Java对象里,当用户浏览章节目录、或者系统做章节列表分页时,这批不需要的正文数据都会白白消耗网络和内存资源。
所以设计数据库时建议坚持两点:第一,chapter表保留content字段,但原则上所有列表相关的SQL都只查询id、book_id、chapter_no、chapter_name、word_count、create_time;第二,获取正文内容的接口单独通过chapterId查询,并且这一步可以配合Redis做内容缓存,同一个热门章节多人同时阅读时,数据库只需要承受一次完整查询的压力。
3.4 索引设计值得花十分钟认真想想
有了表结构,建索引不是把所有字段都加一遍,也不是完全不加。我在项目里通常只会这样设计:
book表的title、author、category_id建普通索引,支撑前台搜索和分类页。chapter表建UNIQUE KEY uk_book_chapter_no (book_id, chapter_no),保证一本书的章节序号不会重复,这是防止TXT导入时重复解析的关键约束。user_shelf表建UNIQUE KEY uk_user_book (user_id, book_id),书架本质上是一对多关系里过滤重复的记录结构,用唯一索引在数据库层面挡住重复加入比应用层判断更可靠。user_read_history表建KEY idx_user_update (user_id, update_time),支撑“最近阅读”的按用户倒序查询。book_comment表建KEY idx_book_id_status (book_id, create_time),支撑书籍详情页的评论列表。
索引的真实作用是在大量数据下让查询少走全表扫描。如果你的数据只有几百条测试数据,索引的效果看不出太大差异,但这属于基础设计素养,会被写进答辩文档和系统设计说明里。
4. SpringBoot接口层最容易写错和忽略的细节
4.1 注册登录不要直接用明文密码,更不要自己发明加密算法
小说平台的用户登录看起来简单,实际上涉及很多容易被检查出来的安全性问题。密码不能以明文直接存数据库,这已经是基本常识,更不要用MD5这种不带盐的散列算法。推荐使用Spring Security里的BCryptPasswordEncoder,单独引入spring-security-crypto这个依赖就能用,不需要引入全套Security。
流程是:注册时把用户输入的明文密码通过BCryptPasswordEncoder.encode()生成加密字符串并入库;登录时用matches()方法比对前端传入的明文和数据库中的哈希值。为什么不用可逆加密或者自己写一个异或变换?因为BCrypt内部带有随机盐,即使两个用户设置了相同的密码,最终落库的密文也不同,能有效对抗彩虹表攻击,而这类细节在讲项目安全性时是很加分的。
java复制PasswordEncoder encoder = new BCryptPasswordEncoder();
String rawPassword = userDto.getPassword();
String encodedPassword = encoder.encode(rawPassword);
// 存库时使用encodedPassword
// 登录校验
boolean isMatch = encoder.matches(rawPassword, dbUser.getPassword());
登录成功后,后端生成一个JWT字符串返回给前端。产生JWT时我通常会把用户ID、用户名放进去,并设置一个合理的过期时间。然后客户端在后续请求的请求头中带Authorization: Bearer <token>,后端写一个HandlerInterceptor统一对请求进行校验,使用HandlerInterceptor而不是在每个Controller方法里重复解析。
4.2 跨域和拦截器同时配置时,最容易踩的坑
很多小说阅读平台是前后端分离开发,Vue跑在8080端口,SpringBoot跑在8081端口,浏览器直接请求另一个端口必然触发同源策略限制。解决方式用WebMvcConfigurer注册一个跨域映射即可,不要把跨域逻辑写在Controller注解上,太碎了。
实际项目里我踩过的坑是:跨域问题表面上配置好了,但登录接口仍然在浏览器里报CORS错误。原因是用HandlerInterceptor做了JWT校验,而浏览器在发起复杂请求前会先发一个OPTIONS预检请求,这个预检请求不带业务参数,也没有Authorization请求头,结果被拦截器直接判定未登录拦截掉了。
解决办法是在拦截器的preHandle方法里,如果请求方法是OPTIONS就直接放行,或者使用专门的CorsFilter,并保证它在过滤器链最外层先执行。这里提醒位很关键,很多人的跨域问题不是因为没配置CORS,而是因为请求压根没走到CORS处理层就被截断了。
配置好之后,我建议你打开浏览器开发者工具直接演示一遍登录,再手动请求一次带token的接口,确认请求头真的带上了。不要小看这一步,我见过太多学生开发时用Swagger测试接口一切正常,到了联调才发现前端拿不到数据。
4.3 阅读进度功能是项目的灵魂,别只存一条记录
一个小说平台做得有没有“产品感”,看它怎么处理用户读到一半关闭页面这个行为就能判断出来。有的系统偷懒,每次用户点击任意一章都老老实实插入一条新记录,时间久了阅读历史表数据量大得吓人;有的系统完全丢弃阅读进度,用户下次进来还得自己翻目录找上次看到哪一章。
合理的方案是在user_read_history表中以user_id + book_id作为业务唯一维度进行update操作,用户每次进入某个章节约几秒后上报一次进度,后端做一次“存在即更新,不存在即插入”的逻辑。在MySQL里可以利用带唯一索引的INSERT ... ON DUPLICATE KEY UPDATE,也可以在MyBatis-Plus里先查询后更新。前者在高并发下更严谨,后者写起来更直观,毕设层面选后者完全够用。
同一本书的阅读记录如果保留多条,在“最近阅读”的列表展示上会乱套,用户看到同一个封面出现在书架好几行的效果很差。所以只用一条记录,并记录最近更新的时间,书架上的“继续阅读”按钮就直接关联到这条记录的chapter_no和chapter_id。
4.4 书架、书籍详情和“上一章下一章”的SQL细节
书架的增删接口核心是判断幂等性,加入时要用user_id + book_id做一次存在性判断,避免重复插入。删除时直接根据用户和书籍删除即可,不需要担心是否会删掉别人的记录,因为SQL的where条件同时包含user_id和book_id,天然做了数据隔离。
书籍详情页的数据接口,通常返回书籍基本信息、最新章节信息、总字数、是否已加入书架。这些数据分布在book、chapter、user_shelf三张不同表里,如果拆成三个接口让前端分别请求,会平白增加网络开销。常见做法是后端组装一个BookDetailVO对象,一次查询完成。
章节阅读页里的“上一章”“下一章”导航,对应的SQL不是SELECT * FROM chapter WHERE book_id = ? AND id < ? LIMIT 1这么粗略地做,因为记录的id和章节序号不一定完全连续。正确思路是依据章节序号chapter_no,查比当前序号大的最小记录,或者比当前序号小的最大记录。用章序号而不是id还有一个隐藏的好处,批量导入时中间某次失败回滚后,书籍ID出现空洞不影响阅读顺序。
4.5 搜索怎么做才不影响其他接口的性能
前台搜索功能一开始直接用LIKE '%关键词%'实现其实问题不大,但你要清醒地知道这个查询大概率不会走索引。当系统只有几条数据时,全表扫描也就几毫秒。当你有几百本书时,效果依旧可以接受。毕设量级下它不会爆,不要因此焦虑。
更好的做法是给book表的title和author加上全文索引。MySQL 5.7以上支持ngram全文解析器,能够对中文进行分词,然后使用MATCH(title) AGAINST('关键词' IN BOOLEAN MODE)完成检索。不过全文索引也有其限制,它无法像搜索引擎那样支持拼音纠错、同义词扩展。如果答辩时被问到“搜索量大了怎么办”,可以从业务角度说:引入Elasticsearch,将书籍基础信息同步到索引中,用更专业的分词器和检索模型。但你只需要把它作为方案演进方向说明即可,不建议在毕设阶段强行引入一套ES集群,资源开销和运维复杂度对毕业设计来说都不划算。
如果不引入全文索引,应用层能做的优化是限制用户的搜索长度、过滤掉空串和特殊符号、控制单次查询返回条数,并通过Redis将高频搜索词对应的结果缓存一段时间。这些都是从实现和用户行为的两个方向保护数据库的有效手段。
5. 最容易踩的坑:分页、长文本、附件上传与跨域
5.1 分页插件失效的完整排查链路
做后台管理章节列表或者前台书籍按分类分页展示时,MyBatis-Plus自带的分页插件几乎是标配。但很多同学按网上的教程引入PaginationInnerInterceptor后,发现分页结果并不生效,查询返回的还是所有数据。我这边总结出了一条通用的排查链路,按顺序检查基本都能解决。
第一步,检查是否在MyBatis-Plus的配置类里正确注册了MybatisPlusInterceptor,并且把PaginationInnerInterceptor加了进去。这是常见问题。
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
第二步,检查自己写的自定义SQL是否在接口方法中传入了IPage作为第一个参数,并且XML里的查询语句没有额外写LIMIT。如果同时写了分页插件的IPage和手动的LIMIT,后者会干扰前者的正常Count查询。
第三步,检查配置的数据库类型是否正确。如果本地是MySQL,但配置里写成了DbType.OTHER,部分版本会无法生成正确的分页方言。想确认插件是否加载成功,后端启动日志里可能会有一些提示,也可以直接打印SQL日志观察是否出现了LIMIT ?。
第四步,也是最容易让人疏忽的,检查当前使用的MyBatis-Plus版本是否和SpringBoot对应上。比如一些3.5版本的分页插件在SpringBoot 3.x环境需要单独适配。排查到这一步时不要靠猜,直接对比pom依赖树和官方文档版本对照表。
5.2 LONGTEXT导致列表页越查越慢
我在本书数据库设计时已经强调过content字段很大。但真正的麻烦是在写Mapper时被简洁代码带偏,比如直接用MyBatis-Plus封装的selectList方法,又没在实体上加字段策略,把整章正文查出来再转成VO丢掉,等于先把大数据量从数据库拉到内存,再做一次无意义过滤。
解决思路很朴素:写SQL时做到按需查询。章节列表页使用的Mapper方法,显式查出id, book_id, chapter_no, chapter_name, word_count, create_time字段。如果是用MyBatis-Plus的LambdaQueryWrapper,也要通过.select()方法指定需要查询的列。这个方法在任何表结构里都适用,大字段和列表数据分开,能带来立竿见影的效果。
阅读正文的详情接口则单独走另一个查询方法,直接通过主键或唯一索引去拿LONGTEXT,这是合理的数据库IO操作。MySQL对单行大字段的读取做了优化,只要不是一次捞几千行,性能不会差到让用户体感卡顿。
再往前一步,可以把每一章的正文按章节维度缓存进Redis,key设计成chapter:content:{chapterId},设置比如24小时的过期时间,热点数据能极大缓解数据库查询压力。但要注意,只要后台编辑修改了章节正文,必须同步删除对应该章节的缓存,否则下一次用户读到的还是旧内容。面试和答辩时能讲出这一层“缓存一致性”的处理逻辑,已经比很多背概念的人强很多了。
5.3 小说TXT批量导入的解析会碰到编码和内存问题
很多小说平台后台的书籍录入功能都支持上传TXT文件。你在正常开发中遇到的第一个问题往往是乱码。网络上流传的TXT文件大多数在Windows环境下编辑,默认编码可能是GBK或GB2312,而Java默认读取时用UTF-8,出来自然全是乱码。
处理时不要直接用new String(Files.readAllBytes(path))这种一把梭的方式,先把文件流转成InputStreamReader并显式指定字符集,常见的兼容策略是先用UTF-8尝试解析,发现乱码再回退到GBK,或者直接在后台提供一个编码选择下拉框由管理员自己判断。读文件时优先使用BufferedReader按行读取,避免一次性把几十MB的文本全加载进内存。一个很常见的低级错误是用Files.readAllBytes()实现了功能,本地测试也没问题,但因为输入文件比较大导致内存占用瞬间拉高,在多用户并发上传时直接把服务拖垮。对毕设来说,使用BufferedReader逐行读取已经足够优雅。
解析章节的标题部分,不要依赖空格、换行符之类的特殊格式,不同来源的TXT文件排版差异巨大。我建议在读取时维护一个正文章节标题的识别方法,比如常见的“第一章”“第1章”“第001章”“第一章 XXX”等等,用正则表达出它们公共的特征。当读到一个新标题行时,说明上一章已经结束,把攒在缓冲区的章节内容插入数据库,比较像用一个“遇到新标题就提交上一段”的分批提交机制。
日志在这里显得格外重要。每插入成功一章就在后台日志里打印一条包含章节号的方法信息,这样如果中途解析失败,你能直接看到是第几章出了问题,而不是面对一个笼统的“上传失败”。
5.4 图片和上传附件相关的隐藏问题
小说平台不只是上传TXT文本,封面图、头像也会涉及文件上传。如果直接用SpringBoot默认的存储方式把文件保存在本地目录,要注意两个问题:一是服务器磁盘空间有限,演示用的项目不会太离谱,但要做好文件命名规则,避免重名覆盖;二是如果后续用Docker部署,容器内部保存的文件在容器重建后就会丢失,需要把上传目录挂载成宿主机目录或者使用对象存储。
上传接口在开发环境测试时,通常会遇到SpringBoot默认限制单个文件1MB的问题。上传几MB的头像或封面时会直接报错,解决方法是调整配置:
yaml复制spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
这里也要提醒一下,当你把资源文件存放在本地时,需要额外实现一个静态资源映射配置,否则前端通过http://localhost:8080/files/xxxx.jpg访问不到。可以通过WebMvcConfigurer的addResourceHandlers方法指定本地磁盘路径的映射关系。这块内容在项目答辩演示时经常被忽略,等评委现场点了图片发现404就尴尬了。
6. 打包部署和演示时,我给学生的准备清单
6.1 从jar包到Docker部署的推荐路线
项目收尾阶段,建议用可部署的jar包来做最终验收。在项目根目录执行mvn clean package -DskipTests,如果一切正常,target目录下会生成可执行的jar文件,然后执行java -jar target/novel-platform.jar --spring.profiles.active=prod就能启动整个后端项目。这是一个很基础的验证步骤,却经常有人在本地用IDEA绿色三角按钮启动没问题,一打包就报“没有主清单属性”或者“找不到静态资源”,多数原因是pom里没有引入spring-boot-maven-plugin,或者前端页面打包后没有复制到src/main/resources/static目录下。
如果项目要求用Docker部署,可以在Dockerfile里设置一个非常简洁的启动方案:
dockerfile复制FROM openjdk:8-jre-alpine
WORKDIR /app
COPY target/novel-platform.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]
构建镜像并启动容器的常用命令:
bash复制docker build -t novel-platform .
docker run -d -p 8080:8080 -v /opt/novel/upload:/app/upload --name novel-platform novel-platform
这里最需要注意的是MySQL和Redis不要也一并塞进同一个容器,建议让SpringBoot容器通过宿主机网络或者Docker Compose配置连接外部MySQL。你可以专门为项目写一个docker-compose.yml,把MySQL、Redis和应用分别定义为三个service,同时对MySQL数据目录做好volume挂载。我在实际部署中见过新手因为容器删除导致全部数据不翼而飞的情况,那感觉比答辩被问住痛苦多了。
6.2 测试数据的准备比写代码更考验耐心
我评审过的很多毕设项目,功能代码没什么问题,演示效果却一塌糊涂,根源在测试数据太敷衍。随便找一本书,只添加了三章内容,前台点开阅读器后翻了两页就到底了,书架里的“继续阅读”功能完全展示不出设计的价值。
演示前一定要准备一组有层次感的数据:至少三个分类下有书籍;同分类下至少有两本以上书籍方便排序和筛选;挑一本作为主演示作品,至少录入30章有意义的章节内容,让用户能真正体验“翻页”的动作;准备一两本完结状态的书,一两本连载状态的书,让章节列表上能看到“已完结”的标识。
阅读进度用户也可以手动制造一条记录,让当前章节停在中间的某一章,再重新登录系统,看“继续阅读”是否跳转到正确位置。把这一系列操作写成一份自己的演示脚本,先自己完整走三遍,答辩时沿着脚本走,就不会因为临时找数据而冷场。
6.3 答辩高频问题可以提前准备的口径
虽然每个老师的风格不同,但围绕SpringBoot小说阅读平台这个题,容易被问到的方向其实很固定。你的项目里事务在哪里体现?我一般建议当场演示一个场景:后台删除一本书时必须同时删除其章节,两步操作放在同一个事务方法里,如果删除章节失败则书籍信息也不能删。
你的项目里哪些地方用到了SpringBoot自动装配?你可以说web场景下自动配置了SpringMVC和内置服务器,也可以说引入了MyBatis-Plus之后只需要配置数据源信息就能使用Mapper。
缓存是怎么用的?如果把章节正文放进了Redis,需要说清更新、过期和删除这几件事;如果只缓存了首页推荐列表和验证码,也要说清为什么选这些数据而不选其他数据。一个负责任的口径是:“我基于访问频率和一致性容忍度选择了缓存对象,比如小说章节正文属于读多写少的数据,修改不频繁,适合做短时间缓存,比如缓存三十分钟;而阅读进度的更新不允许丢失,所以不缓存直接怼数据库。”这句话说完,基本就能让老师认可你对缓存的理解边界。
针对并发量、大数据量这些题,比较安全的说法是先做理性判断,不夸大已经实现的部分,而是把自己了解的开源中间件方案、表结构设计、读写分离思路作为演进方向讲出来,并把问题引导回自己确实做过的部分,比如数据库表索引设计和分页优化。
项目演示完毕收到提问时,不要急着解释代码,先拆解问题层次。如果问题指向明确,就直接给出解决思路和涉及到的类名方法名;如果问题比较宽泛,比如“系统怎么优化并发”,先简短说明当前阶段有哪些局限,然后提供一两条可落地的方案,这样给老师的感受会客观实在很多。
7. 该项目还能横向扩展的方向
如果你做完主流程之后发现自己时间非常充裕,想要在项目里增加一些“有亮点”又不破坏主结构的功能,我建议把扩展点放在内容推荐和数据分析上。
一个方向是基于标签的简单推荐。在book表增加标签字段,比如“玄幻”“爽文”“重生”“系统流”,用户每阅读完一章就把该书籍的标签累加到用户兴趣画像上。当用户下次进入书城首页时,后端根据用户历史阅读中最常出现的标签推荐尚未读过的书,这不涉及复杂的机器学习,本质上就是排序和筛选,但可以包装成一个很有展示效果的功能。
另一个方向是后台数据看板。通过定时任务或前端图表组件,把每日新增用户数、各分类点击数、小说收藏榜这些指标展示成看板。SpringBoot里用@Scheduled注解就可以做简单的定时统计,前端用ECharts的柱状图和折线图展示,技术难度不大,但能让整个项目的完整度上一个大台阶。
有一个方向是我做这类题时比较反对的,就是盲目加入消息队列、微服务、分布式事务等技术。如果只是停留在“引入依赖但没有实际业务场景使用”的程度,对项目帮助不大,反而会让评委质疑你对自己项目的掌握程度。扩展一定建立在想清楚数据和业务链路的基础之上,需要的是真正解决一个实际体验问题或运营问题,而不是在简历上堆关键词。把一个阅读进度、一个章节缓存、一个数据看板想透,比实现三个花架子模块更能体现一名开发者的工程素养。
