:MySQL+MyBatis-Plus+Thymeleaf全棧搭建)
簡介本資源是一套基于SSM框架與Vue前端的完整電影網站系統(tǒng)源碼面向Java Web初學者及課程設計、畢業(yè)設計階段的學生解決Web全棧項目從需求分析到部署上線的實踐閉環(huán)問題。壓縮包共880個文件涵蓋144個Java后端邏輯類、53個Vue組件、167個JS交互腳本、55個CSS樣式文件及79個GIF動效素材輔以SQL建表語句、BAT一鍵部署腳本和ElementUI界面組件結構清晰模塊完整。資源大小18.85MB已吸引2594人學習下載。讀者可直接導入IDE運行獲得含用戶管理、電影信息展示、圖片與視頻素材管理的可執(zhí)行系統(tǒng)配套摘要文檔、三級目錄結構說明及技術選型詳解便于理解B/S架構下前后端分離開發(fā)流程與SSMVue協(xié)同機制。1. 用 Java Spring Boot 快速搭起一個可上線的電影網站不是 Demo是能查片、搜人、看詳情、分頁加載的真實 Web 項目你手頭有一份“電影網站 Java 源碼”但打開后發(fā)現Tomcat 跑不起來、MySQL 連不上、前端頁面全是 404、連登錄入口都找不到——這不是代碼有 bug而是缺了真實業(yè)務場景下的工程骨架。這個標題里反復出現的“基于 Web 的電影網站設計與實現”本質不是寫個 CRUD 就交差而是要解決「用戶怎么找新片」「管理員怎么批量導入豆瓣數據」「海報圖如何不崩」「搜索響應怎么壓到 300ms 內」這些具體問題。它面向的是剛學完 Servlet/JDBC 的 Java 初學者也面向需要快速交付教學系統(tǒng)或課程設計的高校開發(fā)者核心訴求不是炫技而是結構清晰、數據庫可填、接口可測、前端能點、部署不報錯。本文不講 MVC 理論直接從spring-boot-starter-web開始把“電影網站”拆成「數據建模 → 接口分層 → 前端聯動 → 性能兜底」四步每一步都給出可粘貼運行的代碼、必須改的配置項、以及本地調試時最常卡住的三個檢查點。2. 用 MySQL MyBatis-Plus 建好電影領域模型讓「影片」「導演」「演員」「分類」真正關聯起來電影網站的數據絕不是一張movie表就能撐住的。用戶搜“周星馳”得列出他導演參演的所有片點開《大話西游》要顯示類型喜劇/愛情、主演周星馳/朱茵、上映年份、豆瓣評分——這要求至少 5 張表形成主外鍵閉環(huán)。常見錯誤是直接照著豆瓣 API 字段建表比如加douban_id字段結果導致后續(xù)無法做國產片篩選或年代排序。正確做法是先定義業(yè)務實體邊界再映射關系。2.1 設計 5 張核心表并生成實體類我們采用MyBatis-Plus 3.5.3.1 MySQL 8.0組合避免 XML 手寫 SQL。建表語句必須包含邏輯刪除字段deleted和時間戳create_time這是后期支持軟刪和審計的基礎-- 影片主表含基礎信息狀態(tài) 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 狀態(tài)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; -- 人員表統(tǒng)一存導演/演員 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;提示建表后立刻執(zhí)行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若某部電影有超長分類名如“科幻/冒險/動作/奇幻/家庭/兒童”會截斷。生產環(huán)境需在 MySQL 配置中增加group_concat_max_len 4096。3. 用 Spring Boot REST Controller 暴露標準接口讓前端 Vue 或 HTML 頁面能直接調用電影網站的接口不能只返回 JSON 數據還要解決「分頁怎么傳參」「搜索關鍵詞怎么防 SQL 注入」「海報 URL 怎么統(tǒng)一補前綴」這些落地細節(jié)。很多源碼把所有邏輯塞進RestController導致后期無法單元測試。這里采用Controller → Service → Mapper 三層分離且每個層都有明確職責邊界。3.1 定義標準 REST 接口路徑與請求參數遵循 RESTful 最佳實踐 電影資源路徑統(tǒng)一為/api/movies操作動詞用 HTTP 方法區(qū)分請求方式路徑說明示例參數GET/api/movies獲取影片列表支持分頁、搜索、分類篩選?page1size12keyword西游category喜劇GET/api/movies/{id}獲取單部影片詳情含關聯數據id123POST/api/movies后臺新增影片需登錄JSON body 含 title/year/rating 等PUT/api/movies/{id}更新影片狀態(tài)上架/下架{status:0}提示不要用/movie/list這類非標準路徑。搜索引擎和前端路由庫如 Vue Router對/api/movies更友好也便于 Nginx 反向代理統(tǒng)一管理。3.2 編寫帶條件查詢的 Service 層邏輯MovieService是核心業(yè)務膠水層負責拼裝查詢條件、調用 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. 構建動態(tài) 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. 執(zhí)行分頁查詢注意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 執(zhí)行原生 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 里寫業(yè)務邏輯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是統(tǒng)一響應格式避免前端反復解析不同結構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 渲染靜態(tài)頁面零 JavaScript 也能跑通首頁、詳情頁、搜索頁很多“電影網站 Java 源碼”強行用 JSP 或裸 Servlet 輸出 HTML導致樣式混亂、JS 報錯、圖片不顯示。Thymeleaf 是 Spring Boot 官方推薦的模板引擎它允許你在 HTML 文件里寫th:each循環(huán)瀏覽器直接打開.html文件能看到結構開發(fā)時無需啟動服務器即可預覽布局。4.1 配置 Thymeleaf 并創(chuàng)建基礎頁面結構在pom.xml添加依賴dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependencyapplication.yml中啟用熱更新開發(fā)階段必備spring: thymeleaf: cache: false # 開發(fā)時關閉緩存 enabled: true prefix: classpath:/templates/ suffix: .html encoding: UTF-8 servlet: content-type: text/html創(chuàng)建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復用首頁結構僅替換循環(huán)數據源!-- 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 循環(huán) -- /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 連接超時本地開發(fā)跑通不等于能上線。很多“電影網站 Java 源碼”在學生交作業(yè)時正常一放到云服務器就掛——根本原因是沒處理生產環(huán)境特有約束。以下三個問題90% 的 Java Web 電影項目都會遇到且必須在編碼階段就埋入解決方案。5.1 海報圖路徑統(tǒng)一代理用 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 環(huán)境movie: upload-path: /data/movie/posters/前端 HTML 中引用海報圖img th:src/uploads/ ${movie.posterFileName} alt海報注意file:/data/movie/posters/路徑必須提前創(chuàng)建且 Java 進程用戶如springboot要有讀寫權限。執(zhí)行sudo mkdir -p /data/movie/posters sudo chown springboot:springboot /data/movie/posters。5.2 分頁鏈接動態(tài)生成用 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 # 訪問地址變?yōu)?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秒發(fā)一次心跳包 minimum-idle: 5 maximum-pool-size: 20驗證是否生效啟動項目后執(zhí)行show processlist;查看 MySQL 連接應看到Sleep狀態(tài)連接持續(xù)存在且Time字段不會歸零。提示max-lifetime必須小于云數據庫的wait_timeout值通常為 300 秒。若設為 36000001小時連接池會復用已斷開的連接導致后續(xù)請求報錯。本文還有配套的精品資源點擊獲取