
简介针对毕业设计需求这份基于 Spring Boot 的在线小说阅读平台项目包含完整源码与演示视频主要面向计算机相关专业学生、准备毕设的开发者以及想通过实战学习 Spring Boot 的入门者。项目以小说阅读场景为依托覆盖用户注册登录、小说分类浏览、章节阅读、书架收藏等常见业务模块的后端实现可以帮助读者理解 MVC 分层、数据访问、接口设计等关键知识点也方便在源码基础上进行功能扩展。压缩包整体约 64.86MB以项目源码和演示视频为主源码适合在 IDE 中逐步阅读调试视频则用来对照运行效果、梳理部署流程。该资源已有 175 人浏览学习适合作为毕业设计选题参考或 Spring Boot 实战训练的辅助材料。通过源码与视频结合读者既能获得可直接参考的完整项目结构又能降低独立摸索的成本整体具备较高的实用价值。1. 基于Spring Boot的小说阅读平台难点不在CRUD如果毕业设计拿到的是“基于Spring Boot的在线小说阅读平台”多数人第一反应是用户注册、书籍列表、章节详情无非是一堆增删改查接口。真正写完一遍就知道这个题目最花时间的反而在两处一是章节内容动辄几十KB列表页和阅读页要分开设计二是阅读进度、书架、登录态这些状态要和前端来回对齐接口参数一改就是半天。这篇博文按我实际做这类项目的顺序从表结构、核心接口到打包部署把Spring Boot小说平台的设计与实现路径讲清楚。适合正在做springboot毕设的读者也适合打算从零搭一个内容型Web应用的一线工程师参考。方案采用Spring Boot 3.x MyBatis-Plus MySQL Redis的前后端分离结构这是目前毕设和中小项目最常见的组合。2. Spring Boot在小说平台里的职责边界模块划分与表结构设计2.1 从功能性需求拆出子模块小说阅读平台虽然看起来功能不少但拆开以后核心业务就四块用户体系、书籍与章节管理、阅读行为书架、进度、评论互动。后台管理界面另算但它操作的数据表基本覆盖这四块。Spring Boot在这里的职责是提供分层骨架Controller层只做参数接收和响应包装Service层承担业务规则Mapper层接数据库缓存、日志、异常处理这些横向能力由Spring Boot的starter机制拉进来。我一般建议把模块按包名分清楚而不是让所有代码挤在同一个包下面。比较实用的划分是controller、service、mapper、entity、dto、config、common、util。有些人会把entity里的表对象直接返给前端短平快但遇到章节内容这种大字段时响应体会无谓地变大。后面会讲到阅读页和列表页需要不同的出参结构这属于DTO层的设计问题。2.1.1 表设计不要把章节内容和大字段塞进书籍表字段设计直接影响接口性能。书籍主表只放书名、作者、分类、封面URL、简介、状态这些元信息章节单独一张表一对多挂在书籍ID下。章节表的核心字段是chapter_number、title、content、word_count、status。content用mediumtext单章最大能存16MB足够承载TXT文本单章几万字的内容。阅读进度的存法有两种常见方案一种是在书架表里加last_read_chapter和last_read_time另一种是单独开一张阅读记录表。前者适合“只记录每本书看到第几章”的简单场景后者能记录到“章节内滚动位置”但实现成本高。毕设和多数在线阅读站用书架表就够了记录到章节粒度即可。CREATE TABLE t_book ( id bigint NOT NULL AUTO_INCREMENT, book_name varchar(128) NOT NULL COMMENT 书名, author varchar(64) DEFAULT NULL COMMENT 作者, category_id int DEFAULT NULL COMMENT 分类ID, cover_url varchar(255) DEFAULT NULL COMMENT 封面图地址, intro varchar(1024) DEFAULT NULL COMMENT 简介, status tinyint DEFAULT 1 COMMENT 1连载 2完结, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_category_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_chapter ( id bigint NOT NULL AUTO_INCREMENT, book_id bigint NOT NULL COMMENT 所属书籍ID, chapter_number int NOT NULL COMMENT 章节序号从1开始, title varchar(128) NOT NULL, content mediumtext COMMENT 章节正文, word_count int DEFAULT 0 COMMENT 字数统计, status tinyint DEFAULT 1 COMMENT 1正常 0下架, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_book_chapter (book_id, chapter_number), KEY idx_book_status (book_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段DDL里有几个值得注意的地方唯一键uk_book_chapter放在书籍ID加章节序号上防止同一本书出现重复的章节号idx_category_status是给分类列表页用的联合索引能同时过滤分类和连载状态。content字段单独放章节表避免查询书籍列表时把正文也读出来。2.2 实体与Mapper层的选型MyBatis-Plus 的取舍ORM层用MyBatis-Plus是Spring Boot生态里成熟度较高的选择。它内置了BaseMapper单表CRUD不用写XML分页有PaginationInterceptor逻辑删除用TableLogic注解就能开启。对于小说平台来说书籍和章节都是典型的单表操作加少量联表查询MyBatis-Plus的开箱即用性能足够。Data TableName(t_book) public class Book { TableId(type IdType.AUTO) private Long id; private String bookName; private String author; private Integer categoryId; private String coverUrl; private String intro; private Integer status; private LocalDateTime createTime; }实体类里需要解释两个点TableId(type IdType.AUTO)对应数据库自增主键插入后MyBatis-Plus会把生成的主键回填到对象的id属性里方便后续插入章节时引用bookId。TableName(t_book)必须和表名一致如果不写这个注解MyBatis-Plus会默认把Book类映射到book表开发时很容易踩“表名对不上”的坑。2.2.1 分页查询必须配合拦截器很多人写列表接口时习惯用selectList查出全部再在内存里分页数据量小看不出问题章节多了以后慢查询和内存占用会同时暴露。MyBatis-Plus的分页插件需要显式配置才能生效Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }PaginationInnerInterceptor会自动拼接LIMIT语句setMaxLimit(100L)限制单页最大条数防止有人把pageSize传到几万导致数据库压力过大。注意这个配置在Spring Boot 3.x里是标准写法旧博客里常见的Bean PaginationInterceptor写法对应的是MyBatis-Plus 3.4以前版本混用会直接启动报错。3. 章节阅读与登录状态两个核心难点的Spring Boot实现3.1 JWT登录态与Interceptor拦截器小说平台的登录接口不建议用Session方案。前后端分离项目的前端和后端经常分属不同端口或域名Session的Cookie跨域处理麻烦而JWT无状态、JSON化放在请求头里传递服务端不用维护会话表。Spring Boot集成JWT的做法是登录成功后生成token返回前端前端放在Authorization请求头里后端用拦截器解析校验。Component public class JwtInterceptor implements HandlerInterceptor { Value(${jwt.secret}) private String secret; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(401, 未登录); } try { Claims claims Jwts.parser().verifyWith(SecretKeySpecUtil.getKey(secret)) .build().parseSignedClaims(token.substring(7)).getPayload(); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { throw new BusinessException(401, token无效或过期); } } }拦截器里的OPTIONS放行是前后端分离下必须处理的细节。浏览器跨域请求会先发一个预检请求如果拦截器直接拦截前端会收到401而不是真正的业务响应。claims.get(userId)是从token里取出用户标识后续Controller通过request.getAttribute(userId)就能拿到当前操作者。3.1.1 token有效期怎么设小说平台的JWT参数建议按下面的表配置兼顾使用体验和安全性参数建议值说明jwt.secret32字节以上随机字符串不要用短密钥HS256算法下密钥太短容易被暴力破解jwt.expire7天阅读类App登录频率低过期时间太短会频繁要求重新登录签发主体平台域名或项目名便于排查问题时识别token来源存储位置localStorage侵入性小但要注意XSS风险不要存敏感信息生成token时把userId和role放进去角色字段用于区分普通用户和管理员。注意role只做展示用真正判断管理员权限时仍要从数据库查因为token里的数据理论上可以被篡改拦截器只负责验签不负责业务权限。3.2 章节内容接口大字段的响应优化章节内容接口是整个平台数据量最大的接口。如果一个章节有2万字数据库中存储的纯文本大约40KB到60KB经过JSON序列化后响应体更大。优化方向是列表页不查content字段阅读页只返回当前章节并且用Redis缓存。列表页查询时用select只查需要的字段LambdaQueryWrapperChapter wrapper new LambdaQueryWrapper(); wrapper.select(Chapter::getId, Chapter::getChapterNumber, Chapter::getTitle, Chapter::getWordCount) .eq(Chapter::getBookId, bookId) .eq(Chapter::getStatus, 1) .orderByAsc(Chapter::getChapterNumber);select方法里指定列生成的SQL就只查这些字段content因为是mediumtext类型在InnoDB里可能存储在溢出页不查询它就是最有效的优化。阅读页接口则直接用getById取整行。3.3 Redis缓存章节内容与阅读进度章节内容属于典型的读多写少数据Redis缓存的价值非常明显。缓存key设计我一般用chapter:content:{chapterId}value用压缩后的JSON字符串。缓存策略采用Cache Aside Pattern查询时先读缓存没有就查库再写入后台修改章节时删除对应缓存。public ChapterVO getChapterContent(Long chapterId, Long userId) { String cacheKey chapter:content: chapterId; String json redisTemplate.opsForValue().get(cacheKey); if (StringUtils.hasText(json)) { return JSONUtil.toBean(json, ChapterVO.class); } Chapter chapter chapterMapper.selectById(chapterId); ChapterVO vo new ChapterVO(); BeanUtils.copyProperties(chapter, vo); redisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(vo), 2, TimeUnit.HOURS); saveReadingProgress(userId, chapter.getBookId(), chapterId); return vo; }这段代码体现了一个容易被忽略的问题阅读进度更新和缓存读取耦合在同一个方法里。当用户快速连续翻章时每次翻页都会执行一次进度写库频繁更新数据库。改进方案是把进度更新异步化用Spring的Async注解丢到线程池处理或者用本地队列批量刷。毕业设计阶段用Async最简单在启动类加EnableAsync在进度保存方法上加Async即可。Redis缓存的内容需要设置过期时间我用2小时。这样最新章节更新后最迟2小时用户就能看到新内容不用人工清理缓存。chapter:content这类key是典型的缓存穿透风险点如果有人遍历chapterId访问不存在的章节每次都会打到数据库。防御手段是在Redis里缓存空值并设置短过期或者在接口入口对超出范围的id直接拦截。4. 前端调用、接口联调与项目打包把Spring Boot跑成一个能演示的完整系统4.1 前后端分离的接口约定小说平台的接口设计要遵循几个约定统一响应结构、资源路径按书籍/章节/用户划分、查询参数用pageNum和pageSize。我常用下面的接口表格约束后端开发节奏方法路径功能鉴权POST/api/auth/login登录返回token否POST/api/auth/register注册用户否GET/api/book/list分页查询书籍列表否GET/api/book/{id}书籍详情含章节目录否GET/api/chapter/{id}/{pageNum}分页加载章节内容是POST/api/shelf/{bookId}加入书架是GET/api/shelf/list我的书架含最近阅读章节是统一响应体是Result对象包含code、message、data三个字段。前端拿到code不为0时统一弹错误提示后端抛出的BusinessException由全局异常处理器转成对应结构。4.2 yml配置方案的落地springboot的配置文件在毕设里最容易乱。我的习惯是拆成application.yml主配置、application-dev.yml开发配置、application-prod.yml生产配置启动时用spring.profiles.active指定环境。以下是一份经过验证的配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/novel_platform?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD:root} data: redis: host: localhost port: 6379 database: 0 timeout: 3s mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true jwt: secret: 你的随机密钥字符串 expire: 604800map-underscore-to-camel-case这个配置值很多人忽略。数据库字段是book_nameJava属性是bookName开启了驼峰映射后MyBatis才能自动对应。log-impl配成StdOutImpl会把SQL打印在控制台方便调试期看SQL执行情况生产环境应注释掉。密码字段用${DB_PASSWORD:root}这种语法支持从环境变量读取否则源码发出去后密码等于明文泄露。4.3 Maven打包时的两个注意点Spring Boot项目用Maven打包常见问题是前端静态资源没有合并进jar包。如果是前后端一体方案前端代码编译后放到src/main/resources/static目录下打包后访问http://ip:8080就是首页。前置分离方案则要把前端单独部署到Nginx后端只提供/api/下的接口。build finalNamenovel-platform/finalName plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.novel.NovelApplication/mainClass /configuration /plugin /plugins /buildfinalName决定了jar包的名字mainClass在有多个Spring Boot启动类时必须显式指定否则打包时会报找不到主类。打包命令用mvn clean package -DskipTests跳过测试测试类里如果依赖Redis或MySQL环境打包时连不上会导致构建失败跳过测试是毕设演示前最常见的保底操作。5. 上线演示前要做的三件事第一件是给慢查询建立索引。t_chapter表在书籍列表页要查某个书的所有章节数t_book表要按分类和状态筛选。如果数据量上了几十万量级之前建的联合索引就要验证是否真的生效用EXPLAIN SELECT * FROM t_chapter WHERE book_id 1 AND status 1看possible_keys和key是否匹配出现Using filesort说明排序字段需要单独建索引。第二件是Redis缓存穿透和击穿的防御。小说平台的书籍详情页是热点接口直接查数据库没问题但突然来几分钟的高并发就可能拖垮数据库。常见做法是把热门书籍的缓存时间调成24小时冷门书籍不缓存。用Redisson的分布式锁做缓存重建当缓存过期时只允许一个线程去查询数据库其他线程等待后直接取缓存。第三件是给部署加一个健康检查。Spring Boot Actuator引入后访问/actuator/health返回{status:UP}就能确认服务存活。更实用的做法是配合spring-boot-starter-validation给入参加校验注解比如NotNull、Size(max 50)非法参数在Controller层就被拦截不会压到Service层。启动脚本里可以用nohup java -jar novel-platform.jar --spring.profiles.activeprod后台运行配合tee把日志输出到文件排查问题时少很多折腾。本文还有配套的精品资源点击获取