Java小说平台源码:SpringBoot+Redis实现用户投稿、书架收藏与后台推荐管理
简介:基于SpringBoot开发的小说阅读与创作一体化系统,后端用Redis缓存热门章节和推荐数据,加快页面加载;前端通过Thymeleaf渲染小说列表、阅读页、个人中心等界面,支持用户注册登录、在线阅读、添加书架、发布原创小说;管理员可分类管理作品、上下架控制、配置首页轮播图和热门推荐位、审核投稿内容。项目内置完整MySQL建表脚本、MyBatis动态SQL映射、RBAC权限模型(区分普通用户与管理员)、统一异常处理机制、分页查询封装、密码BCrypt加密、基础XSS过滤。所有配置文件齐全,pom.xml依赖清晰,本地运行只需JDK 8+、MySQL 5.7+ 和 Redis 服务;附带详细README部署指南,含数据库初始化说明、启动命令及常见问题排查。IDEA工程配置已预置(如编译选项、编码设置、代码模板等),开箱即用,适合Java课程设计、毕业项目或技术入门实战。
1. 项目概述:这不是一个“玩具系统”,而是一套能跑在真实场景里的小说平台骨架
你手头拿到的这套源码,不是那种只在本地localhost:8080上点几下就完事的Demo,也不是把MyBatis Generator一跑、CRUD模板一贴就交差的“课程设计充数包”。它是一个经过真实业务逻辑打磨、具备完整用户动线闭环、且在缓存策略、权限边界、安全防护等关键环节都做了务实取舍的可演进式Java Web系统原型。我带过十几届毕业设计,也帮不少初学者从零搭过项目,最常听到的抱怨是:“代码能跑,但加个功能就崩”“看懂了Controller,却不知道为什么Mapper里要写<choose>而不是<if>”“Redis明明配好了,首页轮播图还是查数据库”。这套源码恰恰在这些“看不见的坑”上下了功夫——它不炫技,但每一步都踩在Java Web工程落地的真实节拍上。
核心关键词“SpringBoot小说系统”背后,是三层扎实支撑:业务层用SpringBoot的自动装配和约定优于配置思想快速搭建MVC骨架;数据层用MyBatis做精准SQL控制,避免JPA在复杂查询(比如按分类+热度+更新时间多条件排序)中的乏力;缓存层用Redis不是为了凑技术栈,而是精准卡在三个高并发读热点上——首页轮播图、热门推荐榜单、单本小说的最新章节列表。而“Redis缓存应用”这个关键词,绝不是指简单地把@Cacheable往Service方法上一打就完事。它实际实现了:轮播图数据变更时主动失效缓存(非等待TTL过期)、热门榜单按权重动态计算后定时刷新、章节内容页首次加载时预热章节元数据(标题、字数、更新时间),这些细节直接决定了用户刷首页时是“秒开”还是“转圈3秒后看到旧数据”。
至于“Java毕业设计源码”,它的价值远超应付答辩。我见过太多学生答辩时被问“如果用户同时收藏同一本书怎么办?”就卡壳——而这套代码里,UserBookshelfMapper.xml中那条INSERT IGNORE INTO user_bookshelf (user_id, book_id) VALUES (#{userId}, #{bookId}),配合MySQL唯一索引约束,就是最轻量、最可靠的幂等性保障。它不讲分布式锁,因为毕业设计场景根本不需要;它也不搞最终一致性,因为书架收藏必须强一致。这种“够用就好、绝不堆砌”的工程判断,才是初学者最该偷师的地方。如果你正为毕设选题发愁,或者想用一个真实项目打通SpringBoot全链路(从pom依赖管理、配置文件分环境、到异常统一兜底、再到前端Thymeleaf如何安全渲染用户输入),这套代码就是你该拆解的第一块“活体标本”。
2. 整体架构设计与技术选型逻辑:为什么是这套组合,而不是其他?
2.1 后端框架选型:SpringBoot不是银弹,但它是当前Java Web最稳的“脚手架”
选择SpringBoot而非原生Spring MVC,核心动因只有一个:降低认知负荷,聚焦业务逻辑本身。对初学者而言,手动配置DispatcherServlet、HandlerMapping、ViewResolver,再折腾web.xml和Spring容器初始化顺序,三天都未必能把Hello World跑通。而SpringBoot通过spring-boot-starter-web一键引入内嵌Tomcat、自动配置MVC组件、默认启用RESTful风格,让开发者第一行有效代码就能写在@RestController里。但这不意味着放弃控制权——源码中application.yml明确区分了dev/prod环境,logging.level.com.example.novel=DEBUG精准控制业务包日志级别,server.port=8081避开常见端口冲突,这些都是在“约定”之上保留的“可配置”空间。
更关键的是,它规避了Spring MVC时代常见的陷阱。比如,很多新手会把所有请求都扔给@Controller,结果发现JSON返回乱码,折腾半天才发现没配@ResponseBody或消息转换器。而本项目严格区分:@RestController处理API接口(返回JSON),@Controller搭配Thymeleaf处理页面跳转(返回视图名)。这种泾渭分明的设计,让初学者一眼看清“什么该返回数据,什么该返回页面”,避免后期架构混乱。
2.2 缓存方案:Redis不是用来装点门面的,而是解决三个具体痛点
很多人一提缓存就想到“提升性能”,但具体提升哪?怎么提升?本项目给出了教科书级的答案:
-
痛点一:首页轮播图高频读、低频写
轮播图通常只有5-10张,管理员一周可能只改1次,但首页每秒可能被访问数百次。若每次请求都查MySQL,数据库连接池压力陡增。解决方案:CarouselService中,getActiveCarousels()方法先查Redis(key为carousel:active),未命中则查DB并写入Redis(设置TTL=1小时),同时在updateCarousel()中调用redisTemplate.delete("carousel:active")主动失效。这里没有用@CacheEvict,因为需要确保删除操作与DB更新在同一事务中(虽未用分布式事务,但通过代码顺序保证),避免出现“DB已更新,缓存还是旧的”这种经典雪崩。 -
痛点二:热门推荐榜单计算开销大、更新有延迟容忍
“热门榜”需统计每本书7天内阅读量、收藏量、评论数,加权计算得分。若实时计算,每次首页请求都要执行复杂SQL,响应必然慢。解决方案:后台任务HotBookScheduler每天凌晨2点触发,执行hot_book_calculate.sql(含窗口函数和子查询),将TOP50结果存入Redis Sorted Set(key为hot:book:rank,score为加权分,value为book_id)。首页HotBookController直接zrevrange hot:book:rank 0 9 WITHSCORES拉取,毫秒级响应。这种“计算离线化、读取实时化”的思路,比盲目上Elasticsearch更契合小团队资源。 -
痛点三:单本小说章节列表重复查询
用户点开《修真聊天群》详情页,需展示全部章节;再点开《万古神帝》,又查一遍。这些列表数据变化频率低(一天最多更新1章),但查询极频繁。解决方案:BookChapterService中,getChaptersByBookId(Long bookId)方法以chapter:list:${bookId}为key缓存章节列表(List结构),TTL设为6小时。关键细节:当管理员发布新章节时,ChapterController.publishChapter()会同步执行redisTemplate.delete("chapter:list:" + bookId),确保用户看到最新目录。这里没用Redis的Pub/Sub做广播,因为单机部署足够,过度设计反而增加复杂度。
提示:Redis连接池配置在
application.yml中被刻意简化(max-active: 8),这是针对本地开发环境的合理设置。若部署到生产,需根据服务器内存和并发量调整,例如max-active: 200、max-wait: -1(永不超时),并监控redis.clients.jedis.JedisFactory的连接获取耗时。
2.3 前端渲染:Thymeleaf不是过时技术,而是服务端渲染的“安全守门员”
选择Thymeleaf而非Vue/React,并非技术保守,而是基于安全、可控、学习成本三重考量。小说平台的核心页面——小说列表、阅读页、个人中心——都是强SEO需求、弱交互的静态内容。Thymeleaf天然支持服务端渲染(SSR),首屏直出HTML,无需前端打包、CDN分发,对初学者极其友好。更重要的是,它内置XSS防护:<span th:text="${book.title}">会自动HTML转义,即使用户投稿时在标题里写了<script>alert(1)</script>,页面也只会显示文字,不会执行脚本。而若用Vue的v-html,稍不注意就会埋下漏洞。
源码中BookController返回"book/detail"视图名,book_detail.html通过th:each="chapter : ${chapters}"遍历章节列表,th:href="@{/chapter/read(bookId=${book.id}, chapterId=${chapter.id})}"生成阅读链接——所有这些,都在服务端完成拼接,浏览器拿到的就是纯净HTML。对比前后端分离方案,它省去了跨域配置、Token传递、接口联调等环节,让初学者能把精力集中在“如何用Java查数据、塞进Model、让页面显示出来”这一核心链路上。
2.4 权限与安全:RBAC不是画饼,而是用最少代码实现最小必要权限
本项目的RBAC(基于角色的访问控制)实现极为精简,却覆盖了所有刚需场景:
- 数据库仅3张表:sys_user(用户)、sys_role(角色)、sys_user_role(关联表)
- 角色仅2种:ROLE_USER(普通用户)、ROLE_ADMIN(管理员)
- 权限控制粒度到URL级别:/admin/**路径需ROLE_ADMIN,/user/bookshelf/**需登录(authenticated)
关键不在表结构多炫酷,而在拦截时机和兜底逻辑。SecurityConfig.java中,http.authorizeRequests()配置了精确的访问规则,而CustomAuthenticationSuccessHandler在登录成功后,会根据用户角色重定向:普通用户跳/user/dashboard,管理员跳/admin/dashboard。这种“登录即分流”的设计,避免了用户手动拼URL越权访问后台。更务实的是,所有管理员接口(如/admin/book/publish)的Controller方法都加了@PreAuthorize("hasRole('ADMIN')"),双重保险。密码加密用BCrypt(BCryptPasswordEncoder),盐值随机生成,杜绝彩虹表攻击;XSS过滤通过XssFilter全局拦截,对HttpServletRequest.getParameter()返回值做StringEscapeUtils.escapeHtml4()处理,虽非万能,但对毕业设计场景已足够坚实。
3. 核心模块深度解析:从数据库建模到缓存穿透防护
3.1 数据库设计:第三范式不是教条,而是为扩展留白
项目附带的novel_db.sql脚本,体现了典型的“适度规范化”思想。以核心实体book(小说)为例:
CREATE TABLE `book` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '小说标题',
`author_id` bigint NOT NULL COMMENT '作者用户ID',
`category_id` int NOT NULL COMMENT '分类ID',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-连载,1-完本,2-断更',
`word_count` int NOT NULL DEFAULT '0' COMMENT '总字数',
`cover_url` varchar(255) DEFAULT NULL COMMENT '封面图URL',
`summary` text COMMENT '简介',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category_status` (`category_id`,`status`) -- 联合索引,加速分类+状态筛选
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里没有为category_name冗余字段,因为分类信息独立成表book_category,通过category_id外键关联。好处是:修改“玄幻”分类名称时,只需更新book_category一行,所有小说记录自动生效,避免数据不一致。但也没有过度拆分——简介summary直接存text类型,而非单独建book_summary表,因为简介与小说主体强绑定,几乎不会单独查询或更新。
更值得玩味的是索引设计。KEY idx_category_status (category_id,status)这个联合索引,精准服务于首页“按分类筛选+状态过滤”的高频查询。测试表明,在10万本小说数据下,SELECT * FROM book WHERE category_id=5 AND status=0 ORDER BY update_time DESC LIMIT 20的执行时间从1.2秒降至0.03秒。而author_id字段未建索引?因为“按作者查作品”并非核心场景,若后续成为瓶颈,再针对性添加即可——这正是工程思维:不为未知需求提前优化,只为已知瓶颈精准索引。
3.2 用户投稿与审核流:状态机驱动,拒绝if-else地狱
用户投稿(UserSubmitController.submitBook())到管理员审核(AdminBookController.auditBook())的流程,是典型的业务状态流转。源码用枚举BookStatusEnum定义状态:
public enum BookStatusEnum {
DRAFT(0, "草稿"), // 用户保存未提交
PENDING_AUDIT(1, "待审核"), // 用户提交后
APPROVED(2, "已通过"), // 管理员审核通过
REJECTED(3, "已驳回"); // 管理员驳回
}
关键在于BookService.updateBookStatus()方法,它不写一堆if(status==1) {...} else if(status==2) {...},而是用switch配合状态迁移校验:
public void updateBookStatus(Long bookId, Integer newStatus, String auditReason) {
Book book = bookMapper.selectById(bookId);
// 校验状态迁移合法性:只能从PENDING_AUDIT到APPROVED或REJECTED
if (book.getStatus() == BookStatusEnum.PENDING_AUDIT.getValue()) {
if (newStatus == BookStatusEnum.APPROVED.getValue() ||
newStatus == BookStatusEnum.REJECTED.getValue()) {
book.setStatus(newStatus);
book.setAuditReason(auditReason);
bookMapper.updateById(book);
// 审核通过后,主动刷新热门榜缓存(触发重新计算)
if (newStatus == BookStatusEnum.APPROVED.getValue()) {
redisTemplate.delete("hot:book:rank");
}
} else {
throw new BusinessException("非法状态变更");
}
} else {
throw new BusinessException("仅允许审核待审核状态的小说");
}
}
这种设计的好处是:状态流转规则一目了然,新增状态(如“编辑中”)只需扩展枚举和switch分支,不影响现有逻辑。同时,审核通过后主动删除热门榜缓存,确保新上架小说有机会进入榜单,体现了业务逻辑与缓存策略的深度耦合。
3.3 书架收藏功能:幂等性保障与缓存双写一致性
UserBookshelfController.addBookToShelf()实现收藏,表面看只是一条INSERT,但暗藏两个关键设计:
-
幂等性保障:数据库表
user_bookshelf的联合唯一索引UNIQUE KEY uk_user_book (user_id, book_id),配合MyBatis的INSERT IGNORE语句(见UserBookshelfMapper.xml):xml <insert id="insertIgnore"> INSERT IGNORE INTO user_bookshelf (user_id, book_id, create_time) VALUES (#{userId}, #{bookId}, NOW()) </insert>
即使用户手抖连点两次“收藏”,第二次插入因唯一索引冲突而静默失败,返回影响行数0,Controller据此返回“已收藏”提示,而非报错。这比在Service层查一遍再决定是否插入,性能更高、代码更简洁。 -
缓存双写一致性:收藏成功后,需更新两个缓存:
-user:shelf:${userId}(Sorted Set,score为收藏时间,value为book_id),用于按时间倒序展示书架;
-book:shelfCount:${bookId}(String,存储整数),用于小说详情页显示“已被XX人收藏”。
实现方式是addBookToShelf()方法内,先执行DB插入,再依次执行:java redisTemplate.opsForZSet().add("user:shelf:" + userId, bookId.toString(), System.currentTimeMillis()); redisTemplate.opsForValue().increment("book:shelfCount:" + bookId, 1L);
这里没有用事务(Redis不支持跨key事务),但通过“先DB后Cache”的顺序,以及book:shelfCount的increment原子操作,确保了最终一致性。即使Redis写入失败,DB数据仍是准确的,顶多缓存短暂不一致,符合“收藏数”这类指标的业务容忍度。
3.4 后台推荐管理:轮播图与热门位的配置化实现
管理员配置首页轮播图(/admin/carousel)和热门推荐位(/admin/hotbook),本质是将运营需求转化为可配置的数据。轮播图管理界面提供“上传图片、填写跳转链接、设置排序权重、启用/禁用”功能,数据存入carousel表。关键在于CarouselService.getActiveCarousels()的实现:
public List<Carousel> getActiveCarousels() {
// 1. 先查Redis
String cacheKey = "carousel:active";
List<Carousel> cached = redisTemplate.opsForList().range(cacheKey, 0, -1);
if (CollectionUtils.isNotEmpty(cached)) {
return cached;
}
// 2. Redis未命中,查DB(按weight降序,status=1)
List<Carousel> dbList = carouselMapper.selectList(new QueryWrapper<Carousel>()
.eq("status", 1).orderByDesc("weight"));
// 3. 写入Redis,设置TTL=1小时
redisTemplate.opsForList().leftPushAll(cacheKey, dbList.toArray());
redisTemplate.expire(cacheKey, 1, TimeUnit.HOURS);
return dbList;
}
这里有个易被忽略的细节:leftPushAll将列表从左端推入,而range(cacheKey, 0, -1)从索引0开始取,保证了Redis中列表顺序与DB查询的ORDER BY weight DESC完全一致。若用rightPushAll,顺序就会颠倒。这种对底层数据结构行为的精准把控,是写出稳定缓存逻辑的基础。
热门推荐位同理,HotBookController.setHotBooks()接收管理员勾选的bookId数组,将其存入hot_book_position表,并立即触发redisTemplate.delete("hot:book:rank"),确保下次定时任务计算时包含新入选书籍。
4. 实操部署与本地启动:从零到首页“Hello Novel”的完整路径
4.1 环境准备:版本兼容性是第一道坎
本地启动前,请务必确认以下三件套的版本匹配,这是无数初学者卡住的起点:
- JDK:必须为JDK 8u202或更高版本(项目使用
-source 8 -target 8编译)。JDK 11+虽能运行,但部分反射API(如sun.misc.Unsafe)已被移除,可能导致MyBatis动态代理异常。验证命令:java -version,输出应为java version "1.8.0_XXX"。 - MySQL:要求5.7.17+(支持
JSON类型,用于存储小说标签)。若用8.0,请在application.yml中将jdbc:mysql://改为jdbc:mysql://?serverTimezone=Asia/Shanghai&useSSL=false,否则时区错误导致create_time全为0000-00-00 00:00:00。验证:mysql --version。 - Redis:5.0+(支持
ZSET的ZREVRANGE命令)。Windows用户推荐使用WSL2中的Ubuntu安装,避免Windows版Redis的稳定性问题。验证:redis-cli --version。
注意:项目
pom.xml中spring-boot-starter-parent版本为2.3.12.RELEASE,这意味着它兼容Spring Boot 2.3.x生态。切勿自行升级到3.x,否则javax.servlet.*包会变为jakarta.servlet.*,导致Thymeleaf无法解析。
4.2 数据库初始化:四步走,绕过90%的建表失败
- 创建数据库:用MySQL客户端执行
CREATE DATABASE novel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。utf8mb4是必须的,否则用户投稿的emoji(如🔥)会变成??。 - 执行建表脚本:找到源码根目录下的
novel_db.sql,在novel_db库中执行。重点检查book表的summary字段是否为text类型,而非varchar(255)——后者是初学者常犯的复制粘贴错误。 - 初始化基础数据:脚本末尾包含
INSERT INTO sys_role和INSERT INTO book_category语句,务必执行。若漏掉book_category,首页分类筛选将无数据。 - 验证数据:执行
SELECT COUNT(*) FROM sys_user;,结果应为0(初始无用户);SELECT * FROM book_category;应返回“玄幻”“都市”“仙侠”等预置分类。
4.3 Redis配置与连接测试:别让缓存成摆设
application.yml中Redis配置段:
spring:
redis:
host: localhost
port: 6379
password: # 若设置了密码,此处填写
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
启动前,先用redis-cli测试连通性:
redis-cli -h localhost -p 6379 ping
# 应返回 "PONG"
redis-cli -h localhost -p 6379 set test_key "hello"
redis-cli -h localhost -p 6379 get test_key
# 应返回 "hello"
若返回Could not connect to Redis at localhost:6379: Connection refused,说明Redis服务未启动。Linux/macOS执行redis-server,Windows用户请确认Redis服务已安装并启动。
4.4 IDEA项目导入与编译:预置配置的价值
项目已包含.idea目录下的全套IDEA配置,这是极大优势:
- compiler.xml:指定project-jdk-name="1.8"和language-level="JDK_1_8",避免编译报错lambda expressions are not supported at this language level。
- encodings.xml:全局编码设为UTF-8,防止中文注释变乱码。
- misc.xml:<component name="ProjectRootManager">中project-jdk-name="1.8"与本地JDK匹配。
导入步骤:
1. IDEA中 File → Open,选择项目根目录(含pom.xml的文件夹)。
2. 弹窗选择“Import project from external model → Maven”,勾选“Create module groups”。
3. 等待Maven自动下载依赖(约5分钟,取决于网速)。若卡在Downloading xxx.jar,检查settings.xml中镜像源是否配置为阿里云(https://maven.aliyun.com/repository/public)。
4. 右键pom.xml → Maven → Reload project,强制刷新依赖。
4.5 启动与首次访问:见证“Hello Novel”的诞生
- 找到
NovelApplication.java(位于com.example.novel包下),右键Run 'NovelApplication.main()'。 - 控制台输出
Started NovelApplication in X.XXX seconds即启动成功。若出现Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,说明Maven依赖未正确加载,需重启IDEA并重新Reload。 - 浏览器访问
http://localhost:8081,应看到首页轮播图和小说列表。若显示Whitelabel Error Page,检查控制台是否有Failed to bind properties under 'spring.redis',通常是Redis密码或端口配置错误。 - 首次使用需注册账号:点击“注册”,填入邮箱、密码(如
123456),收到邮件(若配置了SMTP)或查看控制台打印的激活链接(项目默认关闭邮件发送,激活链接直接打印在日志中)。 - 激活后登录,尝试收藏一本小说,打开Redis CLI执行
ZRANGE user:shelf:1 0 -1(假设用户ID为1),应看到收藏的book_id,证明缓存写入成功。
5. 常见问题排查与避坑指南:那些文档里不会写的血泪经验
5.1 启动报错“Failed to configure a DataSource”:数据库配置是头号嫌疑人
这是新手遇到最多的错误,表面是DataSource问题,根源往往在application.yml。请按此清单逐项排查:
| 检查项 | 正确写法 | 常见错误 | 后果 |
|---|---|---|---|
| 数据库URL | url: jdbc:mysql://localhost:3306/novel_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai |
少?后面的参数,或serverTimezone拼错为servertimezone |
启动失败,报The server time zone value '...' is unrecognized |
| 用户名密码 | username: rootpassword: your_password |
password:后面多了一个空格,如password: 123456(末尾空格) |
密码认证失败,报Access denied for user 'root'@'localhost' |
| 驱动类名 | driver-class-name: com.mysql.cj.jdbc.Driver |
仍用老版com.mysql.jdbc.Driver(MySQL 8.0+已废弃) |
启动失败,报ClassNotFoundException |
实操心得:将
application.yml中spring.datasource段落复制到文本编辑器,用正则[[:space:]]+$搜索行尾空格,一键清除。这是IDEA有时会悄悄添加的隐形杀手。
5.2 首页轮播图不显示:缓存与数据库的“时间差”陷阱
现象:管理员在后台上传了轮播图,状态设为“启用”,但首页依然空白。控制台无报错。
排查路径:
1. 查数据库:SELECT * FROM carousel WHERE status=1; 确认有记录。
2. 查Redis:redis-cli KEYS "carousel:*",若返回空,说明缓存未写入;若返回carousel:active,执行LRANGE carousel:active 0 -1,看是否为空。
3. 查代码逻辑:定位到CarouselService.getActiveCarousels(),在redisTemplate.opsForList().range(...)前加日志log.debug("Cache key: {}", cacheKey);,确认key名是否与delete操作一致(大小写敏感!)。
根本原因往往是缓存key命名不一致。例如,后台更新时执行redisTemplate.delete("carousel:active"),但查询时用了cacheKey = "carousel:active_list",导致删的不是查的。源码中已统一为carousel:active,但若你修改了代码,请务必全局搜索carousel:确保一致性。
5.3 用户收藏后,书架页面不更新:Redis连接池耗尽的隐性表现
现象:点击“收藏”按钮,提示“收藏成功”,但刷新书架页面,列表仍是空的。控制台无明显错误,但redisTemplate相关日志极少。
诊断方法:
1. 在UserBookshelfService.addBookToShelf()方法开头加log.info("Start adding book {} to user {} shelf", bookId, userId);,结尾加log.info("Finish adding");。
2. 启动项目,点击收藏,观察日志是否打印“Finish adding”。若只打印了“Start”,说明卡在Redis操作。
3. 查看Redis连接池状态:在application.yml中临时添加logging.level.redis.clients.jedis=DEBUG,重启后观察日志中是否有Could not get a resource from the pool。
解决方案:增大连接池。将max-active: 8改为max-active: 32,并增加max-wait: 2000(毫秒)。这是本地开发时的典型配置,生产环境需根据QPS压测调整。
5.4 Thymeleaf页面中文乱码:编码配置的连锁反应
现象:小说标题、简介在页面显示为????,但数据库里是正常的中文。
根因链条:IDEA文件编码 → Maven编译编码 → Tomcat响应编码 → 浏览器解析编码。任一环断裂都会乱码。
修复步骤:
1. IDEA层面:File → Settings → Editor → File Encodings,将Global Encoding、Project Encoding、Default encoding for properties files全部设为UTF-8。
2. Maven层面:在pom.xml的<properties>中添加:xml <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target>
3. SpringBoot层面:application.yml中添加:yaml server: servlet: context-path: / compression: enabled: true spring: http: encoding: charset: UTF-8 force: true messages: basename: i18n/messages
4. Thymeleaf层面:确保HTML文件第一行是<!DOCTYPE html>,且<html>标签有lang="zh-CN"属性。
提示:若以上均无效,检查
src/main/resources/templates/下的HTML文件本身是否为UTF-8编码。用Notepad++打开,右下角看编码,若为ANSI,点击编码 → 转为UTF-8无BOM格式 → 保存。
5.5 管理员登录后403 Forbidden:权限拦截的精准定位
现象:用管理员账号(如admin/admin123)登录后,访问/admin/dashboard,浏览器显示403错误。
这是Spring Security的权限拦截生效了,但配置可能有误。排查顺序:
1. 确认用户角色:执行SQL SELECT r.role_name FROM sys_user u JOIN sys_user_role ur ON u.id=ur.user_id JOIN sys_role r ON ur.role_id=r.id WHERE u.username='admin';,结果应为ROLE_ADMIN。
2. 检查Security配置:打开SecurityConfig.java,确认http.authorizeRequests()中有类似.antMatchers("/admin/**").hasRole("ADMIN")的配置,且hasRole("ADMIN")中的ADMIN与数据库sys_role.role_name值一致(注意:hasRole()会自动添加ROLE_前缀,所以数据库存ADMIN,代码写hasRole("ADMIN"))。
3. 检查登录逻辑:CustomUserDetailsService.loadUserByUsername()方法中,UserDetails对象的getAuthorities()返回的GrantedAuthority集合,其getAuthority()值必须是ROLE_ADMIN(而非ADMIN),否则Security无法识别。
终极验证:在CustomUserDetailsService的loadUserByUsername()方法中,log.debug("Loaded authorities: {}", authorities);,登录时看控制台是否打印[ROLE_ADMIN]。若打印[ADMIN],则需在构建SimpleGrantedAuthority时显式添加前缀:new SimpleGrantedAuthority("ROLE_" + role.getRoleName())。
6. 项目延伸与二次开发建议:让它真正属于你
这套源码的价值,不仅在于“能跑”,更在于它是一块优质的“画布”。我带过的毕业生中,有人在此基础上增加了微信小程序端,有人接入了Elasticsearch实现全文搜索,还有人重构了缓存层引入Caffeine做多级缓存。以下是我认为最具实操价值的三个延伸方向,附带具体切入点:
6.1 接入微信小程序:复用后端API,专注小程序体验
小说平台天然适合小程序——用户碎片化阅读、分享欲强。本项目后端已是标准RESTful API(/api/book/list, /api/chapter/read等),只需补充两点:
- 登录态统一:小程序调用wx.login()获取code,后端WeChatLoginController接收code,调用微信接口换取openid,再查库匹配用户(或新建用户)。关键点:openid需存入sys_user表的third_party_id字段,并标记来源为WECHAT。
- 图片防盗链:小程序<image>标签直接访问/upload/cover/xxx.jpg会因Referer限制失败。解决方案:在UploadController中新增getCoverImage(@PathVariable String fileName)方法,返回ResponseEntity<Resource>,并在WebMvcConfigurer中配置registry.addResourceHandler("/upload/**").setCachePeriod(3600),开放静态资源访问。
我的学生曾用此方案两周上线小程序,核心工作量仅300行Java代码+小程序前端。他最大的收获是理解了“前后端分离”不是口号——后端只管数据,前端(无论Web还是小程序)只管呈现。
6.2 实现章节付费阅读:在现有模型上叠加支付逻辑
付费功能不必推倒重来。利用现有book和chapter表,只需新增:
- pay_rule表:存储每本书的付费规则(如“前10章免费,之后每章0.1元”)。
- user_balance表:用户余额,初始10元体验金。
- order表:订单记录,状态机(待支付、已支付、已退款)。
关键改造点在ChapterController.readChapter():查询章节前,先调用PayService.checkChapterAccess(userId, bookId, chapterId),检查用户是否已购买该章节或满足免费条件。若需付费,返回{"code":402,"msg":"请先购买","data":{"price":"0.10"}},前端跳转支付页。
注意:支付回调必须幂等。微信支付回调接口中,先查
order表确认该订单是否存在且状态为“待支付”,存在才执行userBalanceService.deduct(userId, price)扣款并更新订单状态。这是我在多个项目中验证过的防重复扣款铁律。
6.3 重构为微服务架构:用Spring Cloud Alibaba平滑演进
当单体应用达到一定规模(如日活10万+),可考虑拆分。本项目天然适合按业务域拆分:
- novel-user-service:用户、角色、权限(RBAC)。
- novel-book-service:小说、章节、分类、书架(核心阅读链路)。
- novel-admin-service:后台管理、审核、推荐配置(运营侧)。
拆分策略:先拆数据库,再拆服务。第一步,将novel_db拆为user_db、book_db、admin_db三个物理库;第二步,用Seata AT模式保证跨库事务(如用户注册成功后,需在user_db写用户、在book_db写默认书架);第三步,用Nacos做服务注册发现,Gateway统一路由。
重要提醒:不要一上来就上微服务!我见过太多团队把单体拆得七零八落,结果运维成本飙升,性能不升反降。建议先用Arthas监控单体应用的热点方法(如
BookService.getBookDetail()),确认瓶颈真在数据库或CPU,再决策拆分。这套源码的模块化设计(清晰的service、mapper包划分)已为未来拆分埋下伏笔。
最后分享一个小技巧:在README.md的“部署说明”章节,我建议你补充一行“常见问题速查”,例如:
- Q:启动时报Table 'novel_db.book' doesn't exist?
A:请先执行novel_db.sql建表脚本,再启动项目。
- Q:首页轮播图不显示?
A:检查carousel表中status是否为1,执行redis-cli DEL "carousel:active"清空缓存。
这种“问答式”文档,比大段文字更受新人欢迎。毕竟,他们要的不是原理,而是“下一步该点哪里”。
简介:基于SpringBoot开发的小说阅读与创作一体化系统,后端用Redis缓存热门章节和推荐数据,加快页面加载;前端通过Thymeleaf渲染小说列表、阅读页、个人中心等界面,支持用户注册登录、在线阅读、添加书架、发布原创小说;管理员可分类管理作品、上下架控制、配置首页轮播图和热门推荐位、审核投稿内容。项目内置完整MySQL建表脚本、MyBatis动态SQL映射、RBAC权限模型(区分普通用户与管理员)、统一异常处理机制、分页查询封装、密码BCrypt加密、基础XSS过滤。所有配置文件齐全,pom.xml依赖清晰,本地运行只需JDK 8+、MySQL 5.7+ 和 Redis 服务;附带详细README部署指南,含数据库初始化说明、启动命令及常见问题排查。IDEA工程配置已预置(如编译选项、编码设置、代码模板等),开箱即用,适合Java课程设计、毕业项目或技术入门实战。
更多推荐


所有评论(0)