ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue3+MyBatis电影评论网站全栈开发实战指南

SpringBoot+Vue3+MyBatis电影评论网站全栈开发实战指南 说实话看到Java SpringBootVue3MyBatis 电影评论网站系统源码这个标题我就知道十有八九是冲着毕业设计或简历项目来的。这个组合现在几乎成了全栈入门项目的标配——后端用SpringBoot撑起业务逻辑前端用Vue3做交互界面ORM层交给MyBatis操作MySQL最后配一个前后端分离的架构面子上过得去里子上也基本把主流技术栈的常见问题都踩了一遍。我去年帮人从头到尾捋过一套类似的项目从数据库建表到功能实现再一直到部署上线前前后后改了六七版踩了不少坑。这篇文章就按实际动手的顺序把这个电影评论网站系统从设计到落地的全过程拆开揉碎讲清楚。适合正在做毕设、想写在简历上的Java后端开发者也适合刚学完SpringBoot和Vue3、想做个完整项目练手的朋友看完基本能照着把项目搭起来。1. 项目整体设计与技术选型思路1.1 前后端分离到底在分离什么先说架构。很多人以为前后端分离就是把代码分成两个文件夹前端一个、后端一个这就是天大的误会。前后端分离的核心是职责边界和交互协议的分离代码文件放哪只是表象。在这个电影评论系统里后端只负责两件事处理业务逻辑、提供JSON接口。前端也只负责两件事渲染页面、调用接口。整个系统通过HTTP接口通信前端不碰数据库后端不碰页面渲染。具体到项目目录后端是一个标准的SpringBoot工程包结构按controller、service、mapper、entity四个层级划分前端是一个Vue3工程按views、components、api、router四个目录组织。前后端通过统一的前缀约定比如后端所有接口都以/api开头配合跨域配置进行通信。之所以坚持前后端分离而不是用Thymeleaf那套服务端渲染原因有两个。第一前后端可以并行开发后端定义好接口文档后前端直接用Mock数据推进不用等后端写完才启动项目周期能压缩不少。第二部署时可以前端静态资源放Nginx、后端打jar包独立跑只要接口不跨域或者反向代理配好后期扩展和运维都灵活得多。1.2 技术栈选型的取舍逻辑这套技术栈单拆开看每个都不算新但组合在一起恰好覆盖了Java生态最常见的开发链路SpringBoot: 2.7.x版本是当前兼容性最稳的自带Tomcat、自动配置、starter机制把过去SpringMVC那种繁琐的XML配置全部干掉几分钟就能跑起一个Web服务。Vue3: 用Composition API配合script setup语法组件逻辑复用比Vue2的Options API舒服太多。电影列表、评论区、用户中心这些页面拆成组件后状态管理清晰数据响应式处理也顺手。MyBatis: 比JPA更贴近SQL本身尤其是电影评论这种需要多表联查、动态条件筛选按类型筛选、按评分排序、分页的业务场景手写SQL的可控性远高于自动生成的SQL。MySQL: 8.0以上版本在事务、性能、JSON支持上都够用而且也是大多数公司的生产环境数据库学这个不亏。这套组合最舒服的地方在于踩坑资料多。不管是MyBatis的#{}和${}区别、Vue3的响应式陷阱还是MySQL的连接配置问题网上随便一搜就有大量现成解决方案对于新手项目来说能快速找到答案比什么都重要。1.3 功能模块拆解与需求梳理一个能拿得出手的电影评论网站光有增删改查是不够的。我在设计功能清单时会刻意覆盖一些常见业务场景这样写起简历来也更有话可说。最终敲定的核心功能模块如下用户模块注册、登录、个人信息查看与修改。登录使用JWT实现无状态认证用户密码经过BCrypt加密存储不保存明文。电影模块电影列表展示、按类型/关键字搜索、电影详情页。列表分页支持按上映时间、评分高低排序。评论模块用户对电影发表评论、查看评论列表、删除自己发布的评论。评论支持分页加载。评分模块用户给电影打1到5分系统自动计算并更新电影的平均评分和评分人数。除此之外还有几个容易被忽略但实际必做的环节异常统一处理、参数合法性校验、跨域配置、数据库连接池配置。这些杂活看着不起眼但恰恰是项目能不能稳定跑起来的关键。2. 数据库设计与核心表结构搭建2.1 表结构设计三张表解决问题电影评论系统的数据模型不复杂三张核心表就能覆盖全部业务用户表、电影表、评论表。但真正的细节都在字段设计上这是新手最容易忽略的地方。用户表t_user的大致结构CREATE TABLE t_user ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(BCrypt加密), nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;电影表t_movie的字段要稍微多几个CREATE TABLE t_movie ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 主键ID, title varchar(100) NOT NULL COMMENT 电影标题, poster varchar(255) DEFAULT NULL COMMENT 海报URL, director varchar(50) DEFAULT NULL COMMENT 导演, actors varchar(255) DEFAULT NULL COMMENT 主演, genre varchar(50) DEFAULT NULL COMMENT 类型(动作/科幻/喜剧等), release_date date DEFAULT NULL COMMENT 上映日期, duration int(11) DEFAULT NULL COMMENT 片长(分钟), rating decimal(3,1) DEFAULT 0.0 COMMENT 平均评分, rating_count int(11) DEFAULT 0 COMMENT 评分人数, description text COMMENT 剧情简介, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT电影表;评论表t_comment是业务逻辑的核心先看定义CREATE TABLE t_comment ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 主键ID, movie_id int(11) NOT NULL COMMENT 电影ID, user_id int(11) NOT NULL COMMENT 用户ID, content text NOT NULL COMMENT 评论内容, score int(11) DEFAULT NULL COMMENT 评分(1-5分), like_count int(11) DEFAULT 0 COMMENT 点赞数, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间, PRIMARY KEY (id), KEY idx_movie_id (movie_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;在设计评论表时有个容易纠结的点把评分字段放在评论表里还是单独建一张评分表我的方案是把评分直接放在评论表里用户每发一条带评分的评论就算做一次打分。这样做的逻辑很简单一个用户对一部电影只会有一个有效评分想改评分就更新自己的评论不需要再单独维护评分关系。这样查询某个电影的评分直接对评论表的score字段做聚合就行一步到位。2.2 字符集与存储引擎的坑这个坑我踩过不止一次必须单独拿出来讲。MySQL建表时如果你不指定字符集会继承数据库级别的默认字符集。很多低版本MySQL默认的latin1不支持中文到时候插入数据直接报错或者乱码。所以建表语句里务必显式写上CHARSETutf8mb4。utf8mb4不是utf8它多支持了emoji和生僻字。电影简介里万一有特殊符号用utf8就会插入失败。我这个项目所有表统一用utf8mb4字段类型能选text就选text而不是varchar评论内容这种不限制长度的字段用text更合适能省掉很多不必要的commons。存储引擎选择InnoDB基本没有悬念。它支持事务、行级锁、外键约束和崩溃恢复对于需要保证用户评论不丢失的场景这是唯一靠谱的选择。2.3 初始化数据从哪里来做毕设或演示项目没有电影数据就是空壳子。我有两个建议渠道第一从豆瓣或其他电影网站抓取一批经典电影数据整理成SQL脚本直接导入。注意别一次性导入太多50到100部足够撑起页面展示效果。数据字段至少要覆盖标题、海报、导演、主演、类型、上映日期、简介不然电影详情页会很寒酸。第二如果不想手动整理写一个简单的DataInitializer在SpringBoot启动时自动向数据库植入种子数据。这种方式的好处是项目拉下来一跑就有数据不用手动执行SQL脚本。我实际项目中两种方式都用了开发环境跑种子数据生产环境用固定SQL脚本。种子数据类在项目里要加Profile(dev)限制防止每次启动都重复插入。3. 后端核心模块实现与接口设计3.1 SpringBoot分层结构与启动类后端工程我习惯按这种包结构组织com.example.movie ├── MovieApplication.java # 启动类 ├── common # 通用返回结果、异常处理 │ ├── Result.java │ └── GlobalExceptionHandler.java ├── config # 跨域、拦截器、JWT配置 │ ├── CorsConfig.java │ └── JwtInterceptor.java ├── controller # 接口层 │ ├── UserController.java │ ├── MovieController.java │ └── CommentController.java ├── entity # 数据库实体 │ ├── User.java │ ├── Movie.java │ └── Comment.java ├── mapper # MyBatis数据访问层 │ ├── UserMapper.java │ ├── MovieMapper.java │ └── CommentMapper.java ├── service # 业务逻辑层 │ ├── UserService.java │ ├── MovieService.java │ └── CommentService.java └── vo # 前端展示对象 └── CommentVO.javaMovieApplication.java就是标准的三件套SpringBootApplication注解、main方法、SpringApplication.run()。很多教程会在启动类上扫描MapperScan我习惯把MapperScan(com.example.movie.mapper)直接写在启动类上比在每个Mapper接口上加Mapper注解省事也更整洁。一个好的实践是Controller只做参数接收和结果返回把业务逻辑全部下沉到Service层。这样Controller代码行数会缩到很短真正的逻辑都在Service里方便测试和复用。3.2 MyBatis实战配置与SQL映射集成MyBatis最省力的方式是引入mybatis-spring-boot-starter版本一定要选对。SpringBoot 2.7.x对应2.3.x版本的starter版本跨代太大会出现各种奇怪的兼容性问题。在application.yml里MyBatis相关的核心配置有四项mybatis: # mapper XML文件位置 mapper-locations: classpath:mapper/*.xml # 实体类别名包路径省去每次写全限定类名 type-aliases-package: com.example.movie.entity configuration: # 下划线转驼峰数据库字段create_time自动映射为createTime map-underscore-to-camel-case: true # 控制台打印SQL日志开发排错神器 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl其中map-underscore-to-camel-case务必设为true否则你的实体类要么字段名全改成下划线风格丑和别扭要么在XML里每个字段写resultMap映射累赘。开启后数据库的create_time自动映射到实体的createTime字段省了一大半工作量。再说SQL映射文件。评论列表是这项目最复杂的查询因为要联表拿用户名和用户头像。对应的Mapper接口方法定义是public interface CommentMapper { ListCommentVO selectCommentsByMovieId(Param(movieId) Integer movieId, Param(offset) Integer offset, Param(limit) Integer limit); int insertComment(Comment comment); int deleteComment(Param(id) Integer id, Param(userId) Integer userId); }CommentVO对象里可以直接多定义两个字段username、userAvatar它们不在评论表但SQL能查出来。XML映射select idselectCommentsByMovieId resultTypecom.example.movie.vo.CommentVO SELECT c.id, c.content, c.score, c.create_time, c.like_count, u.username, u.avatar AS userAvatar FROM t_comment c LEFT JOIN t_user u ON c.user_id u.id WHERE c.movie_id #{movieId} ORDER BY c.create_time DESC LIMIT #{offset}, #{limit} /select这个SQL有几个细节值得说。第一LEFT JOIN比INNER JOIN稳妥因为理论上评论的用户一定存在但数据库FK如果没建可能出现孤儿数据LEFT JOIN至少能保证评论不丢。第二用#{offset}, #{limit}做物理分页虽然数据量大时性能不算最优但这个项目是学习用足够实用和直观。特别注意MyBatis里#{}和${}完全是两回事。#{}走的是预编译会生成?占位符能防SQL注入${}是纯字符串拼接能实现动态表名、动态排序字段但有注入风险。除非万不得已一律用#{}。3.3 JWT登录认证与拦截器设计登录认证是全栈项目避不开的模块。这个项目选JWT而不是Session核心原因是前后端分离下后端服务可能是多实例部署的Session复制方案太复杂。JWT把用户信息加密放在token本身服务端无状态后端拿着密钥验签就行。依赖方面我用jjwt库版本选0.9.1这是目前资料最多、用法最稳定的版本。生成token的核心逻辑public String generateToken(User user) { Date now new Date(); Date expireDate new Date(now.getTime() 24 * 60 * 60 * 1000); // 24小时过期 return Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(nickname, user.getNickname()) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }注意secretKey不能太短至少要32个字符不然HS256会报弱密钥异常。这是jjwt 0.9.1版本的硬性规定不少人都栽在这。前端拿到token后一般存localStorage然后每次请求在Authorization头带上。后端拦截器从请求头取token并解析解析失败则返回401。拦截器注册时要配置放行路径注册、登录、电影列表查询这些接口不需要携带token就能访问要是拦错了前端会一直报401死循环。顺带的密码安全问题用户注册时密码用BCryptPasswordEncoder加密后存库登录时用matches方法比对。这是SpringSecurity自带的实用工具类不用引入整个SpringSecurity就能用。绝不允许存明文密码这种项目将来即使只是作为简历展示也要体现安全意识。3.4 统一返回格式与全局异常处理很多新手项目接口返回格式五花八门有的返回String有的直接裸返回Map前端对接时全靠猜。我强烈建议统一用一个ResultT包装所有接口返回。public class ResultT { private Integer code; // 200成功 400业务错误 401未登录 500系统异常 private String message; private T data; // 静态方法success(data)、error(message)... }配合一个GlobalExceptionHandler让所有业务的RuntimeException自动转换成Result.error()返回。这样Controller里的逻辑就干净了不用到处写try-catch。这个模块看起来简单但能让代码整洁度上一个档次也让前端拿到所有响应时都能用一个统一的解析逻辑处理。4. 前端Vue3页面实现与数据交互4.1 Vue3工程搭建与目录规划前端工程用Vite搭建命令就三行npm create vitelatest movie-web -- --template vue cd movie-web npm install前端目录规划我清理掉脚手架自带的杂乱结构后最终是这样的movie-web ├── src │ ├── api # 接口请求封装 │ │ ├── request.js │ │ ├── auth.js │ │ ├── movie.js │ │ └── comment.js │ ├── assets │ ├── components # 公共组件 │ │ ├── MovieCard.vue │ │ └── CommentList.vue │ ├── router # 路由配置 │ │ └── index.js │ ├── stores # Pinia状态管理 │ │ └── userStore.js │ ├── views # 页面组件 │ │ ├── Home.vue │ │ ├── MovieDetail.vue │ │ ├── Login.vue │ │ └── Profile.vue │ ├── App.vue │ └── main.js4.2 Axios封装与认证拦截Axios请求封装是前端能顺畅开发的基础。我的做法是创建一个request.js统一配置baseURL并加请求拦截器和响应拦截器。import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, 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) { return res.data } else if (res.code 401) { // 登录过期跳回登录页 localStorage.removeItem(token) window.location.href /login return Promise.reject(new Error(未登录)) } else { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request这里有个值得注意的细节baseURL设为/api如果前端是Vite开发环境需要在vite.config.js里配置代理把请求转发到后端的http://localhost:8080。如果前端最终部署到Nginx也要用location /api反向代理到后端服务。这个跨域问题不解决前端会一直报CORS错误。4.3 电影详情页和评论提交交互电影详情页是整个系统交互最丰富的页面包含电影信息展示、平均评分展示、历史评论列表、当前用户评分评论框四个核心区块。评论提交的表单我用的Element Plus组件评分组件el-rate绑定评论分数评论内容用el-input多行输入。提交前做两层校验前端Button的disabled属性控制用户没登录或分数为空时不能提交后端Controller再用NotBlank等注解兜底校验。双保险在前后端分离项目里几乎是必须的——前端校验只为了用户体验后端校验才真正保护数据安全。评论列表用CommentList.vue组件抽出来接收movieId属性负责分页加载评论数据。这里我用了Vue3的Composition API的ref和onMounted来管理加载逻辑。对于第一次写这种交互的人来说可能会遇到一个Vue3特有的坑reactive对象深拷贝时响应式丢失。用ref包数组基本能绕开这个坑简单数据一律ref没毛病。4.4 路由守卫与用户状态管理前端路由分为公开页面和需要登录才能访问的页面。我的处理方式是router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })/movie/:id详情页是所有用户都能访问的/profile个人中心需要登录。requiresAuth直接写在路由meta里即可清晰直观。用户状态管理我用Pinia计数器式的store就够维护一个userInfo对象初始从localStorage恢复登录成功时更新并持久化。Vue3的官方推荐状态库就是Pinia相比Vuex写起来少了一半样板代码。4.5 后端单元测试怎么让接口验证更顺手接口开发完验证逻辑对不对。我通常会做两种验证第一用浏览器的Network面板手动调接口确认返回。这在开发阶段最常用配合后端控制台打印SQL能快速定位是SQL问题还是参数问题。第二为Service层写单元测试用spring-boot-starter-test加上H2内存数据库跑用例。测试覆盖评论发布、评分更新这类核心业务。这个动作虽然会多花点时间但能在简历上大方写项目包含单元测试这在同龄候选人里算加分项。5. 完整部署与上线操作流程5.1 从零到一的全流程演示下面是最省心的操作顺序建议按这个步骤一步步来在MySQL里执行建库建表脚本确认表结构和字段正确。修改后端application.yml的数据库账号密码确认能连上库。启动后端MovieApplication观察控制台日志没有报错。用curl http://localhost:8080/api/movie/list?page1验证接口通不通。启动前端npm run dev浏览器打开http://localhost:5173。注册一个新用户登录后尝试发表评论。检查数据库t_comment表是否新增了记录评分是否更新到t_movie表。本地验证通过后构建部署包mvn clean package -DskipTests打包后端jarnpm run build打包前端静态文件。部署到服务器jar用java -jar启动前端静态文件传Nginx的html目录并配置反向代理。5.2 部署过程中必备的Nginx配置参考如果你的服务器上装了Nginx下面这个配置片段可以直接参考server { listen 80; server_name your-domain.com; root /var/www/movie-web/dist; index index.html; # 前端路由history模式回退 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }两个关键点try_files是前端history路由的标配刷新页面时不会404proxy_pass结尾的/务必保留它表示把/api前缀掰掉后再转发给后端。如果漏了/后端接口路径就会变成/api/list而不是/list直接404。5.3 Maven与前端构建的常见坑后端打包强烈建议加-DskipTests跳过测试阶段否则本地如果有不确定的测试用例会直接卡住构建。前端构建最大的坑是Node版本和依赖版本不匹配Vite 5要求Node 18以上老版本Node会直接报错。构建产物在dist目录里面是纯静态文件扔给任意Web服务器就能跑。6. 项目扩展与性能优化进阶建议6.1 部署一台Nginx的缓存配置参考基础版本跑通后有几个低成本高收益的优化思路很值得做。缓存优化电影列表和详情这类读多写少的数据后端可以用Spring Cache配合Redis做缓存设置合理的过期时间。请求到达时先查缓存缓存没有才走数据库。评论新增时主动淘汰对应电影的缓存保证数据一致性。索引优化评论表已经建了movie_id索引但如果用户量多大后经常按用户查他的评论记录user_id的索引也建议建上。对于大表的排序分页查询可以用覆盖索引减少回表。全文搜索当前的LIKE %关键字%写法在数据量大时必定慢MySQL的全文索引或引入Elasticsearch都可以解决。前者实现简单后者更专业。这个优化点写到简历的项目亮点是很扎实的一条。6.2 从单机到微服务不必过度设计诚实说这个体量的项目上微服务纯属炫技。业务边界只有一个聚合根服务拆了反而增加通信成本和运维复杂度。但如果是为了面试聊架构演进你可以把潜在拆分点列出来用户认证服务、电影信息服务、评论服务。说明清楚什么规模该拆、拆之后面临的分布式事务问题怎么处理这比真拆一个微服务工程能聊的东西更多。6.3 给毕业设计交作业的加分细节最后补充几个能让你在答辩或面试时多聊几句的细节在README里写清楚环境要求、启动步骤、接口文档和项目结构说明这是负责任的项目该有的样子。后端写几个关键接口的单元测试面试官问你这项目怎么保证质量的时候你就有具体内容可以讲。统一定义Result全局返回格式这比MapString, Object看起来专业很多。密码加密存储 统一鉴权 接口参数校验这三点能直接体现工程素养答辩或面试的时候有得聊。我个人的实际体会是这类项目做出来不难难的是每个细节都能说出为什么这么做以及换一种方案的差别是什么。照着本文把项目搭起来只是第一步真正有价值的是在调试过程中把SpringBoot的启动流程、MyBatis的代理机制、Vue3的响应式原理这些底层概念弄明白。这样面试官深挖技术栈的时候你才不会只停留在会用的层面。最后再分享一个小技巧所有代码写完后把项目丢到GitHub上并开启GitHub Actions自动构建每次push都能自动跑测试和打包。这一个小小的习惯会让你的项目完整度直接上一个台阶面试官看到你懂CI/CD兴致会明显不一样。
返回列表