ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue小说阅读平台毕设:前后端分离实战指南

SpringBoot+Vue小说阅读平台毕设:前后端分离实战指南 1. 项目整体设计与技术选型思路1.1 为什么是SpringBootVue而不是SSMJSP先说个现象这些年我看了不少计算机毕设的选题小说阅读平台大概是JavaWeb方向出场率最高的题目之一。原因很简单——它麻雀虽小五脏俱全涉及用户、内容、检索、阅读、互动、后台管理几乎把JavaWeb的核心知识点都串起来了。但同样做小说平台有人用SSMJSP有人用SpringBootJSP有人用SpringBootVue。我个人的建议很明确选SpringBootVue这套前后端分离的方案。为什么第一SpringBoot彻底解决了传统SSM项目里让人头疼的XML配置问题。做过SSM整合的人都知道spring、springmvc、mybatis三份配置文件来回倒腾一个jar包版本不对就启动不起来。SpringBoot把内置容器、自动配置、starter机制都封装好了写一个SpringBootApplication入口类就能跑起来学习成本低得多也符合企业对Java技术栈的主流预期。第二Vue作为前端框架入门平缓。相比直接操作DOM的jQuery写法Vue的数据双向绑定、组件化开发思路让前端逻辑清晰不少尤其适合一个人同时搞定前后端的毕设场景。页面多、交互杂的小说平台用Vue拆组件管理要比JSP里嵌一堆Java代码舒服太多。第三前后端分离这个架构本身就自带答辩晒点。面试官或答辩老师问你用了什么架构你回答基于RESTful风格的前后端分离架构前端Vue通过Axios调用后端接口后端只负责业务逻辑并返回JSON数据这个回答比我用JSP直接查数据库高级不止一个层次。而且前后端分离意味着你可以把前端部署到Nginx、后端打成jar包独立运行部署架构也可以顺带讲一讲。注意我并不是说SSM和JSP一无是处如果你对Servlet、Filter、Listener、Session这套更底层的机制想有更深的理解做一个简单的JSP项目练手是有价值的。但作为毕业设计要兼顾完成度、展示效果、答辩说服力SpringBootVue是性价比最高的组合。1.2 系统功能模块怎么划分才叫完整一个合格的小说阅读与管理系统功能上至少要覆盖两条线面向读者的前台阅读线面向管理员的运营管理线。很多同学的功能说明书写得天花乱坠真到了实现阶段就露馅问题往往出在模块划分太粗、边界不清。我的做法是先画清楚角色和用例再落到模块。这个项目我划分为以下核心模块模块角色核心功能用户认证模块游客/用户注册、登录、身份校验、个人信息管理小说浏览模块游客/用户首页推荐、分类筛选、关键字搜索、排行榜阅读器模块用户章节加载、翻页阅读、字体调节、目录跳转、书签书架与历史模块用户收藏小说、加入书架、保存阅读进度、历史记录互动模块用户评论、回复、点赞、评分内容管理模块管理员/作者小说发布、章节管理、上下架、封面上传审核与统计模块管理员评论审核、用户管理、基础数据统计这里说一个我见过很多次的翻车案例有人把小说管理和章节管理混在一起改个书名要把章节一起处理删除一部小说逻辑散落各处。实际上小说和章节是典型的一对多关系必须拆开管理。小说表管元数据书名、分类、简介、封面、状态章节表管正文章节名、内容、字数、排序号通过novel_id关联这样无论是分页加载章节还是更新阅读进度逻辑都清晰得多。1.3 数据库设计的几个关键决策数据库设计是毕设里最能拉开档次的环节。同样是建几张表有的人外键、索引、冗余字段拎不清有的人设计出来就能看出业务思考深度。小说平台的表结构我建议至少要包含这些核心表user用户表id、用户名、密码BCrypt密文存储、昵称、头像、角色、状态、创建时间novel小说表id、书名、作者、分类id、简介、封面URL、状态连载中/已完结、点击量、收藏量、上架状态novel_category分类表id、分类名、排序chapter章节表id、小说id、章节序号、标题、内容、字数、创建时间bookmark书签表id、用户id、小说id、章节id、阅读进度页码/偏移量comment评论表id、用户id、小说id、章节id、内容、回复目标id、点赞数、状态几个容易出问题的点我必须多说两句。密码字段的长度。很多同学设成varchar(20)一用BCrypt加密就炸了——BCrypt生成的密文固定是60个字符字段长度必须给到varchar(60)以上。这个坑我见过不止一次联调时数据库直接报Data too long for column。章节内容的存储。小说章节正文是长文本MySQL里用text类型够用如果需要存超过64KB的内容要用mediumtext甚至longtext。别为了省事全塞在varchar(255)里保存一半丢了数据排查起来非常痛苦。书签表的设计。我是用(user_id, novel_id, chapter_id)做联合唯一索引保证一个用户对同一本书的某个章节只保留一条阅读记录重复打开时就更新进度而不是插入新纪录。这个设计后来在续读功能上非常实用。关于数据库引擎直接用InnoDB就好支持事务、支持外键约束。至于要不要真的给表加外键我的建议是毕设项目可以加面试时可以讲你明白外键约束与性能权衡但因为毕设数据量小加外键不会暴露性能问题反而显得你数据结构概念扎实。2. 核心功能模块解析与实操要点2.1 用户认证用JWT还是Session你得想清楚用户登录认证是几乎所有Web项目的入口功能也是很多毕设同学第一个写崩的地方。小说平台的注册登录多为两种方案一是传统的SessionCookie二是无状态的JWT。我最终选择的是JWT理由有两条前后端分离架构下后端接口与前端页面不在同一个域名/端口的场景非常常见Session跨域处理很麻烦。JWT放在请求头Authorization里跨域只要配置好即可。JWT天然适合移动端和后续扩展一次签发、到处使用。JWT方案的核心实现在后端前端只需要在登录成功后把token存起来我放在localStorage在Axios请求拦截器里统一带上token即可。关键代码大概是这个思路// axios拦截器 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }, error { return Promise.reject(error) })后端对应有一个JwtUtil工具类负责生成token和解析token核心步骤是登录成功→用用户的id和角色生成token→设置过期时间我设的是7天→返回前端。之后每个需要登录的接口通过拦截器或AOP统一校验token解析出用户信息放进ThreadLocal供业务层使用。关于Spring Security和Sa-Token这类现成权限框架我建议是如果你时间充裕且对框架熟悉可以用Spring Security体验一下完整的认证授权链路但毕设场景下框架学习成本容易失控自己写一个拦截器JwtUtil完全够用而且答辩时你能更清楚地讲出每一步在做什么。我最终选了自己实现120行左右代码搞定稳定可靠。密码安全是另一个必须强调的点。不管用什么方案密码绝不能明文存库。用BCryptPasswordEncoder对原始密码加密后再存储登录校验时调用matches方法比对。BCrypt的好处是每次加密结果都不同即使两个用户密码一样密文也不一样可以从根本上防彩虹表攻击。2.2 小说管理内容从哪来、怎么发布小说管理是后台最核心的业务模块。所谓管理至少要覆盖以下几个动作新增小说、编辑信息、上传封面、上下架、删除小说、维护章节列表。新增小说的业务逻辑本身不难就是组装Novel实体然后insert但有几个细节需要前置想清楚封面图片是静态资源。我的方案是后端提供/api/upload接口接收MultipartFile存储到服务器本地的upload/cover/目录下然后把可访问的URL存进数据库。存储路径要按日期分目录避免一个目录下文件越积越多。分类数据要维护成独立的字典表。如果你把分类名直接写死在小说表的字段里后续想加一个分类、改一个分类名就要全表更新。独立分类表外键关联是更稳妥的做法。小说删除要做级联考虑。删除小说时它的章节、评论、书架记录怎么处理我的方案是状态字段做逻辑删除status0表示下架不物理删除数据防止评论区出现这本书去哪了的问题。物理删除只保留在管理员的强制操作里并且要先清理关联的章节表记录。章节发布是小说平台特有的高频操作。文本编辑我推荐用wangEditor这类开箱即用的富文本编辑器集成方便而且支持粘贴Word内容和图片上传。但也有一个坑富文本编辑器会把内容存成HTML片段如果你用thymeleaf直接渲染容易触发XSS注入风险。我的做法是在后端写一个HtmlUtils.clean()的过滤逻辑允许的标签白名单只保留p、br、img、strong这些基础标签其余全部过滤。效果很好前端怎么插入脚本都到不了数据库。2.3 阅读器的体验细节决定了这个项目的好评度小说平台的功能里阅读器是最容易暴露做了还是没做的模块。有的同学把章节内容用表格列出来点一章跳一个页面这只能叫章节列表不叫阅读器。真正能放进作品集和答辩演示的阅读器至少要解决三个问题章节内容的加载方式。我建议接口设计成GET /api/chapter/{id}返回单个章节的标题和HTML内容。前端翻页时用一个精美的阅读页组件页面上方显示书名和章节名中间展示正文底部是上一章/下一章的按钮。加载下一章时前端先判断当前章节是不是最后一章避免把用户指向空接口。阅读进度的记录。用户在阅读页停留、滚动、翻页这些行为都可以触发进度保存。但要注意频率——如果每滚动一下都调一次后端接口服务器会被打爆。我的方案是每隔30秒自动保存一次加上退出阅读页时保存一次两个时机配合既不怕数据丢失也扛得住并发。保存的字段是chapter_id加scroll_position滚动条位置下次进入时后端返回上次进度前端用scrollTo方法定位到该位置。目录与书签的配合。目录面板用el-drawer抽屉组件实现从右侧滑出列出所有章节点击跳转。书签的功能我做成记住当前章节和手动添加备注两种实测下来前者使用频率最高因为读者真正需要的是接着上次看而不是花哨的备注功能。这也是一个产品上的取舍功能可以多做但要把最核心的那一个做到极致。阅读器的数据交互是一个典型的一次请求多次组件更新场景。章节内容返回后要同步更新当前章序号、上一章/下一章的可用状态、阅读进度绑定、章节列表高亮位置。这些状态我统一放在Vuex或Pinia的reader模块里管理避免组件之间EventBus满天飞。2.4 互动功能评论、点赞、收藏背后的数据设计互动模块是小说平台区别于普通内容CMS的亮点所在也是答辩时可以重点展开的部分。评论功能的实现遵循评论属于某个章节还是评论属于某本小说我用的是双层级设计评论表里既保存novel_id也保存chapter_id前端有两种入口——在阅读页评论区聊这一章在书籍详情页聊整本书。这样一张表同时支撑两个场景避免拆成两张表造成的数据冗余。评论回复是树形结构实现上有两种流派一种是每条评论记录parent_id前端递归渲染成树另一种是评论全部平铺回复目标通过reply_to字段指定用户名。毕设场景我推荐第二种实现简单、渲染性能好用户体验也不差——你在回复某个用户时前端把被回复者的用户名回显出来保存时记录到reply_to字段里。真要搞递归树分页问题会把你拉进泥潭。点赞和收藏是典型的高频写操作。数据模型上分别建like_record和bookshelf表都记录(user_id, novel_id)唯一索引防重复。这里有一个性能小技巧点赞数、收藏数、点击量这类统计字段可以直接冗余在novel表里每次点赞成功后novel表的like_count字段加一查询时直接读这张表不用count(*)实时统计。数据量大了以后这个设计可能带来写锁竞争但毕设场景完全够用还能体现你对读多写少用冗余的理解。3. 前后端交互与关键流程实现3.1 RESTful接口设计谁也别想糊弄你前后端分离项目最怕的就是接口设计混乱。我今天给自己定的规矩很简单资源用名词复数不用动词/api/novels、/api/chapters、/api/comments动作交给HTTP方法GET查询、POST新增、PUT更新、DELETE删除每个接口必须有统一的响应结构{ code, message, data }分页接口统一用pageNum和pageSize两个参数返回结构统一为{ total, list }统一响应结构的核心代码是一个泛型类public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }这个类虽然简单但价值很大。前端所有接口的响应都长一个样Axios响应拦截器里只要判断code 200就统一放行code 401就统一跳登录页不需要每个页面各自处理异常分支。后端的分层结构我用的是标准的Controller→Service→Mapper三层。Controller层只做参数接收和结果返回不写任何业务逻辑业务逻辑全部收敛到Service层Mapper层就是MyBatis-Plus的单表操作。有人问为什么不直接Controller里调用Mapper我只能说等你要在Service里加事务、加缓存、加组合逻辑时就知道分层不是表演是为了让你后期少流眼泪。我在实际编码中最常用的是MyBatis-Plus的ServiceImpl和IService它们提供了一套CRUD模板方法配合LambdaQueryWrapper能写出无SQL的条件查询。比如搜索书名里包含《剑来》的连载小说LambdaQueryWrapperNovel wrapper new LambdaQueryWrapper(); wrapper.like(Novel::getTitle, keyword) .eq(Novel::getStatus, 1) .eq(Novel::getIsOnShelf, 1) .orderByDesc(Novel::getClickCount);这一套下来既避免了手写XML的碎碎念又保留了条件查询的表达能力。但要注意MyBatis-Plus不等于万能复杂的多表联查还是老老实实写Select注解SQL千万别为了省事用LambdaQueryWrapper硬扛最后拼出一坨难维护的条件逻辑。3.2 分页与搜索小说列表的性能细节小说首页、分类页、搜索结果页全都离不开分页和搜索。分页我用MyBatis-Plus的Page分页插件配置一个分页拦截器即可Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }Service层里用novelMapper.selectPage(page, wrapper)就能得到带total的分页结果。注意一个问题Page对象需要从query参数里解析pageNum和pageSize并且给pageSize设一个上限我设的是20防止有人传pageSize99999把全表数据一次捞走这既是性能防护也是接口健壮性的一部分。搜索功能我分了两个层级。第一层是最简单的关键字模糊搜索用like实现搜书名和作者第二层是组合条件搜索支持分类筛选、状态筛选、上架时间排序。组合条件的设计核心在后端用QueryWrapper动态拼接条件前端做成一个搜索表单每个字段都有默认值所有条件通过GET请求参数传递。这里有一个体验点必须提搜索接口要处理空关键字的情况。前端搜索框为空时点击搜索后端不要报参数错误而是直接返回热门列表或最近更新的小说。我是在Controller里做了兜底StringUtils.isBlank(keyword)就走热门推荐的查询逻辑前端不用特判空串逻辑体验顺畅很多。用搜索引擎的人都知道关键词匹配有个相关性问题。但毕设项目没必要引入ElasticsearchMySQL的like配合前缀索引和LFU缓存完全可以应付几千条到几十万条的数据规模。答辩时如果有人问搜索性能怎么办你可以大方地说当前阶段使用数据库模糊查询能够满足访问需求如果数据量达到百万级以上我预留了引入ES的演进方案。这种回答远比硬拗说我用了ES但没真的做更可信。3.3 文件上传封面图片存哪里、怎么访问小说封面上传是后台管理里最常见的文件操作。我的实现流程是前端用Element UI的el-upload组件配置action指向后端的/api/upload/coverheaders里带上token。上传成功后返回一个URL前端把URL绑定到表单的coverUrl字段预览时直接img :srccoverUrl。后端上传接口的代码逻辑也很常规PostMapping(/upload/cover) public ResultString uploadCover(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); if (!Arrays.asList(.jpg, .png, .jpeg, .webp).contains(suffix.toLowerCase())) { return Result.error(图片格式不支持); } String fileName UUID.randomUUID() suffix; String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); File dir new File(UPLOAD_DIR datePath); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dir.getAbsolutePath() / fileName)); return Result.success(/upload/ datePath / fileName); }代码的重点在于文件名不能被用户控制必须用UUID重命名防止路径穿越攻击扩展名要白名单校验防止有人传jsp文件到服务器上借机执行存储路径用日期分目录方便定期清理。静态资源的对外访问需要在SpringBoot里做映射配置。在application.yml中把本地磁盘的目录映射到/upload/**这个URL前缀spring: servlet: multipart: max-file-size: 5MB max-request-size: 10MB web: resources: static-locations: classpath:/static/,file:${upload.path}关于文件存储还有一个值得思考的点本地存储和OSS对象存储的选择。我用本地存储是因为毕设部署环境简单一个jar包加一个上传目录就能跑不需要额外依赖云服务。但如果你有云服务器环境用OSS也是不错的加分项在答辩时可以提生产环境可以平滑切换到云存储只需要替换存储实现类这也是扩展性思维的表现。3.4 前端页面组织Vue路由与组件设计前端开发中的页面组织思路直接决定了后期迭代的效率。小说平台我建议至少拆分成这些顶级路由路由页面功能/首页轮播图、推荐位、热门榜单、分类入口/novel/:id书籍详情页封面、简介、章节列表、评论区/reader/:novelId阅读页正文阅读、目录抽屉、字体设置、进度保存/bookshelf书架页收藏列表、最近阅读、删除收藏/login、/register认证页表单校验、登录注册/admin/*后台管理页小说管理、章节管理、评论管理、数据统计组件设计上我坚持两个原则一是公共组件尽量拆分。比如小说封面卡片这个组件在首页推荐位、搜索结果、书架列表里都会用到拆出来做成NovelCard.vue接收一个novel对象props只负责展示。二是页面级组件按功能聚合。阅读页面是独立性最强的页面它有自己的路由守卫、状态管理、异步加载逻辑把它单独拆成一个view文件夹下的ReaderView.vue不要把阅读逻辑混到首页组件里。Vue路由的懒加载也值得用上const ReaderView () import(/views/ReaderView.vue)懒加载让首屏只加载必要的chunk阅读页的代码在用户点击进入时才拉取对首屏性能有明显改善打包体积也从一刀切变成了按需分包。这个细节做完就能在性能优化汇报里有话可说。关于状态管理我用的是Vue 3的Pinia。相比VuexPinia的API更加现代简洁去掉了mutations的概念直接用setup语法定义state和actionsTypeScript支持也更好。阅读器的章节状态、用户登录态、书架列表刷新标记这些跨组件共享的数据统一放Pinia里管页面之间通过store传值天然响应式省掉了事件总线的破事。3.5 登录态保持与路由守卫别让用户到处碰壁用户登录后刷新页面Vue应用重新初始化localStorage里的token还在但用户信息丢了。这时候需要做一步恢复登录态的操作应用启动时在App.vue的onMounted里调用GET /api/user/info用token换用户信息拿到之后放进Pinia。这个接口在后端要做token无效的处理——code返回401前端响应拦截器捕获到401就跳转登录页并清空localStorage。路由守卫的需求也很清晰。阅读页、书架页、后台管理页都要求登录未登录的用户被强行跳转到这些路由时需要被拦截到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })有人会问前端路由守卫能拦住未登录用户可如果别人绕过前端直接调后端接口怎么办这个问题的答案是后端接口同样要做登录校验前端路由守卫只是用户体验层面的防护真正的安全防线在后端拦截器上。所以我在后端写了一个AuthInterceptor对所有/api/**接口统一放行登录注册和首页数据的GET请求其他请求全部校验token。前端路由守卫和后端拦截器一前一后形成一个完整的认证闭环。4. 常见问题与排查技巧实录4.1 跨域问题前端的报错九成都是它前后端分离项目联调阶段最高频的报错就是浏览器控制台的CORS error或者请求直接变成net::ERR_FAILED。这个问题的本质是前端页面跑在http://localhost:8080Vue默认端口后端接口跑在http://localhost:9090SpringBoot自定义端口两个端口不同浏览器认为这就是跨域。解决方案在后端加一个CORS全局配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }有一个坑值得单独提醒如果你用了Nginx反向代理把前后端放在同一个域名下比如/api转发到后端、其他路径走前端静态文件那么浏览器层面根本没有跨域这时候后端就不需要再开CORS。但如果你还在localhost:8080直接调localhost:9090前后端都要各就各位——后端允许跨域前端Axios不要手动加Content-Type: application/json以外的花活。排查跨域问题时我推荐一个高效率流程先打开开发者工具Network面板确认请求是否真的发出再看响应头里有没有Access-Control-Allow-Origin最后看是前端预检请求OPTIONS没过还是实际请求没过。八十年代的排查流程走到这里基本定位了。4.2 数据库连接与编码启动就能炸的经典问题SpringBoot连MySQL时最容易出两个问题。第一个是时区报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。解决办法是JDBC连接串上加上serverTimezoneAsia/Shanghai或者干脆用serverTimezoneCTT。第二个是连接名和驱动新版本MySQL的驱动类已经变成com.mysql.cj.jdbc.Driver别再抄老博客的com.mysql.jdbc.Driver了。中文乱码的问题在小说平台里几乎必现。小说章节的正文都是中文如果数据库连接串上没有指定编码存进库里的中文十有八九变成问号串。数据库连接URL里务必加上useUnicodetruecharacterEncodingutf8同时建库时选择utf8mb4字符集。注意utf8mb4和utf8的区别前者是完整的UTF-8支持emoji和生僻字小说内容的兼容性更好。另一个常见现象是CannotGetJdbcConnectionException数据库连接池打满了。这个问题的根源多半是某个事务没有正常释放连接——比如Service方法上加了Transactional但内部捕获异常没有抛出导致事务未提交、连接一直占用。排查思路是先看日志里有没有长时间未关闭连接再用SHOW PROCESSLIST查看MySQL当前连接数最后检查代码里事务注解的边界是否合理。我在实际项目中一般建议能不加Transactional就不加只给真正需要原子性的写操作加上控制事务粒度是避免连接池问题最有效的习惯。4.3 联调阶段的接口对接痛苦先用工具再上代码前后端联调最痛苦的场景是前端觉得后端返回错了后端觉得前端传参错了。这个问题的核心原因是双方对接口的约定不一致。我现在的流程是后端写完一个接口立刻用Postman或Apifox跑一遍把正常情况和异常情况的返回截图/示例存到接口文档里前端根据文档写请求。接口文档工具方面如果追求轻量可以直接用SpringDocOpenAPI 3自动生成Swagger页面前端直接看页面上的参数示例。如果追求更好的协作体验用Apifox这类工具可以把接口定义和Mock数据打通前端没等后端写完就能开始页面开发。联调时还容易踩到的一个坑是日期格式。后端的LocalDateTime序列化成JSON时默认是一长串数组或带T的ISO字符串前端显示出来很难看。统一在application.yml配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样所有日期字段都输出成2025-01-01 12:00:00这样友好的格式前端new Date(str)解析也不会歧义。这个细节不处理好阅读历史里显示火星时间的例子我见过太多了。4.4 部署上线本地能跑服务器跑不起来怎么办毕设评审时要现场演示最怕的是在我电脑上能跑变成到你电脑上跑不了。我的建议是尽早把部署流程跑一遍别等答辩前一天才开始。后端打包用Maven的package命令生成jar包部署到服务器上直接java -jar novel-platform.jar运行。配置文件的区分方面开发环境用application-dev.yml指向本地MySQL生产环境用application-prod.yml指向云服务器MySQL通过启动参数--spring.profiles.activeprod切换。这样本地开发和生产部署互不干扰也非常符合企业里的多环境管理习惯。前端部署是很多同学的盲区。npm run build之后生成的是一个dist静态目录你要做的是把这个目录交给Nginx然后配置反向代理把/api转发到后端服务server { listen 80; server_name your_domain; location / { root /var/www/novel/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /upload/ { alias /data/novel/upload/; } }try_files $uri $uri/ /index.html;这一行极其重要——它保证了Vue Router的history模式下的路由直接刷新不会404。不用这行你访问/novel/1刷新一下页面直接白屏就是一个典型的history路由部署坑。我见过太多人卡在这里最后只好被迫改成hash模式或者后端做fallback转发实际上Nginx一行配置就能解决。部署完成之后有一个必须做的小验证从服务器外部访问/api/novels接口以及上传过的封面图片URL确认静态资源和API都通。很多同学的部署看起来成功实际上封面图片加载404、接口400全都有只是因为本地Demo时URL是写死的相对路径没有暴露问题。5. 项目亮点的打磨方向与答辩准备5.1 给项目加分的几个扩展点毕设做完只是及格线想拿高分还需要在亮点上下功夫。这里说的亮点不是胡编乱造而是基于现有架构做合理增强面试和答辩都能讲出实际细节。第一个方向是Redis缓存。阅读页的详情接口是热点数据每次刷新都查一次数据库。引入Redis后可以把novel详情和章节列表缓存起来设置合理的过期时间比如10分钟点击量这类高频计数也丢到Redis里做原子自增定时同步回数据库。这个操作不仅让接口响应时间明显下降还能在简历里写一行使用Redis对热点数据进行缓存降低了数据库压力含金量马上不一样。第二个方向是搜索的拼音首字母匹配。用pinyin4j库对书名做拼音转换存一个pinyin字段这样用户输入jianshen也能搜索到《健身笔记》。这个功能看着小但答辩演示时的效果很炸——搜索框输入拼音能出结果评委第一反应就是这个项目做了细节。第三个方向是数据统计可视化。后台管理增加一个数据面板用ECharts画几组曲线每日新增用户数、每日阅读量、小说分类占比、热门书籍Top10。数据来源是个简单的统计表或从novel、chapter、comment三张表聚合。这个模块实现难度不大但视觉效果极佳能瞬间提升答辩演示的整体观感。我在实际做的时候发现数据统计最容易踩的坑是时间粒度。如果统计表里只有create_time要按天聚合得先格式化日期再GROUP BY。正确做法是统计表里加一个冗余的date_str字段格式yyyy-MM-dd写入时就存好查询时直接按这个字段分组SQL简单一个量级。5.2 答辩现场最容易被追问的四个问题答辩评委对你做的项目一般不会只看你能跑不能跑他们更关心的是你理解不理解你在做什么。下面这四个问题几乎是必问的我建议提前想好答案为什么选择SpringBoot而不是SSM我的回答角度SpringBoot自动配置省略了大量XML配置内嵌Tomcat简化部署同时SpringBoot的依赖管理机制starter能更好控制jar包版本矛盾让开发者聚焦业务。但我会补充一句SSM底层原理我了解SpringBoot是基于Spring生态的进一步封装并不是替代。什么是前后端分离前后端怎么通信这个问题要讲清楚三个词RESTful API、JSON、Axios。前端页面通过HTTP协议向后端发送GET/POST请求后端返回JSON格式数据前端拿到数据后动态渲染。同时要提到跨域和token的含义说明你不仅会用还理解背后的通信机制。你的权限控制是怎么做的从后端拦截器 JWT 角色字段三个层面回答。普通用户和管理员的权限差异通过后端判断JWT里解析出的role字段即可前端再配合按钮级别的v-if控制。这里最忌讳的回答是我用了一个框架就实现了你要能拆到拦截器做了什么、JWT里有什么、前端怎么配合。小说热度排行榜的数据从哪来答案要从表设计说起novel表冗余了click_count字段每次进入详情页时click_count1排行榜接口按这个字段排序。如果追一句为了避免并发下的计数误差可以用Redis的自增计数异步同步那就是高级回答了。5.3 时间规划一个人怎么把项目稳稳做完说句实在话毕设最大的敌人是拖延和返工。如果按我的经验排一个比较合理的时间表应该是这样的阶段耗时交付物技术预研与环境搭建3-5天前后端项目骨架、数据库建表、基本联调通过用户认证与小说浏览7-10天注册登录、首页列表、搜索筛选、书籍详情阅读器与书架5-7天章节读取、翻页、进度保存、书签互动与后台管理7-10天评论、收藏、后台CRUD、上传打磨与部署3-5天界面美化、异常处理、服务器部署、测试总周期大概一个半月到两个月前提是你每天能保证三四个小时的有效编码时间。最怕的情况是先把代码写到一半突然发现表结构设计不合理又回头重写了一遍——这种返工才是时间杀手。所以我的建议很直接写第一行业务代码之前花完整的一天把ER图和数据字典定下来后面所有的编码都围绕这个结构展开即便要改也只是小调字段不至于推翻重来。开发过程中还有一个实用技巧用Git做版本管理每个功能模块完成就提交一次commit信息写清楚。这不仅是为了备份更是答辩前复盘项目进度、讲我遇到了什么问题、什么时候解决的的最好素材。有些同学的答辩毫无细节可讲就是因为全程没有记录到最后一问三不知白白浪费了一整个项目的付出。写在最后这个选题做下来我自己最大的感触是一个小说平台看起来简单但真要把它做到结构清晰、功能完整、能讲出道理需要动脑子的地方远比想象中多。技术选型不是越新越好而是要能讲清楚为什么功能实现不是能跑就行而是要理解每个设计决策背后的权衡答辩准备不是背稿子而是要把项目从里到外讲成一个完整的故事。如果你正在做类似的毕设或者JavaWeb练手项目记得把数据库设计当回事把接口设计当回事把前后端联调当回事——这些平时不起眼的环节恰恰是毕业后进入真实开发岗位最用得上的能力。别急着抄网上现成代码动手之前把表关系画清楚、把模块边界想明白你后面写代码的速度反而会快得多。祝顺利。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进