1. 项目概述与整体设计思路
1.1 这个项目到底在解决什么问题
小说阅读平台,听起来是个老生常态的毕业设计选题,但真正动手做的时候你就会发现,它比想象中的“CRUD管理系统”要复杂得多。它既要有用户端流畅的阅读体验,又要运营后台的书籍管理、数据统计,还要考虑搜索、推荐、书架同步这些偏业务的细节。我见过很多同学选了这个题,最后做出来的东西就两张表——一个book表一个user表,登录注册一写,分页一查,完事。这种项目答辩的时候一戳就穿,因为阅卷老师随便问几个业务问题你就接不上来。
这个项目名为“基于SpringBoot的小说阅读平台”,本质上是做一个数字化的在线文学阅览系统,覆盖从小说的录入、审核、发布、检索到阅读、打赏、评论、书架的完整链路。它适合两类读者参考:一是做毕业设计的在校生,把这个项目当成一块可以反复打磨的“样板田”;二是想快速上手SpringBoot生态、想接一个完整业务系统练手的开发者。这个项目几乎把SpringBoot体系里的热门组件都串了一遍——SpringBoot核心、MyBatis-Plus、Redis、Elasticsearch、MinIO、JWT、Docker,做完一遍,SpringBoot面试题里那些“自动装配、starter原理、循环依赖、事务失效”你就不再是背答案,而是真正见过。
1.2 技术选型背后的取舍逻辑
为什么选SpringBoot而不是Spring MVC加一堆XML配置?因为SpringBoot的核心价值不在于“快”,而在于“约定大于配置”带来的工程化能力。对小说阅读平台这种业务复杂度中等的系统来说,SpringBoot的自动装配机制能帮你把大量基础设施一次性拉起来,让你把精力集中在业务代码上。特别适合单人开发的毕业设计场景。
再说说持久层。我建议直接用MyBatis-Plus,不要用JPA,也不要纯手写MyBatis。理由很简单:小说阅读平台的核心表有用户表、书籍表、章节表、书架表、评论表,这些表的关联查询不算特别复杂,但是单表操作极其频繁——比如查询一本书的所有章节、查某个用户的书架列表。MyBatis-Plus的BaseMapper能让你零SQL完成绝大多数单表操作,遇到复杂的统计报表再手写XML,这种组合在中小型项目里效率最高。
Redis在这个项目里不是可选项,是必需品。小说阅读平台有一个很典型的场景——章节内容天然适合做缓存。高频访问的热门书籍章节,如果每次都查数据库,数据库压力会非常大,而且章节内容是不可变数据(发布后基本不改),缓存命中率极高。另外一个场景就是书架同步和阅读进度,这些数据读写频繁但单个数据很小,适合用Redis的Hash结构维护。
搜索引擎这块,如果你要对接答辩的“亮点”,Elasticsearch是加分项。但大多数毕业设计的书籍量可能就几百本,用MySQL的LIKE查询完全够用。我会在后续章节里具体讲这两种方案的分界线在哪里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能模块拆解与数据库设计
2.1 小说阅读平台的功能图谱
一个完整的在线文学阅览系统,功能上可以拆成四个端:
- 读者端:注册登录、浏览书籍、搜索、分类筛选、书籍详情、阅读器(分页阅读、字号调节、背景切换)、书架(加入/移除)、评论(发表/回复/点赞)、打赏(积分或模拟支付)。
- 作家端(可选):章节上传、草稿箱、作品管理、数据看板(点击量、收藏量)。
- 管理后台:书籍审核、分类管理、标签管理、用户管理、评论管理、数据统计(日活、书籍热度排行)。
- 系统公共模块:登录认证(JWT)、文件上传(封面、章节附件)、全局异常处理、日志记录。
注意一点,很多同学做着做着就把“作家端”和“管理后台”合并了,这是不对的。作家只能管理自己的作品,管理员能管理所有内容,权限边界必须清晰。这个在答辩的时候很容易被问到“你的系统怎么保证作家看不到别人的作品”,如果你的作品查询条件写的是WHERE book_id = ?而不是WHERE book_id = ? AND author_id = ?,那就露怯了。
2.2 数据库表结构设计,我踩过的坑
小说阅读平台的核心表大概有这些:user(用户)、book(书籍)、book_volume(卷)、chapter(章节)、bookshelf(书架)、comment(评论)、tag(标签)、book_tag(书籍标签关联表)。下面我挑几个关键设计说。
book 表不要存章节数
我第一版设计的时候把“章节数”作为字段存在book表里,每次作家发布新章节都要UPDATE book SET chapter_count = chapter_count + 1。看起来没什么问题,但并发发布的时候会有更新丢失的风险,而且这个字段完全可以通过SELECT COUNT(*) FROM chapter WHERE book_id = ?算出来。如果担心性能,可以在Redis里维护一个计数,定时同步到数据库。count字段在读书系统里属于典型的可推导数据,尽量别冗余。
chapter 表一定要有 volume_id
刚开始做章节表的时候,我偷懒只设计了book_id,后来发现小说是有“卷”的概念的——第一卷第1-100章,第二卷第101-200章。读者看的时候是按卷展开的,如果你没有卷的概念,API返回的目录就是平铺一长串,几千章的小说翻起来极其痛苦。所以至少保留book_volume表,chapter表通过volume_id关联卷,目录查询按卷分组。
bookshelf 表唯一索引是必须的
书架表容易出现重复数据。用户加一本书到书架,快速点两下“加入书架”按钮,前端没做防抖,后端又没做唯一索引,就可能插入两条记录。毕业设计答辩现场演示的时候,这种情况一旦出现,后续所有基于书架的功能全乱套。所以书架表的(user_id, book_id)一定要建联合唯一索引,插入的时候用INSERT IGNORE或者先查后插,从源头杜绝。
章节内容字段类型选择
章节正文如果存MySQL,建议用LONGTEXT,不是TEXT。TEXT最大64KB,普通小说一章3000-5000字其实够用,但如果你支持富文本或者有作者上传带格式的章节,很容易超。另外存储的时候可以适当做压缩,MyBatis-Plus支持字段类型处理,实现一个压缩TypeHandler,存的时候压缩,取的时候解压,2000章的小说能省不少空间。
2.3 书的检索方案:MySQL还是Elasticsearch
我见过很多SpringBoot小说项目动不动就上Elasticsearch,其实没必要。这里有个判断标准:你的数据量多少,你的搜索需求是“LIKE查询”还是“分词检索+相关性排序”。
如果你的数据量在几千到几万本这个量级,MySQL的LIKE '%关键字%'配合全文索引已经能用了。但小说阅读平台的搜索有个很现实的问题——用户搜的是“斗破苍穹”,如果你书名匹配不上,能不能匹配到作者名?能不能匹配到标签“玄幻”?这就是为什么要引入ES。ES的multi_match可以同时匹配书名、作者、简介、标签多个字段,而且支持中文分词,搜索结果的说服力比全表扫描高一个档次。
我做这个项目的时候用ES做了书籍索引,索引结构大概是这样的:
json复制{
"book_index": {
"mappings": {
"properties": {
"bookId": {"type": "long"},
"bookName": {"type": "text", "analyzer": "ik_max_word"},
"authorName": {"type": "keyword"},
"intro": {"type": "text", "analyzer": "ik_max_word"},
"tagList": {"type": "keyword"},
"status": {"type": "integer"},
"wordCount": {"type": "long"},
"updateTime": {"type": "date"}
}
}
}
}
这里有个细节——authorName用keyword而不是text,因为作者名搜索是要精确匹配的,不需要分词。而bookName和intro用ik_max_word分词器,这样搜“斗破”能匹配到“斗破苍穹”,搜“退婚流”能匹配到简介里含这个词的书。
要注意一个常见的坑:ES和MySQL的数据同步。千万别在业务代码里写完MySQL再写一遍ES,那叫双写,迟早出问题。正确做法是应用里操作MySQL时同步更新ES,但一旦更新失败就产生了数据不一致。好在毕业设计这个量级,可以用定时任务全量同步兜底——每天凌晨全量刷一次ES,白天基本不会出现严重的不一致。
3. 框架核心机制解读:SpringBoot怎么帮你省事
3.1 自动装配原理,以及你必须会看的那几个注解
SpringBoot最核心的机制就是自动装配(Auto Configuration)。简单说,你引入一个spring-boot-starter-data-redis依赖,SpringBoot就会在启动时自动帮你创建RedisTemplate、StringRedisTemplate这些Bean。你不需要写任何配置类,只要在application.yml里写好spring.redis.host就行。
面试和答辩经常问“自动装配原理”,你需要能说出这几个关键点:
@SpringBootApplication是一个组合注解,核心是@EnableAutoConfiguration。@EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class)引入一批配置类。AutoConfigurationImportSelector会读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(SpringBoot 2.7之后,之前是spring.factories),拿到所有自动配置类的全限定名。- 然后通过
@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,按当前classpath里面的类是否存在来决定要不要创建对应的Bean。
我用一个夸张的比喻来解释:自动装配就像是点外卖——你点的菜品(starter依赖)被送到厨房(classpath),厨房里每个厨师(自动配置类)会根据菜品名单决定要不要开工。菜没点,厨师就不干活;菜点了,厨师以你给的个性化备注(自定义配置)为准来炒。
3.2 自定义starter的场景:这个项目里用不用得上
看热搜词里有很多“SpringBoot自动装配原理”和“SSpringBoot框架介绍”相关的内容,提到自定义starter,很多同学觉得高大上,但我不建议在小说阅读平台这个项目里硬做一个自定义starter。理由很简单——你找不到一个“能被多个业务模块复用”的场景。
自定义starter最适合的场景是:有跨项目复用的公共组件。比如你做了多个微服务项目,每个项目都要记录操作日志、都要做接口签名校验,这时候把日志组件、签名组件做成starter,各项目引入即可。但小说阅读平台是单体应用,你写了自定义starter就属于为了用而用,答辩的时候如果被追问“这个starter被哪些项目引入了”,你会尴尬。
真正在单项目里有价值的做法是,利用@ConditionalOnProperty实现功能开关。比如小说平台里“模拟支付”功能,测试环境要开着,正式环境要关掉。你可以在配置类上这样写:
java复制@Configuration
@ConditionalOnProperty(name = "app.payment.mock-enabled", havingValue = "true")
public class MockPayConfig {
@Bean
public PaymentService paymentService() {
return new MockPaymentService();
}
}
这样你在application-dev.yml里配置app.payment.mock-enabled: true,在application-prod.yml里配成false,就实现了环境的优雅切换。这种实践比自定义starter更能在答辩中体现你对SpringBoot配置机制的理解。
3.3 SpringBoot版本选择与升级避坑
说句扎心的,SpringBoot版本选不好,项目刚启动就能卡死你半天。
SpringBoot 3.x的要求是JDK 17+,SpringBoot 2.7才能跑在JDK 8上。我看到热词里有“springboot jdk1.8打包到docker desktop”和“springboot版本太高”,说明这是很多人的痛点。如果你用JDK 8,老老实实用SpringBoot 2.7.x。如果你用JDK 8但又想体验SpringBoot 3.x的新特性,那是不可能的事,容器基础镜像都起不来。
另外要注意的是,SpringBoot 2.7和3.0之间有一个巨大的变化——spring.factories自动配置注册文件变成了AutoConfiguration.imports,很多老SpringBoot版本的第三方starter在3.x下直接失效。我见过有人把SpringBoot升级到3.x之后,springfox-swagger(Swagger 2的starter)用不了了,因为旧版springfox对Spring MVC的路径匹配策略不兼容。后来只能换成springdoc-openapi。
如果你看到热词里“springboot 4.0 找不到aop”,这个属于SpringBoot版本和AOP starter版本不匹配导致的。建议每个SpringBoot大版本对应的官方文档里找配套的starter版本,别瞎猜。锁版本的最稳方式是什么?用Spring Initializr生成项目,而不是手动引入依赖。它给你的版本组合是被测试过的。
4. 核心代码实现:从登录到阅读的完整链路
4.1 JWT认证 + 拦截器的实现细节
小说阅读平台的绝大多数接口都需要登录态,但阅读接口本身又是高频访问的。所以认证方案我用的是无状态的JWT,而不是传统的Session。JWT把用户信息(user_id、角色)加密放在Token里,服务端不需要保存会话状态,天然适合水平扩展。
核心代码结构大概是这样的:
java复制@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(User user) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(user.getId().toString())
.claim("username", user.getUsername())
.claim("role", user.getRole())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
}
这里有个必须注意的安全细节:JWT的secret不能硬编码在代码里,要放在配置文件里,并且至少256位长。HS256算法对密钥长度有要求,太短会直接报错。另外expiration不要设置太长,我一般设置24小时,给用户端做“记住我”的时候才考虑7天。
拦截器的处理要区分“白名单”和“鉴权”。登录、注册、书籍列表、书籍详情、章节内容这些接口是白名单,不用校验Token。书架、评论、打赏、个人中心这些接口必须校验Token。用SpringBoot的HandlerInterceptor实现时,重点在preHandle方法里判断:
java复制public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
token = token.substring(7);
}
try {
Claims claims = jwtTokenProvider.parseToken(token);
request.setAttribute("userId", claims.get("userId"));
return true;
} catch (Exception e) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("{\"code\":401,\"message\":\"登录已过期\"}");
return false;
}
}
}
然后添加拦截器时的路径匹配规则是关键:
java复制registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/**", "/api/book/**", "/api/search/**");
这里的/api/book/**里包含了书籍详情和章节阅读的接口。但如果你后续加了“打赏”功能,打赏接口也在/api/book/**下面,就会被排除掉了。所以路径排除要精确到接口级别,比如书籍详情接口是/api/book/detail,章节阅读是/api/book/chapter,这两个排除掉,但/api/book/reward不排除。
4.2 章节阅读接口:如何设计才不卡顿
阅读器的体验核心在于“翻页不卡、章节切换流畅”。后端接口设计上,不要一次把整个章节内容返回给前端再让前端分页,而是让前端一次只拿一章或者一页。
章节内容接口我这样设计:
java复制@GetMapping("/chapter/{chapterId}")
public Result<ChapterVO> getChapter(@PathVariable Long chapterId, HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
// 读取上一章、当前章、下一章的id,方便前端做上一页/下一页
ChapterVO vo = chapterService.getChapterDetail(chapterId, userId);
return Result.success(vo);
}
ChapterVO里除了章节正文,还要带上章节ID、上一章ID、下一章ID、书名、卷名。这样前端拿到一个JSON,就能直接渲染当前页,同时把上一章/下一章的按钮地址也准备好了,不需要再额外调一次“查询上下章”的接口。
章节正文在查询时有个大坑——全文返回可能几百KB,特别是有插图的小说。前端渲染会卡,移动端流量也废。可以做一个“内容分页”或“内容截断”接口,后端按字数切好,前端翻页时增量加载。毕业设计如果是演示场景,建议后端暂时不做分页加载,但你要在答辩PPT里提到“这里可以用分段懒加载优化”,这能体现你的工程思维。
4.3 书架与阅读进度:Redis还是MySQL
书架列表肯定要持久化,用MySQL表存没问题。但“用户读到哪一章”这个阅读进度,是最适合放Redis的场景——读写频率高、数据允许短暂丢失。
我用Redis存阅读进度的结构:
bash复制key: "read:progress:{userId}"
value: hash格式,field是bookId,value是chapterId
这样用户阅读时,每次翻页都调用接口更新进度,Redis的写入是内存级速度,完全没压力。而书架列表接口则混用MySQL + Redis缓存:
java复制@Cacheable(value = "bookshelf", key = "#userId")
public List<BookShelfVO> getUserBookshelf(Long userId) {
// 查MySQL,组装详情
}
这里直接用了Spring Cache的@Cacheable注解,底层Redis。有一个坑是缓存穿透和缓存雪崩。书籍详情接口如果每一个都加缓存,冷门书籍第一次访问会打到MySQL,量大了会慢。我的做法是,只缓存“热书”——后台配置一个热书ID列表,查询时判断如果ID在热书列表里就走Redis缓存,否则直接查库。这个方案简单有效,比用布隆过滤器好实现,适合毕设场景。
4.4 大文件上传与下载:封面图片和章节附件
热搜词里有“springboot 如何上传下载大文件”,小说阅读平台虽然以文本为主,但封面图片、作家上传的附件也是要处理的。这里的原则是:文件不落应用服务器本地,用MinIO对象存储。
SpringBoot集成MinIO的步骤很清晰:
- 引入依赖
io.minio:minio。 - 配置MinIO的连接信息:endpoint、accessKey、secretKey、bucketName。
- 封装一个
MinioService,提供uploadFile、getFileUrl、removeFile方法。
java复制public String uploadFile(MultipartFile file, String objectName) {
try {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
// 返回访问路径,这里可以拼接MinIO地址
return endpoint + "/" + bucketName + "/" + objectName;
} catch (Exception e) {
throw new RuntimeException("文件上传失败", e);
}
}
关于对象名,记得不要用用户原始文件名,否则中文文件名、特殊字符会出问题。用UUID或者时间戳拼接后缀:
java复制String objectName = "cover/" + UUID.randomUUID().toString().replace("-", "") + "." + fileExt;
上传大文件时,SpringBoot默认的max-file-size是1MB,max-request-size是10MB,如果传封面图超过这个限制会直接报错。必须在配置里调大:
yaml复制spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 50MB
4.5 解决SpringBoot循环依赖,这题答辩必问
我在热词里看到“springboot 循环依赖”,这可能是个让很多人头疼的问题。SpringBoot 2.6开始默认禁止循环依赖,如果你项目里A服务调B服务、B服务又调A服务,启动时会直接报错。
小说阅读平台里最容易出现循环依赖的场景是:ChapterService需要调用BookService查书籍信息,而BookService又需要调用ChapterService统计章节数。解决方式有三种,按推荐顺序排:
- 重新设计依赖关系,把公共查询下沉到一个新的
BookQueryService,让两个Service都依赖它。 - 使用
@Lazy注解延迟注入其中一个Bean。 - 把属性注入改成
Setter注入(不推荐,治标不治本)。
答辩的时候如果被问到循环依赖,最好的回答不是背“三级缓存”的源码,而是说“我通过重构服务拆分,让依赖关系变成单向的,彻底消除了循环引用风险”。这比讲三级缓存原理更显工程能力。
5. 从开发到上线:部署配置与常见问题排查
5.1 application.yml不是玄学,这些配置你必须明白
配置是SpringBoot项目最容易出问题的地方。小说阅读平台一个完整的配置文件至少包含以下区块:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/novel_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: localhost
port: 6379
database: 0
servlet:
multipart:
max-file-size: 20MB
max-request-size: 50MB
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
configuration:
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
jwt:
secret: "your-256-bit-secret-key-change-me"
expiration: 86400000
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: novel
注意几个容易踩坑的点:
- datasource的
serverTimezone要写对,不写会报时区错误,Asia/Shanghai别少拼。 - MyBatis-Plus的逻辑删除配置,字段名要和实体类里的
@TableLogic注解对应。 - 数据库密码尽量不要明文写在配置里,简单做法是用环境变量占位符:
password: ${DB_PASSWORD:123456}。看到热词里有人问“springboot项目中对数据库用户密码采用sm4的加密方式并且在jasyptstringencrypt”,这说明你可以在答辩里主动提“生产环境可以用Jasypt对配置加密”,作为进阶亮点。
5.2 IDEA里application.yml不提示怎么办
有热词问“idea中,springboot项目的application.yml不提示怎么办”。这个问题90%的原因是IDEA没有把yml文件识别为Spring配置文件。解决办法有两种:
- 右键点击
application.yml,选择“Add as Spring Boot Configuration File”,让IDEA识别它。 - 检查是否安装了Spring插件(Spring Boot、Spring Assistant),没有的话Marketplace里装一个。
另外一个隐藏问题是:如果你的application.yml编码不对,IDEA里打开中文注释乱码,里面的配置即使写对了也会解析失败。建议把IDEA的文件编码统一设成UTF-8:Settings → Editor → File Encodings,全部选UTF-8。
5.3 项目启动全流程与Docker部署
毕业设计如果能演示Docker部署,绝对是答辩加分项。热词里很多“docker部署springboot项目”,这里我把完整步骤写出来。
先写Dockerfile:
dockerfile复制FROM openjdk:8-jre-alpine
WORKDIR /app
COPY target/novel-platform-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]
然后mvn clean package -DskipTests打出jar包,再docker build -t novel-platform .构建镜像。运行容器时,通过-e参数注入数据库和Redis的连接信息:
bash复制docker run -d --name novel-platform \
-p 8080:8080 \
-e DB_HOST=192.168.1.100 \
-e DB_PASSWORD=yourpassword \
-e REDIS_HOST=192.168.1.100 \
novel-platform
这时候注意一点:镜像里的应用连接数据库,不能用localhost,要用宿主机IP或者Docker Compose里的服务名。很多初学者容器起来了,但应用连不上数据库,就因为写了localhost。容器内的localhost是容器自己,不是宿主机。
如果连Docker Desktop都装了,热词里“springboot打包到docker desktop”就是一个固定操作:本机打镜像,Docker Desktop里面跑容器,浏览器访问http://localhost:8080就能看到项目。这套流程演示给答辩老师看,比只跑在IDEA里要好得多。
5.4 常见启动失败与运行异常排查清单
我把做这个项目碰到的典型问题整理成了一个速查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
启动报Failed to configure a DataSource |
缺少数据源依赖或配置不完整 | 检查pom.xml是否引入spring-boot-starter-jdbc或mybatis-plus-boot-starter,检查application.yml的url/用户名/密码 |
| 404错误,访问不到接口 | 启动类位置不对,没扫描到Controller | 启动类必须在所有Controller和Service的父包下 |
| 接口能访问但返回500 | Mapper XML路径配置不对 | 检查mybatis-plus.mapper-locations是否指向了正确的classpath路径 |
| Redis连接超时 | 没启动Redis或host配置错误 | 本地先启动Redis服务,redis-cli ping验证 |
| 中文乱码 | 数据库连接串没加characterEncoding=utf8 |
连接URL添加useUnicode=true&characterEncoding=utf8 |
| 上传大文件报错 | 最大文件大小没调 | spring.servlet.multipart.max-file-size调大 |
5.5 单元测试不能省,这是你和别人拉开差距的地方
热词里有“springboot 单元测试最佳实战”,我建议小说阅读平台至少给核心Service层写几个测试用例。不用面面俱到,但书架、章节查询、用户注册这几个核心接口要覆盖。
java复制@SpringBootTest
@Transactional
public class BookServiceTest {
@Autowired
private BookService bookService;
@Test
public void testGetBookDetail() {
BookVO book = bookService.getBookDetail(1L, null);
assertNotNull(book);
assertEquals("斗破苍穹", book.getBookName());
}
}
用@Transactional标记测试方法,测试完自动回滚,不会污染数据库。这里有个事项:SpringBoot测试默认会加载整个Spring上下文,启动比较慢,但这是值得的,因为测试环境能提前暴露配置问题。
6. 我踩过的一些坑,提前帮你避开
6.1 单元测试时回滚不掉的数据怎么处理
如果测试方法里调用了bookService.publishChapter(),这个方法内部可能开启了新事务(使用了REQUIRES_NEW传播级别),@Transactional的外部回滚就管不到它,数据会留在库里。解决方法是测试数据用随机的用户ID,测试结束手动清理。或者设计测试用例的时候,尽量只调用不涉及嵌套事务的方法。
6.2 数据库的emoji表情存不进去
小说的评论区用户可能会发emoji,MySQL默认的utf8字符集是utf8mb3,存不了4字节的emoji。建库的时候一定要用:
sql复制CREATE DATABASE novel_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
否则插入评论的时候会报Incorrect string value错误。这个坑特别隐蔽,因为开发初期测试数据很少用emoji,等答辩前演示真机评论的时候突然报错,心态容易崩。
6.3 前端和后端的跨域问题
如果你用Vue搭建管理后台,前后端分离模式下必然面对CORS跨域。在SpringBoot里加一个CORS配置类,用WebMvcConfigurer的addCorsMappings方法:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:8081")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
有一个容易让人栽跟头的地方:allowCredentials(true)的时候,allowedOrigins不能是"*",你必须写明具体的域名。如果前端是http://localhost:8081,配置里就要写http://localhost:8081。
6.4 后台管理系统的权限校验容易漏
管理后台的接口前缀建议单独用/admin/**,然后单独加一个AdminInterceptor,校验当前用户的角色是不是ADMIN。别把管理接口混在/api/**里用同一个认证拦截器,否则权限校验逻辑里到处是if (role.equals("ADMIN")),代码没法维护。
7. 项目扩展方向与经验总结
这个项目做完之后,如果想让自己在答辩中更有底气,可以做几个“小而精”的扩展。比如接入一个WebSocket做一个在线聊天室或“作家和编辑实时沟通”的小功能,技术面立刻拓宽。或者用定时任务做个热度排行刷新,每天凌晨重新计算热门书籍Top100。这两个扩展都算功能小而完整,能讲清楚实现思路,比堆砌技术栈更让老师认可。
再分享一个让我后来在职场上都受益的经验:项目中的SQL和配置多写注释,模块之间保持清晰的依赖方向,Controller层只做参数校验和结果包装,Service层管业务逻辑,Mapper层管数据访问。这个分层习惯会让你的代码在后续扩展时非常轻松,比如某天你想把书籍详情接口从MySQL+本地缓存升级为Redis缓存,只需要改Service层,其他层完全不动。
我在实际开发中发现,很多同学做毕设项目失败,不是不会用SpringBoot,而是需求梳理不清楚就急于敲代码,导致中间反复改表结构。小说阅读平台这种业务,先花两天时间把表结构、接口返回的JSON结构定义好,后面写代码的速度会快很多。这可能是整个项目里最值得借鉴的工作方式。
