最近后台一直有人私信问,说自己在做Java Web方向的毕业设计,导师给的题目是“电影评论网站系统”,看了不少开源项目,要么是用JSP这种老古董,要么前端还是传统的模板渲染,很难体现“前后端分离”这个加分项。今天我就拿一套我自己调试过、也帮学生跑通过的技术栈组合——SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0——来完整拆解一个电影评论网站系统该怎么做。这篇内容不光是给你一份能直接运行的源码逻辑说明,更会讲清楚系统设计、核心模块怎么拆、前后端怎么对接,以及你写文档和准备答辩时最容易被问到的那些技术细节。
这套系统我建议计算机科学与技术、软件工程专业的同学重点关注,尤其是正在开题、写任务书或者准备中期答辩的阶段。整个项目覆盖了登录注册、电影信息管理、分类筛选、评论打分、个人中心这几个最常见的业务模块,技术栈又恰好是当前中小型公司招聘JD里出现频率很高的那一套。你做完这个项目,简历上写“基于SpringBoot2和Vue3的电影评论网站”,面试官基本不会觉得陌生。
1. 系统的功能定位与核心业务拆解
先说清楚这个项目是干什么的。电影评论网站,本质上是把“电影信息展示”和“用户UGC评论”两个典型场景捏在一起。它既要做传统的内容管理——比如管理员录入电影、管理分类、上下架影片;又要做用户互动——普通用户注册登录后可以对某部电影打分、写影评、修改或删除自己的评论,还能在个人中心看到自己评论过的电影列表。这类业务看起来不难,但麻雀虽小五脏俱全:涉及到用户权限区分、实体间的关联查询、评论的增删改查、以及前端页面的动态渲染。
我建议你把系统拆成两个大端:前台用户端和后台管理端。
前台用户端主要面向普通访客和注册用户,功能包括:
- 浏览电影列表、按类型筛选、按上映时间或评分排序;
- 查看电影详情页,包括简介、海报、评分、已有的用户评论列表;
- 注册、登录、退出登录;
- (登录后)发表评论、打分、删除自己的评论;
- 个人中心:查看自己发过的评论、修改昵称或头像。
后台管理端主要面向管理员角色,功能包括:
- 管理员登录,和普通用户走同一张用户表但角色不同;
- 电影管理:新增电影、编辑电影信息、删除电影、上下架;
- 分类管理:维护电影类型(动作、科幻、喜剧等)的增删改;
- 评论管理:查看全站评论,可删除违规评论。
从角色权限上看,你需要在一开始就设计一个role字段来区分用户和管理员。我见过很多毕设项目把管理员单独建一张表,结果用户表和管理员表结构几乎完全重复,接口也要写两套,纯属给自己找活干。正确做法是在用户表上加一个整数或者枚举字段,0表示普通用户,1表示管理员。前端根据这个字段决定路由能不能访问,后端在Controller层用拦截器或者注解做权限校验。这样既简洁,答辩时也能讲清楚你的RBAC(基于角色的访问控制)设计思路。
这里有一个容易被忽略的需求源头,就是“评分”和“评论”的关系。从产品设计上来讲,用户对一部电影的评价往往同时包含一个分数(比如1到10分)和一段文字评论。从数据库设计的角度来说,最简单的方式是把两者放在同一张评论表里:一条评论记录自带一个打分字段。但如果你想让电影列表页直接展示平均分且不用每次联表聚合,我建议在电影表中冗余一个average_score字段,每次用户新增或修改评论时,通过一条SQL重新计算该电影的平均分并同步更新。这个设计我在后面“数据库设计”部分会展开讲清楚,这也是答辩时的一个亮点。
2. 技术选型的逻辑:为什么是SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0
这套技术栈不是你随便拼出来的,背后每个选择都对应着毕业设计的实际需求。我先从后端说起。
SpringBoot2相比更早的SSH(Struts2 + Spring + Hibernate)或者SSM(Spring + SpringMVC + MyBatis)组合,最大的优势是自动化配置和内嵌容器。你做毕设的时候不需要自己装Tomcat、不需要写一堆XML配置文件,一个SpringBootApplication主类启动,直接内嵌Tomcat跑起来。现在绝大多数教学用的还是SpringBoot2.7.x版本,因为这个版本既稳定,又和很多第三方starter兼容得最好。如果你的学校环境和我的类似,可能老师自己也还在用SpringBoot2,你交上去的代码他能直接看懂。
很多人纠结要不要直接用最新的SpringBoot3。我的建议是:除非你非常熟悉SpringBoot3带来的Jakarta命名空间迁移和GraalVM这些新概念,否则不要冒险。SpringBoot2.7.x + MyBatis-Plus 3.5.x + MySQL8的搭配已经被无数项目验证过,坑少,资料多,随便搜一个问题都能找到解决方案。你的目标是顺利毕业而不是做技术验证,选成熟方案永远比选最新方案更明智。
MyBatis-Plus在MyBatis基础上封装了通用Mapper和通用Service。以前你要写一个BaseMapper接口然后手动写很多XML的CRUD方法,现在MyBatis-Plus直接给你提供insert、selectById、selectPage、deleteById这些通用方法。配合QueryWrapper或LambdaQueryWrapper,连SQL都不用写,直接用链式条件构造器实现动态条件查询。这对于评论列表按电影ID查询、电影列表按分类和关键字筛选这种需求来说,开发效率提升非常明显。我在第4部分的代码示例里会具体演示。
MySQL8.0则是数据库层的选择。相比于5.7,8.0的开窗函数、公共表表达式(CTE)在复杂统计查询里非常有用。比如你要查“评论数最多的前10部电影”,8.0用一条ROW_NUMBER()开窗SQL就搞定了。当然这个项目用不用的上另说,但你在文档里写一句“MySQL8.0支持窗口函数,为后续扩展高难度统计需求留有余地”,答辩老师就觉得你做过功课。数据库安装的时候有个老生常谈的坑:时区设置。MySQL8.0默认时区跟中国差了8个小时,你在JDBC连接串里必须加上serverTimezone=Asia/Shanghai,不然操作时间字段时会出现各类奇怪问题。
前端选择Vue3也很自然。Vue3的Composition API(组合式API)比Vue2的Options API更适合业务逻辑复用。假如你的电影卡片和评论区是两个组件,它们都要调用某几个共同的接口方法,组合式API里你可以把一个完整的“评论相关操作”抽成一个useComment()函数,任何组件都能使用。Vue3配合Vite构建工具,开发环境启动速度比Webpack快好几个量级。刚开始跑项目的时候,Vite一旦启动几乎是秒开,热更新也及时,这体验对赶毕设的人来说太重要了。
Element Plus这个UI组件库现在也是Vue3项目的事实标准。表格、表单、弹窗、分页、消息提示全都有现成组件。写后台管理端的页面时几乎不用自己造轮子,一套表格组件加几个字段列,配合“新增/编辑”弹窗表单,就是一个标准的CRUD页面。这部分视觉上非常加分,因为评委看系统演示时,第一直观感受就是“界面是否整洁、交互是否流畅”。
3. 数据库设计与后端分层架构:从建表SQL到工程目录规划
现在进入动手的核心环节。一个电影评论网站,数据库设计不复杂,但E-R关系一定要画得清楚。你答辩的时候大概率会被问到“你是怎么设计表的”和“为什么这么设计”。我建议你从三张核心表入手,然后再扩展附属表。
用户表(user):id(主键自增)、username(用户名,唯一索引)、password(加密后的密码,建议用BCrypt)、nickname(昵称)、avatar(头像URL)、role(角色0/1)、create_time。
电影表(movie):id、title(片名)、poster(海报URL)、director(导演)、actors(主演)、description(简介)、category_id(关联分类表)、release_date(上映日期)、status(1上架0下架)、average_score(平均分)、rating_count(参与评分人数)、create_time。这里的average_score和rating_count就是我在第一部分说的冗余字段,用空间换查询性能。
评论表(comment):id、user_id(评论人,关联用户表)、movie_id(评论的电影,关联电影表)、rating(本次评分,比如1-10分)、content(评论内容)、create_time、update_time。这条表是核心联表查询的关键,几乎所有的业务接口都会和它打交道。
另外可以加一张分类表(category):id、name。电影和分类是“多对一”的关系,电影表存分类ID即可。
表关系梳理清楚后,建表SQL就不会乱。下面是我实际项目里用的核心建表脚本:
CREATE DATABASE movie_review DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE movie_review; CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT '加密密码', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像地址', `role` TINYINT DEFAULT 0 COMMENT '角色:0普通用户,1管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `category` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电影分类表'; CREATE TABLE `movie` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `title` VARCHAR(100) NOT NULL, `poster` VARCHAR(255) DEFAULT NULL, `director` VARCHAR(50) DEFAULT NULL, `actors` VARCHAR(255) DEFAULT NULL, `description` TEXT, `category_id` BIGINT DEFAULT NULL, `release_date` DATE DEFAULT NULL, `status` TINYINT DEFAULT 1, `average_score` DECIMAL(3,1) DEFAULT 0.0, `rating_count` INT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电影表'; CREATE TABLE `comment` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `movie_id` BIGINT NOT NULL, `rating` TINYINT NOT NULL COMMENT '评分1-10', `content` VARCHAR(1000) NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_movie` (`movie_id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';注意我把movie表的category_id和comment表的movie_id、user_id都建了索引。网易的各类性能优化面试题里一定会有一道问你为什么索引能加速查询。这里的原理就是:索引是一棵B+树,等值查询和范围查询都可以走树的路径快速定位,避免全表扫描。虽然这个数据量级的毕设项目可能感觉不到性能差异,但写进文档里是加分项。
后端分层架构,业内已经很成熟了,我强烈建议你不要把所有代码堆在一个Controller里。标准分层是四层结构:
- Controller层:负责接收请求、参数校验、返回统一响应体;
- Service层:负责业务逻辑,比如发评论时校验电影是否存在、用户是否登录;
- Mapper层(也叫DAO层):继承MyBatis-Plus的
BaseMapper,负责数据库持久化操作; - Entity层(也叫pojo/model层):实体类,和数据库表字段一一对应。
举个例子,你的实体类可以这样写:
@Data @TableName("movie") public class Movie { @TableId(type = IdType.AUTO) private Long id; private String title; private String poster; private String director; private String actors; private String description; private Long categoryId; private Date releaseDate; private Integer status; private BigDecimal averageScore; private Integer ratingCount; private Date createTime; }@TableName、@TableId都是MyBatis-Plus提供的注解。@TableId(type = IdType.AUTO)意思是主键自增。很多新手在这容易翻车:实体类主键字段明明叫id,结果不写@TableId注解,MyBatis-Plus默认也能识别,但一旦字段名不规范,比如数据库字段是movie_id,实体是movieId,就会因为驼峰映射问题报错。强烈建议所有实体类都显式标注主键和表名,避免连锁排查。
4. 后端核心接口实现:登录鉴权、电影分页查询、评论CRUD的完整链路
接口设计是整个系统的灵魂。我把三类最核心的接口单独拿出来讲:登录鉴权、电影分页查询、评论的增删改查。这三块是你做完整个系统后一定会反复被问到的地方。
先说登录鉴权。
毕设项目里常见的登录方案有两种:Session和JWT。Session是传统的服务端会话管理,但前后端分离的场景下,跨域携带Cookie比较麻烦,我更推荐用JWT(JSON Web Token)。原理一句话概括:用户登录成功后,服务器生成一个包含用户ID和角色信息的签名字符串返回给前端。前端每次请求时把它放在HTTP请求头Authorization字段里,后端写一个拦截器解析这个token,就能识别当前用户是谁、有没有权限。
JWT的好处是无状态——服务器不用保存会话记录,每次请求自行校验签名。这在答辩时是个很加分的“考点”:无状态扩展性好,方便以后改成微服务架构。但要注意JWT也有缺点:无法主动吊销。你可以在token里设置过期时间(比如24小时),同时在用户表里维护一个token_version或login_status字段来变相实现强制下线,不过这属于进阶优化,时间不够可以不写。答辩时能说清楚JWT的原理和缺陷就很棒了。
登录接口的核心代码大概这样:
@PostMapping("/login") public Result<LoginVO> login(@RequestBody LoginDTO dto) { // 1. 根据用户名查用户 User user = userService.getOne( new LambdaQueryWrapper<User>().eq(User::getUsername, dto.getUsername())); if (user == null) { return Result.error("用户名不存在"); } // 2. 校验密码(BCrypt加密校验) if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error("密码错误"); } // 3. 生成JWT String token = jwtUtil.generateToken(user.getId(), user.getRole()); return Result.success(new LoginVO(token, user)); }passwordEncoder是Spring Security里BCryptPasswordEncoder的实例。为什么用BCrypt而不是MD5?因为MD5是可逆的,网上有大量彩虹表可以直接碰撞出明文密码;BCrypt每次加密会混入随机盐值,相同的密码加密出来的字符串都不同,暴力破解成本极高。我特别建议你在文档里提这一句,答辩老师听到“彩虹表”“加盐”这类词,第一印象就会好很多。
然后是电影列表的分页查询。这个接口要支持按电影名模糊搜索、按分类ID过滤、按评分排序、按上映时间排序。如果用传统的MyBatis写动态SQL,你要写一堆``标签判断。用MyBatis-Plus的LambdaQueryWrapper,代码立刻就清爽了:
@GetMapping("/page") public Result<Page<MovieVO>> page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId) { Page<Movie> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Movie> wrapper = new LambdaQueryWrapper<>(); // 关键字模糊搜索 if (StringUtils.hasText(keyword)) { wrapper.like(Movie::getTitle, keyword) .or().like(Movie::getDirector, keyword); } // 分类筛选 if (categoryId != null) { wrapper.eq(Movie::getCategoryId, categoryId); } // 默认按上架时间倒序 wrapper.eq(Movie::getStatus, 1) .orderByDesc(Movie::getCreateTime); Page<Movie> result = movieService.page(page, wrapper); return Result.success(result); }LambdaQueryWrapper解决了一个大痛点:直接用方法引用Movie::getTitle来指定字段,不会出现字符串硬编码。如果你用老式的QueryWrapper写"title",一旦数据库字段改名或者实体类字段改名,这里会静默报错或者漏查。Lambda表达式的写法是编译期检查,字段不存在直接编译失败,能提前拦住很多低级错误。
StringUtils.hasText是Spring提供的方法,比StringUtils.isNotEmpty更严格,它会额外忽略只有空格的情况。搜索场景里这个判断很有必要,因为前端传过来的keyword可能是一堆空格,不处理的话查出来的结果会很迷惑。
最后说评论的完整CRUD链路。发评论接口涉及三个表的数据变更,是我认为整个项目中最能体现业务逻辑的地方:
@Transactional(rollbackFor = Exception.class) public void addComment(CommentDTO dto, Long userId) { // 1. 校验电影是否存在且已上架 Movie movie = movieService.getById(dto.getMovieId()); if (movie == null || movie.getStatus() != 1) { throw new BusinessException("电影不存在或已下架"); } // 2. 判断是否已经评论过(可换成:一电影一人一评) Long count = commentService.count( new LambdaQueryWrapper<Comment>() .eq(Comment::getUserId, userId) .eq(Comment::getMovieId, dto.getMovieId())); if (count > 0) { throw new BusinessException("你已评论过该电影"); } // 3. 保存评论 Comment comment = new Comment(); comment.setUserId(userId); comment.setMovieId(dto.getMovieId()); comment.setRating(dto.getRating()); comment.setContent(dto.getContent()); commentService.save(comment); // 4. 更新电影的平均分和评分人数 updateMovieScore(dto.getMovieId()); }注意我在方法上加了@Transactional(rollbackFor = Exception.class)。这个注解的意思是:如果这个方法内任意一步抛出异常,前面所有数据库操作都回滚。为什么强调rollbackFor = Exception.class?因为Spring默认只在遇到RuntimeException时才回滚事务,如果业务方法抛出的是自定义的检查异常(Exception的子类),不加这个参数的话事务不会回滚,就会出现“评论保存成功但平均分没更新”这种数据不一致的尴尬情况。这个知识点在很多公司的面试题里就是实打实的考点,毕设答辩老师问到事务传播行为,你能答上来这一点绝对加分。
更新平均分的逻辑也不难,直接用一条聚合SQL:
private void updateMovieScore(Long movieId) { // 查询该电影所有评论的平均分和数量 QueryWrapper<Comment> wrapper = new QueryWrapper<>(); wrapper.select("IFNULL(AVG(rating),0) AS avgScore, COUNT(*) AS cnt") .eq("movie_id", movieId); Map<String, Object> map = commentService.getMap(wrapper); BigDecimal avgScore = new BigDecimal(map.get("avgScore").toString()) .setScale(1, RoundingMode.HALF_UP); Integer cnt = ((Long) map.get("cnt")).intValue(); Movie movie = new Movie(); movie.setId(movieId); movie.setAverageScore(avgScore); movie.setRatingCount(cnt); movieService.updateById(movie); }这里我用了比较多原生风格的QueryWrapper加上select聚合函数,因为LambdaQueryWrapper对聚合查询的支持没有这么直观。评分保留一位小数,四舍五入取整规则用的是RoundingMode.HALF_UP(四舍五入而不是银行家舍入)。这里不需要新写SQL文件,MyBatis-Plus的getMap返回的是单行查询结果映射。
删除评论接口要注意权限控制:管理员可以删除任意评论,普通用户只能删除自己的评论。所以Service层接收两个参数,一个是评论ID,一个是当前登录用户ID,删除前先查这条评论属于谁,再判断是否匹配:
public void deleteComment(Long commentId, Long userId, Integer role) { Comment comment = commentService.getById(commentId); if (comment == null) { throw new BusinessException("评论不存在"); } if (!comment.getUserId().equals(userId) && role != 1) { throw new BusinessException("无权删除他人的评论"); } commentService.removeById(commentId); updateMovieScore(comment.getMovieId()); }这段逻辑不复杂,但是把“业务权限控制”的颗粒度表现得很清楚。很多新手把权限判断全部丢给拦截器,拦截器只管“是否已经登录”,而“是否拥有操作该数据的权限”必须在具体业务里再查一次。两层校验是标准的做法,第一层是网关级,第二层是业务级。
5. 前端Vue3工程化实现:从环境搭建到页面组件与请求封装
前端这块,如果你是Vue小白,我建议你最开始就不要试图手写所有代码,而是把环境搭建跑通了再填充业务。Vue3 + Vite的项目初始化命令非常简单:
npm create vite@latest movie-web -- --template vue cd movie-web npm install npm install vue-router@4 pinia axios element-plus npm run dev第一条命令会创建一个名为movie-web的Vite+Vue3项目。vue-router@4是Vue3对应的路由版本,注意不要安装成vue-router@3,那是给Vue2用的。Pinia是Vue3官方推荐的状态管理库,替代了Vue2时代的Vuex。如果你只需要存一个登录token,用Pinia就够了。
Element Plus安装之后,可以按需引入也可以全局注册。我建议毕设时间紧就直接全局引入,不用折腾unplugin-auto-import那些按需加载插件:
import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' app.use(ElementPlus)虽然这样会让打包体积大一点,但你的目标是快速看到效果,而不是优化包体积。项目本身是毕设,又不是生产环境要追求秒开,所以别把时间浪费在这里。
axios封装这一步是整个前端的关键。统一封装能让所有请求自动携带token、自动处理错误提示。下面是一个你能直接复制使用的axios实例配置:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', // 开发环境走Vite代理 timeout: 10000 }) // 请求拦截器:自动携带token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) // 响应拦截器:统一错误处理 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response?.status === 401) { ElMessage.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { ElMessage.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default request这里有一个关键点:baseURL我写的是/api,而不是完整的后端地址。这是为了让开发环境和生产环境都通过代理转发请求,避免跨域问题。在Vite的配置文件vite.config.js里这样写:
export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })你肯定遇到过“前端请求后端接口报CORS错误”的问题。解决方式有两种:一种是在后端加CORS配置类放行所有来源;另一种就是我上面的方式,开发时通过Vite代理,部署时通过Nginx反向代理。为什么推荐代理方案?因为代理模式下浏览器请求的是同源地址,根本不会触发CORS校验。后端也就不用暴露服务给任何来源,安全性更好。实际上后端那边的WebMvcConfigurer我也建议配置上,双保险,后面部署联调的时候减少莫名其妙的报错。
Vue3的页面组织,我建议按“视图组件”的维度拆文件,而不是按“类名”堆代码。比如src/views/下面放页面级组件:
HomeView.vue(电影列表首页)MovieDetail.vue(电影详情,内含评论组件)LoginView.vue(登录注册页)AdminMovie.vue(后台电影管理)
再建src/components/放可复用子组件,比如MovieCard.vue(单张电影卡片)、CommentList.vue(评论列表)。电影卡片组件里直接接收一个movie对象,用Props传参。这里顺手说一下Vue3 setup语法糖的写法:
<script setup> import { useRouter } from 'vue-router' const props = defineProps({ movie: { type: Object, required: true } }) const router = useRouter() const goDetail = () => { router.push(`/movie/${props.movie.id}`) } </script> <template> <div class="movie-card" @click="goDetail"> <el-image :src="props.movie.poster" fit="cover" /> <div class="title">{{ props.movie.title }}</div> <div class="score"> <el-rate :model-value="props.movie.averageScore" disabled /> <span>{{ props.movie.averageScore }}分</span> </div> </div> </template>defineProps是Vue3的编译宏,不需要显式import。el-image是Element Plus的图片组件,fit="cover"表示图片等比例缩放并裁剪填满容器。el-rate是评分组件,用disabled属性让它变成只读展示,正好用来显示电影的平均分。
路由守卫也是必须要做的。如果你不做页面访问控制,用户没登录也能打开个人中心,后端接口虽然会拦截,但体验很差。前端路由守卫的逻辑通常是:进入需要登录的页面之前,检查本地是否有token;有token再看是否需要跳转登录页:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.requiresAdmin && !isAdmin()) { next('/403') } else { next() } })meta是路由的元信息,你在路由配置里给需要登录的页面加上requiresAuth: true,给后台管理页面加requiresAdmin: true。这种按路由元信息做权限控制的方式,在Vue社区里是主流做法,也方便后续扩展。
前端有个非常容易踩的坑是:Vue3 + Element Plus的表单校验。你以为填了rules就万事大吉了?如果表单里的字段值变化没有被响应式系统检测到,校验永远不触发。我建议你写表单时全部用ref包响应式对象,比如:
const formRef = ref(null) const form = reactive({ title: '', director: '', rating: 10, content: '' }) const submit = async () => { await formRef.value.validate() // 校验通过后发送请求 }reactive确保对象属性是响应式的,formRef.value.validate()会走Element Plus的表单校验统一出口。这个比你在每个字段上手动绑定@change触发校验要优雅得多。
6. 从本地调试到部署演示:环境配置、数据初始化与常见报错修复
整个项目跑通的流程,我帮你梳理成五个步骤。照着这个顺序做,至少能节省你一天的排查时间。
第一步:准备基础环境。JDK版本建议1.8或者11,Maven用3.6及以上的版本。MySQL8.0装好之后,记得在配置文件my.ini中设置字符集为utf8mb4。这个字符集和utf8的区别在于:utf8mb4是MySQL8的标准,支持4字节的emoji表情,而你前端的评论框里用户可能会发emoji,如果不设置,直接报Incorrect string value错误。别问我为什么知道这个坑,当年我调试用户评论功能测试“😀”这个表情时卡了整整一下午。
第二步:初始化数据库。在Navicat或命令行里执行我上面给的建表SQL,再插入几条测试数据。测试数据很重要,尤其是电影数据,你可以去豆瓣或者各大电影平台爬一点公开信息来做演示数据,包括片名、导演、主演、简介和海报图。海报图建议用网上公开的图片链接,前端展示出来效果很直观。如果找不到外链图片,也可以用占位图,但答辩演示时观感会差一些。我额外插入了一些经典电影数据作为首屏展示,比如《肖申克的救赎》《霸王别姬》《星际穿越》等。
第三步:启动后端。把后端项目导入IDEA,在application.yml里改数据库连接串:
spring: datasource: url: jdbc:mysql://localhost:3306/movie_review?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4 username: root password: 你自己的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8useSSL=false很重要,本地MySQL如果没配SSL证书,这个参数不关掉会一直报警告日志。characterEncoding=utf8mb4保证数据库字段中文显示正常。jackson的时间格式化保证后端返回的日期是yyyy-MM-dd HH:mm:ss,不然前端拿到的时间对象会是一串时间戳数字。
第四步:启动前端。在movie-web目录下执行npm run dev。这时打开浏览器访问http://localhost:3000,看到的应该就是带测试数据的电影列表页。你先登录管理员账号,把电影管理、评论管理的CRUD都操作一遍,确认接口正常。有一个典型报错是:前端请求/api路径时404。别急,检查Vite代理是否生效,以及后端是否真的监听在8080端口。如果后端用的是9090,记得同步改代理目标端口。
第五步:打包部署。前后端分离项目部署很简单。前端执行npm run build,会生成dist目录。后端执行mvn clean package,会生成movie-review-0.0.1-SNAPSHOT.jar。把jar包和dist目录放到同一台Linux服务器上,用Nginx托管前端静态文件,并反向代理后端API。Nginx配置片段如下:
server { listen 80; server_name your-domain.com; location / { root /path/to/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意两个关键点。第一,location /里必须有try_files $uri $uri/ /index.html;,否则前端路由刷新时是404。因为Vue是SPA单页应用,路由切换全靠前端history API,实际服务器上并没有对应路径的物理文件,用户刷新页面时Nginx找不到文件就要回退到index.html。第二,proxy_pass http://localhost:8080/;结尾的斜杠有讲究,它会去掉原本请求路径里的/api前缀再转发,让后端Controller不用额外处理前缀。
部署时还有一个容易忽略的坑:后端打包前记得检查application.yml里的数据库地址。如果你本地开发写的是localhost:3306,部署到服务器后不改成服务器的数据库地址,线上环境启动就会连接失败。当年我帮人排查线上Bug时,发现他把本地配置文件的账号密码原封不动地带上线了,这个安全意识问题在现实中很常见。
7. 文档撰写与答辩准备的加分细节:把系统讲出亮点
很多同学以为项目做完就万事大吉,实际上毕业设计的文档和答辩才是决定你成绩好坏的重头戏。同样的系统,不同的人讲出来效果天差地别。我这里分享几个让评委觉得你“有思考、有深度”的包装手法。
文档目录结构建议按这个框架来写:
- 绪论:选题背景、国内外研究现状、研究意义、论文组织结构;
- 关键技术介绍:SpringBoot、Vue3、MyBatis-Plus、MySQL,以及为什么选它们;
- 系统分析:可行性分析(技术可行、经济可行、操作可行)、功能需求分析(画用例图)、非功能需求分析(性能、安全、可用性);
- 系统设计:系统架构设计、功能模块设计、数据库设计(E-R图、表结构说明);
- 系统实现:按模块展示核心代码并解释逻辑;
- 系统测试:测试环境、测试用例表格、测试结果分析;
- 总结与展望:总结完成的工作,指出不足和后续改进方向。
数据库设计章节里必须画E-R图。可视化地展示用户、电影、评论、分类之间的实体关系,图片一放,评委就直观知道你搞清楚数据模型了。工具可以用PowerDesigner、Navicat的逆向工程,也可以用Draw.io徒手画。
在系统实现章节,每个功能模块至少配一两张“运行效果截图”+关键代码片段。截图是证明你系统能跑的最有力证据。关键代码不需要大段全贴,挑最有代表性的片段贴上去,比如核心Controller的接口方法、自定义的查询逻辑、权限校验代码。贴代码的格式要用“等宽字体”排版,和正文区分开。
答辩时常见的问题我帮你梳理下,并根据评分标准来答:
- “你这个系统的架构是什么样的?”答:前后端分离。前端Vue3构建SPA,通过axios请求后端RESTful API。后端SpringBoot分为Controller、Service、Mapper三层,各层职责分明。数据库用MySQL8.0,通过MyBatis-Plus完成ORM映射。
- “为什么用MyBatis-Plus而不用原生MyBatis?”答:MyBatis-Plus在MyBatis基础上增强了通用Mapper和条件构造器,单表CRUD可以直接继承接口完成,不需要手写XML,开发效率更高。遇到复杂多表查询时,仍可退回到XML方式编写定制SQL。灵活性没有降低。
- “评论功能如何保证数据一致性?”答:在Service层方法添加
@Transactional注解,任何一个环节失败都会让整个创建评论并更新评分的过程回滚。另外对评分做了冗余存储设计,避免每次查询电影列表都做全表聚合计算,用空间换时间。 - “如果并发量大了怎么办?”答:当前是单体架构,毕设侧重功能实现。若并发量上来,可以引入Redis缓存热点电影数据,用消息队列削峰,数据库从单机改为主从读写分离,再逐步演进到微服务架构。评委要的就是你有这套思路,而不是真的去重构系统。
还有一个很多同学容易忽略的细节:项目README。你的源码里最好附一份能跑的README,写清楚环境要求、启动步骤、数据库导入方式、测试账号。这不仅方便导师验收,也是职场素养的体现。README里能放张彩色截图就放。当年我带了几个实习生,入职时交上来的代码连怎么启动都没写,后来我花了半个小时才把环境跑起来。你想想看,如果导师验收时也因为环境问题跑不起来,要多尴尬。
最后再分享一个启动系统时的演示技巧:先用管理员账号登录后台,演示电影新增操作——录一段新电影填表单、提交、列表刷新;然后登出,用普通用户账号在详情页刷一条评论,打上评分,回列表看到平均分更新。这一套动作行云流水,15分钟就能把你的系统核心亮点全部展示完。然后再切到数据库看一眼movie表里average_score的变化,直观证明前后端数据链路是通的。
我实际跑下来,这套SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0的组合,从零搭到功能完成,对一个有一定Java基础的学生来说大概需要两到三周。如果你卡在某一步,多按我上面说的排查思路走,绝大多数问题都是配置或版本造成的,不要死磕代码。也欢迎在评论区交流跑项目过程中遇到的问题,我能解答的都会尽量解答。