SpringBoot漫画阅读网站实战:从自动装配到JWT与部署
2026/9/10 1:38:39 网站建设 项目流程

SpringBoot 漫画阅读网站,听起来像个平平无奇的题目,但真正动手做过一遍的同学都知道,它把 SpringBoot 开发里最常碰到的几件事——用户登录授权、文件上传、数据分页查询、前后端联调、线上部署——全部串起来了。我最早接触这个项目也是从一套“毕设附源码”的模板开始的,后来一路改了半个多月,把很多坑都填平了。这篇就当我给你交个底,从技术选型到答辩话术,按你做项目的顺序彻底讲清楚。适合正在选毕业设计题目、或者已经下载了源码包但跑不起来、又或者想给简历里加一个 SpringBoot 项目的同学直接参考。

1. 项目整体设计与技术选型

1.1 为什么 SpringBoot 是这类内容站点的默认答案

内容型网站的骨架无非是:内容展示 + 用户体系 + 内容管理。漫画阅读网站特殊一点,在内容展示上多了“章节多图”这种结构,但本质上还是分类、列表、详情、播放/阅读这一套 CRUD。这类业务用 SpringBoot 来做,最大的收益是它把配置做得足够薄,你只需要关注控制器、服务、Mapper 三层怎么写。

关于 SpringBoot 本身,有一点需要先在认知层面打通:它不是一个比 Spring 更强的框架,而是 Spring 的“自动化装配外壳”。@SpringBootApplication 里包含 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 三个注解。加载自动配置类时,SpringFactoriesLoader 会读取配置文件里注册的 AutoConfiguration 类,再通过 @ConditionalOnClass 之类的条件注解决定要不要装配。我常用一句话跟人解释:SpringBoot 帮你在条件满足时自动把组件准备好,你不用管创建过程,只需要在条件没满足时自己动手覆盖一行配置。

这个认知在答辩时特别值钱,因为很多同学被问到“SpringBoot 自动装配原理是什么”的时候,只会回答“反正它帮我自动配置了”,这不是好的回答。你能说出条件注解和加载机制,老师很容易判断出你是真的写过。

版本选择上,我强烈建议刚开始做毕设时不要盲目追新。当前源码模板最常见的组合是 SpringBoot 2.7 + JDK 8,因为兼容性最好,网上遇到的报错也基本都有现成解法。如果你非要用 SpringBoot 3.x,那 JDK 17 是跑不掉的,同时要留意 javax 包名改成 jakarta,Swagger、MyBatis-Plus 这些第三方库的版本也要升级。这个“版本太高”的问题,后续部分我会专门展开。

记住一条原则:版本匹配优先于功能新潮。模板是什么版本,先把对应 JDK 和依赖装对,能跑起来再谈升级。

1.2 项目功能模块怎么拆

我用一张表把功能模块整理出来,方便你对照需求文档写自己的开发计划:

模块用户端管理端
账号注册、登录、退出管理员登录
内容浏览首页推荐、分类浏览、关键词搜索漫画增删改查、上下架
章节阅读章节列表、图片阅读、翻页章节管理、图片批量上传
交互操作收藏漫画、保存阅读记录评论管理
个人中心收藏列表、阅读历史、资料修改用户管理、统计

这张表看起来平淡,但能帮你把数据库表和 Controller 接口一次性规划清楚。我见过不少同学做到一半,发现用户表和管理员表混在一起,权限没法区分,最后返工。我的建议是:即使你复用同一张 user 表,也要加上 role 字段,0 表示普通用户,1 表示管理员,接口层用拦截器做角色校验,这样扩展性最好,也最好解释。

1.3 源码包下载后,先按这三个步骤检查

标题里的 36567 是模板源码项目编号,这类压缩包里面基本会包含 sql 脚本、后端代码和前端页面。但质量参差不齐,先别急着解压到 IDEA 里点运行,按顺序做三次检查:

  1. 打开 README 或部署文档,看数据库版本要求、JDK 版本要求是否和本机一致。
  2. 检查 sql 目录里是否有建库脚本和初始数据脚本,没有的话项目跑起来也只是空壳。
  3. 检查 application.yml 中 MySQL 密码、端口、静态资源路径是否改成你自己的环境。

