ARTICLE DETAIL

资讯详情

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

SpringBoot2+Vue3电影评论网站系统实战:前后端分离毕设全解析

SpringBoot2+Vue3电影评论网站系统实战:前后端分离毕设全解析 最近后台一直有人私信问说自己在做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相比更早的SSHStruts2 Spring Hibernate或者SSMSpring 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.78.0的开窗函数、公共表表达式CTE在复杂统计查询里非常有用。比如你要查“评论数最多的前10部电影”8.0用一条ROW_NUMBER()开窗SQL就搞定了。当然这个项目用不用的上另说但你在文档里写一句“MySQL8.0支持窗口函数为后续扩展高难度统计需求留有余地”答辩老师就觉得你做过功课。数据库安装的时候有个老生常谈的坑时区设置。MySQL8.0默认时区跟中国差了8个小时你在JDBC连接串里必须加上serverTimezoneAsia/Shanghai不然操作时间字段时会出现各类奇怪问题。前端选择Vue3也很自然。Vue3的Composition API组合式API比Vue2的Options API更适合业务逻辑复用。假如你的电影卡片和评论区是两个组件它们都要调用某几个共同的接口方法组合式API里你可以把一个完整的“评论相关操作”抽成一个useComment()函数任何组件都能使用。Vue3配合Vite构建工具开发环境启动速度比Webpack快好几个量级。刚开始跑项目的时候Vite一旦启动几乎是秒开热更新也及时这体验对赶毕设的人来说太重要了。Element Plus这个UI组件库现在也是Vue3项目的事实标准。表格、表单、弹窗、分页、消息提示全都有现成组件。写后台管理端的页面时几乎不用自己造轮子一套表格组件加几个字段列配合“新增/编辑”弹窗表单就是一个标准的CRUD页面。这部分视觉上非常加分因为评委看系统演示时第一直观感受就是“界面是否整洁、交互是否流畅”。3. 数据库设计与后端分层架构从建表SQL到工程目录规划现在进入动手的核心环节。一个电影评论网站数据库设计不复杂但E-R关系一定要画得清楚。你答辩的时候大概率会被问到“你是怎么设计表的”和“为什么这么设计”。我建议你从三张核心表入手然后再扩展附属表。用户表userid主键自增、username用户名唯一索引、password加密后的密码建议用BCrypt、nickname昵称、avatar头像URL、role角色0/1、create_time。电影表movieid、title片名、poster海报URL、director导演、actors主演、description简介、category_id关联分类表、release_date上映日期、status1上架0下架、average_score平均分、rating_count参与评分人数、create_time。这里的average_score和rating_count就是我在第一部分说的冗余字段用空间换查询性能。评论表commentid、user_id评论人关联用户表、movie_id评论的电影关联电影表、rating本次评分比如1-10分、content评论内容、create_time、update_time。这条表是核心联表查询的关键几乎所有的业务接口都会和它打交道。另外可以加一张分类表categoryid、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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE category ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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比较麻烦我更推荐用JWTJSON Web Token。原理一句话概括用户登录成功后服务器生成一个包含用户ID和角色信息的签名字符串返回给前端。前端每次请求时把它放在HTTP请求头Authorization字段里后端写一个拦截器解析这个token就能识别当前用户是谁、有没有权限。JWT的好处是无状态——服务器不用保存会话记录每次请求自行校验签名。这在答辩时是个很加分的“考点”无状态扩展性好方便以后改成微服务架构。但要注意JWT也有缺点无法主动吊销。你可以在token里设置过期时间比如24小时同时在用户表里维护一个token_version或login_status字段来变相实现强制下线不过这属于进阶优化时间不够可以不写。答辩时能说清楚JWT的原理和缺陷就很棒了。登录接口的核心代码大概这样PostMapping(/login) public ResultLoginVO login(RequestBody LoginDTO dto) { // 1. 根据用户名查用户 User user userService.getOne( new LambdaQueryWrapperUser().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 ResultPageMovieVO page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId) { PageMovie page new Page(pageNum, pageSize); LambdaQueryWrapperMovie 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); PageMovie 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 LambdaQueryWrapperComment() .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的子类不加这个参数的话事务不会回滚就会出现“评论保存成功但平均分没更新”这种数据不一致的尴尬情况。这个知识点在很多公司的面试题里就是实打实的考点毕设答辩老师问到事务传播行为你能答上来这一点绝对加分。更新平均分的逻辑也不难直接用一条聚合SQLprivate void updateMovieScore(Long movieId) { // 查询该电影所有评论的平均分和数量 QueryWrapperComment wrapper new QueryWrapper(); wrapper.select(IFNULL(AVG(rating),0) AS avgScore, COUNT(*) AS cnt) .eq(movie_id, movieId); MapString, 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 vitelatest movie-web -- --template vue cd movie-web npm install npm install vue-router4 pinia axios element-plus npm run dev第一条命令会创建一个名为movie-web的ViteVue3项目。vue-router4是Vue3对应的路由版本注意不要安装成vue-router3那是给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 classmovie-card clickgoDetail el-image :srcprops.movie.poster fitcover / div classtitle{{ props.movie.title }}/div div classscore el-rate :model-valueprops.movie.averageScore disabled / span{{ props.movie.averageScore }}分/span /div /div /templatedefineProps是Vue3的编译宏不需要显式import。el-image是Element Plus的图片组件fitcover表示图片等比例缩放并裁剪填满容器。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或者11Maven用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?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 username: root password: 你自己的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8useSSLfalse很重要本地MySQL如果没配SSL证书这个参数不关掉会一直报警告日志。characterEncodingutf8mb4保证数据库字段中文显示正常。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的变化直观证明前后端数据链路是通的。我实际跑下来这套SpringBoot2Vue3MyBatis-PlusMySQL8.0的组合从零搭到功能完成对一个有一定Java基础的学生来说大概需要两到三周。如果你卡在某一步多按我上面说的排查思路走绝大多数问题都是配置或版本造成的不要死磕代码。也欢迎在评论区交流跑项目过程中遇到的问题我能解答的都会尽量解答。
返回列表