做全栈项目最怕什么?不是技术选型纠结,也不是功能设计头疼,而是代码写完跑不起来,或者跑起来一堆环境问题,最后时间全耗在调试上。所以当我把这套电影评论网站打磨到“拿下来就能跑”的状态时,第一个念头就是:得把整个过程的思路、拆解和踩过的坑整理出来,让后面接手的人少走弯路。
这个项目用的是 SpringBoot + Vue + MySQL 的经典前后端分离组合,覆盖了一个信息管理系统的完整闭环:前台用户浏览电影、搜索、看详情、发评论、打分,后台管理员维护电影数据、管理评论、处理用户状态。前后端通过 RESTful API 通信。如果你正在做毕设、想练手前后端分离项目,或者打算把一套可运行的代码作为脚手架二次开发,这篇文章都能帮你省不少时间。
我先把整个项目的脉络、设计思路、关键代码结构、数据库表和运行步骤逐一拆开讲,最后再把最容易炸的几个问题列出来,每条都是实打实踩过的坑,照着走比看文档高效得多。
1. 项目整体设计与功能拆解
1.1 这个项目解决了什么问题
电影评论网站这类系统,看起来简单,但小麻雀五脏俱全。它涉及典型的用户体系、内容管理、互动行为和后台审核,几乎把 Web 系统常见的 CRUD 和权限控制全过了一遍。从业务层面看,它至少包含四类需求:
- 用户侧:注册、登录、浏览电影列表、按分类或关键词检索、查看电影详情、发表评论、打分。
- 内容侧:电影信息的增删改查,包括封面、导演、演员、简介、上映年份、类型标签。
- 互动侧:评论列表的展示与排序、用户评分的聚合计算(比如平均分)、评论的敏感词过滤或后台审核。
- 管理侧:管理员登录后台,对电影和评论进行管理,对用户进行封禁或启用。
用这套功能矩阵换到企业的真实场景,其实就是内容管理系统加上一层用户互动。理解了这一点,后面做表结构设计和接口规划就有了主线。
1.2 为什么选 SpringBoot + Vue + MySQL 这套组合
这个选型不是拍脑袋定的,而是基于几个硬性考量:
第一,SpringBoot 把配置做了大量简化,内嵌 Tomcat,一个 main 方法就能起服务。相比传统 SSM 要整一堆 XML 配置,开发效率明显高,排查问题也容易定位。这一点在快速产出可用代码的场景里非常重要。
第二,Vue 的双向绑定和组件化开发,让前端页面结构清晰,数据和视图自动同步,不需要手动操作 DOM。对做管理后台和 C 端页面混合的项目来说,组件复用能省掉大量重复代码。
第三,MySQL 作为最常用的关系型数据库,事务支持稳定,生态成熟,配套的 Navicat、Workbench 等工具齐全,基本人手都会用。对于评论这种带有明显关联关系的数据,用关系型数据库管理比 NoSQL 更清晰。
1.3 前后端分离的架构边界
整个项目按端口区分服务:
- 后端 SpringBoot 默认跑在 8080,只负责提供 API 接口和静态资源访问时的跨域配置。
- 前端 Vue 开发服务器跑在 5173(Vite 默认),通过 axios 请求后端地址。
- 生产部署时,可以前端打包后交给 Nginx,或者直接把 dist 静态文件放到 SpringBoot 的 resources/static 下,二选一即可。
前后端分离最大的好处是职责清晰,后端专心处理数据,前端专心处理交互。但代价是联调阶段必须处理好跨域和接口格式约定,这一点后面专门讲。
2. 后端核心实现:SpringBoot 从接口到服务的完整链条
2.1 后端工程结构与分层思想
整个后端工程按照常见的 Controller → Service → Mapper 三层来组织:
- Controller 层:接收 HTTP 请求,负责参数校验和结果封装。
- Service 层:写业务逻辑,比如评论发布时校验用户状态、计算电影平均分。
- Mapper 层:操作数据库,用的是 MyBatis-Plus,单表 CRUD 大部分无需手写 SQL。
Java 代码的包结构大致是这样的:
com.example.movie ├── controller │ ├── UserController.java │ ├── MovieController.java │ ├── CommentController.java │ └── AdminController.java ├── service │ ├── UserService.java │ ├── MovieService.java │ └── CommentService.java ├── mapper │ ├── UserMapper.java │ ├── MovieMapper.java │ └── CommentMapper.java ├── entity │ ├── User.java │ ├── Movie.java │ └── Comment.java ├── config │ └── CorsConfig.java └── common ├── Result.java └── JwtUtil.java2.2 统一返回结构与全局异常处理
前后端联调最怕接口返回格式不统一。有的接口返回{code:0, data:{}},有的返回{success:true},前端就要写一堆判断,很恶心。所以项目里第一个要统一的就是返回体。
我的做法是定义一个通用Result类,结构固定为三个字段:code(200成功,500失败)、message(提示信息)、data(业务数据)。所有接口都返回这个对象,前端 axios 封装里只需要判断 code 是否为 200,就能决定走成功逻辑还是错误提示。
对应的,项目里还加了全局异常处理器,用@RestControllerAdvice捕获业务异常和系统异常,统一包装成 Result 返回。这样做的好处是,即使代码里某个环节出错了,前端拿到的依然是一个结构完整的 JSON,不会出现裸的 500 错误页。
2.3 JWT 登录认证的实现细节
用户登录用的是 JWT(JSON Web Token)方案,流程如下:
- 用户提交用户名和密码,后端校验密码是否正确。
- 校验通过后,用 JwtUtil 生成一个 token,里面包含用户 id、用户名和角色。
- 将 token 返回给前端,前端存在 localStorage 里。
- 后续请求中,前端在请求头里带上
Authorization: Bearer <token>。 - 后端通过拦截器解析 token,判断用户是否登录、是否为管理员。
这里有两个容易踩的细节。第一,密码绝对不能明文存储,项目中使用 BCrypt 对密码进行哈希加密。注册时把加密后的密文入库,登录时用 matches 方法比对。第二,token 要设置过期时间,常见做法是 24 小时或 7 天,过期后强制重新登录,避免 token 泄露后长期有效。
2.4 核心接口的业务逻辑解析
举两个典型接口来说明设计思路。
电影列表分页查询:前端传 current(页码)和 size(每页条数),后端用 MyBatis-Plus 的 Page 对象接收,查询条件支持电影名模糊匹配、分类筛选、按评分排序。这里有个细节,分页查询返回的数据里要包含 total 总条数,前端才能计算出总页数,并渲染分页组件。
发布评论:前端提交内容时,后端先校验用户是否登录,再校验评论内容长度(比如限制 5 到 500 字),然后插入评论记录,最后更新电影的评论数和平均分。平均分的计算不能只靠一条 SQL,我是在插入评分后,重新查询该电影所有评分的平均值再更新到 movie 表,避免多次评论产生脏数据。
这套逻辑本质上就是在业务层做了事务联动,理解了评论和电影表之间的更新关系,就理解了系统里最核心的数据一致性处理。
3. 前端实现:Vue 组件化页面与接口对接
3.1 前端工程结构与路由设计
前端用 Vite 创建 Vue 3 项目,工程结构按页面和组件拆分:
src ├── api │ ├── movie.js │ ├── user.js │ └── comment.js ├── router │ └── index.js ├── store │ └── user.js ├── views │ ├── Home.vue │ ├── MovieList.vue │ ├── MovieDetail.vue │ ├── Login.vue │ ├── Register.vue │ └── admin │ ├── AdminDashboard.vue │ ├── AdminMovie.vue │ └── AdminComment.vue └── components ├── MovieCard.vue ├── CommentList.vue └── Pagination.vue路由设计上,前端主要分两块:C 端页面和管理后台。C 端用户可以访问首页、电影列表、详情和登录注册;管理后台需要路由守卫,未登录或者不是管理员角色的一律重定向到登录页。这个路由守卫是前端权限控制的第一道门,真正的数据校验还是要在后端接口里做,前端守卫只是提升体验。
3.2 axios 封装与请求拦截
前端所有接口请求都走 axios,但绝不建议在每个组件里直接axios.get(...)。我在 api 目录下统一封装了一个 request 模块,做了三件事:
- 设置基础路径
baseURL指向http://localhost:8080/api。 - 在请求拦截器里从 localStorage 取出 token,添加到请求头。
- 在响应拦截器里统一处理业务错误,比如 code 不为 200 时弹出错误提示,401 时自动跳转登录页。
这样封装后,页面里只需要调用movieApi.getList(params)这样语义化的方法,代码清爽很多。接口变动时也只需改一个文件。
3.3 电影列表页和详情页的实现思路
电影列表页是 C 端用户第一个看到的页面,我把它做成一个异步加载数据、展示卡片式列表的典型场景:
- 页面加载时调用
movieApi.getList({current:1, size:12})获取第一页数据。 - 渲染 MovieCard 组件展示封面、名称、类型、评分。
- 点击分页组件时,更新请求参数并重新拉数据。
- 搜索框输入关键词后,触发带条件的分页查询。
电影详情页更复杂一点,它需要同时加载电影详情、评论列表和用户评分状态。我用了两个并行请求配合Promise.all来拉取,减少白屏等待时间。评论区则拆成了 CommentList 子组件,把评论列表的渲染和提交逻辑都收敛在里面,父组件只负责传电影 id。
3.4 状态管理与前端缓存策略
项目里用到了 Vuex(也可以换成 Pinia,都行)来管理用户登录态。登录成功后把用户信息存一份到 store,再持久化到 localStorage。页面里需要展示用户名、头像或判断管理员权限时,直接从 store 读取,避免每个组件都去 localStorage 里翻。
还有一个容易被忽略的缓存点:电影详情页在用户切换电影时,如果每次都重新请求,体验很差。我的做法是加一个简单的 keep-alive 缓存组件,对列表页进行缓存,返回时保留之前的滚动位置和筛选条件。这个小细节对 C 端用户体感提升非常明显。
4. 数据库设计与核心表关联
4.1 表结构设计思路
数据库取名为 movie_comment,核心表就三张:用户表、电影表、评论表。设计时遵循了一个原则:能用简单关联解决的,就不要引入多余字段。
用户表 t_user 核心字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键自增 | 用户编号 |
| username | varchar(50) 唯一 | 用户名 |
| password | varchar(100) | BCrypt 加密后的密码 |
| role | tinyint | 0 普通用户,1 管理员 |
| status | tinyint | 0 正常,1 禁用 |
| create_time | datetime | 注册时间 |
电影表 t_movie 核心字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键 | 电影编号 |
| title | varchar(200) | 电影名 |
| cover_url | varchar(500) | 封面图地址 |
| director | varchar(100) | 导演 |
| actors | varchar(500) | 主演 |
| genre | varchar(100) | 类型,多个用逗号分隔 |
| release_year | int | 上映年份 |
| description | text | 剧情简介 |
| avg_rating | decimal(3,1) | 平均评分 |
| comment_count | int | 评论总数 |
评论表 t_comment 核心字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键 | 评论编号 |
| movie_id | bigint 索引 | 所属电影 |
| user_id | bigint 索引 | 评论用户 |
| content | varchar(500) | 评论内容 |
| rating | int | 评分,1到10 |
| status | tinyint | 0 待审核,1 通过,2 删除 |
| create_time | datetime | 评论时间 |
4.2 索引设计与查询优化
评论表是典型的读多写少且按电影维度查询的表,所以针对 movie_id 建立了普通索引,针对 create_time 也建议建索引。这样在做“查某部电影的所有评论并按时间倒序”时,走索引效率明显优于全表扫描。
电影表的 title 字段可以考虑前缀索引,但就本项目数据量来说,模糊查询用 LIKE 完全够用,不需要引入搜索引擎。真正需要注意的反而是关联查询时的 N+1 问题:不要在循环里逐条查评论用户信息,而是先查出评论列表,再用 user_id 批量查用户,最后在内存里组装。
4.3 数据初始化的顺序与细节
项目自带的 SQL 初始化脚本里,建议按这样的顺序执行:
- 创建数据库并指定字符集为 utf8mb4,因为要存用户评论里的表情符号,utf8 不够用。
- 创建三张表结构。
- 插入管理员账号(比如 admin/admin123,密码是 BCrypt 加密后的字符串)。
- 插入若干测试电影数据。
- 插入若干测试评论数据。
这里有个很实用的经验:测试数据的封面图地址不要用本地路径,直接用一些公开可访问的图片占位地址,否则本地跑起来时页面上一片空白,还以为是代码出了问题。
5. 项目运行与本地部署:从环境到页面
5.1 环境准备清单
要跑起来这套项目,本地环境必须满足以下条件:
- JDK 1.8 或 11(推荐 8,稳定性最好,SpringBoot 2.x 完全兼容)
- Maven 3.6 以上
- Node.js 16 以上,npm 或 yarn 均可
- MySQL 5.7 或 8.0
- IDEA 或 Eclipse(强烈推荐 IDEA,社区版就够用)
- Navicat 或 MySQL Workbench(用于执行 SQL 脚本)
这里提醒一句:SpringBoot 版本别追太高,项目用的是 SpringBoot 2.7.x,比较稳定。如果换成 SpringBoot 3.x,JDK 版本必须升到 17,Java 代码里很多包名也变了,没那么好迁移。
5.2 后端启动步骤
- 用 IDEA 打开后端工程,等待 Maven 自动下载依赖。
- 修改 application.yml 里的数据库连接信息,改成自己本地的地址、端口、用户名、密码。
- 用 Navicat 新建数据库,执行项目里的 init.sql 脚本。
- 启动 Application 主类,看到控制台打印出 Tomcat started on port(s): 8080 就表示成功。
我遇到过一上来就报数据库连接失败的情况,十有八九是 MySQL 密码没改对,或者是 MySQL 8.0 的驱动类型要加时区参数。在数据库连接 URL 上加上?serverTimezone=Asia/Shanghai&useSSL=false,可以省掉一堆不必要的报错。
5.3 前端启动步骤
- 用 IDEA 或 VS Code 打开前端工程。
- 在终端执行
npm install安装依赖,这一步取决于网络状况,可能会比较慢。 - 修改 src/api/request.js 里的 baseURL,确保指向后端地址。
- 执行
npm run dev,看到 Vite 启动信息后,浏览器访问http://localhost:5173。
前端启动最容易出问题的就是 npm install,版本冲突或依赖缺失都会导致启动失败。建议直接用 npm 官网源安装,别开镜像代理,反而更稳。
5.4 前后端联调时的跨域处理
本地开发时前端在 5173,后端在 8080,属于跨域请求。解决方式有两种,我在项目里用的是后端配置 CORS:
简单在 config 包下新建 CorsConfig,放行所有来源和所有请求头,开发阶段完全够用。生产环境再收紧为指定域名即可。
如果在浏览器控制台看到 CORS policy 报错,先检查后端这个配置类有没有生效,再检查是不是请求路径写错了导致走到了非 Controller 的路径上。前端用 vite 的 proxy 也能解决跨域,但后端的通用 Cors 配置可以免去前端每个环境改 proxy 的麻烦,我实际用下来体验更好。
6. 常见问题与排查技巧实录
6.1 数据库连接报错:Access denied or SSL
MySQL 8.0 默认开启 SSL 加密,本地测试时建议在连接串里加上useSSL=false&allowPublicKeyRetrieval=true。allowPublicKeyRetrieval 这个参数我一开始没加,结果连 MySQL 8.0 时报错Public Key Retrieval is not allowed,很容易被坑。
还有一种情况是时区问题,MySQL 8.0 的默认时区跟驱动对不上,控制台会反复出现异常提示。处理方式就是在连接串里明确指定serverTimezone=Asia/Shanghai。
6.2 Long 类型 ID 传到前端精度丢失
MySQL 主键是 bigint,Java 对应类型是 Long,但 JavaScript 的数字类型最大安全整数是 2 的 53 次方减 1,超过这个范围精度就会丢失。数据量小的时候看不出来,数据量一旦上去,前端拿到的 id 和实际 id 对不上,点击详情时就会 404。
解决办法是在后端实体类的 id 字段上加上@JsonSerialize(using = ToStringSerializer.class),把 Long 转成字符串传给前端。或者干脆把所有主键设计成字符串类型,但改造量太大,不建议。
6.3 前端登录后刷新页面,路由跳回登录页
这个问题的根源是 Vuex 里的用户状态存在内存中,刷新后内存数据清空,路由守卫判断未登录就强制跳转。解决办法是在应用初始化时,从 localStorage 里读取用户信息并重新提交到 Vuex store,保证刷新后登录态还在。
如果连 localStorage 里也没有,那就说明登录成功后没有做持久化存储,检查登录接口的处理逻辑,确认是否执行了localStorage.setItem('userInfo', JSON.stringify(res.data))。
6.4 评论提交后,电影评分不更新或更新错误
这个问题的根源多半是事务边界没控制好。发布评论的操作涉及三步:插入评论、更新电影评论数、更新平均分。如果这三步没有放在同一个事务里,中途出现异常就可能导致评论插入成功,但评分没更新。
解决办法是在 Service 方法上加上@Transactional注解,保证这三步要么全部成功,要么全部回滚。这是我调试时排查了半小时才发现的问题,非常典型。
6.5 前端页面空白或接口报 404
页面空白先看浏览器控制台,确认是路由的问题还是组件报错。路由 404 最常见的原因是自己没配 catchAll 通配路由,Vue Router 在历史模式下刷新某个子路由会 404,这个情况开发模式下很少遇到,生产环境下部署到 Nginx 时就要配 try_files 规则。
接口 404 则检查后端 Controller 的 @RequestMapping 路径和前端 api 文件里的路径是否一致,尤其注意不要多写或少写/api前缀。
7. 一些实用的扩展思路
这个项目做完之后再回看,能往上加的功能还有很多,而且都是比较实际的需求:
- 电影封面上传:引入 MinIO 做对象存储,替代目前的图片外链,这样数据完全自治。
- 管理员操作日志:记录谁在什么时间删除、修改了什么数据,审计必备。
- 评论点赞和举报:给评论表加一个 like_count 字段,配合举报状态实现更完整的互动体系。
- 首页推荐逻辑:根据用户历史评分,用简单的协同过滤思路推荐电影,哪怕算法粗糙一点,效果也很明显。
- 部署上线:后端打包成 jar,前端打包后交给 Nginx 托管,配好 HTTPS 证书,就是一整套可对外服务的系统。
就我个人做了一整套下来最深的感触是:这种前后端分离的管理系统,难点不在于某个技术有多深,而在于所有环节能不能顺畅地咬合在一起。数据库字段设计时就要想到前端怎么展示,后端返回结构就要想到 axios 怎么统一处理,接口命名时就要想到前端 api 文件怎么归类。把这些衔接的细节处理好,项目跑起来才真正顺滑,二次开发时也不会骂前任留下的代码。
如果你正在做类似的全栈项目,我的建议是先别急着写代码,把表结构和接口清单先整理出来,让前后端的数据流在纸面上走通一遍,后面能节省你至少三分之一的时间。这套电影评论网站的源码可以直接运行,把这个框架吃透,换一个业务场景改成图书、视频或者商品评论系统,也就是一周之内的事。