简介:本资源是一套基于SSM框架与Vue前端的完整电影网站系统源码,面向Java Web初学者及课程设计、毕业设计阶段的学生,解决Web全栈项目从需求分析到部署上线的实践闭环问题。压缩包共880个文件,涵盖144个Java后端逻辑类、53个Vue组件、167个JS交互脚本、55个CSS样式文件及79个GIF动效素材,辅以SQL建表语句、BAT一键部署脚本和ElementUI界面组件,结构清晰,模块完整。资源大小18.85MB,已吸引2594人学习下载。读者可直接导入IDE运行,获得含用户管理、电影信息展示、图片与视频素材管理的可执行系统,配套摘要文档、三级目录结构说明及技术选型详解,便于理解B/S架构下前后端分离开发流程与SSM+Vue协同机制。
1. 用 Java + Spring Boot 快速搭起一个可上线的电影网站,不是 Demo,是能查片、搜人、看详情、分页加载的真实 Web 项目
你手头有一份“电影网站 Java 源码”,但打开后发现:Tomcat 跑不起来、MySQL 连不上、前端页面全是 404、连登录入口都找不到——这不是代码有 bug,而是缺了真实业务场景下的工程骨架。这个标题里反复出现的“基于 Web 的电影网站设计与实现”,本质不是写个 CRUD 就交差,而是要解决「用户怎么找新片」「管理员怎么批量导入豆瓣数据」「海报图如何不崩」「搜索响应怎么压到 300ms 内」这些具体问题。它面向的是刚学完 Servlet/JDBC 的 Java 初学者,也面向需要快速交付教学系统或课程设计的高校开发者;核心诉求不是炫技,而是结构清晰、数据库可填、接口可测、前端能点、部署不报错。本文不讲 MVC 理论,直接从spring-boot-starter-web开始,把“电影网站”拆成「数据建模 → 接口分层 → 前端联动 → 性能兜底」四步,每一步都给出可粘贴运行的代码、必须改的配置项、以及本地调试时最常卡住的三个检查点。
2. 用 MySQL + MyBatis-Plus 建好电影领域模型,让「影片」「导演」「演员」「分类」真正关联起来
电影网站的数据绝不是一张movie表就能撑住的。用户搜“周星驰”,得列出他导演+参演的所有片;点开《大话西游》,要显示类型(喜剧/爱情)、主演(周星驰/朱茵)、上映年份、豆瓣评分——这要求至少 5 张表形成主外键闭环。常见错误是直接照着豆瓣 API 字段建表(比如加douban_id字段),结果导致后续无法做国产片筛选或年代排序。正确做法是先定义业务实体边界,再映射关系。
2.1 设计 5 张核心表并生成实体类
我们采用MyBatis-Plus 3.5.3.1 + MySQL 8.0组合,避免 XML 手写 SQL。建表语句必须包含逻辑删除字段deleted和时间戳create_time,这是后期支持软删和审计的基础:
-- 影片主表(含基础信息+状态) CREATE TABLE `movie` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '中文片名', `original_title` varchar(100) DEFAULT NULL COMMENT '原名', `year` smallint DEFAULT NULL COMMENT '上映年份', `rating` decimal(2,1) DEFAULT NULL COMMENT '豆瓣评分', `duration` varchar(20) DEFAULT NULL COMMENT '片长', `summary` text COMMENT '剧情简介', `poster_url` varchar(255) DEFAULT NULL COMMENT '海报地址', `status` tinyint DEFAULT '1' COMMENT '状态:1-上架, 0-下架', `deleted` tinyint DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 分类表(如:剧情 / 动作 / 爱情) CREATE TABLE `category` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL UNIQUE, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 影片-分类关联表(多对多) CREATE TABLE `movie_category` ( `movie_id` bigint NOT NULL, `category_id` bigint NOT NULL, PRIMARY KEY (`movie_id`,`category_id`), KEY `idx_category_id` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 人员表(统一存导演/演员) CREATE TABLE `person` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `avatar_url` varchar(255) DEFAULT NULL, `type` tinyint NOT NULL COMMENT '1-导演, 2-演员', `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 影片-人员关联表(带角色字段) CREATE TABLE `movie_person` ( `movie_id` bigint NOT NULL, `person_id` bigint NOT NULL, `role` varchar(20) DEFAULT NULL COMMENT '导演/主演/配角', PRIMARY KEY (`movie_id`,`person_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;提示:建表后立刻执行
SELECT COUNT(*) FROM movie;确认表存在且无语法错误。若用 Navicat 导入失败,请检查是否启用了innodb_file_per_table=ON(MySQL 8.0 默认开启)。
2.2 用 MyBatis-Plus 自动生成实体与 Mapper
在pom.xml中引入关键依赖(Spring Boot 2.7.x 兼容版):
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>配置application.yml数据源(注意serverTimezone=GMT%2B8防止时间错乱):
spring: datasource: url: jdbc:mysql://localhost:3306/movie_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver使用 MyBatis-Plus 官方代码生成器(AutoGenerator)生成Movie.java、Category.java等实体类。关键参数必须设置:
| 参数 | 值 | 说明 |
|---|---|---|
globalConfig.setAuthor("dev") | 固定作者名 | 避免生成文件含非法字符 |
packageConfig.setModuleName("movie") | 模块名设为movie | 保证包路径为com.example.movie.entity |
strategyConfig.addInclude("movie","category","movie_category") | 显式指定表名 | 防止生成无关表 |
strategyConfig.setTablePrefix("t_") | 留空 | 本例表名无前缀,设为空字符串 |
生成后,Movie实体需手动添加关联注解:
// Movie.java @TableName("movie") public class Movie { @TableId(type = IdType.AUTO) private Long id; private String title; private Integer year; private BigDecimal rating; // ...其他字段 @TableField(exist = false) // 不映射到数据库字段 private List<Category> categories; // 关联分类列表 @TableField(exist = false) private List<Person> directors; // 导演列表 @TableField(exist = false) private List<Person> actors; // 演员列表 }2.3 编写多表关联查询的自定义 SQL
MyBatis-Plus 的QueryWrapper无法处理movie → movie_category → category三层 JOIN。必须手写 XML 映射。在MovieMapper.xml中添加:
<!-- 根据 ID 查询影片详情(含分类、导演、主演) --> <select id="selectMovieWithRelations" resultType="com.example.movie.entity.Movie"> SELECT m.*, GROUP_CONCAT(c.name SEPARATOR ',') as categoryNames, GROUP_CONCAT(d.name SEPARATOR ',') as directorNames, GROUP_CONCAT(a.name SEPARATOR ',') as actorNames FROM movie m LEFT JOIN movie_category mc ON m.id = mc.movie_id LEFT JOIN category c ON mc.category_id = c.id AND c.deleted = 0 LEFT JOIN movie_person mp_d ON m.id = mp_d.movie_id AND mp_d.role = '导演' LEFT JOIN person d ON mp_d.person_id = d.id AND d.type = 1 AND d.deleted = 0 LEFT JOIN movie_person mp_a ON m.id = mp_a.movie_id AND mp_a.role = '主演' LEFT JOIN person a ON mp_a.person_id = a.id AND a.type = 2 AND a.deleted = 0 WHERE m.id = #{id} AND m.deleted = 0 GROUP BY m.id </select>对应 Mapper 接口方法:
// MovieMapper.java @Mapper public interface MovieMapper extends BaseMapper<Movie> { @Select("SELECT * FROM movie WHERE deleted = 0 ORDER BY create_time DESC LIMIT #{offset}, #{limit}") List<Movie> selectPage(@Param("offset") int offset, @Param("limit") int limit); Movie selectMovieWithRelations(@Param("id") Long id); }注意:
GROUP_CONCAT在 MySQL 中默认长度限制为 1024,若某部电影有超长分类名(如“科幻/冒险/动作/奇幻/家庭/儿童”),会截断。生产环境需在 MySQL 配置中增加group_concat_max_len = 4096。
3. 用 Spring Boot REST Controller 暴露标准接口,让前端 Vue 或 HTML 页面能直接调用
电影网站的接口不能只返回 JSON 数据,还要解决「分页怎么传参」「搜索关键词怎么防 SQL 注入」「海报 URL 怎么统一补前缀」这些落地细节。很多源码把所有逻辑塞进@RestController,导致后期无法单元测试。这里采用Controller → Service → Mapper 三层分离,且每个层都有明确职责边界。
3.1 定义标准 REST 接口路径与请求参数
遵循 RESTful 最佳实践 ,电影资源路径统一为/api/movies,操作动词用 HTTP 方法区分:
| 请求方式 | 路径 | 说明 | 示例参数 |
|---|---|---|---|
| GET | /api/movies | 获取影片列表(支持分页、搜索、分类筛选) | ?page=1&size=12&keyword=西游&category=喜剧 |
| GET | /api/movies/{id} | 获取单部影片详情(含关联数据) | id=123 |
| POST | /api/movies | 后台新增影片(需登录) | JSON body 含 title/year/rating 等 |
| PUT | /api/movies/{id} | 更新影片状态(上架/下架) | {"status":0} |
提示:不要用
/movie/list这类非标准路径。搜索引擎和前端路由库(如 Vue Router)对/api/movies更友好,也便于 Nginx 反向代理统一管理。
3.2 编写带条件查询的 Service 层逻辑
MovieService是核心业务胶水层,负责拼装查询条件、调用 Mapper、组装返回对象:
@Service public class MovieService { @Autowired private MovieMapper movieMapper; public PageResult<Movie> listMovies(int page, int size, String keyword, String category) { // 1. 计算分页偏移量 int offset = (page - 1) * size; // 2. 构建动态 WHERE 条件(防 SQL 注入!) StringBuilder whereSql = new StringBuilder(); List<Object> params = new ArrayList<>(); if (StringUtils.isNotBlank(keyword)) { whereSql.append(" AND (m.title LIKE ? OR m.original_title LIKE ?)"); params.add("%" + keyword + "%"); params.add("%" + keyword + "%"); } if (StringUtils.isNotBlank(category)) { whereSql.append(" AND c.name = ?"); params.add(category); } // 3. 执行分页查询(注意:MyBatis-Plus 的 IPage 不支持多表 JOIN 分页,必须手写) String sql = "SELECT m.*, COUNT(*) OVER() AS total FROM movie m " + "LEFT JOIN movie_category mc ON m.id = mc.movie_id " + "LEFT JOIN category c ON mc.category_id = c.id " + "WHERE m.deleted = 0 " + whereSql.toString() + " GROUP BY m.id ORDER BY m.create_time DESC LIMIT ?, ?"; params.add(offset); params.add(size); // 使用 JdbcTemplate 执行原生 SQL(比 MyBatis XML 更灵活) List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, params.toArray()); // 4. 封装分页结果 long total = rows.isEmpty() ? 0 : ((Number) rows.get(0).get("total")).longValue(); List<Movie> movies = rows.stream().map(this::mapToMovie).collect(Collectors.toList()); return new PageResult<>(movies, total, page, size); } private Movie mapToMovie(Map<String, Object> row) { Movie movie = new Movie(); movie.setId(((Number) row.get("id")).longValue()); movie.setTitle((String) row.get("title")); movie.setYear(((Number) row.get("year")).intValue()); // ...其他字段赋值 return movie; } }3.3 Controller 层做参数校验与响应封装
MovieController只做三件事:接收参数、调用 Service、包装响应。禁止在 Controller 里写业务逻辑:
@RestController @RequestMapping("/api/movies") public class MovieController { @Autowired private MovieService movieService; @GetMapping public ResponseEntity<ApiResponse<PageResult<Movie>>> list( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "12") int size, @RequestParam(required = false) String keyword, @RequestParam(required = false) String category) { // 参数校验:页码不能为负数 if (page < 1 || size < 1 || size > 100) { return ResponseEntity.badRequest() .body(ApiResponse.error("参数错误:page>=1, size in [1,100]")); } try { PageResult<Movie> result = movieService.listMovies(page, size, keyword, category); return ResponseEntity.ok(ApiResponse.success(result)); } catch (Exception e) { log.error("查询影片列表失败", e); return ResponseEntity.status(500) .body(ApiResponse.error("服务内部错误,请稍后重试")); } } @GetMapping("/{id}") public ResponseEntity<ApiResponse<Movie>> detail(@PathVariable Long id) { if (id == null || id <= 0) { return ResponseEntity.badRequest() .body(ApiResponse.error("影片ID无效")); } Movie movie = movieService.getDetail(id); if (movie == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(ApiResponse.success(movie)); } }其中ApiResponse是统一响应格式(避免前端反复解析不同结构):
public class ApiResponse<T> { private int code; private String message; private T data; public static <T> ApiResponse<T> success(T data) { ApiResponse<T> res = new ApiResponse<>(); res.code = 200; res.message = "success"; res.data = data; return res; } public static <T> ApiResponse<T> error(String message) { ApiResponse<T> res = new ApiResponse<>(); res.code = 400; res.message = message; return res; } }注意:
@RequestParam(required = false)表示该参数可选,但@PathVariable必须存在。若前端传/api/movies/(末尾无 ID),Spring Boot 会直接返回 404,无需额外判空。
4. 用 Thymeleaf 渲染静态页面,零 JavaScript 也能跑通首页、详情页、搜索页
很多“电影网站 Java 源码”强行用 JSP 或裸 Servlet 输出 HTML,导致样式混乱、JS 报错、图片不显示。Thymeleaf 是 Spring Boot 官方推荐的模板引擎,它允许你在 HTML 文件里写th:each循环,浏览器直接打开.html文件能看到结构,开发时无需启动服务器即可预览布局。
4.1 配置 Thymeleaf 并创建基础页面结构
在pom.xml添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>application.yml中启用热更新(开发阶段必备):
spring: thymeleaf: cache: false # 开发时关闭缓存 enabled: true prefix: classpath:/templates/ suffix: .html encoding: UTF-8 servlet: content-type: text/html创建src/main/resources/templates/layout.html作为母版(含公共导航栏、CSS/JS 引入):
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>电影网站</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> <link rel="stylesheet" th:href="@{/css/style.css}"> </head> <body> <div class="container-fluid"> <!-- 顶部导航 --> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <a class="navbar-brand" href="/">🎬 电影网站</a> <form class="form-inline my-2 my-lg-0" th:action="@{/search}" method="get"> <input class="form-control mr-sm-2" type="search" name="q" placeholder="搜索影片..." aria-label="Search"> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">搜索</button> </form> </nav> <!-- 主体内容占位符 --> <div th:fragment="content"> <div th:replace="this :: content"></div> </div> </div> <script th:src="@{/js/bootstrap.bundle.min.js}"></script> </body> </html>4.2 编写首页(index.html)展示最新上映影片
首页需调用MovieService.listMovies(1, 12, null, null)获取最新 12 部影片,并用 Bootstrap Card 布局:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> <head> <title>首页 - 电影网站</title> </head> <body> <div layout:fragment="content"> <h2 class="mt-4 mb-3">最新上映</h2> <div class="row"> <div class="col-md-3 mb-4" th:each="movie : ${movies.data}"> <div class="card h-100"> <!-- 海报图:若为空则显示默认占位图 --> <img th:src="${#strings.isEmpty(movie.posterUrl) ? '/images/default-poster.jpg' : movie.posterUrl}" class="card-img-top" alt="海报" height="300"> <div class="card-body"> <h5 class="card-title" th:text="${movie.title}">肖申克的救赎</h5> <p class="card-text"> <small class="text-muted" th:text="${movie.year} ?: '未知年份'">1994</small> <span th:if="${movie.rating != null}" th:text="'★ ' + ${movie.rating}">★ 9.7</span> </p> <a th:href="@{'/movie/' + ${movie.id}}" class="btn btn-primary">查看详情</a> </div> </div> </div> </div> <!-- 分页控件 --> <nav aria-label="分页"> <ul class="pagination justify-content-center"> <li class="page-item" th:classappend="${page == 1} ? 'disabled'"> <a class="page-link" th:href="@{/?page=${page - 1}}" th:text="'上一页'">上一页</a> </li> <li class="page-item active" th:each="i : ${#numbers.sequence(1, totalPages)}"> <a class="page-link" th:href="@{/?page=${i}}" th:text="${i}" th:classappend="${i == page} ? 'active'">1</a> </li> <li class="page-item" th:classappend="${page == totalPages} ? 'disabled'"> <a class="page-link" th:href="@{/?page=${page + 1}}" th:text="'下一页'">下一页</a> </li> </ul> </nav> </div> </body> </html>对应 Controller 返回 Model:
@GetMapping("/") public String index(Model model) { PageResult<Movie> result = movieService.listMovies(1, 12, null, null); model.addAttribute("movies", result); model.addAttribute("page", 1); model.addAttribute("totalPages", (int) Math.ceil((double) result.getTotal() / 12)); return "index"; // 对应 templates/index.html }4.3 实现搜索功能:GET 请求跳转 + 后端模糊匹配
搜索页search.html复用首页结构,仅替换循环数据源:
<!-- search.html --> <div layout:fragment="content"> <h2 class="mt-4 mb-3" th:text="'搜索结果:' + ${keyword}">搜索结果</h2> <div class="alert alert-info" th:if="${movies.data.empty}"> 未找到与“[[${keyword}]]”相关的影片 </div> <div class="row" th:if="${!movies.data.empty}"> <!-- 同 index.html 的 card 循环 --> </div> </div>Controller 处理搜索请求(注意:@GetMapping("/search")接收表单 GET 提交):
@GetMapping("/search") public String search(@RequestParam String q, Model model) { // 调用 service 的 listMovies 方法,keyword=q PageResult<Movie> result = movieService.listMovies(1, 12, q, null); model.addAttribute("movies", result); model.addAttribute("keyword", q); model.addAttribute("page", 1); model.addAttribute("totalPages", (int) Math.ceil((double) result.getTotal() / 12)); return "search"; }提示:Thymeleaf 的
th:each会自动处理空集合,无需在 HTML 里写th:if="${not #lists.isEmpty(movies.data)}"。但th:if用于显示提示文案更直观。
5. 解决三大高频部署问题:海报图 404、分页跳转失效、MySQL 连接超时
本地开发跑通不等于能上线。很多“电影网站 Java 源码”在学生交作业时正常,一放到云服务器就挂——根本原因是没处理生产环境特有约束。以下三个问题,90% 的 Java Web 电影项目都会遇到,且必须在编码阶段就埋入解决方案。
5.1 海报图路径统一代理:用 Spring ResourceHandler 映射 uploads 目录
用户上传的海报图若直接存src/main/resources/static/images/,打包成 JAR 后无法写入。正确做法是将图片存到服务器磁盘(如/data/movie/posters/),再通过ResourceHandler映射为 Web 路径:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${movie.upload-path:/data/movie/posters/}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将 /uploads/** 请求映射到 uploadPath 目录 registry.addResourceHandler("/uploads/**") .addResourceLocations("file:" + uploadPath); } }application.yml中配置路径(Linux 环境):
movie: upload-path: /data/movie/posters/前端 HTML 中引用海报图:
<img th:src="'/uploads/' + ${movie.posterFileName}" alt="海报">注意:
file:/data/movie/posters/路径必须提前创建,且 Java 进程用户(如springboot)要有读写权限。执行sudo mkdir -p /data/movie/posters && sudo chown springboot:springboot /data/movie/posters。
5.2 分页链接动态生成:用 Thymeleaf 的@{...}保持上下文路径
当项目部署到子路径(如https://example.com/movie/)时,硬编码<a href="/page/2">会跳转到根域名。Thymeleaf 的@{...}自动拼接上下文路径:
<!-- 错误写法(绝对路径) --> <a href="/?page=2">第2页</a> <!-- 正确写法(相对路径 + 上下文自动补全) --> <a th:href="@{/?page=${page + 1}}">下一页</a>同时,在application.yml中声明上下文路径(若部署在子目录):
server: servlet: context-path: /movie # 访问地址变为 https://example.com/movie/5.3 MySQL 连接池保活:配置 HikariCP 防止连接闲置断开
阿里云 RDS 或腾讯云 CDB 默认 300 秒无活动连接自动断开。若 Spring Boot 未配置连接池心跳,用户首次访问会报Communications link failure。在application.yml中加入:
spring: datasource: hikari: connection-timeout: 30000 validation-timeout: 3000 idle-timeout: 600000 # 10分钟空闲后回收连接 max-lifetime: 1800000 # 连接最长存活30分钟(小于RDS的300秒) keepalive-time: 30000 # 每30秒发一次心跳包 minimum-idle: 5 maximum-pool-size: 20验证是否生效:启动项目后,执行show processlist;查看 MySQL 连接,应看到Sleep状态连接持续存在,且Time字段不会归零。
提示:
max-lifetime必须小于云数据库的wait_timeout值(通常为 300 秒)。若设为 3600000(1小时),连接池会复用已断开的连接,导致后续请求报错。
本文还有配套的精品资源,点击获取