这三步做完,再启动项目,能把六成以上的“启动报错”挡在门外。剩下的报错,不管是数据库连不上还是端口占用,后面第 4 部分我会给出排查清单。

2. 从需求到表结构:核心功能拆解

2.1 用户端和管理端的功能边界

要做到功能不混淆,必须先把角色想清楚。普通用户的核心动作是“找漫画、看漫画、记录进度”,管理员的核心动作是“传漫画、管章节、看数据”,所以数据库设计也要围绕这两个核心动作展开。

用户端的真实点击路径,我整理成一段话:用户注册登录后,进入首页看到轮播图和热门漫画;点击漫画封面,进入详情页,能看到简介、分类、作者、章节数、收藏数;点击“开始阅读”进入阅读页,页面按顺序加载该章节所有图片,用户可以点击上一章/下一章;阅读过程中如果中途退出,系统要记录当前章节,下次进入时提示“继续阅读”。

管理端的路径相对简单:上传封面、填写漫画信息;添加章节时批量上传多张图片;编辑章节顺序;下架违规内容。整个管理端可以做成一个独立的前端页面,也可以做成同一个页面区分角色,但接口上必须做权限校验,不然任何用户都能调管理接口就很危险。

2.2 数据库核心表设计

我直接给出一套最实用的表结构,这些表在大多数漫画阅读模板里都有,只是字段名可能略有不同:

  • user:id、username、password、nickname、avatar、role、status、create_time
  • category:id、name、sort
  • comic:id、title、author、category_id、cover_url、intro、status、click_count、collect_count、create_time
  • chapter:id、comic_id、chapter_name、sort、create_time
  • chapter_image:id、chapter_id、img_url、img_order
  • comment:id、comic_id、user_id、content、status、create_time
  • collect:id、user_id、comic_id、create_time
  • reading_history:id、user_id、comic_id、chapter_id、update_time

这个设计的核心是 chapter 和 chapter_image 两张表。很多新手在建模时会把“章节内所有图片”存成一个字符串,用逗号分隔,结果查询时还要 split,排序也很痛苦。正确的做法是每张图一行,通过 chapter_id 关联,用 img_order 控制顺序,查询时直接按 img_order 排序就是阅读顺序。

另外,reading_history 和 collect 的区别要能说明白:collect 是用户主动收藏,一张表里同一个用户对同一本漫画只会有一条记录;reading_history 是阅读进度,用户每读一个新章节就要更新,所以判断存在时用 user_id + comic_id 查,存在就 update chapter_id,否则 insert。这也是一个典型的“先查后改”场景,建议放进事务里。

2.3 索引和事务设计,是答辩时能说的细节

很多同学交上去的表设计只有主键,功能也能跑,但面试官一看就知道没考虑过数据量。我建议至少在 comic 表的 category_id、title 上建索引,在 chapter 表的 comic_id 和 sort 上建联合索引,在 reading_history 表的 user_id 和 update_time 上建索引。理由很简单:这些字段就是日常查询条件,没有索引,数据量一大全表扫描会越来越慢。

事务方面,用户注册、添加章节、阅读记录保存这类操作建议加上 @Transactional。要特别注意的是,图片上传和数据库记录不是同一个事务能解决的:文件已经传到磁盘了,如果数据库插入失败,文件就成了孤儿文件。最好的办法是先上传到临时目录,等数据库操作成功后再把正式文件移动过去,或者定时清理孤儿文件。这个点在毕设里不一定会被问到,但如果你能主动提出来,评价会非常高。

3. 关键流程的实现细节

3.1 用户注册登录与 JWT 授权

这个模块是整个项目的核心,因为所有管理接口都需要身份验证。项目里比较常见的是用 JWT 做无状态认证,流程是:登录成功后生成 token 返回给前端;前端把 token 存在 localStorage;请求时在请求头加上 Authorization 字段;后端定义一个拦截器,在 preHandle 里校验 token。

后端生成 token 的核心逻辑类似:

