尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Spring Boot电影网站实战:MySQL+MyBatis-Plus+Thymeleaf全栈搭建

发布时间:2026/9/23 18:46:21

资讯中心
01
ARTICLE

Spring Boot电影网站实战:MySQL+MyBatis-Plus+Thymeleaf全栈搭建

Spring Boot电影网站实战:MySQL+MyBatis-Plus+Thymeleaf全栈搭建
简介本资源是一套基于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架构下前后端分离开发流程与SSMVue协同机制。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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 分类表如剧情 / 动作 / 爱情 CREATE TABLE category ( id bigint NOT NULL AUTO_INCREMENT, name varchar(20) NOT NULL UNIQUE, deleted tinyint DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 影片-分类关联表多对多 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 人员表统一存导演/演员 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 影片-人员关联表带角色字段 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;提示建表后立刻执行SELECT COUNT(*) FROM movie;确认表存在且无语法错误。若用 Navicat 导入失败请检查是否启用了innodb_file_per_tableONMySQL 8.0 默认开启。2.2 用 MyBatis-Plus 自动生成实体与 Mapper在pom.xml中引入关键依赖Spring Boot 2.7.x 兼容版dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency配置application.yml数据源注意serverTimezoneGMT%2B8防止时间错乱spring: datasource: url: jdbc:mysql://localhost:3306/movie_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneGMT%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.entitystrategyConfig.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 ListCategory categories; // 关联分类列表 TableField(exist false) private ListPerson directors; // 导演列表 TableField(exist false) private ListPerson actors; // 演员列表 }2.3 编写多表关联查询的自定义 SQLMyBatis-Plus 的QueryWrapper无法处理movie → movie_category → category三层 JOIN。必须手写 XML 映射。在MovieMapper.xml中添加!-- 根据 ID 查询影片详情含分类、导演、主演 -- select idselectMovieWithRelations resultTypecom.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 BaseMapperMovie { Select(SELECT * FROM movie WHERE deleted 0 ORDER BY create_time DESC LIMIT #{offset}, #{limit}) ListMovie 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获取影片列表支持分页、搜索、分类筛选?page1size12keyword西游category喜剧GET/api/movies/{id}获取单部影片详情含关联数据id123POST/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 PageResultMovie listMovies(int page, int size, String keyword, String category) { // 1. 计算分页偏移量 int offset (page - 1) * size; // 2. 构建动态 WHERE 条件防 SQL 注入 StringBuilder whereSql new StringBuilder(); ListObject 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 更灵活 ListMapString, Object rows jdbcTemplate.queryForList(sql, params.toArray()); // 4. 封装分页结果 long total rows.isEmpty() ? 0 : ((Number) rows.get(0).get(total)).longValue(); ListMovie movies rows.stream().map(this::mapToMovie).collect(Collectors.toList()); return new PageResult(movies, total, page, size); } private Movie mapToMovie(MapString, 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 ResponseEntityApiResponsePageResultMovie 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(参数错误page1, size in [1,100])); } try { PageResultMovie 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 ResponseEntityApiResponseMovie 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 ApiResponseT { private int code; private String message; private T data; public static T ApiResponseT success(T data) { ApiResponseT res new ApiResponse(); res.code 200; res.message success; res.data data; return res; } public static T ApiResponseT error(String message) { ApiResponseT res new ApiResponse(); res.code 400; res.message message; return res; } }注意RequestParam(required false)表示该参数可选但PathVariable必须存在。若前端传/api/movies/末尾无 IDSpring Boot 会直接返回 404无需额外判空。4. 用 Thymeleaf 渲染静态页面零 JavaScript 也能跑通首页、详情页、搜索页很多“电影网站 Java 源码”强行用 JSP 或裸 Servlet 输出 HTML导致样式混乱、JS 报错、图片不显示。Thymeleaf 是 Spring Boot 官方推荐的模板引擎它允许你在 HTML 文件里写th:each循环浏览器直接打开.html文件能看到结构开发时无需启动服务器即可预览布局。4.1 配置 Thymeleaf 并创建基础页面结构在pom.xml添加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependencyapplication.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:thhttp://www.thymeleaf.org head meta charsetUTF-8/ title电影网站/title link relstylesheet th:href{/css/bootstrap.min.css} link relstylesheet th:href{/css/style.css} /head body div classcontainer-fluid !-- 顶部导航 -- nav classnavbar navbar-expand-lg navbar-light bg-light a classnavbar-brand href/ 电影网站/a form classform-inline my-2 my-lg-0 th:action{/search} methodget input classform-control mr-sm-2 typesearch nameq placeholder搜索影片... aria-labelSearch button classbtn btn-outline-success my-2 my-sm-0 typesubmit搜索/button /form /nav !-- 主体内容占位符 -- div th:fragmentcontent div th:replacethis :: content/div /div /div script th:src{/js/bootstrap.bundle.min.js}/script /body /html4.2 编写首页index.html展示最新上映影片首页需调用MovieService.listMovies(1, 12, null, null)获取最新 12 部影片并用 Bootstrap Card 布局!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org xmlns:layouthttp://www.ultraq.net.nz/thymeleaf/layout layout:decorate~{layout} head title首页 - 电影网站/title /head body div layout:fragmentcontent h2 classmt-4 mb-3最新上映/h2 div classrow div classcol-md-3 mb-4 th:eachmovie : ${movies.data} div classcard h-100 !-- 海报图若为空则显示默认占位图 -- img th:src${#strings.isEmpty(movie.posterUrl) ? /images/default-poster.jpg : movie.posterUrl} classcard-img-top alt海报 height300 div classcard-body h5 classcard-title th:text${movie.title}肖申克的救赎/h5 p classcard-text small classtext-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}} classbtn btn-primary查看详情/a /div /div /div /div !-- 分页控件 -- nav aria-label分页 ul classpagination justify-content-center li classpage-item th:classappend${page 1} ? disabled a classpage-link th:href{/?page${page - 1}} th:text上一页上一页/a /li li classpage-item active th:eachi : ${#numbers.sequence(1, totalPages)} a classpage-link th:href{/?page${i}} th:text${i} th:classappend${i page} ? active1/a /li li classpage-item th:classappend${page totalPages} ? disabled a classpage-link th:href{/?page${page 1}} th:text下一页下一页/a /li /ul /nav /div /body /html对应 Controller 返回 ModelGetMapping(/) public String index(Model model) { PageResultMovie 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:fragmentcontent h2 classmt-4 mb-3 th:text搜索结果 ${keyword}搜索结果/h2 div classalert alert-info th:if${movies.data.empty} 未找到与“[[${keyword}]]”相关的影片 /div div classrow th:if${!movies.data.empty} !-- 同 index.html 的 card 循环 -- /div /divController 处理搜索请求注意GetMapping(/search)接收表单 GET 提交GetMapping(/search) public String search(RequestParam String q, Model model) { // 调用 service 的 listMovies 方法keywordq PageResultMovie 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/?page2第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 秒。若设为 36000001小时连接池会复用已断开的连接导致后续请求报错。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。