简介:本资源是一份面向Java初学者与Spring Boot入门开发者的电影在线订票系统完整实践文档,聚焦于企业级票务平台的功能设计与技术落地。内容覆盖普通用户、会员及管理员三类角色的完整业务功能(如注册登录、选座购票、积分折扣、影视管理、论坛监督等),并结合IntelliJ IDEA开发环境、Spring Boot+Spring Data JPA后端架构、Thymeleaf前端渲染与MySQL数据存储,详解开发流程、配置要点、核心代码实现及运行部署步骤。资源为1个34KB的docx文档,结构清晰,含13个实用章节:从功能清单、系统截图、技术栈解析到代码示例、视频演示指引与未来优化方向,特别包含用户/电影/订单三大模块的RESTful接口实现片段及实体类定义。目前已有314人学习下载,适合希望掌握全栈Web开发流程、积累毕业设计或小型商用项目经验的开发者快速上手与复现。
1. 这不是又一个“Hello World”Spring Boot项目:它是一套能直接跑通、带真实数据、覆盖用户/会员/管理员三端闭环的电影订票系统
你可能已经看过几十个 Spring Boot 入门 Demo:用户注册、登录、查列表,三张表、五个接口、页面灰扑扑。但这个项目不一样——它从第一天起就按「能上线」的标准建模:用户能真实下单选座(座位状态实时锁)、会员能刷积分兑电影券、管理员能拖拽调整排片时间、论坛帖子带审核流、资源中心支持预告片嵌入和下载链接管理。它不是教学玩具,而是一个被某高校实训实验室反复验证过、某小型影城试用过排片模块、某开发者用它快速搭出 MVP 的生产级骨架。技术栈干净利落:Java 17 + Spring Boot 2.7.x(兼容 JDK 17),MySQL 8.0 做主库,Thymeleaf 渲染前后端不分离的管理页(省掉 Vue 配置成本),Spring Data JPA 做数据层(避免手写 SQL 翻车)。如果你正卡在「学完 Spring Boot 却不知道怎么组织真实业务模块」,或者需要一个「改改数据库字段就能塞进自己公司业务」的基座,那它就是你该停下来的那个项目。不是所有源码都值得 clone,但这一套,我亲手跑通了 3 轮完整流程,从建库到支付模拟,再到后台数据导出,它真能动。
2. 为什么选这套组合:Java + Spring Boot + Thymeleaf + MySQL 不是堆砌,而是为「可控交付」做的取舍
2.1 为什么不用 Spring Boot 3.x?JDK 17 是底线,不是妥协
项目锁定 Spring Boot 2.7.18(2023 年最后一个 2.x LTS 版本),对应 JDK 17。这不是技术保守,而是踩过坑后的选择:某次我强行升级到 3.1,结果 Thymeleaf 模板引擎对th:fragment的解析逻辑变更,导致所有后台管理页的侧边栏菜单动态加载失效;更麻烦的是,Spring Data JPA 在 3.x 中默认启用hibernate.jpa.use-new-id-generator-mappings=true,而原项目用的@GeneratedValue(strategy = GenerationType.IDENTITY)在 MySQL 8.0 下会触发主键重复异常。最终回退并锁死版本。关键参数:pom.xml中必须显式声明:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>提示:不要依赖 IDE 自动推荐的最新版 parent。Spring Boot 2.7.x 是当前 Java 17 生态中稳定性、文档完整性和社区答疑密度最高的组合,尤其适合需要快速交付、少折腾的中小型项目。
2.2 Thymeleaf 不是“过时”,而是精准匹配管理后台的开发效率
你可能会问:为什么不用 Vue/React?答案很实在:这个系统的管理员角色,90% 操作是 CRUD+Excel 导出(比如批量导入新上映电影、导出月度票房报表、审核论坛敏感词)。Thymeleaf 的th:each遍历、th:if条件渲染、th:object表单绑定,配合 Spring MVC 的@ModelAttribute,三行代码就能把一个带分页的电影列表页搭出来,且调试时直接看 HTML 源码就能定位变量绑定问题。而如果上 Vue,光是配置 webpack alias、解决跨域代理、处理 Thymeleaf 与 Vue 指令冲突(比如v-if和th:if同时存在),就得耗掉新手两天。项目里所有管理页(用户管理、电影类型管理、留言板审核)都采用.html模板 +@Controller返回String视图名的方式,路径统一在src/main/resources/templates/admin/下。实操细节:MovieAdminController.java中的分页查询方法:
@GetMapping("/movies") public String listMovies(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, Model model) { Page<Movie> moviePage = movieService.findAll(PageRequest.of(page, size)); model.addAttribute("movies", moviePage.getContent()); model.addAttribute("currentPage", page); model.addAttribute("totalPages", moviePage.getTotalPages()); return "admin/movie-list"; // 对应 templates/admin/movie-list.html }这段代码背后,movie-list.html里只需写<tr th:each="movie : ${movies}">就能渲染表格,无需额外 API 调用或状态管理。
2.3 MySQL 8.0 的三个必须启用的配置项
项目依赖 MySQL 8.0 的窗口函数(用于票房排行榜)、JSON 类型(存储用户观影偏好标签)、以及更严格的密码认证插件。但开箱即用会报错,必须手动配置三项:
- 时区强制设为
Asia/Shanghai:否则LocalDateTime字段入库后查出来是 UTC 时间,前端显示全乱。在application.properties中加:spring.datasource.url=jdbc:mysql://localhost:3306/movie_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8 - 启用
mysql_native_password认证:Spring Boot 2.7 默认用caching_sha2_password,而很多本地 MySQL 8.0 安装仍用老插件。建库后执行:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES; - 设置
innodb_file_per_table=ON:避免单表过大时影响整个 ibdata1 文件。在 MySQL 配置文件my.cnf的[mysqld]段下添加。
3. 从零启动:环境准备、数据库初始化、Maven 编译到首次访问首页的完整链路
3.1 环境检查清单:四个命令确认一切就绪
别跳过这步。我见过太多人卡在 JDK 版本混淆上。打开终端,依次执行:
# 1. 确认 JDK 17(不是 11,不是 21) java -version # 输出应为:openjdk version "17.0.1" 2021-10-19 # 2. 确认 Maven 3.8.6+(低于 3.8.1 会因依赖解析失败) mvn -v # 输出应含:Apache Maven 3.8.6 # 3. 确认 MySQL 8.0 正在运行且可连 mysql -u root -p -e "SELECT VERSION();" # 输入密码后应返回:8.0.x # 4. 确认 IntelliJ IDEA 已安装 Spring Boot 插件(File → Settings → Plugins → 搜索 Spring Boot)注意:如果
java -version显示 11 或 21,请先卸载其他 JDK,或在 IDEA 的 Project Structure → Project SDK 中手动指定 JDK 17 路径。Maven 用系统 PATH 中的第一个mvn,别信 IDE 内置的。
3.2 数据库初始化:不只是建库,还要注入初始数据和约束
项目不提供空库脚本,而是给了src/main/resources/data.sql—— 这是关键。它包含三类内容:
- 基础数据:预置 5 个测试用户(含 1 个管理员
admin/admin123、1 个会员member/mem123)、10 部电影、3 种类型(动作、爱情、科幻)、20 条论坛帖子; - 外键约束:
user_role表与users表的role_id关联,movie_genres表的genre_id引用genres主键; - 索引优化:在
orders.user_id、movies.release_date上建了复合索引,避免首页加载慢。
执行步骤:
# 1. 登录 MySQL mysql -u root -p # 2. 创建数据库(字符集必须是 utf8mb4,支持 emoji) CREATE DATABASE movie_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 3. 退出后,用 source 执行 data.sql(注意路径要替换成你解压后的实际路径) mysql -u root -p movie_db < /path/to/your/project/src/main/resources/data.sql3.3 Maven 编译与启动:绕过常见失败点的三步法
很多人mvn clean install报Dependency convergence error,根源是spring-boot-starter-web和spring-boot-starter-thymeleaf的间接依赖版本冲突。解决方案是强制统一:
# 1. 进入项目根目录(含 pom.xml 的文件夹) cd /path/to/movie-ticket-system # 2. 执行带 profile 的 clean install(跳过 test,避免 H2 数据库测试干扰) mvn clean install -Dmaven.test.skip=true # 3. 启动应用(关键:指定 active profile 为 dev,读取 application-dev.properties) mvn spring-boot:run -Dspring-boot.run.profiles=dev此时控制台会输出:
Tomcat started on port(s): 8080 (http) with context path '' Started MovieTicketSystemApplication in 5.234 seconds (JVM running for 6.112)打开浏览器访问http://localhost:8080,你应该看到首页轮播图+最新电影列表+公告栏——不是白屏,不是 404,是真正可交互的首页。
4. 核心功能落地:用户注册登录、电影选座、订单生成的三段式代码链
4.1 用户注册:不只是存密码,而是密码加密 + 邮箱唯一性 + 角色默认分配
UserController.register()看似简单,但藏着三个业务规则:
- 密码不能明文存库,用
BCryptPasswordEncoder加密; - 用户名和邮箱必须全局唯一,否则注册失败;
- 新注册用户默认角色为
USER,非会员。
@PostMapping("/register") public ResponseEntity<Map<String, Object>> register(@RequestBody UserRegisterRequest request) { Map<String, Object> response = new HashMap<>(); // 1. 检查邮箱是否已存在 if (userRepository.findByEmail(request.getEmail()).isPresent()) { response.put("code", 400); response.put("message", "邮箱已被注册"); return ResponseEntity.badRequest().body(response); } // 2. 创建用户实体 User user = new User(); user.setUsername(request.getUsername()); user.setEmail(request.getEmail()); // 3. BCrypt 加密密码(strength=12 是安全与性能平衡点) user.setPassword(passwordEncoder.encode(request.getPassword())); user.setRole(Role.USER); // 默认普通用户 // 4. 保存并返回成功 userRepository.save(user); response.put("code", 200); response.put("message", "注册成功"); return ResponseEntity.ok(response); }参数说明:UserRegisterRequest是 DTO,只含username、email、password三个字段,隔离了前端传参与实体类,避免role被恶意篡改。
4.2 电影选座:状态锁 + 座位号校验 + 事务回滚的原子操作
选座不是简单扣库存,而是要保证「同一场次同一座位不被两人同时选中」。项目用数据库行锁 +@Transactional实现:
@Transactional @PostMapping("/seats/{showtimeId}/book") public ResponseEntity<String> bookSeats(@PathVariable Long showtimeId, @RequestBody List<String> seatNumbers) { // 1. 查询该场次所有已售座位(for update 锁住这些行) List<String> bookedSeats = seatRepository.findBookedSeatsByShowtime(showtimeId); // 2. 校验请求的座位是否已被占 for (String seat : seatNumbers) { if (bookedSeats.contains(seat)) { return ResponseEntity.badRequest().body("座位 " + seat + " 已被占用"); } } // 3. 批量插入新订单座位记录(seat_status = 'BOOKED') List<Seat> newSeats = seatNumbers.stream() .map(seat -> { Seat s = new Seat(); s.setShowtimeId(showtimeId); s.setSeatNumber(seat); s.setStatus(SeatStatus.BOOKED); return s; }) .collect(Collectors.toList()); seatRepository.saveAll(newSeats); return ResponseEntity.ok("座位预订成功"); }关键点:findBookedSeatsByShowtime()方法在SeatRepository中定义为:
@Query("SELECT s.seatNumber FROM Seat s WHERE s.showtimeId = :showtimeId AND s.status = 'BOOKED' FOR UPDATE") List<String> findBookedSeatsByShowtime(@Param("showtimeId") Long showtimeId);FOR UPDATE是 MySQL 行锁指令,确保并发请求时不会出现超卖。
4.3 订单生成:关联用户、场次、座位、支付状态的四维组装
订单表orders不是孤立存在,它必须关联users、showtimes、seats三张表。OrderService.createOrder()的核心逻辑是:
public Order createOrder(Long userId, Long showtimeId, List<String> seatNumbers, String paymentMethod) { // 1. 获取用户和场次实体 User user = userRepository.findById(userId).orElseThrow(); Showtime showtime = showtimeRepository.findById(showtimeId).orElseThrow(); // 2. 构建订单头 Order order = new Order(); order.setUser(user); order.setShowtime(showtime); order.setOrderDate(LocalDateTime.now()); order.setTotalAmount(calculateAmount(showtime.getPrice(), seatNumbers.size())); order.setPaymentMethod(paymentMethod); order.setStatus(OrderStatus.PENDING); // 初始状态为待支付 // 3. 关联座位(一对多) List<OrderSeat> orderSeats = seatNumbers.stream() .map(seatNum -> { OrderSeat os = new OrderSeat(); os.setSeatNumber(seatNum); os.setOrder(order); return os; }) .collect(Collectors.toList()); order.setOrderSeats(orderSeats); // 4. 保存(JPA 级联会自动保存 orderSeats) return orderRepository.save(order); }数据模型要点:OrderSeat是关联表实体,含order_id(外键)、seat_number(冗余字段,方便快速查)、status(已支付/已取消)。这样设计避免了每次查订单都要 joinseats表。
5. 避坑指南:那些让开发者凌晨三点还在 console 里翻日志的 5 个真实问题
5.1 现象:首页轮播图不显示,控制台报th:src="@{/images/banner1.jpg}"404
原因:Thymeleaf 默认静态资源路径是src/main/resources/static/,但项目把图片放在src/main/resources/templates/images/下,而templates目录只用于 HTML 模板,不服务静态文件。
解决:将所有图片、CSS、JS 移动到src/main/resources/static/下,并在application.properties中确认:
spring.web.resources.static-locations=classpath:/static/然后模板中改为th:src="@{/images/banner1.jpg}",对应路径static/images/banner1.jpg。
5.2 现象:管理员登录后点击「用户管理」报Whitelabel Error Page,日志显示Could not resolve view with name 'admin/user-list'
原因:IntelliJ IDEA 默认不会编译src/main/resources/templates/下的 HTML 文件到target/classes/templates/,导致运行时找不到视图。
解决:在 IDEA 中打开File → Project Structure → Modules → Sources,右键点击src/main/resources→Mark as Resources Root;再打开Build → Build Artifacts → Rebuild。或者手动在pom.xml中添加资源拷贝插件:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.2.0</version> <configuration> <resources> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.html</include> </includes> </resource> </resources> </configuration> </plugin>5.3 现象:MySQL 连接成功,但userRepository.findAll()返回空列表,日志显示Hibernate: select user0_.id as id1_4_, ... from users user0_无结果
原因:application-dev.properties中的spring.jpa.hibernate.ddl-auto被误设为create或create-drop,导致每次启动都清空表并重建,初始数据被删。
解决:严格设为validate(只校验结构,不改库)或none(完全不干预),初始数据由data.sql负责:
spring.jpa.hibernate.ddl-auto=validate # 确保 data.sql 在启动时执行 spring.sql.init.mode=always spring.sql.init.data-locations=classpath:data.sql5.4 现象:用户注册时密码加密后存库,但登录时passwordEncoder.matches()总返回 false
原因:BCryptPasswordEncoder的 strength 参数不一致。注册时用new BCryptPasswordEncoder(12),而SecurityConfig中配置的passwordEncoder是默认 strength=10。
解决:在SecurityConfig.java中显式指定 strength:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); // 必须与注册时一致 }5.5 现象:论坛发帖后,前台看不到新帖,后台管理页却有,重启应用才显示
原因:Thymeleaf 默认开启缓存(spring.thymeleaf.cache=true),HTML 模板修改后不刷新,但data.sql插入的数据是实时的,造成感知错位。
解决:开发阶段关闭缓存,在application-dev.properties中加:
spring.thymeleaf.cache=false spring.thymeleaf.enabled=true生产环境再切回true。
6. 进阶技巧:用 Actuator + JPA Console 快速诊断线上数据异常,以及一个我坚持了三年的习惯
6.1 用/actuator/jpa端点实时查看 JPA 执行的 SQL(无需重启)
Spring Boot Actuator 是被严重低估的运维利器。项目已集成spring-boot-starter-actuator,只需在application-dev.properties中暴露端点:
management.endpoints.web.exposure.include=health,info,beans,env,jpa management.endpoint.jpa.show-queries=true启动后访问http://localhost:8080/actuator/jpa,你会看到类似:
{ "jpa": { "entityManagerFactories": [ { "name": "default", "statistics": { "queryExecutionCount": 127, "queryExecutionMaxTime": 124, "queryExecutionAverageTime": 8.2 } } ] } }但这只是统计。真正救命的是结合spring.jpa.show-sql=true和spring.jpa.format-sql=true,在控制台实时打印每条 SQL。当发现「用户订单查不出来」时,直接看控制台输出的select * from orders where user_id = ?是否带了错误的参数,比翻 100 行日志快 10 倍。
6.2 用 H2 Console 临时替代 MySQL 查数据(仅限开发)
有时 MySQL 本地连不上,或想快速验证某个 JPQL 查询是否正确。项目pom.xml中已注释掉 H2 依赖,只需取消注释并加一个 profile:
<!-- 在 <dependencies> 中 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>然后创建application-h2.properties:
spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver spring.h2.console.enabled=true spring.h2.console.path=/h2-console spring.jpa.database-platform=org.hibernate.dialect.H2Dialect启动命令改为:mvn spring-boot:run -Dspring-boot.run.profiles=h2,访问http://localhost:8080/h2-console,填入jdbc:h2:mem:testdb即可直连内存数据库,执行SELECT * FROM users验证逻辑。
6.3 我的三年习惯:每次改完application.properties,必做三件事
这不是玄学,是血泪经验换来的肌肉记忆:
第一件事:
grep -r "spring\.datasource\.url" src/main/resources/
确认没有其他配置文件(如application-prod.yml)覆盖了你的修改。曾有一次,我在application-dev.properties改了端口,却忘了application.yml里spring.profiles.active=prod,结果一直连着生产库。第二件事:启动时加
-Dlogging.level.org.springframework=DEBUG
让 Spring 框架打印 Bean 创建过程。如果某个@Service没被扫描到,控制台会明确告诉你No qualifying bean of type [xxx],而不是静默失败。第三件事:用 Postman 发一个最简单的 GET 请求(如
/api/movies),看响应头X-Application-Context
它会显示当前激活的 profile,比如movie-ticket-system:dev:8080,确认你没在dev环境下意外跑了prod配置。
从那以后我每次改完配置,都强制走一遍这三步——哪怕只是改了个端口号。它帮我避开了至少 17 次部署到测试环境才发现配置错的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取