public String login(String username, String password) { User user = userMapper.selectByUsername(username); if (user == null) { throw new BusinessException("用户不存在"); } if (!BCrypt.checkpw(password, user.getPassword())) { throw new BusinessException("密码错误"); } if (user.getStatus() == 0) { throw new BusinessException("账号已被禁用"); } return JwtUtil.createToken(user.getId(), user.getRole()); }

这里有个非常常见的坑:很多老模板用的是 MD5 加密密码,MD5 加不加盐都极其容易破解,并不适合用在公开项目里。你拿到源码后,哪怕不重写注册逻辑,至少要把密码存储方式从 MD5 换成 BCrypt。Spring Security 包里自带 BCryptPasswordEncoder,即使不引入完整 Security,也能单独拿来用。我在实际改代码时,一般把密码字段长度设置成 60 或 64,这样能存下 BCrypt 的哈希结果。

拦截器校验 token 的代码比较直接,但要注意两步:第一,前端预检请求 OPTIONS 必须直接放行,否则跨域请求永远会失败;第二,token 过期后要返回统一的 401 结果,而不是让 Spring 默认把异常抛到页面上。下面这种写法就是标准答案:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token != null && JwtUtil.verify(token)) { return true; } response.setStatus(401); response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录过期\"}"); return false; }

3.2 漫画与章节图片上传

漫画网站里大量图片要上传,前端一般用 form-data 方式 POST 到后端接口。后端用 MultipartFile 接收,然后把文件写到磁盘。这里的关键不是“用不用 MultipartFile”,而是文件要存到哪里、怎么访问。

我的实践方案是:在项目根目录或服务器上单独建一个 upload 目录,比如upload/comic/2025/03/,按月份分目录,防止单个目录文件太多。文件重命名用 UUID + 原文件后缀,这样既能避免中文文件名乱码,也能避免重名覆盖。数据库里只存相对路径,比如/files/comic/2025/03/xxx.jpg,前端访问时拼接服务器域名或 IP 就行。

图片访问的映射,我在 SpringBoot 里这样配:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }

