ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

在线小说阅读平台毕设实战:SpringBoot+Vue+MySQL全栈开发与避坑指南

在线小说阅读平台毕设实战:SpringBoot+Vue+MySQL全栈开发与避坑指南 简介采用 Java、Spring Boot、Vue 和 MySQL 技术栈开发的在线小说阅读平台完整源码与数据库文件面向需要毕业设计、课程设计或期末大作业参考的学生及开发者。项目功能完善包含用户注册登录、小说检索浏览、阅读进度保存、章节更新提醒、阅读模式切换等前后端分离便于独立开发与二次扩展。资源共 835 个文件涵盖 Java 后端源码、Vue 组件、JS/CSS 前端脚本、HTML 页面、SVG 图标、GIF/JPG/PNG 图片素材以及 SQL 数据库脚本、Maven 配置与启动脚本压缩包约 15.6MB。数据库采用 MySQL 8.0并提供 Navicat 管理支持。已有 50 人浏览学习。项目经严格调试可运行免改即用可作为高分毕设的直接参考也是学习 Spring Boot 与 Vue 全栈开发的实践案例系统界面美观、管理便捷可快速搭建私有阅读平台或用于课程设计演示。1. 在线小说阅读平台为什么这个毕设题目性价比高到少见写毕设最怕的不是不会写代码而是选了一个听起来高级、做起来到处是坑最后连演示都跑不顺的题目。基于 Java SpringBoot Vue MySQL 的在线小说阅读平台名字听着普通但它其实把登录鉴权、分页检索、阅读进度、评论互动、后台管理这一整套真实业务的前后端分离闭环全占齐了。对大多数计算机专业学生来说这是一条可以按步骤跑通的路线对想冲高分毕设的人来说它又有足够的纵深可以展开。这类项目交付时一般是一个 zip一套 SpringBoot 后端源码、一套 Vue 前端代码外加一份 .sql 建库脚本。数据库不是现成的需要你自己导入源码也要按本机环境去改配置。这篇文章就围绕这个标题背后的完整落地链路来写——从设计层面为什么这么选型到后端接口怎么搭、前端页面怎么接、最后数据库和部署有哪些坑要躲直接按“能复现”的标准来。2. 技术选型与数据模型先把答辩要讲的道理立住2.1 为什么是 SpringBoot Vue MySQL四件套背后的选型逻辑很多同学选型是看网上哪个教程多就抄哪个但答辩时老师一定会问一句为什么用这套不用别的这句话答不好前面代码写得再顺也会被扣分。SpringBoot 解决的核心痛点是“配置地狱”。过去用 SSM 搭一个能跑的项目要配一堆 XML、还要装外部 TomcatSpringBoot 内嵌容器加自动装配java -jar就能起服务这对毕设场景太合适了——写代码的时间不会浪费在环境上。Vue 这边组件化和响应式让小说列表、书架、阅读器这类交互密集的页面写起来比原生 JS 顺手得多而且 Vue 在国内的资料密度远超 React遇到问题时能搜到的答案更多。MySQL 胜在免费、普及率高而且使用 InnoDB 后事务和行级锁的知识点都是答辩老师喜欢追问的方向你可以在数据一致性的回答上拿到加分。对比一下其他方案就明白如果做单体 JSP 页面现在的主流公司很少这么写老师会觉得你的技术栈停留在五年前如果换成 Django 或 Flask虽然开发更快但和 Java 后端岗位的匹配度又下降了。SpringBoot Vue MySQL 这套组合刚好把“前后端分离”“工程化”“关系型数据库设计”三块常见面试点全部覆盖了这也是它长期作为毕设热门的原因。这里不要随便上微服务、消息队列这类中间件项目规模撑不住反而暴露你对复杂度把控的陌生感。2.2 数据库设计六七张表把“书、章、用户、书架、评论”拆干净在线小说阅读平台的数据模型并不复杂但拆分是否合理直接影响后端的代码量。我习惯把核心表拆成六到七张小说表、分类表、章节表、用户表、书架表、评论表再加一张管理员表或直接用用户表里的角色字段区分。核心的两张表建表脚本我通常这样写CREATE TABLE tb_book ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) DEFAULT COMMENT 作者, category_id INT DEFAULT 0 COMMENT 分类ID, intro TEXT COMMENT 简介, cover_url VARCHAR(255) DEFAULT COMMENT 封面图URL, views INT DEFAULT 0 COMMENT 点击量, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT小说表; CREATE TABLE tb_chapter ( id BIGINT AUTO_INCREMENT PRIMARY KEY, book_id BIGINT NOT NULL COMMENT 所属小说ID, chapter_no INT NOT NULL COMMENT 章节序号, title VARCHAR(100) NOT NULL COMMENT 章节标题, content MEDIUMTEXT COMMENT 章节正文, word_count INT DEFAULT 0 COMMENT 字数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_book_no (book_id, chapter_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT章节表;这里有几个参数值得展开。tb_chapter必须单独拆出来因为章节正文是典型的大字段如果和小说的书名、简介放在同一张表列表页查询时会把所有正文字段都带上查询性能明显下降。book_id和chapter_no的联合唯一索引保证了一本书下不可能出现两个相同编号的章节这是数据完整性的底线。正文类型用MEDIUMTEXT单章两三万字毫无压力如果书名和简介混在一起用TEXT一页小说列表要拖几百行只为了显示 20 个字的书名那才是设计失误。书架表我通常会加一个last_chapter_id字段来记录用户读到哪一章CREATE TABLE tb_bookshelf ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, last_chapter_id BIGINT DEFAULT 0 COMMENT 最近阅读章节, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT书架表;这个表的唯一键往往会被忽略等实现“加入书架”时才发现用户对同一本书可以插入多条记录接口返回的数据重复。提前把uk_user_book建好后端做插入时直接ON DUPLICATE KEY UPDATE更新进度又能拿到当前阅读位置代码能少写不少。字符集我统一用utf8mb4而不是utf8因为utf8在 MySQL 里最多存三字节遇到 emoji 或生僻字就会报错或变问号。这个坑在后面避坑章节还会再出现一次但建库这一层就定好后面省心很多。2.3 后端代码分层Controller、Service、Mapper 各自该管什么很多毕设代码最大的问题是 Controller 里写 SQL、Service 里拼 HTML看起来也能跑但老师让你讲架构时你自己都说不清楚。前后端分离项目里我推荐按最传统的三层结构拆包src/main/java/com/example/novel/ ├── controller/ 接口入口只做参数接收和结果返回 ├── service/ 业务逻辑事务边界在这里 ├── mapper/ 数据库访问 ├── entity/ 实体类 ├── config/ JWT、跨域等配置 └── common/ 统一返回结果 R、异常处理持久层我一般用 MyBatis-Plus而不是原生 MyBatis。原因很现实毕设里大量接口是简单的单表 CRUDMyBatis-Plus 的BaseMapper内置了增删改查和分页不需要为每张表手写 XML 映射。手写 SQL 的精力应该留给多表关联和统计类查询比如首页热门榜单、分类下的书籍列表剩下那些“按 ID 查一条、插入一条”的工作交给内置方法就好。统一返回结果类R也建议从一开始就定好。前后端约定一个 JSON 结构比如{ code: 200, data: {...}, message: ok }前端的 axios 响应拦截器只需要判断一次code不用每个接口单独处理异常。这个约定后端的每个 Controller 都必须遵守否则前端拦截器形同虚设。实体类字段名和数据库列名保持小驼峰对应MyBatis-Plus 默认开启驼峰映射能省掉一大半人工字段映射的代码。3. 后端落地SpringBoot 工程从初始化到核心接口能跑3.1 pom.xml 和 application.yml先把依赖和环境定死搭建后端的第一步是选 SpringBoot 版本。常见做法是 JDK 8 环境配 Spring Boot 2.7.xJDK 17 环境配 Spring Boot 3.x。如果你的电脑里已经装好 JDK 8就不要硬上 3.x否则启动直接报UnsupportedClassVersionError。反过来如果你装的是 JDK 17用 2.7 也能跑但为了省事我建议按照本机 JDK 版本选择对应的 Boot 大版本。依赖文件里最核心的几项parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency /dependenciesspring-boot-starter-web是后端服务的骨架内嵌 TomcatController 注解、JSON 序列化全在这里。MyBatis-Plus 的 starter 会自动装配数据源和 SqlSessionFactory省去手写 MyBatis 配置。MySQL 驱动版本这里要注意如果你的 MySQL 是 5.7用 8.0.x 的驱动没问题反过来如果 MySQL 是 8.0 却用了 5.1 的老驱动连接时经常会报认证协议错误。JWT 相关我用 jjwt 的 API 包功能全且坑少比手写 Base64 拼 token 安全得多。然后是核心配置文件。我习惯把数据库连接参数单独写在application.ymlserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/novel_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autoURL 里的四个参数一个都不能省。useUnicode和characterEncoding控制中文写入不乱码serverTimezone指定为Asia/Shanghai否则 MySQL 8.x 默认 UTC 时区插入时间会比本地晚八个小时日志里还经常报Server returns invalid timezone。useSSLfalse是本地开发避坑防止连接时 SSL 握手警告刷屏。allowPublicKeyRetrievaltrue是 MySQL 8.x 使用 caching_sha2_password 认证时可能需要的参数不加可能遇到Public Key Retrieval is not allowed。mybatis-plus.log-impl设为 StdOutImpl 后控制台能看到每条 SQL 和参数调试阶段非常有用。但正式演示可以把这项注释掉避免几百条日志把启动信息刷到看不见。3.2 登录注册与 JWT 鉴权第一个绕不开的核心功能在线小说阅读平台的登录注册不能做成装饰品因为书架、评论这些功能都要知道当前用户是谁。这里我用 JWT 做无状态鉴权流程是用户登录成功后后端生成一个 token 返回给前端前端存到 localStorage之后每次请求在 Header 里带上。后端通过拦截器校验 token 并解析出用户 ID放入请求上下文。注册与登录的接口可以放在同一个AuthController中RestController RequestMapping(/api/auth) public class AuthController { Resource private UserService userService; PostMapping(/register) public RString register(RequestBody RegisterDTO dto) { if (!dto.getPassword().equals(dto.getConfirmPassword())) { return R.error(两次密码不一致); } userService.register(dto); return R.ok(注册成功); } PostMapping(/login) public RString login(RequestBody LoginDTO dto) { String token userService.login(dto.getUsername(), dto.getPassword()); return R.ok(token); } }RegisterDTO和LoginDTO这种对象建议单独建类不要直接用Map接收参数。用 DTO 的好处是可以在类上用NotNull、Size这类校验注解参数校验代码不用堆在 Controller 里。密码的存储必须加密常见的做法是使用 BCrypt 算法Spring Security 里自带BCryptPasswordEncoder不想引入 Spring Security 也可以直接引入spring-security-crypto这个轻量包单独使用。登录校验的核心逻辑在 Service 层Override public String login(String username, String rawPassword) { LambdaQueryWrapperTbUser wrapper new LambdaQueryWrapper(); wrapper.eq(TbUser::getUsername, username); TbUser user this.getOne(wrapper); if (user null) { throw new BizException(用户名不存在); } if (!BCrypt.checkpw(rawPassword, user.getPassword())) { throw new BizException(密码错误); } String token JwtUtil.createToken(user.getId(), user.getUsername()); return token; }这里BCrypt.checkpw是校验明文密码和数据库密文是否匹配JwtUtil.createToken生成 token。token 的有效期建议设成 7 天太短会让用户频繁重新登录太长又不安全。JWT 的密钥不要写在代码里明文可以放到application.yml配置中答辩时也可以说是从配置中心注入的显得工程化程度更高。然后在 SpringBoot 里注册一个拦截器Component public class JwtInterceptor 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 (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Long userId JwtUtil.parseToken(token); request.setAttribute(userId, userId); return true; } catch (Exception e) { response.setStatus(401); return false; } } }注意preHandle里第一行判断handler是否为HandlerMethod。这是因为拦截器不只拦截 Controller 方法某些静态资源请求也会经过这里不做类型判断会把静态资源也拦死。解析成功后把userId放进 request 属性里后面的 Controller 通过RequestAttribute Long userId直接取不要在拦截器里再去查一次数据库省一次 IO。3.3 分页查询与章节详情让前端有数据可用小说列表接口是首页的命脉。常见做法是支持关键词搜索、按分类筛选、按点击量排序同时做分页。MyBatis-Plus 的分页需要配置一个拦截器插件否则Page对象不会自动生成 count 查询和 LIMIT 语句Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }有了这个配置Service 里就可以直接写Override public PageTbBook listBooks(int pageNum, int pageSize, String keyword) { LambdaQueryWrapperTbBook wrapper new LambdaQueryWrapper(); if (keyword ! null !keyword.isEmpty()) { wrapper.like(TbBook::getTitle, keyword); } wrapper.eq(TbBook::getStatus, 1); wrapper.orderByDesc(TbBook::getViews); PageTbBook page this.page(new Page(pageNum, pageSize), wrapper); return page; }Page的构造函数有两个参数当前页码和每页条数。注意页码从 1 开始前端传 0 或负数时这里会直接报错。可以在 Controller 入口做一次参数校验pageNum小于 1 时统一改成 1pageSize超过 50 时封顶为 50防止请求参数把数据库压到。wrapper.like在 keyword 为空时不会拼进 SQL所以不用纠结要不要判空。章节内容接口有一个容易忽略的设计细节读者跳章阅读时前端先拿到的是章节目录再按章节号请求正文。这个接口要在返回值里同时给出上一章和下一章的编号这样阅读器底部才能直接渲染“上一章/下一章”按钮省得前端再查一次目录。Override public ChapterVO getChapterDetail(Long bookId, Integer chapterNo) { LambdaQueryWrapperTbChapter wrapper new LambdaQueryWrapper(); wrapper.eq(TbChapter::getBookId, bookId) .eq(TbChapter::getChapterNo, chapterNo); TbChapter chapter this.getOne(wrapper); if (chapter null) { throw new BizException(章节不存在); } ChapterVO vo new ChapterVO(); vo.setChapter(chapter); vo.setPrevNo(chapterNo - 1); vo.setNextNo(chapterNo 1); return vo; }这里随手设置的prevNo和nextNo会在最新章节出现尴尬用户读到最后一章时nextNo传过去一个不存在的编号。前端拿到后如果直接点击会请求报错。更稳妥的做法是在 Service 里先查这本书的最大章节号如果当前chapterNo已经是最大值就把nextNo置为 0前端判断为 0 时禁用按钮。这个细节做到位交互上看不出毛病代码评审时还能讲一句“我做了边界处理”。4. 前端落地Vue 工程从初始化到页面联调4.1 Vue Vite 初始化与 axios 封装把请求基础打好前端我一般用 Vue 3 加 Vite。Vite 的开发服务器启动快热更新也比 Vue CLI 时代舒服很多。初始化命令常用npm create vitelatest novel-front -- --template vue生成后安装项目依赖再装两个必要库vue-router和axios。如果后台管理用现成组件库我习惯加上element-plus表格、表单、上传组件直接拿来用省掉手写组件的大量时间。开发环境有个必须处理的点前端跑在 5173 端口后端跑在 8080 端口浏览器直接访问后端接口会跨域。常见做法是配 Vite 代理让前端把/api开头的请求转发到后端// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })changeOrigin: true表示让后端拿到的请求头中 Host 是目标地址如果不配这一项后端某些框架做重定向时会出现路径错乱。通过 Vite 代理转发后前端代码里请求就写/api/auth/login这种相对路径不用把http://localhost:8080写死在代码里以后换服务器地址只需要改这一处代理配置。axios 的封装建议单独放一个文件因为整个项目的请求都要从这里过拦截器可以统一管 token 和错误状态import axios from axios const http axios.create({ baseURL: /api, timeout: 10000 }) http.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) http.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) window.location.href /login } return res }, error { return Promise.reject(error) } ) export default http请求拦截器里如果本地存在 token就自动加到 Header 中响应拦截器里统一判断 401token 失效时直接跳回登录页业务页面里就不用每个请求都重复写这套逻辑。token 存储用localStorage还是sessionStorage各有取舍毕设项目用任何一个都可以关键是整个项目要统一不要一会存 sessionStorage 一会又读 localStorage这种低级错误排查起来非常浪费时间。4.2 书架与阅读器两个核心页面的组件拆分书架页的功能是从接口拉取当前用户的书架列表展示小说封面、书名、最近阅读章节和更新时间。阅读器页面则负责展示正文内容和切换章节这两个页面的组件拆分方式直接影响后期的维护成本。书架列表页核心逻辑不复杂关键在卡片组件和列表组件的通信。如果一个页面里既写数据请求又写卡片 DOM 又写分页逻辑代码会膨胀到几百行。我习惯拆成三个组件书架页面负责请求数据小说卡片是一个纯展示组件空状态单独一个组件。这样接口字段改动时只动书架页面样式改动只动卡片组件。阅读器页面最大的坑是正文排版。从后端返回的章节内容是一大段带换行符的文本如果直接用div{{ content }}/div渲染浏览器会把换行符当成空格处理整章内容挤成一坨。常见做法有两种第一种在后端导入章节时就把段落拆好每段用p包起来前端用v-html渲染第二种是前端取到原文后用正则把\n替换成br再渲染。我推荐第一种后端控制数据结构前端不拼 HTML职责更清晰。阅读器的上一章下一章逻辑可以写成这样const loadChapter async (bookId, chapterNo) { const res await http.get(/book/${bookId}/chapter, { params: { chapterNo } }) if (res.code 200) { chapter.value res.data.chapter prevNo.value res.data.prevNo nextNo.value res.data.nextNo } } const nextChapter () { if (nextNo.value 0) return loadChapter(bookId.value, nextNo.value) }拿到prevNo和nextNo后判断 0 值来禁用按钮。同时每读完一章应该调用一次更新阅读进度的接口把lastChapterId写回书架表。这个动作一定要做成异步且不能堵塞阅读操作用户切章节时不需要等进度保存完成再渲染正文。阅读器的滚动位置还有一个优化点从书架点击“继续阅读”时应该直接滚到上次阅读的位置而不是重新从页首开始。常见做法是后端在书架列表接口里返回lastChapterId前端拿到后请求对应章节并在页面渲染完成后尝试滚动到滚动条高度。这个逻辑如果一开始不做后面加需求时改动面会更大。4.3 管理后台用 Element Plus 搭出上传解析功能管理后台通常需要支持导入小说内容最常见的是管理员上传一个 TXT 文本文件后端读取后按章节切分存入数据库。这块业务逻辑全在后端前端只需要一个文件选择按钮和一个确认按钮。SpringBoot 接收上传文件的接口这样写PostMapping(/api/admin/book/import) public RString importBook(RequestParam(file) MultipartFile file, RequestParam(bookId) Long bookId) { bookService.importTxt(file, bookId); return R.ok(导入完成); }Service 的处理逻辑是按章节标题拆分文本。一个 TXT 小说通常是类似“第一章 xxx”“第二章 xxx”这样的格式解析时用正则匹配标题行然后按目标段落切分正文public void importTxt(MultipartFile file, Long bookId) { try { BufferedReader reader new BufferedReader( new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8) ); StringBuilder sb new StringBuilder(); ListChapterItem chapters new ArrayList(); String line; while ((line reader.readLine()) ! null) { if (line.matches(^第[0-9一二三四五六七八九十百千]章.*$)) { if (sb.length() 0) { chapters.add(createChapter(bookId, chapters.size() 1, title, sb.toString())); } title line; sb.setLength(0); } else { sb.append(line).append(\n); } } // 批量插入章节 this.saveBatch(chapters); } catch (IOException e) { throw new BizException(文件读取失败); } }核心正则^第[0-9一二三四五六七八九十百千]章.*$能同时匹配“第1章”和“第一章”但匹配失败时文本不会被切分整本小说会变成一章。所以解析完要做好验证如果切出来的章节数量小于 10就提示用户检查 TXT 的章节标题格式。批量插入用saveBatch这个方法是 MyBatis-Plus 提供的内部按批提交比循环单条插入快上一个数量级。插入完再更新一次小说的总字数统计展示给用户看。这一段的业务足够复杂也适合作为答辩时“你做过什么复杂逻辑”的答案。5. 避坑从启动到演示最容易翻车的 5 个点5.1 数据库连接失败时区、驱动版本和字符集三座大山现象SpringBoot 启动时控制台报Communications link failure或者Server returns invalid timezone页面完全起不来。原因本地 MySQL 时区默认不是中国时区而连接串里没有指定serverTimezone或者 MySQL 服务用的认证插件与驱动版本不匹配。解决连接串上把所有参数写全useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse。如果 MySQL 是 8.0务必确认mysql-connector-java用了 8.x 版本如果是 5.7用 8.x 的驱动也可以但反过来用 5.1 驱动连 8.0 时几乎必踩认证协议问题。这里的血泪经验是连接串参数不要靠搜索引擎从网上复一段就粘贴必须对照自己本机的 MySQL 版本和字符集修改。5.2 跨域问题前端拿到了后端数据却被浏览器拦截现象前端页面控制台报CORS policy: No Access-Control-Allow-Origin网络请求状态码是 200但浏览器把响应拦截了。原因前端开发服务器在 5173 端口后端在 8080 端口浏览器同源策略限制跨端口的 AJAX 请求。开发阶段如果用了 Vite 代理就不会出现这个问题但如果前端直接用http://localhost:8080访问就一定要后端配置跨域。解决在 SpringBoot 里配置一个全局跨域常见做法是写一个实现WebMvcConfigurer的配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里要提醒的是生产或演示环境能不能直接用*允许所有源如果项目是公开部署这么写有安全风险。毕设场景下可以接受但答辩老师问起来正确的回答是“开发环境放开生产环境改成白名单”。如果前端已经配了 Vite 代理后端这个跨域配置甚至可以不加两种方案共存也不会互相冲突。5.3 循环查询拖垮接口N1 问题的定位现象小说列表接口响应越来越慢点击首页后要等两三秒打开 MyBatis 日志发现查询一本书的时候日志里刷出来几十条 SQL 全是SELECT * FROM tb_book WHERE id?。原因这是典型的 N1 查询。Service 层先查了章节列表拿到每个章节的bookId后在 Java 代码里循环去查询每本书的信息查了 N 次。日志里每本书一次 select几十本书就是几十次。解决改用批量查询先一次查出全部章节再去重收集bookId组装成一个List后调一次selectBatchIds把书全部查回来在 Java 内存里做关联ListTbChapter chapters chapterService.list(...); SetLong bookIds chapters.stream().map(TbChapter::getBookId).collect(Collectors.toSet()); MapLong, TbBook bookMap bookService.listByIds(bookIds) .stream().collect(Collectors.toMap(TbBook::getId, b - b));做完这步SQL 数量从“1 N”降到“1 1”接口时间能快一个数量级。排查这类问题时优先看 MyBatis 日志总条数发现 select 数量异常膨胀就先找循环位置这是最快的定位路径。5.4 阅读器排版和乱码前端显示和数据库读取双重踩坑现象章节内容在阅读器里全部挤成一行看不到段落或者在后台导入的 TXT 小说数据库里存的中文全变成了问号。原因问题是两个。排版问题是因为 Web 页面渲染时把文本中的换行符当作空白处理了乱码问题是因为导入时没有指定 UTF-8 编码BufferedReader按平台默认编码读了一个 GBK 编码的 TXT 文件。解决排版问题用后端切分段落的方式导入 TXT 时按章节切分后每个自然段用p标签包裹再存入数据库前端直接用v-html渲染段落结构完整保留。乱码问题的解决是读文件时强制指定编码new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))如果你的 TXT 文件本身是 GBK 编码指定 UTF-8 读出来照样乱。怎么办常见做法是给上传接口加一个charset参数让管理员选文件编码更稳妥的做法是先尝试用 UTF-8 读取如果解析出的内容中包含大量 Unicode 替换符就改用 GBK 重新读一遍。这个“自动探测编码”的逻辑可以作为加分项写进答辩文档里。5.5 端口冲突与 IDE 缓存那些“昨天还能跑”的玄学错误现象后端启动报Port 8080 was already in use或者修改代码后重新启动页面上的功能还是旧逻辑怎么刷新都没变化。原因另一个 Java 进程占用了 8080 端口常见是之前启动的后端服务没有关闭而另一种情况是 IDE 的构建缓存没清理代码没重新编译就启动了。解决端口占用时在命令行用netstat -ano | findstr 8080找到占用进程干掉对应 PID或者直接把后端端口改成 8081一劳永逸。IDE 缓存层面养成一个习惯每次大改完代码先执行mvn clean再启动把 target 目录清干净。IntelliJ IDEA 里还可以在File - Invalidate Caches里清一次缓存。这类问题之所以被叫“玄学”本质上是你没有按“先清缓存、再查日志、后看端口”这个固定顺序排查乱翻一通就归因到玄学上了。6. 答辩前的一小时自检把“注册到阅读”的链路完整跑一遍等到代码全部写完还有一个环节比写代码更影响最终成绩就是答辩现场演示。我见过太多项目平时能跑一上台就翻车的案例原因无他演示环境没有提前做一次完整链路自检。我给自己定的习惯是答辩前一天不写新功能只做一件事——用一条完整用户路径把所有接口过一遍。路径从注册开始清空数据库中测试用户数据打开前端页面注册一个新账号登录拿到 token浏览首页推荐列表搜索一个关键词点击进入小说详情页打开第一章翻到第五章加入书架退出登录换管理员账号登入上传一本书的 TXT确认章节解析成功最后在书架里验证这个新用户的书架记录。这条链路走完我会顺手用一条curl命令验证后端某个核心接口确保脱离前端也能通curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}返回 JSON 里包含 token 字段说明后端服务和数据库连接都正常。接下来再验证数据库里确实写出了数据登录 MySQL 查tb_user表里新增的记录查tb_bookshelf里的书架记录。这一步做到位演示时就算前端临时出问题你也可以切到数据库管理工具向老师证明数据已经落库系统是完整的不需要慌张。另外有个小技巧演示前把 MySQL 的 binlog 或数据备份出一份需要恢复时直接重导建库脚本。我经历过一次演示现场误删了演示账号因为没有备份整个流程从注册开始重来特别狼狈。现在我的习惯是演示前先导出一份干净数据库万一中途数据被搞乱直接导入备份一分钟回到初始状态。这个项目如果能稳稳跑通“注册→登录→看小说→加书架→后台传书”这条链路它作为一个毕业设计的完整度已经站在前列。剩下的高阶功能——比如用 Redis 做热门榜单缓存、用 ElasticSearch 做正文搜索——都属于锦上添花先把链路跑通再谈加分项。希望这次从选型到落地再到排障的完整梳理能帮你把这个在线小说阅读平台做成一个真正能讲、能跑、能拿高分的毕设项目。本文还有配套的精品资源点击获取
返回列表