尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+Vue.js轻小说在线阅读网站:数据库建模、JWT鉴权与前后端联调实践

SpringBoot+Vue.js轻小说在线阅读网站:数据库建模、JWT鉴权与前后端联调实践 简介面向计算机相关专业学生与开发者的SpringBootVue.js轻小说在线阅读网站前后端分离项目内置完整前端工程、后端打包文件与详细文档可用于毕业设计、课程设计、项目初期立项演示或小白学习进阶。压缩包共30个文件涵盖14个vue组件页面、5个js逻辑文件、2个json配置、1个入口html以及favicon、图片、README、项目授权码等整体仅1.93MB目录结构清晰前端按组件、路由、Store状态管理划分后端单独压缩打包配套提供项目授权码。其中README对项目整体结构作了说明图片与图标用于前端界面展示便于按需参考。目前已有57人学习下载项目运行测试通过曾获导师指导认可且答辩评审分达95分适合在此基础上二次开发也可作为人工智能、通信工程、自动化、电子信息、物联网等专业的练手项目。1. 基于SpringBootVue.js的轻小说在线阅读网站资料包到底缺在哪一环解压这类“基于SpringBootVue.js的轻小说在线阅读网站前后端分离全部资料详细文档.zip”的项目包第一步往往不是翻Word文档而是先后启动后端和前端然后立刻撞上跨域或401。这是SpringBootVue.js前后端分离项目最典型的起跑线后端跑在8080前端开发服务器跑在5173页面请求被CORS挡下来控制台一行ERR_CONNECTION_REFUSED就能让整个资料包看起来像坏的。详细文档通常把篇幅花在功能截图却不写接口路径、token携带方式和数据库初始化顺序。这篇拆开表结构、接口契约、SpringBoot鉴权、Vue.js联调和验收五段适合拿到资料包想快速跑通的开发者也适合照骨架从零写一个轻小说站的人。2. 轻小说在线阅读网站的数据建模卷、章节与接口契约2.1 轻小说站与普通小说站的差异决定了表结构轻小说和一般网络小说的最大区别在两层结构小说下面有卷卷下面有章节而且卷标题本身有展示价值。资料包里如果只有t_novel和t_chapter两张表把卷号写成章节标题的前缀目录树前端就不太好按卷折叠。另外一个容易被忽略的对象是插画很多轻小说站会为卷或章节配图但考虑到演示项目体量插画可以放进t_novel的cover_url字段先不做独立的图片表。对这种量级的网站数据库设计的目标是让“书库列表、书籍详情、章节目录、阅读正文”四条查询路径都能用简单SQL完成不引入复杂的中间关系表。2.2 卷、章节与阅读历史表的建表要点一个常见的轻小说站基本表如下。没有引入权限、角色、支付字段所有大字段只留在最需要的表里CREATE TABLE t_volume ( id BIGINT PRIMARY KEY AUTO_INCREMENT, novel_id BIGINT NOT NULL COMMENT 所属小说ID, volume_no INT NOT NULL COMMENT 卷号从1开始, title VARCHAR(100) NOT NULL COMMENT 卷标题, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_volume_novel (novel_id, volume_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT轻小说卷; CREATE TABLE t_chapter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, volume_id BIGINT NOT NULL COMMENT 所属卷ID, chapter_no INT NOT NULL COMMENT 章号, title VARCHAR(200) NOT NULL, content LONGTEXT NOT NULL COMMENT 正文内容, word_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_chapter_volume (volume_id, chapter_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT章节表;两张表的分页、排序都靠联合索引支撑。t_volume的索引(novel_id, volume_no)保证一次查出一本书所有卷时按卷号排好t_chapter的索引(volume_id, chapter_no)保证一次查出某卷全部章节时按章号排好。注意不要在content字段上加索引LONGTEXT本身不能进普通索引正文检索就接受LIKE %词%数据量在十万章节以内配合WHERE条件里先过滤卷ID查询仍在可接受范围内。如果一定要全文搜索那应该引入Elasticsearch一份毕设项目里通常没必要。阅读历史的保存用t_read_history要点是(user_id, novel_id)加唯一索引防止同一个人反复开同一本书时插入多行CREATE TABLE t_read_history ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, novel_id BIGINT NOT NULL, chapter_id BIGINT NOT NULL, percent DECIMAL(5,2) DEFAULT 0 COMMENT 阅读进度百分比0-100, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_history_user_novel (user_id, novel_id), KEY idx_history_user (user_id, update_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT阅读历史;唯一索引uk_history_user_novel是这段设计的核心。配合INSERT ... ON DUPLICATE KEY UPDATE chapter_id VALUES(chapter_id), percent VALUES(percent)前端每次上报进度都执行同一条SQL不存在先查后插的并发问题。DECIMAL(5,2)能存0到100.00阅读百分比够用如果想精确到字数位置推荐改成position INT记录第几个字阅读器可以精确跳回上次位置。2.3 前后端接口契约按资源划分的分组表下一章的SpringBoot后端代码需要围绕接口契约来写先约定路径避免前端写完后端返工。我一般按资源分组这个点常常被资料包的文档草草带过但它决定联调效率分组方法路径鉴权说明账号POST/api/auth/register否用户名密码注册账号POST/api/auth/login否登录返回token和refreshToken账号POST/api/auth/refresh否刷新token书库GET/api/novels否分页列表接受page、size、tag、keyword书库GET/api/novels/{id}否详情含卷结构阅读GET/api/chapters/{id}否正文返回prevChapterId和nextChapterId书架GET/POST/DELETE/api/shelf是当前用户书架带token阅读GET/POST/api/history是阅读历史与进度上报用/api/novels、/api/chapters这种名词式路径比动词式路径/getNovel更接近REST风格答辩时被问“为什么前后端分离”也可以以此举例。GET /api/chapters/{id}同时返回上一章和下一章的ID是很多人漏掉的设计否则前端翻页时还要再查一次章节列表多一次请求。接口统一返回Result对象code为0表示成功非0对应业务错误data放实际数据。这一章把边界定死表结构决定Service层怎么写接口契约决定Controller暴露什么下一章开始写SpringBoot端的鉴权和查询。3. SpringBoot端实现轻小说站后端JWT鉴权、分页与阅读链路3.1 依赖选型别让SpringBoot版本拖累整个项目启动资料包里的后端最常见的失败是版本错配。SpringBoot 3.x强制要求JDK 17把javax.servlet改成了jakarta.servlet如果代码照着3.x写却在JDK 8环境编译会在一堆“找不到包”的报错里打转。所以拿到代码先看pom.xml里的spring-boot-starter-parent版本2.7.x就配JDK 8或113.x就配JDK 17。“SpringBoot版本太高”在本地环境里通常表现为启动报错而不是运行期逻辑问题。我习惯用的依赖组合是spring-boot 2.7.13加MyBatis-Plus 3.5.5外围配java-jwt 4.4.0dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependencyMyBatis-Plus比原生MyBatis少写大量XML单表操作用BaseMapper内置方法这对以增删改查为主的轻小说站是合理选择。java-jwt的API比jjwt简单生成token只需create().withClaim().withExpiresAt().sign()不用配置复杂的SecretKey。数据源和JWT配置统一放application.yml数据库账号密码不要写死在Java类里。把SpringBoot版本与JDK的对应关系提前列出来比看一堆教程更省时间SpringBoot版本依赖JDK关键包数据源连接串2.7.x8/11javax.servletjdbc:mysql://localhost:3306/light_novel3.x17及以上jakarta.servletjdbc:mysql://localhost:3306/light_novel?useUnicodetrue3.2 登录、拦截器与token刷新机制登录接口的常规写法是接收用户名密码selectOne查用户用BCrypt比对密码比对通过后签发token。密码一定不要用MD5明文比对用spring-security-crypto里的BCryptPasswordEncoder即使资料包里原代码用了MD5答辩前也建议改成BCrypt这一条在SpringBoot面试题里经常被追问。token校验用拦截器做避免每个Controller里重复解析Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String auth request.getHeader(Authorization); if (auth null || !auth.startsWith(Bearer )) { response.setStatus(401); return false; } String token auth.substring(7); try { DecodedJWT jwt JWT.require(Algorithm.HMAC256(secret)) .build().verify(token); request.setAttribute(userId, jwt.getClaim(userId).asLong()); return true; } catch (JWTVerificationException e) { response.setStatus(401); return false; } } }secret来自application.yml中的jwt.secret不要硬编码。这个拦截器只做“校验与放行”真正的白名单配置在WebMvcConfigurer里addInterceptor(jwtInterceptor).addPathPatterns(/api/**).excludePathPatterns(/api/auth/login, /api/auth/register)。这样书库、章节正文这些公开接口不进拦截器书架、历史记录这些受保护接口统一校验。再补一个token刷新接口。很多项目省略这个只设一个很长的过期时间更稳妥的做法是登录时同时返回accessToken2小时过期和refreshToken7天前端在响应拦截器收到401时调/api/auth/refresh。刷新接口只认refreshTokenPostMapping(/api/auth/refresh) public Result refresh(RequestBody MapString, String body) { String refreshToken body.get(refreshToken); try { DecodedJWT jwt JWT.require(Algorithm.HMAC256(secret)) .build().verify(refreshToken); Long userId jwt.getClaim(userId).asLong(); String newAccessToken createAccessToken(userId); return Result.ok(Map.of(token, newAccessToken)); } catch (JWTVerificationException e) { return Result.error(401, refreshToken已过期); } }注意完整的刷新机制应把refreshToken存入Redis并做状态管理演示项目可以直接依赖token过期时间但答辩时要能说出这个权衡。两个token用同一个secret签名区别只在expiresAt和claim里的type字段。刷新接口本身放在白名单里它接收的是单独的refreshToken逻辑上更干净。这层设计写进答辩材料里比“登录后返回一个字符串token”站得住脚。3.3 分页查询与条件过滤书库列表接口是访问量最大的公开接口GET /api/novels接受page、size、tag和keyword。MyBatis-Plus的LambdaQueryWrapper把条件拼装写得很直观GetMapping(/api/novels) public Result listNovels(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 12) int size, RequestParam(required false) String tag, RequestParam(required false) String keyword) { LambdaQueryWrapperNovel wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(tag)) { wrapper.like(Novel::getTags, tag); } if (StringUtils.hasText(keyword)) { wrapper.like(Novel::getTitle, keyword); } wrapper.orderByDesc(Novel::getClickCount); PageNovel p novelMapper.selectPage(new Page(page, size), wrapper); return Result.ok(p); }这里的like对应SQL的LIKE %奇幻%标签字段在t_novel里以逗号分隔存比如奇幻,冒险,轻小说LIKE能命中。page从1开始和Vue.js里el-pagination的current-page默认值一致避免前端第一页拿到空数据的误会。Page对象自带total字段前端用它计算总页数不用额外再写count查询。3.4 阅读链路中的更新语句详情页的点击量递增、书架添加、阅读历史更新都适合在Service层配合UpdateWrapper完成。点击量用下面这条避免先select再update两个请求novelMapper.update(null, new LambdaUpdateWrapperNovel() .eq(Novel::getId, novelId) .setSql(click_count click_count 1));阅读历史保存用insertOrUpdate先selectOne查唯一索引再决定插入或更新。percent按0-100的整数上传约定好不传小数前端进度条显示也方便。这里有个坑用户暂停阅读后直接关页面onbeforeunload里发异步请求很不可靠真正的兜底是每次翻页时上报把“最后实际阅读到的地方”记录下来就够。4. Vue.js端实现轻小说阅读器axios封装、token注入与翻页4.1 用Vite还是Vue CLI看资料包环境决定现在新建Vue.js项目普遍用Vite但很多资料包给的还是Vue CLI模板路由文件是router/index.js、入口是main.js两种脚手架不影响业务代码写法影响的是环境变量和代理配置。如果资料包是Vue CLI就保留vue.config.js如果自己重新搭我更推荐Vue3Vite配上Vite内置的server.proxy开发环境跨域问题比改baseURL省心。前端与后端的分离边界在这里体现得很直接Vue只负责页面状态数据全部来自SpringBoot接口。4.2 axios实例封装请求注入token、响应拦截401前后端分离项目Vue端最重要的公共文件是src/utils/request.js。它要做三件事统一加baseURL、每次请求自动带token、统一处理401。下面这段是常规做法import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器每个请求自动带token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error)) // 响应拦截器统一处理业务错误码和401 service.interceptors.response.use(response { const res response.data if (res.code ! 0) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res.data }, error { const status error.response?.status if (status 401) { router.push(/login) } else if (status 500) { ElMessage.error(服务器内部错误) } return Promise.reject(error) }) export default service这里有个取舍响应拦截器直接返回res.data后端Result里的data字段被解包出来调用api.getNovels()拿到的直接就是分页对象不用在业务代码里反复写.data.data。token放localStorage而不是sessionStorage好处是刷新页面后登录态还在坏处是XSS能偷走所以正文渲染v-html时要做服务端XSS过滤。401直接跳登录页的写法有个隐患“先刷新token再重放请求”的场景没处理。正确的顺序应该是先调刷新接口刷新失败才跳登录页刷新成功就继续执行原请求这个差异在前后端分离项目实战里非常常见。提示不要在response拦截器里统一弹“请先登录”否则登录页自己被请求时也会弹一遍用路由跳转代替弹窗更安静。4.3 路由守卫与页面访问控制轻小说站的用户操作路径是首页看榜单点进详情看目录展开卷进入阅读器翻页。路由守卫要保护的不是详情页而是书架和阅读历史router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })带redirect参数的意义是登录成功后还能回到原来的阅读页。meta.requiresAuth写在路由表里像书架页meta: { requiresAuth: true }阅读页给requiresAuth: false允许游客试读前几章。对公开与私有资源做分层控制前端路由层面挡住大部分未登录操作后端接口再同步校验属于双保险。路由与页面权限的对应关系可以整理成一张小表方便和接口文档对照路由组件需要登录说明/Home.vue否书库首页/novel/:idDetail.vue否小说详情与目录/reader/:chapterIdReader.vue否阅读器游客可试读/shelfShelf.vue是书架/historyHistory.vue是阅读历史4.4 阅读器组件翻页、字号、进度上报阅读器是轻小说前端区别于普通后台管理页面最明显的地方。一个可用的阅读器不一定要做卷轴特效用整块滚动区域加字号调节就足够。组件的核心结构template div classreader-wrap div classreader-toolbar button clickfontSize--缩小字号/button button clickfontSize放大字号/button /div div classreader-content :style{ fontSize: fontSize px } scrollhandleScroll {{ chapter.content }} /div footer classreader-footer button clickgoChapter(chapter.prevChapterId)上一章/button span进度 {{ percent }}%/span button clickgoChapter(chapter.nextChapterId)下一章/button /footer /div /templatechapter.content是一大段文案字号变化会直接引起scrollHeight变化所以进度计算必须在handleScroll里实时取scrollTop和scrollHeight。组件在onMounted里根据路由参数chapterId调getChapter拿到章节正文、上一章ID、下一章ID。翻页时只需要改chapterId再拉一次数据同时把scrollTop归零。进度上报时机选在点击“下一章”时先把当前进度POST /api/history再跳转这样上报和页面导航合并成一次用户交互。阅读器组件里还有一个容易被忽视的细节如果正文包含换行符{{ }}插值在普通div里会把换行折叠成空格需要在.reader-content样式里加white-space: pre-wrap;让每个自然段保持独立。如果后端正文里存带p标签的富文本则改用v-html并做XSS过滤两种方案选一种别混着用。5. 把SpringBootVue.js轻小说项目验收成可交付的完整资料5.1 按后端、前端、联调的顺序跑通环境面对一份“全部资料详细文档”的压缩包正确的启动顺序是先MySQL再后端最后前端。MySQL执行sql/里的建库脚本时注意utf8mb4_0900_ai_ci这个排序规则只在MySQL 8存在本地是5.7就会报错把建库语句里的排序规则统一改成utf8mb4_general_ci即可。后端启动重点看application.yml的密码有没有被YAML转义比如密码是abc123在未加引号的YAML值里可能被解析成其他含义给密码加双引号最稳。前端npm install卡住时用国内镜像源一次到位npm config set registry https://registry.npmmirror.com前端启动后如果页面请求报CORS优先在vite.config.js里写代理而不是去改后端跨域配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }开发环境用代理生产环境把前端构建产物交给nginx配置一个location /api反代到Java服务的8080端口浏览器看到的只有同一个域名跨域问题从根上消失。注意如果项目里有上传封面功能nginx代理时还要把上传的静态资源目录也代理出去否则图片会404。5.2 用curl验证全链路不依赖页面答辩前最怕现场点页面时遇到不可控的前端报错。先用命令行把后端链路打通心里有底。登录换取token、带token访问书架两步就是一串curlcurl -s -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}拿到返回里的token后设成环境变量再请求受保护的接口curl -s http://localhost:8080/api/history \ -H Authorization: Bearer $TOKEN两条命令能正常返回说明数据库、后端、鉴权三层是通的。对接口做可视化调试时在pom.xml里加springdoc-openapi依赖启动后访问/swagger-ui.html既能看所有接口的参数说明又能直接点“Authorize”按钮填token这是接手SpringBoot项目最实用的排错工具。5.3 把详细文档整理成能维护的三件套资料包里的详细文档往往写了完整需求分析、数据库设计、核心流程但文档和代码常常脱节。我的建议是补三个轻量级文件放进代码库后端README写JDK版本、MySQL版本和启动命令api.md写接口路径、请求参数示例、响应示例deploy.md写前端构建命令和nginx配置片段。这三个文件加起来不到10页但评审老师或接手的人照着能完整跑起来比一份几十页的Word更解决问题。Word原文保留作为毕设论文素材代码库里的README作为运行手册两者各司其职到答辩现场被问到“接口文档在哪”时指一下项目里那个api.md比翻几十页Word要快得多。本文还有配套的精品资源点击获取
返回列表