在实际 Java Web 项目中,一个完整的电影评论网站是检验全栈开发能力的经典场景。它要求开发者不仅要串联起 Java 后端、Vue 前端、Spring Boot 框架和 MySQL 数据库,还要处理用户认证、数据交互、前后端分离部署等一系列工程问题。很多同学在毕业设计或入门全栈时,虽然能分别找到 Java、Vue、Spring Boot、MySQL 的教程,但将它们有机整合成一个可运行、可演示、可答辩的项目时,常常在环境配置、接口联调、数据结构和部署环节遇到阻碍。本文将以一个电影评论网站为蓝本,带你从零开始,使用 Spring Boot 构建 RESTful API 后端,使用 Vue 3 构建 SPA 前端,并完成数据库设计、前后端联调及基础部署。整个过程会详细解释每一步的技术选型理由、配置关键点和常见排错路径,确保你不仅能跑通项目,更能理解每个环节背后的设计逻辑。
1. 项目整体架构与技术栈选型
在开始编码之前,明确项目的技术边界和组件职责至关重要。一个清晰的分层架构能避免后期代码混乱和调试困难。
1.1 为什么选择 Spring Boot + Vue + MySQL 组合
对于电影评论网站这类信息展示与用户交互并重的项目,Spring Boot + Vue + MySQL 是一个经过大量实践验证的稳健组合。Spring Boot 提供了快速构建 RESTful 服务的脚手架,其自动配置和起步依赖能极大简化后端开发;Vue 作为渐进式前端框架,数据驱动视图的特性非常适合构建动态交互的评论界面;MySQL 作为关系型数据库,在存储结构化的电影信息、用户数据和评论内容方面具有天然优势,且事务支持能保证数据一致性。这种前后端分离的架构,使得后端可以专注于业务逻辑和数据安全,前端可以专注于用户体验和界面渲染,两者通过 HTTP API 进行清晰的数据交换。
1.2 系统核心模块与数据流设计
一个基础的电影评论网站通常包含以下核心模块:
- 用户模块:注册、登录、登出、个人信息管理。
- 电影模块:电影信息的增删改查(CRUD)、分类浏览、搜索。
- 评论模块:对电影的评论发布、查看、删除(通常用户只能删除自己的评论)。
- 交互模块:对电影或评论的点赞、收藏。
数据流向遵循典型的前后端分离模式:用户在 Vue 前端页面进行操作(如点击提交评论) -> Vue 通过 Axios 等库发送 HTTP 请求到 Spring Boot 后端定义的 API 接口 -> Spring Boot 控制器(Controller)接收请求,调用服务层(Service)处理业务逻辑 -> 服务层调用数据访问层(Repository)与 MySQL 数据库交互 -> 处理结果沿原路返回,最终由 Vue 更新页面视图。
2. 后端开发:Spring Boot 与 MySQL 集成
后端是整个应用的数据和逻辑核心。我们将从数据库设计开始,逐步构建 Spring Boot 项目。
2.1 数据库设计与建表
根据核心模块,我们设计四张主要表:user(用户表)、movie(电影表)、comment(评论表)、like(点赞表)。这里使用 MySQL 8.0。
首先,创建数据库:
CREATE DATABASE IF NOT EXISTS `movie_review_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `movie_review_db`;接着,创建表结构。注意字段类型、索引和约束的设计。
-- 用户表 CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名,唯一', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; -- 电影表 CREATE TABLE `movie` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `title` varchar(200) NOT NULL COMMENT '电影标题', `director` varchar(100) DEFAULT NULL COMMENT '导演', `actors` varchar(500) DEFAULT NULL COMMENT '主演', `release_year` int DEFAULT NULL COMMENT '上映年份', `description` text COMMENT '剧情简介', `poster_url` varchar(500) DEFAULT NULL COMMENT '海报图片URL', `avg_rating` decimal(3,1) DEFAULT '0.0' COMMENT '平均评分', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_title` (`title`), KEY `idx_release_year` (`release_year`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='电影表'; -- 评论表 CREATE TABLE `comment` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `movie_id` bigint NOT NULL COMMENT '关联电影ID', `user_id` bigint NOT NULL COMMENT '关联用户ID', `content` text NOT NULL COMMENT '评论内容', `rating` tinyint DEFAULT NULL COMMENT '用户评分(1-5)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_movie_id` (`movie_id`), KEY `idx_user_id` (`user_id`), CONSTRAINT `fk_comment_movie` FOREIGN KEY (`movie_id`) REFERENCES `movie` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_comment_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评论表'; -- 点赞表(可扩展为收藏) CREATE TABLE `user_like` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `user_id` bigint NOT NULL COMMENT '用户ID', `target_type` tinyint NOT NULL COMMENT '点赞目标类型:1-电影,2-评论', `target_id` bigint NOT NULL COMMENT '点赞目标ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_target` (`user_id`,`target_type`,`target_id`), -- 防止重复点赞 KEY `idx_target` (`target_type`,`target_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户点赞表';设计要点说明:
- 使用
utf8mb4字符集以支持完整的 Unicode,包括 Emoji 表情(评论中可能出现)。 - 为频繁查询的字段(如
movie.title,comment.movie_id)建立索引,提升查询性能。 - 使用外键约束保证数据完整性,
ON DELETE CASCADE表示主表记录删除时,关联的从表记录自动删除。 user_like表设计了联合唯一索引,确保一个用户对同一目标只能点赞一次。movie.avg_rating字段通过触发器或应用层逻辑更新,这里为了简化,可由服务层计算。
2.2 初始化 Spring Boot 项目与依赖配置
使用 Spring Initializr(或 IDE 如 IntelliJ IDEA 内置的创建工具)生成项目。关键依赖选择:
- Spring Web: 用于构建 RESTful API。
- Spring Data JPA: 简化数据库操作。
- MySQL Driver: 连接 MySQL 数据库。
- Lombok: 简化实体类代码(可选但推荐)。
- Spring Security: 用于用户认证和授权(本文为简化,先使用基于 Session 的简单认证,生产环境需更完善方案)。
生成的pom.xml关键依赖部分如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>在application.yml或application.properties中配置数据库连接和 JPA 属性。这里使用 YAML 格式:
spring: datasource: url: jdbc:mysql://localhost:3306/movie_review_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: your_username # 替换为你的数据库用户名 password: your_password # 替换为你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 开发环境可用 update,生产环境建议设为 validate 或 none,并通过 SQL 脚本管理表结构 show-sql: true # 开发时显示 SQL,便于调试 properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect format_sql: true # 格式化输出的 SQL server: port: 8080 # 后端服务端口注意:
ddl-auto: update在开发初期很方便,但存在覆盖数据的风险。生产环境务必使用validate(仅验证表结构)或none,并通过 Flyway/Liquibase 等工具进行版本化数据库迁移。
2.3 实体类(Entity)与数据访问层(Repository)编写
根据数据库表,创建对应的 JPA 实体类。使用 Lombok 注解减少 getter/setter 等样板代码。
User实体类示例:
package com.example.moviereview.entity; import lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "user") @Data public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username", nullable = false, unique = true, length = 50) private String username; @Column(name = "password", nullable = false) private String password; @Column(name = "email", length = 100) private String email; @Column(name = "avatar", length = 500) private String avatar; @Column(name = "create_time", updatable = false) private LocalDateTime createTime; @Column(name = "update_time") private LocalDateTime updateTime; @PrePersist protected void onCreate() { createTime = LocalDateTime.now(); updateTime = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updateTime = LocalDateTime.now(); } }Movie和Comment实体类结构类似,需定义好关联关系。例如,在Comment实体中:
@Entity @Table(name = "comment") @Data public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "movie_id", nullable = false) private Movie movie; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; @Column(name = "content", nullable = false, columnDefinition = "TEXT") private String content; @Column(name = "rating") private Integer rating; // 1-5 @Column(name = "create_time", updatable = false) private LocalDateTime createTime; @PrePersist protected void onCreate() { createTime = LocalDateTime.now(); } }接着,为每个实体创建对应的 Repository 接口,继承JpaRepository,即可获得基础的 CRUD 方法。
package com.example.moviereview.repository; import com.example.moviereview.entity.Movie; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface MovieRepository extends JpaRepository<Movie, Long> { // 可以自定义查询方法 List<Movie> findByTitleContaining(String keyword); List<Movie> findByReleaseYearOrderByCreateTimeDesc(Integer year); }2.4 服务层(Service)与控制器(Controller)实现
服务层封装业务逻辑,控制器处理 HTTP 请求和响应。以电影查询和评论发布为例。
首先,创建MovieService:
package com.example.moviereview.service; import com.example.moviereview.entity.Movie; import com.example.moviereview.repository.MovieRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.Optional; @Service public class MovieService { @Autowired private MovieRepository movieRepository; public Page<Movie> getAllMovies(Pageable pageable) { return movieRepository.findAll(pageable); } public Optional<Movie> getMovieById(Long id) { return movieRepository.findById(id); } public List<Movie> searchMoviesByTitle(String keyword) { return movieRepository.findByTitleContaining(keyword); } public Movie saveOrUpdateMovie(Movie movie) { return movieRepository.save(movie); } public void deleteMovie(Long id) { movieRepository.deleteById(id); } }然后,创建MovieController提供 REST API:
package com.example.moviereview.controller; import com.example.moviereview.entity.Movie; import com.example.moviereview.service.MovieService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/movies") public class MovieController { @Autowired private MovieService movieService; @GetMapping public ResponseEntity<Map<String, Object>> getMovies( @RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "10") int size, @RequestParam(value = "sortBy", defaultValue = "createTime") String sortBy, @RequestParam(value = "direction", defaultValue = "desc") String direction) { Sort sort = direction.equalsIgnoreCase("asc") ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending(); Pageable pageable = PageRequest.of(page, size, sort); Page<Movie> moviePage = movieService.getAllMovies(pageable); Map<String, Object> response = new HashMap<>(); response.put("movies", moviePage.getContent()); response.put("currentPage", moviePage.getNumber()); response.put("totalItems", moviePage.getTotalElements()); response.put("totalPages", moviePage.getTotalPages()); return ResponseEntity.ok(response); } @GetMapping("/{id}") public ResponseEntity<Movie> getMovieById(@PathVariable Long id) { return movieService.getMovieById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @GetMapping("/search") public ResponseEntity<List<Movie>> searchMovies(@RequestParam String keyword) { List<Movie> movies = movieService.searchMoviesByTitle(keyword); return ResponseEntity.ok(movies); } // 后续可添加需要权限的 POST, PUT, DELETE 接口 }评论相关的CommentService和CommentController逻辑类似,但发布评论时需要关联当前登录用户。这里涉及用户认证,为简化演示,我们先假设通过某种方式(如拦截器)能从请求中获取用户ID。一个简单的实现是在请求头中传递用户标识(仅用于学习,生产环境必须使用安全的认证机制如 JWT)。
2.5 处理跨域请求(CORS)
由于前端(Vue)和后端(Spring Boot)通常运行在不同端口(如 8080 和 3000),浏览器会因同源策略阻止请求。需要在 Spring Boot 后端配置 CORS。
创建一个配置类:
package com.example.moviereview.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { // 开发环境允许所有来源,生产环境需指定具体前端域名 registry.addMapping("/api/**") .allowedOrigins("http://localhost:3000") // Vue 开发服务器地址 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); // 如果前端需要传递 Cookie 或 Authorization 头,此项应为 true } }; } }至此,一个具备基础 CRUD 功能的 Spring Boot 后端 API 服务已经搭建完成。可以通过mvn spring-boot:run或运行主类启动应用,并使用 Postman 或浏览器测试GET http://localhost:8080/api/movies等接口。
3. 前端开发:Vue 3 项目构建与界面实现
前端负责呈现电影列表、详情页、评论表单和用户交互。我们将使用 Vue 3 的 Composition API 和 Vue Router、Axios 等常用库。
3.1 初始化 Vue 项目与安装依赖
使用 Vue CLI 或 Vite 创建项目。这里推荐使用 Vite,因其更轻量快速。
# 使用 npm npm create vue@latest movie-review-frontend # 按照提示选择项目特性:推荐加入 TypeScript, Vue Router, Pinia (状态管理) cd movie-review-frontend npm install # 安装额外依赖:Axios (HTTP请求), Element Plus (UI组件库,可选) npm install axios npm install element-plus @element-plus/icons-vue项目结构大致如下:
movie-review-frontend/ ├── public/ ├── src/ │ ├── assets/ │ ├── components/ # 可复用组件 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia 状态管理 │ ├── views/ # 页面组件 │ ├── App.vue │ ├── main.ts │ └── vite-env.d.ts ├── index.html ├── package.json └── vite.config.ts3.2 配置 Axios 与 API 调用封装
在src目录下创建utils/request.ts文件,封装 Axios 实例,统一处理请求基地址、超时、请求/响应拦截器。
// src/utils/request.ts import axios from 'axios'; import { ElMessage } from 'element-plus'; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; // 创建 axios 实例 const service: AxiosInstance = axios.create({ baseURL: 'http://localhost:8080/api', // 后端 API 基地址 timeout: 10000, // 请求超时时间 }); // 请求拦截器 service.interceptors.request.use( (config: AxiosRequestConfig) => { // 在发送请求之前做些什么,例如添加 token // const token = localStorage.getItem('token'); // if (token) { // config.headers['Authorization'] = `Bearer ${token}`; // } return config; }, (error) => { // 对请求错误做些什么 console.error('Request Error:', error); return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( (response: AxiosResponse) => { // 对响应数据做点什么 const res = response.data; // 这里根据后端统一的响应格式处理,例如 { code: 200, data: ..., message: 'success' } // if (res.code !== 200) { // ElMessage.error(res.message || 'Error'); // return Promise.reject(new Error(res.message || 'Error')); // } return res; }, (error) => { // 对响应错误做点什么 console.error('Response Error:', error); ElMessage.error(error.message || 'Network Error'); return Promise.reject(error); } ); export default service;然后,创建src/api/目录,按模块组织 API 函数。例如movie.ts:
// src/api/movie.ts import request from '@/utils/request'; // 获取电影列表(分页) export function getMovieList(params: any) { return request({ url: '/movies', method: 'get', params, // { page, size, sortBy, direction } }); } // 根据ID获取电影详情 export function getMovieDetail(id: number) { return request({ url: `/movies/${id}`, method: 'get', }); } // 搜索电影 export function searchMovies(keyword: string) { return request({ url: '/movies/search', method: 'get', params: { keyword }, }); }3.3 构建核心页面组件
使用 Vue 3 的<script setup>语法和 Composition API 编写页面。
电影列表页 (src/views/MovieListView.vue):
<template> <div class="movie-list"> <h1>电影列表</h1> <!-- 搜索框 --> <el-input v-model="searchKeyword" placeholder="搜索电影..." @keyup.enter="handleSearch" style="width: 300px; margin-bottom: 20px;" > <template #append> <el-button @click="handleSearch">搜索</el-button> </template> </el-input> <!-- 电影卡片列表 --> <el-row :gutter="20"> <el-col :span="6" v-for="movie in movieList" :key="movie.id"> <el-card :body-style="{ padding: '0px' }" shadow="hover"> <img :src="movie.posterUrl || '/default-poster.jpg'" class="poster" /> <div style="padding: 14px;"> <h3>{{ movie.title }}</h3> <p>导演:{{ movie.director }}</p> <p>年份:{{ movie.releaseYear }}</p> <p>评分:<el-rate v-model="movie.avgRating" disabled show-score text-color="#ff9900" score-template="{value}" /></p> <div class="bottom"> <el-button type="text" @click="goToDetail(movie.id)">查看详情</el-button> </div> </div> </el-card> </el-col> </el-row> <!-- 分页 --> <el-pagination @current-change="handlePageChange" :current-page="currentPage" :page-size="pageSize" layout="prev, pager, next, jumper" :total="total" style="margin-top: 20px;" > </el-pagination> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { useRouter } from 'vue-router'; import { getMovieList, searchMovies } from '@/api/movie'; import type { Movie } from '@/types'; // 需要定义 Movie 类型 const router = useRouter(); const movieList = ref<Movie[]>([]); const searchKeyword = ref(''); const currentPage = ref(1); const pageSize = ref(12); const total = ref(0); const fetchMovies = async (page = 1) => { try { const params = { page: page - 1, size: pageSize.value, sortBy: 'createTime', direction: 'desc' }; const res = await getMovieList(params); movieList.value = res.movies; total.value = res.totalItems; currentPage.value = page; } catch (error) { console.error('获取电影列表失败:', error); } }; const handleSearch = async () => { if (!searchKeyword.value.trim()) { fetchMovies(); return; } try { const res = await searchMovies(searchKeyword.value); movieList.value = res; total.value = res.length; currentPage.value = 1; } catch (error) { console.error('搜索失败:', error); } }; const handlePageChange = (page: number) => { fetchMovies(page); }; const goToDetail = (id: number) => { router.push(`/movie/${id}`); }; onMounted(() => { fetchMovies(); }); </script> <style scoped> .poster { width: 100%; height: 300px; object-fit: cover; } </style>电影详情与评论页 (src/views/MovieDetailView.vue)需要展示电影信息和评论列表,并提供发布评论的表单。其逻辑类似,需要调用getMovieDetail和评论相关的 API。
3.4 路由配置与状态管理
在src/router/index.ts中配置路由:
import { createRouter, createWebHistory } from 'vue-router'; import MovieListView from '@/views/MovieListView.vue'; import MovieDetailView from '@/views/MovieDetailView.vue'; // ... 导入其他视图 const routes = [ { path: '/', name: 'Home', redirect: '/movies', }, { path: '/movies', name: 'MovieList', component: MovieListView, }, { path: '/movie/:id', name: 'MovieDetail', component: MovieDetailView, props: true, // 将路由参数 `id` 作为 prop 传入组件 }, // ... 其他路由,如登录页、用户中心等 ]; const router = createRouter({ history: createWebHistory(), routes, }); export default router;对于用户登录状态、全局提示等,可以使用 Pinia 进行状态管理。创建一个userStore:
// src/stores/user.ts import { defineStore } from 'pinia'; import { ref } from 'vue'; export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || ''); const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}')); const setToken = (newToken: string) => { token.value = newToken; localStorage.setItem('token', newToken); }; const setUserInfo = (info: any) => { userInfo.value = info; localStorage.setItem('userInfo', JSON.stringify(info)); }; const logout = () => { token.value = ''; userInfo.value = {}; localStorage.removeItem('token'); localStorage.removeItem('userInfo'); }; return { token, userInfo, setToken, setUserInfo, logout }; });4. 前后端联调与项目运行
前后端分别开发完成后,需要进行联调,确保数据能正确流动。
4.1 启动后端服务
- 确保 MySQL 服务已启动,并且
movie_review_db数据库及表已创建。 - 在 Spring Boot 项目的
application.yml中确认数据库连接信息正确。 - 在项目根目录运行
mvn spring-boot:run或通过 IDE 启动MovieReviewApplication主类。 - 观察控制台日志,确认无报错且看到类似
Tomcat started on port(s): 8080的信息。
4.2 启动前端开发服务器
- 进入 Vue 项目根目录
movie-review-frontend。 - 运行
npm run dev。Vite 通常会启动在http://localhost:3000(具体看终端输出)。 - 浏览器访问
http://localhost:3000,应能看到电影列表页。
4.3 联调测试与常见问题
问题1:前端访问后端 API 出现 CORS 错误
- 现象:浏览器控制台报错
Access-Control-Allow-Origin。 - 检查:确认后端
CorsConfig中allowedOrigins包含前端地址(如http://localhost:3000)。检查后端是否已重启使配置生效。 - 解决:调整 CORS 配置,或在前端开发服务器配置代理(在
vite.config.ts中配置proxy)。
问题2:前端请求成功但数据为空
- 现象:网络请求返回 200,但
data为空。 - 检查:
- 检查后端控制器方法是否正确返回数据。使用 Postman 直接测试后端接口
GET http://localhost:8080/api/movies。 - 检查数据库
movie表中是否有测试数据。可以在后端启动时通过CommandLineRunner或data.sql脚本初始化一些数据。 - 检查前端 Axios 响应拦截器是否对数据做了额外处理,导致结构不对。
- 检查后端控制器方法是否正确返回数据。使用 Postman 直接测试后端接口
问题3:图片等静态资源无法加载
- 现象:电影海报显示为裂图。
- 检查:
movie.posterUrl字段存储的是完整的 URL 还是相对路径。如果是相对路径,需要确保该路径对应的图片文件存在于后端服务的静态资源目录或专门的静态文件服务器(如 Nginx)下。 - 解决:开发阶段,可以将图片放在后端的
src/main/resources/static/目录下,然后通过http://localhost:8080/图片名.jpg访问。或者使用完整的网络图片 URL。
问题4:分页参数传递错误
- 现象:点击分页器,数据没有变化或报错。
- 检查:前端传递给后端的
page参数。Spring Data JPA 的PageRequest.of(page, size)中page是从 0 开始的,而 Element Plus 分页组件的current-page是从 1 开始的。需要在前端做转换(如page: page - 1)。
5. 项目部署与生产环境考量
毕业设计答辩通常需要演示,因此需要将项目部署到可公开访问的服务器或本地稳定运行。
5.1 后端打包与运行
- 打包:在 Spring Boot 项目根目录执行
mvn clean package -DskipTests,会在target目录生成movie-review-0.0.1-SNAPSHOT.jar。 - 运行:确保目标服务器已安装 Java 运行环境(JRE 8 或 11+)。使用命令
java -jar movie-review-0.0.1-SNAPSHOT.jar启动。可通过--server.port=8081指定端口。 - 生产数据库:将
application.yml中的数据库连接信息改为生产环境的 MySQL 地址、用户名和密码。建议使用application-prod.yml配置文件,并通过--spring.profiles.active=prod激活。 - 进程管理:使用
nohup或 systemd 等服务管理工具保持后端进程常驻。
5.2 前端构建与部署
- 构建:在 Vue 项目根目录执行
npm run build,生成静态文件在dist目录。 - 部署:可以将
dist目录下的文件:- 放置到 Nginx 或 Apache 的 Web 根目录。
- 上传到对象存储(如阿里云 OSS)并配置静态网站托管。
- 使用 Docker 容器化部署。
- API 地址配置:构建前,需将
src/utils/request.ts中的baseURL改为生产环境的后端 API 地址(如http://your-server-ip:8080/api)。更好的做法是通过环境变量注入。
5.3 生产环境检查清单
在将项目用于演示或上线前,请对照以下清单进行检查:
| 检查项 | 开发环境常见状态 | 生产环境要求与建议 |
|---|---|---|
| 数据库连接 | 本地 localhost,弱密码 | 使用强密码,限制访问 IP,考虑连接池配置(如 HikariCP) |
| CORS 配置 | 允许localhost:* | 严格指定前端域名,如https://your-domain.com |
| 日志输出 | 控制台输出,show-sql: true | 关闭 SQL 日志,日志输出到文件,并配置日志级别和滚动策略 |
| 异常处理 | 可能暴露堆栈信息给前端 | 全局异常处理器,返回友好的错误信息,记录详细日志到后端 |
| 用户认证 | 可能简单或未实现 | 实现完整的认证(如 JWT)、密码加密存储、会话管理 |
| 静态资源 | 本地文件或开发服务器 | 使用 CDN 或专用静态资源服务器,配置缓存 |
| API 文档 | 无或简单注释 | 使用 Swagger/OpenAPI 生成接口文档,便于前后端协作和答辩展示 |
| 配置管理 | 写在application.yml | 敏感信息(密码、密钥)使用环境变量或配置中心 |
| 性能 | 未优化 | 数据库查询考虑索引、分页;前端图片懒加载、组件按需引入 |
6. 毕业设计扩展方向与答辩准备
一个基础的电影评论网站完成后,可以从以下方向进行扩展,提升项目复杂度和技术深度,为答辩加分。
6.1 功能扩展建议
- 用户系统增强:邮箱验证、第三方登录(微信、GitHub)、个人中心、修改密码。
- 电影数据:接入公开电影 API(如 TMDB)自动获取电影信息、海报和演职员表。
- 评论系统:支持回复、点赞、举报、敏感词过滤、富文本编辑。
- 推荐系统:基于用户历史评分或浏览记录,实现简单的协同过滤或内容推荐。
- 管理后台:使用 Vue 或 React 构建独立的管理端,实现对电影、评论、用户的管理。
- 实时功能:使用 WebSocket 实现新评论通知、在线人数统计。
6.2 技术深度挖掘
- 缓存优化:使用 Redis 缓存热门电影数据、用户会话,减轻数据库压力。
- 搜索优化:集成 Elasticsearch,实现电影标题、简介、演员等多字段全文检索。
- 文件上传:实现用户头像、电影海报上传至云存储(如阿里云 OSS、七牛云)。
- 微服务化:尝试将用户服务、电影服务、评论服务拆分为独立的 Spring Boot 应用,通过 Spring Cloud 组件进行通信。
- 容器化部署:编写 Dockerfile 将前后端分别容器化,使用 Docker Compose 一键部署。
6.3 答辩要点与文档整理
毕业设计答辩时,除了演示系统,清晰的讲述和文档同样重要。
- 项目介绍:简明扼要说明项目背景、目标用户、核心功能。
- 技术架构图:绘制一张清晰的架构图,展示前端、后端、数据库、缓存等组件及其关系。
- 核心代码讲解:准备 1-2 个核心功能的代码片段,如用户登录的认证流程、电影评论的发布与关联查询,解释其中的技术难点和解决方案。
- 数据库设计:展示 E-R 图,解释表结构设计和关系,说明索引和约束的作用。
- 遇到的问题与解决方案:准备 2-3 个在开发中遇到的实际问题(如跨域、分页、N+1 查询问题)以及你是如何排查和解决的。
- 项目总结与展望:总结项目的收获、不足,以及未来可以改进和扩展的方向。
将以上内容整理成毕业设计论文或报告时,确保结构完整,包含需求分析、系统设计、数据库设计、详细实现、系统测试等章节,并附上核心代码和运行截图。
通过以上步骤,你不仅完成了一个可运行、可演示的电影评论网站,更掌握了从需求分析到部署上线的全链路开发流程。在实际操作中,务必理解每个配置和代码块的作用,而不是简单复制粘贴,这样才能在遇到问题时快速定位并解决,真正提升工程能力。