这段代码的意思是,所有/files/**开头的请求,都去本地磁盘的{项目运行目录}/upload/下找文件。如果你遇到图片能上传但访问 404,八成是路径的问题——要么 upload 后面少了斜杠,要么在 Windows 和 Linux 下运行时工作目录不一样。建议在配置里把路径打出来看一眼,别猜。

3.3 章节阅读与阅读记录的实现

阅读页是漫画网站的灵魂页面。一个章节通常有几十张图片,如果一次性把整章接口返回,浏览器压力也不大,但更好的体验是分页加载。不过作为毕设,一次性返回所有图片 URL 完全够用,重点是图片加载顺序和切换逻辑。

我实际用的逻辑是:进入阅读页时,通过/chapter/{id}接口拿到当前章节信息和所有图片列表,前端用 JS 循环生成图片标签。点击上一章/下一章时,重新请求下一个章节的接口并刷新图片区域。同时,每次进入阅读页,页面加载完成就调用 saveRecord 接口,把用户、漫画、章节存到阅读历史表。

保存阅读记录的接口要处理“插入还是更新”的逻辑,最稳妥的代码是:

@Transactional public void saveRecord(Integer userId, Integer comicId, Integer chapterId) { ReadingHistory history = readingHistoryMapper.selectByUserAndComic(userId, comicId); if (history == null) { ReadingHistory newHistory = new ReadingHistory(); newHistory.setUserId(userId); newHistory.setComicId(comicId); newHistory.setChapterId(chapterId); readingHistoryMapper.insert(newHistory); } else { history.setChapterId(chapterId); readingHistoryMapper.updateById(history); } }

这个接口在多人并发时可能有问题,但毕设场景完全可以接受。你只要在答辩时说明白“这里我用了事务保证一致性”,就已经超过大多数模板的完成度了。

3.4 搜索与分类过滤的实现

搜索是用户端高频功能,尤其在漫画很多的时候。用 MyBatis 写动态 SQL 比较直观,比如:

<select id="searchComic" resultType="com.example.entity.Comic"> select * from comic <where> <if test="keyword != null and keyword != ''"> and (title like concat('%', #{keyword}, '%') or author like concat('%', #{keyword}, '%')) </if> <if test="categoryId != null and categoryId != 0"> and category_id = #{categoryId} </if> </where> order by click_count desc </select>

注意这里用<where>标签而不是手写where 1=1<where>能自动去掉第一个条件前面的 and,更规范,也不会出现“永远为真”这种危险条件。like 语法直接传%keyword%也可以,但我更推荐上面这种 concat 写法,防止关键字里混入特殊字符时把整条 SQL 弄脏。

如果需要更深度的搜索,可以引入 Elasticsearch,但毕业设计基本没必要。用 MySQL like 加个索引已经能覆盖题目要求的“搜索”功能,先把基础链路做扎实比什么都强。

4. 启动项目与日常开发的实战问题排查

4.1 SpringBoot 版本太高造成的启动报错,怎么判断

我接手这类源码包时,遇到过最多的问题就是本机 JDK 版本和项目要求的 SpringBoot 版本对不上。常见报错包括:

  • 启动类直接报UnsupportedClassVersionError,意思是 class 文件版本过高,当前 JVM 不支持;
  • 用 SpringBoot 3.x 的时候,项目里还在用javax.annotation.Resource,直接红色报错;
  • 用 SpringBoot 2.6+ 集成旧版 Swagger,启动时报Failed to start bean 'documentationPluginsBootstrapper',这个很好解决,在 application.yml 加一行spring.mvc.pathmatch.matching-strategy=ant_path_matcher

我的判断方法是:先看 pom.xml 里的<parent>版本,再看自己java -version的结果。模板是老的,就别硬用新版去跑;反过来,如果你已经写了不少 SpringBoot 3 的代码,也尽量别把版本降回去,因为 jakarta 命名空间和 starter 版本可能有兼容问题。实在要调整版本,就统一升齐,别只改 parent 版本,其他依赖也要跟着变。

4.2 前后端分离的跨域拦截

很多模板是 Vue + SpringBoot 前后端分离,本地开发时前端在 5173 或 8081 端口,后端在 8080,跨域是绕不开的问题。如果你用 JWT,请求头带着 Authorization,而浏览器预检 OPTIONS 请求不会带 token,如果拦截器拦住了 OPTIONS,前端就会出现“请求成功但拿不到数据”的诡异现象。

处理方式我推荐在 WebMvcConfig 里统一配置 CORS:

registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600);

同时确保拦截器放行 OPTIONS 请求。这两步做了,跨域问题基本清零。如果你用的是 @CrossOrigin 注解,也能解决大部分场景,但全局配置更省事,也方便以后扩展。

4.3 图片上传成功但访问 404 的排查思路

这个问题我在前面提到过,但值得单独列出来。启动项目后,接口能返回图片路径,浏览器打开却 404,排查顺序如下:

  1. 看数据库里存的是相对路径还是完整路径。完整路径要包含协议、域名和端口,否则前后端分离部署时,前端拿到的 URL 无法访问。
  2. 看静态资源配置是否生效,直接访问http://localhost:8080/files/test.png,如果 404 大概率是映射路径写错。
  3. 看上传文件到底保存在哪个目录,最简单的办法是打印System.getProperty("user.dir"),确认是不是你预期的工作目录。

提示:数据库里最好只存相对路径,把 IP、端口留给前端拼接。这样将来换服务器换域名,不用改数据库,只要改前端配置。

这里有个我踩过的坑:IDEA 里运行和java -jar部署运行时,工作目录不一样,导致同样一段上传代码,本地没问题,服务器上就找不到文件。后来我把上传目录配置写进 application.yml,用配置项指定固定路径,比如/home/app/upload/,就彻底解决了。

4.4 MySQL 连接报错与账号配置

下载下来的模板,数据库连接往往不是你的本地环境。常见的报错和修法我直接列成表:

报错原因处理方式
Access denied for user账号密码不对,或数据库没建检查 application.yml,改成自己的账号密码
Unknown database数据库名不存在先用 SQL 脚本建库,再启动
Public Key Retrieval is not allowedMySQL 8 + caching_sha2_passwordJDBC URL 加 allowPublicKeyRetrieval=true
The server time zone value时区没指定JDBC URL 加 serverTimezone=Asia/Shanghai
ClassNotFoundException

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询