做毕设选这个题目的话说明你已经对“前后端分离”这条路有感觉了。Spring Boot Vue的组合在计算机毕业设计里属于最稳的搭配之一覆盖面广、难度适中、演示效果好漫画信息管理网站这个业务场景又足够具体——侧栏分类、热门榜单、详情页、收藏功能、后台增删改查每一个模块都能把主流技术栈里最重要的知识点串起来。我当初做这个项目的时候从搭建骨架到最后跑通整体演示前后花了大概一个月。中间踩过不少坑也掉过头重做一些表设计。这篇就把整个项目从需求拆解到前后端实现再到联调排错的经验完整写下来给准备做类似题目的同学当一份参考。1. 项目整体定位与核心需求拆解1.1 为什么选Spring Boot Vue这个技术组合这个组合在毕设里受欢迎是有原因的。Spring Boot是目前Java后端最主流的企业级框架它把繁琐的配置简化成了一堆starter依赖让你可以用最少的代码搭起一个能跑通的REST API服务。Vue作为前端框架学习曲线相对平缓组件化开发思路清晰适合一个人同时兼顾前后端。更重要的是前后端分离这种开发模式本身就是当前行业里的标准做法。后端只负责提供接口前端只负责渲染页面两边通过JSON数据交互。这个思想比具体的框架更重要哪怕你以后换用React或者其他后端框架这套协作方式不会变。毕设选这个组合等于用一次项目实践把“一个产品从零到上线”的全过程走了一遍。还有个实际的好处答辩的时候好讲。技术点都是面试和课程里高频出现的Spring的依赖注入、AOP思想、Vue的响应式原理、路由守卫、axios请求封装随便挑一个都能展开说。不会出现“我调了个库但说不清原理”的尴尬局面。1.2 漫画信息管理网站的业务场景漫画信息管理网站的核心业务通俗点说就是“让用户能找漫画、看详情、收收藏让管理员能维护漫画数据”。用户端要解决的是浏览效率问题。漫画网站的内容量通常很大如果没有分类筛选、关键词搜索、排行榜这类功能用户找一部漫画全靠翻页体验会非常糟糕。所以前端页面的核心任务是把后端返回的数据用清晰、有层次的方式呈现出来让用户用最少的操作找到想看的内容。管理端要解决的是数据维护问题。漫画的基本信息书名、作者、封面、分类、简介、章节更新、推荐位的调整都需要一个后台界面去操作。后端接口设计的核心任务是提供一套完整的CRUD增删改查能力同时做好权限控制——普通用户只能看管理员才能改。这个项目的复杂度控制得刚刚好比单纯的学生管理系统多了一些业务细节又不会像电商系统那样涉及订单、支付、库存等强流程性模块。作为毕设既有深度又不至于做不完。1.3 功能模块划分我在做项目前先把功能拆成了一棵树这样开发的时候心里有底不会写着写着漏掉需求。用户端模块首页轮播图展示推荐位、热门漫画排行、最新更新列表漫画列表页按分类筛选、按更新时间/热度排序、分页加载漫画详情页基本信息展示、章节列表、收藏按钮搜索按漫画名称或作者名做模糊查询个人中心注册登录、我的收藏列表管理端模块仪表盘漫画总数、用户总数、分类数量的统计卡片漫画管理新增、编辑、删除漫画设置是否推荐、是否热门分类管理分类的增删改关联漫画数量统计用户管理用户列表、启用/禁用账号、角色调整这个功能列表并不复杂但每一个点都能对应到一个明确的技术实现。比如推荐位对应后端的isHot字段排行榜对应一个orderBy热度值的查询搜索对应like查询页面刷新后收藏状态保持对应一个根据用户和漫画ID的联合查询。需求拆得越细后面开发就越顺。1.4 技术选型与版本匹配版本选择是很多同学容易忽略但实际会卡人的地方。我最初用Spring Boot 3.x配JDK 17后来因为部分依赖兼容问题换回了Spring Boot 2.7.x配JDK 1.8。如果你的电脑装的是Java 8直接选Spring Boot 2.7即可不要盲目追新版本。我最终用到的技术栈如下后端Spring Boot 2.7.18、MyBatis-Plus 3.5.x、MySQL 8.0、JWT登录鉴权前端Vue 2.7 Vue Router Vuex Element UI Axios构建工具Maven、Node.js 16.x为什么用MyBatis-Plus而不是直接用MyBatis毕设项目的时间本来就紧张MyBatis-Plus提供了通用Mapper、分页插件、代码生成器简单查询不用手写SQL能把大量时间从重复代码里省出来。复杂查询仍然可以自己写SQL灵活性不受影响。如果你对SQL能力有展示方面的考量可以在分页查询里故意写一段多表联查的SQL这样既用了框架又展示了基本功。2. 前端实现Vue项目搭建与核心页面实战2.1 项目初始化与目录结构用脚手架创建Vue项目两条命令就能启动一个基础工程# 创建项目选择Vue 2 vue create comic-frontend # 安装需要的基础依赖 npm install axios element-ui vue-router3 vuex3注意Vue 2和Vue 3对应的路由和状态管理库版本不同。Vue 2要用vue-router 3.x和vuex 3.x版本不对会导致启动直接报错。这是我在项目里踩过的最初级的坑。初始化的目录结构我会建议大家按模块重新组织一下而不是直接用脚手架默认的散装结构。我的前端目录是这样的src/ ├─ api/ # 接口请求模块按业务拆分 │ ├─ request.js # axios实例封装 │ ├─ comic.js │ ├─ user.js │ └─ admin.js ├─ router/ # 路由配置 ├─ store/ # vuex状态管理 ├─ views/ # 页面组件 │ ├─ user/ # 用户端页面 │ └─ admin/ # 管理端页面 ├─ components/ # 公共组件 ├─ utils/ # 工具函数 └─ App.vue这样的结构让代码的归属非常清楚。api目录把每个后端接口封装成函数页面里只调用函数不直接写axiosviews按用户端和管理端分文件夹避免所有页面挤在一起找的时候头皮发麻。后期维护和答辩时讲项目结构都会省很多事。2.2 路由设计与导航守卫路由设计的核心思路是区分用户端和管理端两套布局。用户端页面共用一个整体布局顶部导航栏 内容区管理端页面用另一个布局侧边栏 头部 内容区。实现方式是通过嵌套路由{ path: /, component: Layout, children: [ { path: , name: Home, component: Home }, { path: comic/:id, name: ComicDetail, component: ComicDetail } ] }, { path: /admin, component: AdminLayout, children: [ { path: comics, name: AdminComics, component: AdminComicList }, { path: comics/edit, name: AdminComicEdit, component: AdminComicEdit } ] }管理端路由需要加一层登录校验。Vue Router的导航守卫能在每次路由跳转前检查token是否存在router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path.startsWith(/admin) !token) { next(/login) } else { next() } })这个守卫就是路由层面的权限控制。配合后端的接口鉴权两层下来安全性和功能演示效果都有了。答辩被问“怎么做权限控制”的时候可以把这两层分别讲一遍。2.3 axios封装与请求拦截器如果不做封装每个页面都要重复写axios配置、错误处理代码会非常散。我的做法是建一个request.js创建一个统一的axios实例然后在拦截器里做两件事请求拦截器自动带上token响应拦截器统一处理错误码。// request.js import axios from axios import { Message } from element-ui const request axios.create({ baseURL: process.env.VUE_APP_BASE_URL || http://localhost:8080/api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { Message.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default requestbaseURL我建议不要写死通过.env文件配置。这样部署的时候只改环境变量不用改代码。实际做的时候我因为前后端端口不一致转过弯来——前端跑在8080后端跑在8081如果后端没开跨域请求就会被浏览器拦下来。这个问题的解决办法我放在后面联调里详细说。2.4 核心页面实现要点漫画列表页是体现Vue核心能力的重点页面。它需要做四件事请求分类列表、请求漫画分页数据、渲染卡片、处理筛选和分页。关键代码如下// views/user/ComicList.vue data() { return { list: [], total: 0, queryParams: { page: 1, size: 12, keyword: , categoryId: , sortBy: updateTime } } }, methods: { async fetchList() { const res await getComics(this.queryParams) this.list res.data.records this.total res.data.total } }这里要注意一个细节切换筛选条件时页码要重置为1。如果不重置你在第5页筛选分类后端返回第5页的数据但总页数可能只有3页就会出现空白页。这个小bug很容易忽略但演示的时候被老师翻到就很尴尬。漫画详情页要处理的是“不同来源进入同一页面”的数据加载。无论是从首页、列表页还是搜索页点进来的都要通过路由参数comicId去请求详情。收藏按钮的状态要等后端返回后才知道——根据当前用户ID和漫画ID查收藏表。点击收藏按钮时若未登录要跳转到登录页这个逻辑放在按钮事件里判断即可。3. 后端实现Spring Boot接口开发与业务逻辑3.1 项目初始化与三层架构后端项目用Spring Initializr初始化核心依赖加这几个就够了dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.2/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency项目结构采用标准的Controller-Service-Mapper三层。Controller层负责接收请求和返回响应不写业务逻辑Service层处理业务规则Mapper层跟数据库打交道。很多同学一开始容易把代码全堆在Controller里短期看能跑但项目一大就乱答辩也不好讲。分层是后端开发最基本的整洁度要求别偷懒。3.2 数据库表结构设计数据库设计是后端开发的第一步也是决定项目后续是否好扩展的关键。漫画网站虽然业务不算复杂但五张表的基本结构是少不了的。用户表userid主键自增username用户名唯一索引password加密存储nickname昵称avatar头像地址role角色1为用户2为管理员status状态1正常0禁用create_time创建时间分类表categoryid主键name分类名description描述sort_order排序值status是否启用漫画表comicid主键title漫画名author作者cover_url封面图地址category_id分类外键description简介status1连载中2已完结total_chapters章节总数is_hot是否热门is_recommend是否推荐click_count点击数update_time最后更新时间章节表chapterid主键comic_id所属漫画外键chapter_number章节序号chapter_title章节名content_url内容地址create_time创建时间收藏表favoriteid主键user_id用户外键comic_id漫画外键create_time收藏时间联合唯一索引(user_id, comic_id)联合唯一索引非常关键它能在数据库层面保证同一个用户不会重复收藏同一个漫画。即使前端逻辑出了漏洞数据库也能兜底。3.3 统一返回格式与代码生成前后端分离的项目里后端接口返回的数据格式必须统一否则前端解析逻辑会写得非常痛苦。我定义了这样一个统一结构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(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }所有接口都返回Result对象前端拿到后端返回的JSON时结构永远是固定的code表示状态、message表示提示信息、data表示业务数据。这样前端拦截器才能统一判断code的逻辑。表结构确定后用MyBatis-Plus的代码生成器把实体类和Mapper层的代码一次性生成然后手动补Service层和Controller层。代码生成器是一个配置类根据数据库表自动生成对应的实体、Mapper接口和XML文件。这个工具值得学它省下的那部分时间可以拿去把接口文档和服务层逻辑写得更扎实。3.4 分页查询接口实现列表页的分页查询是后端最核心的接口。MyBatis-Plus提供了分页插件配置好之后查询就很简单// ComicService.java public PageComic queryComics(int page, int size, String keyword, Long categoryId, String sortBy) { PageComic pageResult new Page(page, size); LambdaQueryWrapperComic wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(Comic::getTitle, keyword) .or().like(Comic::getAuthor, keyword)); } if (categoryId ! null) { wrapper.eq(Comic::getCategoryId, categoryId); } if (clickCount.equals(sortBy)) { wrapper.orderByDesc(Comic::getClickCount); } else { wrapper.orderByDesc(Comic::getUpdateTime); } return comicMapper.selectPage(pageResult, wrapper); }分页插件配置需要注册大约这样Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这样搭建出来的接口返回的Page对象自动包含records当前页数据、total总条数、current当前页码等字段。前端拿到之后直接渲染就行。3.5 登录鉴权与JWT登录鉴权我选择了JWT方案。用户登录成功后后端生成一个包含用户信息的token返回给前端。前端每次请求在请求头中带上token后端拦截器从中解析出用户身份。JWT的好处是服务端不需要存session适合前后端分离场景。核心代码分成三部分登录时签名、拦截器验签、业务中获取当前用户。// JwtUtil.java核心部分 public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }拦截器里通过过滤器拦截所有需要登录的接口验签失败则返回401错误码。一个完整的拦截器配置大概涉及注册拦截器、定义排除路径登录、注册、漫画列表和详情这类公开接口不拦截、在preHandle方法里做token解析。这里有个容易忽略的点前端传的token放在请求头Authorization里值是Bearer 开头的字符串。后端解析时要先把Bearer 这个前缀去掉只拿后面的token部分。我第一次联调时就在这个细节上卡了半小时。3.6 管理端接口与权限控制管理端接口和管理端前端页面都需要额外的权限判断不能只用登录状态来区分。用户登录后拿到token里面包含了userId后端还要查一次用户表确认角色是管理员才能允许执行管理操作。我的做法是写了一个RequireAdmin注解配合拦截器实现在管理端接口上加上这个注解拦截器就会先验证登录、再验证角色。比在每个Controller方法里手写判断要整洁得多。漫画的新增编辑接口需要处理封面上传问题。图片文件用MultipartFile接收并存到服务器指定目录数据库里存的是图片访问路径。这个方式对毕设来说够用了不需要引入庞大的文件系统组件。但要注意上传目录最好放在项目外部路径不要放在target目录下否则每次重新打包会被清掉。4. 前后端联调从本地开发到整体跑通4.1 跨域问题的本质与解决前后端分离开发的必然结果是前端和后端跑在不同的端口上。前端的地址是http://localhost:8080后端是http://localhost:8081。浏览器在8080端口页面的JavaScript代码向8081端口发起请求这时候就触发了跨域。跨域安全机制是浏览器自身的行为保护不是后端访问不了而是浏览器层不允许非同一侧的请求直接读取返回数据。解决办法是在后端配置CORS规则// WebConfig.java Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowCredentials(true)用于允许前端请求携带token。这里如果设置成trueallowedOrigins不能直接用得用allowedOriginPatterns()才行一个很小的细节但会导致前端明明发请求了后端却返回跨域错误。4.2 接口对接中的常见细节前端调用接口时传参方式要对应后端的接收方式。GET请求的参数如果放在URL后面后端用RequestParam接收如果放在请求体里后端用RequestBody接收。Vue里axios的get请求走params传参post请求可以走data传参这个习惯要养成否则接口很容易返回Required request body is missing之类的报错。时间格式的传递也容易出问题。后端返回的LocalDateTime默认序列化成ISO格式的数组或字符串前端显示日期经常出现不预期的格式。解决办法是对全局的JSON序列化做配置让LocalDateTime统一输出成yyyy-MM-dd HH:mm:ss格式。配置一个Jackson的ObjectMapper定制类就能解决。另外后端实体类名如果叫Comic前端请求体里的字段名要与实体字段名保持一致。Java的驼峰命名在JSON序列化后默认也是驼峰前端直接用title、author这类字段名即可不用额外做下划线到驼峰的转换。4.3 联调必踩的坑第一个坑数据库里的中文字段在插入时变成问号。解决办法是创建数据库时显式指定UTF-8字符集CREATE DATABASE comic_db DEFAULT CHARACTER SET utf8mb4;同时application.yml里配置url时加上characterEncodingutf8参数spring: datasource: url: jdbc:mysql://localhost:3306/comic_db?characterEncodingutf8serverTimezoneAsia/Shanghai第二个坑用axios调用delete请求时后端使用RequestParam接收参数。Vue里delete请求传参要写成config格式request.delete(/api/favorites/ comicId)直接把ID拼进URL里是最省心的方式不会出现body传参导致的后端接收不到问题。第三个坑刷新页面后登录状态丢失。token存在localStorage里刷新不会丢但如果没在axios请求拦截器里带上token后端一律返回401页面数据全部加载失败。这个逻辑要在一开始封装request时就要做对否则后面每个接口都要处理一遍token。5. 常见问题与排查技巧实录5.1 后端启动失败排查项目启动时报端口被占用# 查看占用端口的进程 lsof -i :8081 # 或通过任务管理器找到进程结束更常见的启动失败原因是依赖版本问题。Spring Boot的父POM版本与MyBatis-Plus版本不兼容会直接启动报错。后端的依赖版本尽量选择一个在网络上验证过的稳定组合别全用最新版。启动时如果报数据库连接失败先检查MySQL服务是否启动再去检查用户名密码配置和数据库名是否一致。这属于基础问题但确实会浪费不少时间。5.2 前端启动与页面问题npm install安装依赖时经常遇到卡住或报peer dependency冲突。解决办法是使用npm镜像源或者pnpm安装并且锁定Node版本。Vue 2的项目在Node 18以上版本编译时偶尔会有兼容问题。前端页面白屏是个经典问题控制台如果报类似Cannot read properties of undefined的错大多数情况是接口返回的数据结构没对上。前端拿res.data.records后端却返回了别的字段名页面渲染时某一层数据为undefined就会报错。处理方式先在浏览器Network面板看后端返回的JSON长什么样再对前端的解析逻辑别一上来就怀疑代码写错。5.3 数据相关的问题排查列表页搜索无结果先确认是查询条件写错还是数据没入库。最简单的排查方式是用数据库工具直接执行一遍同样的SQL如果SQL能出数据问题就在查询条件封装上如果SQL也出不来数据那就是数据入库时字段有问题。表格中汇总我遇到的高频问题如下问题现象可能原因解决方案登录后请求接口返回401token未携带或格式不对检查请求拦截器Authorization头前端始终显示跨域错误CORS配置没生效或allowCredentials冲突检查CorsRegistry配置确认请求头允许插入中文数据乱码数据库字符集不对或连接url缺少编码用utf8mb4重建数据库url加characterEncoding分页数据总条数为0分页插件没注册或注册顺序错误确认MybatisPlusInterceptor注册成功管理页面直接访问被跳到登录路由守卫失效检查beforeEach配置确认路径前缀判断合理上传图片刷新后找不到文件存在target目录配置静态资源映射到外部目录5.4 视觉样式与演示细节除了技术问题答辩演示时的视觉体验也不能忽视。Element UI的默认样式虽然能看但太普通。我当时对首页做了一些定制轮播图用了Vue组件封装漫画卡片加了hover放大效果排行榜用了一个简洁的列表布局。花的时间不多但整体观感提升很明显。演示前一定要把假数据准备充分。一部漫画的各项字段信息、至少五六个分类、每个分类下有若干条数据封面图用本地图片或免费图床链接。数据量太少分页效果和筛选效果展示不出来数据内容太假答辩时容易被质疑项目的实用性。6. 项目总结与扩展建议这个项目做完之后我自己最大的感受是毕设最难的往往不是某个单个技术难点而是把所有模块串到一起的过程。单个页面、单个接口都不难但用户模块、漫画模块、收藏模块、后台管理模块之间的数据流转需要你把数据库设计、接口设计、前端状态管理整体考虑清楚。这种全栈式的思维才是做这个题目真正的收获。如果你学有余力这个项目还可以做几个方向上的扩展。引入Redis做热门榜单的缓存把点击量统计的压力从数据库里解放出来给自己的网站加一个评论模块能增加一处关联查询和一对多设计的练习题部署到云服务器用Nginx做反向代理、将前端build后的静态文件和后端接口放在同一域名下顺便解决生产环境的跨域问题。每一个扩展方向都能让这个项目的含金量再上一层。最后分享一个小技巧把整个项目的接口文档整理成一份简洁的表格标注好每个接口的请求方式、路径、参数和返回格式。这份文档不仅在你写前端时会帮你理清思路答辩时放在PPT里展示也能让老师直观看到你做了完整的设计工作。很多人做完项目就丢掉了接口文档其实它才是展示项目规范性的重要素材。